着色器学习(一) 基本语法
  前一篇
参考地址:https://docs.godotengine.org/en/4.1/tutorials/shaders/shader_reference/shading_language.html
# 术语
# 插值
在数学和计算机图形学中,插值是指在已知数值点的基础上,通过某种算法推算出在两个已知点之间的数值。在上下文中,插值通常用于平滑地估算或推断在两个已知点之间的值,以便在整个区域内获得更连续的过渡。
在图形渲染中,特别是在顶点着色器和片段着色器之间,插值通常用于在图元表面上平滑地传递数据。这可以让我们在顶点处定义的值(比如颜色、法线、纹理坐标等)在图元的表面上自动过渡和插值,从而在片段着色器中使用。
例如,如果你在一个三角形的三个顶点上定义了一个颜色,图形渲染管道将在这些顶点之间插值这些颜色值,以便在三角形的内部每个像素都有一个平滑的颜色过渡。这样的插值确保了渲染结果在图形表面上看起来更加自然和连续。
# 预处理
在Godot中,Shader预处理语法与大多数GLSL着色器编译器支持的语法相似。以下是一些常见的Shader预处理指令及其示例:
- 定义常量:
 
#define PI 3.14159265359
 使用常量:
float radius = PI * 2.0;
 - 条件编译:
 
#ifdef ENABLE_FEATURE
    // 执行某些代码
#endif
 2
3
- 条件不成立时执行:
 
#ifndef DISABLE_FEATURE
    // 执行某些代码
#endif
 2
3
- 包含其他文件:
 
#include "common_functions.glsl"
 在common_functions.glsl文件中定义通用函数,然后在当前着色器中使用这些函数。
- 版本指令:
 
#version 330
 指定GLSL的版本。
# 数据类型
| 数据类型 | 描述 | 
|---|---|
void |  仅用于无返回值函数。 | 
bool |  布尔数据类型,表示真或假。 | 
bvec2, bvec3, bvec4 |  由2、3或4个布尔分量组成的布尔向量。 | 
int |  有符号标量整数。 | 
ivec2, ivec3, ivec4 |  由2、3或4个有符号整数组成的向量。 | 
uint |  无符号标量整数;不能包含负数。 | 
uvec2, uvec3, uvec4 |  由2、3或4个无符号整数组成的向量。 | 
float |  浮点数标量。 | 
vec2, vec3, vec4 |  由2、3或4个浮点数值组成的向量。 | 
mat2, mat3, mat4 |  列主序2x2、3x3或4x4矩阵。 | 
sampler2D |  绑定2D纹理的采样器类型,按浮点数读取。 | 
isampler2D |  绑定2D纹理的采样器类型,按有符号整数读取。 | 
usampler2D |  绑定2D纹理的采样器类型,按无符号整数读取。 | 
sampler2DArray |  绑定2D纹理数组的采样器类型,按浮点数读取。 | 
isampler2DArray |  绑定2D纹理数组的采样器类型,按有符号整数读取。 | 
usampler2DArray |  绑定2D纹理数组的采样器类型,按无符号整数读取。 | 
sampler3D |  绑定3D纹理的采样器类型,按浮点数读取。 | 
isampler3D |  绑定3D纹理的采样器类型,按有符号整数读取。 | 
usampler3D |  绑定3D纹理的采样器类型,按无符号整数读取。 | 
samplerCube |  绑定立方体贴图的采样器类型,按浮点数读取。 | 
samplerCubeArray |  绑定立方体贴图数组的采样器类型,按浮点数读取。 | 
# 定义使用
bool isTrue = true;                  // 布尔变量,值为真
bool isFalse = false;                // 布尔变量,值为假
bvec2 boolVector2 = bvec2(true, false);       // 2维布尔向量
bvec3 boolVector3 = bvec3(true, false, true); // 3维布尔向量
bvec4 boolVector4 = bvec4(true, false, true, false); // 4维布尔向量
int integerNumber = 42;               // 有符号整数
ivec2 intVector2 = ivec2(1, 2);        // 2维有符号整数向量
ivec3 intVector3 = ivec3(3, 4, 5);     // 3维有符号整数向量
ivec4 intVector4 = ivec4(6, 7, 8, 9);  // 4维有符号整数向量
uint unsignedInteger = 123u;          // 无符号整数
uvec2 unsignedIntVector2 = uvec2(10u, 11u);       // 2维无符号整数向量
uvec3 unsignedIntVector3 = uvec3(12u, 13u, 14u);  // 3维无符号整数向量
uvec4 unsignedIntVector4 = uvec4(15u, 16u, 17u, 18u); // 4维无符号整数向量
float floatingPointNumber = 3.14;     // 浮点数
vec2 floatVector2 = vec2(1.0, 2.0);   // 2维浮点数向量
vec3 floatVector3 = vec3(3.0, 4.0, 5.0);// 3维浮点数向量
vec4 floatVector4 = vec4(6.0, 7.0, 8.0, 9.0); // 4维浮点数向量
mat2 matrix2x2 = mat2(1.0, 2.0, 3.0, 4.0);      // 2x2矩阵
mat3 matrix3x3 = mat3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0); // 3x3矩阵
mat4 matrix4x4 = mat4(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0); // 4x4矩阵
// 纹理采样器通常在着色器头部定义,并在外部进行绑定。
sampler2D textureSampler;
// 其他类型的纹理采样器,用于2D数组、3D纹理和立方体贴图。
sampler2DArray textureArraySampler;
sampler3D texture3DSampler;
samplerCube textureCubeSampler;
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 嵌套使用
嵌套使用时只要提供的参数数量和类型本身的参数数量相同即可
// The required amount of scalars
vec4 a = vec4(0.0, 1.0, 2.0, 3.0);
// Complementary vectors and/or scalars
vec4 a = vec4(vec2(0.0, 1.0), vec2(2.0, 3.0));
vec4 a = vec4(vec3(0.0, 1.0, 2.0), 3.0);
// A single scalar for the whole vector
vec4 a = vec4(0.0);
mat2 m2 = mat2(vec2(1.0, 0.0), vec2(0.0, 1.0));
mat3 m3 = mat3(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0));
mat4 identity = mat4(1.0);
 2
3
4
5
6
7
8
9
10
vec4 a = vec4(0.0, 1.0, 2.0, 3.0);
vec3 b = a.rgb; // Creates a vec3 with vec4 components.
vec3 b = a.ggg; // Also valid; creates a vec3 and fills it with a single vec4 component.
vec3 b = a.bgr; // "b" will be vec3(2.0, 1.0, 0.0).
vec3 b = a.xyz; // Also rgba, xyzw are equivalent.
vec3 b = a.stp; // And stpq (for texture coordinates).
float c = b.w; // Invalid, because "w" is not present in vec3 b.
vec3 c = b.xrt; // Invalid, mixing different styles is forbidden.
b.rrr = a.rgb; // Invalid, assignment with duplication.
b.bgr = a.rgb; // Valid assignment. "b"'s "blue" component will be "a"'s "red" and vice versa.
 2
3
4
5
6
7
8
9
10
# 关键字
# varying插值
varying 是一个关键字,用于在着色器之间传递数据。它允许在顶点着色器中计算的数据在片段着色器中进行插值,以便在片段级别获得更平滑的效果。
具体来说,当你在顶点着色器中计算了某些值(如颜色、法线、纹理坐标等),并且你希望在片段着色器中使用这些值时,你可以将它们标记为varying。这使得这些值在顶点着色器输出和片段着色器输入之间进行插值,以便在图形的不同部分获得平滑的过渡效果。
:::note{title="通俗来讲"}
当谈到图形渲染中的varying时,你可以将其想象成一种在图形的不同部分之间传递信息的方式。让我们用一个简单的比喻来解释:
比喻:舞台上的光影
想象一场舞台表演,舞者们穿着不同颜色的服装,在舞台上移动。舞台的两侧有两个灯光,它们的颜色分别是红色和蓝色。舞者在舞台上移动时,他们的颜色会根据他们的位置在红色和蓝色之间过渡。
在这里:
- 舞者就是图形中的顶点。
 - 他们穿着的服装颜色就是在顶点着色器中定义的
varying变量。 - 舞台两侧的灯光颜色就是片段着色器中最终使用的颜色。
 
varying就像是舞者服装的颜色,它在顶点着色器中设置,并且系统会在舞台上的不同位置插值这些颜色,最终影响到整个舞台上的灯光颜色。
因此,varying在图形渲染中是一种在不同图形部分之间传递信息的机制,使得颜色、法线等在不同部分之间平滑过渡。
:::
shader_type spatial;
varying vec3 some_color;
void vertex() {
    some_color = NORMAL; // 在顶点着色器中赋值
}
void fragment() {
    ALBEDO = some_color;  // 在片段着色器中拿到的some_color是进行平滑过渡后的值
}
// varying 修饰的变量不可以在 普通函数或者灯光着色器中赋值
void foo() {
    some_color = NORMAL; // Error.
}
void light() {
    some_color = NORMAL; // Error too.
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
当涉及到 Godot 的着色器语言时,一个着色器通常会经历多个阶段,其中至少包括 vertex 和 fragment 阶段。
着色器阶段:
vertex阶段处理 3D 模型的每个顶点,而fragment阶段处理屏幕上的每个像素。
Varying 变量:
varying关键字用于声明一个在vertex阶段的顶点之间进行插值的变量,并在fragment阶段使用。- 它的值在图元表面(例如三角形)的顶点之间自动插值。
 
自定义函数:
- 在你的着色器中,你定义了一个名为 
foo的自定义函数,并试图在其中为varying变量test赋值。这是不允许的,因为varying的值是在插值过程中确定的。 
- 在你的着色器中,你定义了一个名为 
 光照处理函数:
- 同样地,你尝试在 
light函数内为test赋值,但出于相同的原因这是不允许的。 
- 同样地,你尝试在 
 正确使用方式:
- 为 
varying变量赋值的正确位置是在vertex函数内。这是你可以根据顶点属性执行计算或设置值的地方。 - 然后在 
fragment函数中可以使用插值后的值进行进一步的计算,例如确定像素的最终颜色。 
- 为 
 示例更正:
- 在示例的更正中,对 
test的赋值是在vertex函数内完成的,确保它发生在顶点处理阶段。 test的值然后可以在fragment函数中使用,其中它代表了图元表面上的插值值。
- 在示例的更正中,对 
 
这样的结构确保了 varying 值在着色器管道的 vertex 和 fragment 阶段之间无缝地插值。
# 插值修饰符
在Shader中,插值修饰符告诉计算机如何在图形的不同部分之间平滑过渡数据。这就好比你在画图时,用铅笔在两个点之间画一条线,插值修饰符就是告诉计算机如何把这条线画得更加平滑。
smooth:- 描述: 默认的插值方式,就像用直尺连接两个点,线条会自然弯曲,考虑了远近透视效果。
 - 示例: 
varying float myValue;(默认是 smooth 插值方式) 
flat:- 描述: 不插值,就像你直接用直尺连接两个点,没有弯曲,整个区域都使用相同的值。
 - 示例: 
varying flat float myValue; 
noperspective:- 描述: 使用透视校正插值,考虑了远近透视效果,但插值时会更加平均。
 - 示例: 
varying noperspective vec3 myVector; 
通过选择合适的插值修饰符,你可以让图形的渐变看起来更加自然和平滑,就像画一条漂亮的曲线一样。
# uniform
在Godot引擎中,使用和设置 uniforms 主要涉及GDScript或其他支持的脚本语言。以下是在Godot中使用和设置 uniforms 的一般步骤:
- 在Shader中声明Uniforms: 
在你的Shader中,使用
uniform关键字声明你希望在应用程序中设置的变量。例如:shader_type canvas_item; uniform vec4 custom_color; // 一个表示颜色的 uniform 变量1
2
3着色器代码编译之后, 使用
uniform定义的变量就会出现在Shader Parameters中
 

在脚本中设置Uniforms的值:
在GDScript或其他脚本中,你可以通过
set_shader_param方法设置Shader中的uniforms的值。例如:extends Node2D func _ready(): # 获取ShaderMaterial var material = ShaderMaterial.new() material.shader = preload("res://path/to/your_shader.shader") # 设置uniform变量custom_color的值 material.set_shader_param("custom_color", Color(1, 0, 0, 1)) # 设置为红色 # 应用ShaderMaterial到节点 self.material = material1
2
3
4
5
6
7
8
9
10
11
12
这样,你就可以在运行时动态设置着色器中
uniforms的值。在Shader中使用Uniforms:
在Shader中,你可以像使用其他变量一样使用
uniforms。例如,在片段着色器中使用custom_color:shader_type canvas_item; uniform vec4 custom_color; void fragment() { COLOR = custom_color; // 使用uniform变量设置颜色 }1
2
3
4
5
6
7
通过这种方式,你可以通过脚本动态地控制Shader的外观,而无需在每次更改时重新编译Shader。这在动态调整图形外观、实现特效等方面非常有用。
# 全局 uniform
在项目设置中添加
在任意着色器中使用
shader_type canvas_item;
global uniform vec4 my_color;
void fragment() {
    COLOR = my_color.rgb;
}
 2
3
4
5
6
7
在脚本中使用
RenderingServer.global_shader_parameter_set("my_color", Color(0.3, 0.6, 1.0))
RenderingServer.global_shader_parameter_add("my_color", RenderingServer.GLOBAL_VAR_TYPE_COLOR, Color(0.3, 0.6, 1.0))
RenderingServer.global_shader_parameter_remove("my_color")
 2
3
:::warning{title="注意"} 全局uniform的访问非常消耗性能,尽量减少使用,或者在onread阶段完成存取 :::
# uniform的hints
变量 冒号后面的为hit
shader_type spatial;
uniform vec4 color : source_color;
uniform float amount : hint_range(0, 1);
uniform vec4 other_color : source_color = vec4(1.0);
uniform sampler2D image : source_color;
 2
3
4
5
6
hint提供了更多的信息,使得编辑器和引擎在处理这些变量时能够更好地理解它们的预期用途。
| 类型 | 提示 | 描述 | 
|---|---|---|
| vec3, vec4 | source_color | 用作颜色。 | 
| int, float | hint_range(min, max[, step]) | 限制在指定范围内的值(带有 min/max/step)。 | 
| sampler2D | source_color | 用作反照率颜色。 | 
| sampler2D | hint_normal | 用作法线贴图。 | 
| sampler2D | hint_default_white | 作为值或反照率颜色,如果未指定值,则默认为不透明白色。 | 
| sampler2D | hint_default_black | 作为值或反照率颜色,如果未指定值,则默认为不透明黑色。 | 
| sampler2D | hint_default_transparent | 作为值或反照率颜色,如果未指定值,则默认为透明黑色。 | 
| sampler2D | hint_anisotropy | 作为流动图,如果未指定值,则默认为右侧方向。 | 
| sampler2D | hint_roughness[_r, _g, _b, _a, _normal, _gray] | 用于在导入时限制粗糙度,尝试减少高光锯齿。_normal 是指导粗糙度限制器的法线图,粗糙度在具有高频细节的区域增加。 | 
| sampler2D | filter[_nearest, _linear][_mipmap][_anisotropic] | 启用指定的纹理过滤。 | 
| sampler2D | repeat[_enable, _disable] | 启用或禁用纹理重复。 | 
| sampler2D | hint_screen_texture | 纹理是屏幕纹理。 | 
| sampler2D | hint_depth_texture | 纹理是深度纹理。 | 
| sampler2D | hint_normal_roughness_texture | 纹理是法线粗糙度纹理(仅在Forward+中支持)。 | 
# 流程控制
和大部分语言一样
// `if` 和 `else`。
if (条件) {
} else {
}
// 三元运算符。
// 这是一个像 `if`/`else` 一样的表达式,并返回一个值。
// 如果 `条件` 为 `true`,`结果` 将为 `9`。
// 否则,`结果` 将为 `5`。
int 结果 = 条件 ? 9 : 5;
// `switch`。
switch (i) { // `i` 应为有符号整数表达式。
    case -1:
        break;
    case 0:
        return; // `break` 或 `return` 以避免运行下一个 `case`。
    case 1: // 落入(没有 `break` 或 `return`):将运行下一个 `case`。
    case 2:
        break;
    //...
    default: // 仅在以上没有匹配的 `case` 时运行。可选。
        break;
}
// `for` 循环。最适用于已知要提前迭代的元素数量的情况。
for (int i = 0; i < 10; i++) {
}
// `while` 循环。最适用于不知道要提前迭代的元素数量的情况。
while (条件) {
}
// `do while`。与 `while` 类似,但即使 `条件` 从不评估为 `true`,它也至少运行一次。
do {
} while (条件);
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 函数
:::tip{title="提示"} 不支持函数重载 :::
# 函数参数
可以有特殊的修饰符:
in: 表示参数仅用于读取(默认)。
out: 表示参数仅用于写入。
inout: 表示参数完全通过引用传递。
const: 表示参数是常量,不能更改,可以与in修饰符组合使用。
void sum2(int a, int b, inout int result) { \\ 这里的result就是引用
    result = a + b;
}
 2
3
# 着色器函数
# 1. vertex 函数:
作用: 处理顶点数据,通常涉及顶点坐标的变换等。
输入: 接收顶点的输入数据,如坐标、法线、颜色等。
输出: 输出变换后的顶点坐标以及其他可能用于后续阶段的数据。
void vertex() {
    // 顶点着色器代码...
}
 2
3
# 2. fragment 函数:
作用: 处理片段数据,通常涉及颜色计算、纹理映射等操作。
输入: 接收来自顶点着色器的输出(如变换后的顶点坐标、插值的颜色等),以及与片段相关的数据,例如纹理坐标。
输出: 输出最终的颜色值,该颜色值将用于渲染图形。
void fragment() {
    // 片段着色器代码...
}
 2
3
# 3. light 函数:
作用: 处理光照计算。通常用于光照着色器,用于计算光照效果。
输入: 接收来自顶点着色器和片段着色器的输出,以及与光照相关的数据。
输出: 输出光照计算后的颜色值。
void light() {
    // 光照着色器代码...
}
 2
3
# 4. unshaded 函数:
作用: 用于关闭默认的光照计算,使物体不受光照影响。
输入: 接收来自顶点着色器和片段着色器的输出。
输出: 输出不受光照影响的颜色值。
void unshaded() {
    // 关闭光照的代码...
}
 2
3
# 5. particles 函数:
作用: 处理粒子系统的着色器逻辑。
输入: 接收来自顶点着色器和片段着色器的输出,以及与粒子系统相关的数据。
输出: 输出粒子系统的渲染效果。
void particles() {
    // 粒子系统着色器代码...
}
 2
3
# 6. canvas_item 函数:
作用: 处理 2D 画布中项(canvas item)的着色器逻辑。
输入: 接收来自顶点着色器和片段着色器的输出,以及与 2D 画布项相关的数据。
输出: 输出 2D 画布项的渲染效果。
void canvas_item() {
    // 2D 画布项着色器代码...
}
 2
3
:::note{title="使用场景"} 在Godot引擎中,这些着色器函数的调用频率取决于它们的类型以及场景中的使用方式。
vertex函数:- 调用频率: 对于每个顶点,该函数被调用一次。
 - 典型用途: 用于处理每个顶点的变换和计算。
 
fragment函数:- 调用频率: 对于每个像素(片段),该函数被调用一次。
 - 典型用途: 用于处理每个像素的颜色计算、纹理映射等。
 
light函数:- 调用频率: 对于每个像素,该函数被调用一次。通常与光照着色器相关。
 - 典型用途: 用于处理每个像素的光照计算。
 
unshaded函数:- 调用频率: 对于每个像素,该函数被调用一次。通常用于关闭默认的光照计算。
 - 典型用途: 用于不考虑光照的简单着色。
 
particles函数:- 调用频率: 对于每个像素(片段),该函数被调用一次。通常用于粒子系统。
 - 典型用途: 用于处理每个像素的粒子系统效果。
 
canvas_item函数:- 调用频率: 对于每个像素(片段),该函数被调用一次。通常用于处理 2D 画布项的着色。
 - 典型用途: 用于处理每个像素的 2D 画布项效果。
 
了解这些调用频率有助于优化着色器代码。例如,vertex 函数在处理每个顶点时调用,因此它的计算量通常较小。相比之下,fragment 函数在处理每个像素时调用,因此应谨慎处理,以避免过多计算导致性能问题。
:::