Skip to content

着色器

概念

着色器(Shader)是一种在图形处理单元(GPU)上运行的特殊程序。他们最初使用来为 3D 场景着色的,不过现在能做的事情就更多了。你可以用它们来控制引擎在屏幕上绘制几何体以及像素的方式,可以用来实现各种特效。 类似 Godot 的现代渲染引擎都会用着色器来执行所有绘制操作:图形卡可以并行执行成千上万条指令,可以达到惊人的渲染速度。 因为天生就是并行的,所以着色器处理信息的方式与普通的程序有所不同。着色器代码是单独针对顶点或像素执行的。你也无法在帧与帧之间存储数据。因此,使用着色器时,你需要使用与其他编程语言不同的编码和思考方式。

如果想把画面的每个位置颜色进行调整,脚本形式大概是:

gdscript
for x in range(width):
  for y in range(height):
	set_color(x, y, some_color)

而着色器已经包含的循环的部分,每次专注当前位置的像素就好

c
void fragment() {
  COLOR = some_color;
}

Godot 所提供的着色语言是基于流行的 OpenGL 着色语言(GLSL)的简化。引擎会为你处理一些底层的初始化工作,让编写复杂着色器更为简单。

函数

  • vertex() 函数会为网格中的所有顶点各运行一次,用来设置顶点的位置和其他与顶点相关的变量。在 canvas_item 着色器和空间着色器中使用。
  • fragment() 函数会为网格所覆盖的所有像素各运行一次。这个函数会用到 vertex() 函数输出的值,这些值会在顶点之间进行插值。在 canvas_item 着色器和空间着色器中使用。
  • light() 函数会为每个像素和每个灯光各运行一次。这个函数会用到 fragment() 函数以及前几次运行中的变量。在 canvas_item 着色器和空间着色器中使用。

vertex 顶点处理器

在 spatial 和 canvas_item 着色器中,会为每一个顶点调用 vertex() 处理函数。在 particles 着色器中则会为每一个粒子调用一次。

你的世界中的几何体上,每一个顶点都有位置、颜色等属性。该函数会修改这些值,并将其传入片段函数。你也可以借助 varying 向片段着色器传递额外的数据。

默认情况下,Godot 会为你对顶点信息进行变换,这是将几何体投影到屏幕上所必须的。你可以使用渲染模式来自行变换数据;

fragment 片段处理器

fragment() 处理函数的作用是设置每一个像素的 Godot 材质参数。这里的代码会在绘制的对象或图元的每一个可见像素上执行。只能在 spatial、canvas_item、sky 着色器中使用。

片段函数的标准用途是设置用于计算光照的材质属性。例如,你可以为 ROUGHNESS、RIM、TRNASMISSION 等设置值,告诉光照函数光照应该如何处理对应的片段。这样就可以控制复杂的着色管线,而不必让用户编写过多的代码。如果你不需要这一内置功能,那么你可以忽略它,自行编写光照处理函数,Godot 会将其优化掉。例如,如果你没有向 RIM 写入任何值,那么 Godot 就不会计算边缘光照。编译时,Godot 会检查是否使用了 RIM;如果没有,那么它就会把对应的代码删除。因此,你就不会在没有使用的效果上浪费算力。

其他不常用的还有

  • start() 函数会在粒子系统中的每个粒子出生时各运行一次。在粒子着色器中使用。
  • sky() 函数会在辐射度立方体贴图需要更新时为辐射度立方体贴图中的每个像素各运行一次,也会为当前屏幕上的每个像素运行一次。在天空着色器中使用。
  • fog() 函数会为体积雾片段体素缓冲中与 FogVolume 相交的每个片段体素运行一次。在雾着色器中使用。

着色器类型

你所编写的着色器必须指定类型(2D、3D、粒子、天空、雾),不存在所有场景都可以使用的通用配置。不同的类型支持不同的渲染模式、内置变量、处理函数。

在 Godot 中,所有的着色器都需要在第一行指定它们的类型,类似这样:

c
shader_type spatial;

有以下类型可用:

  • 用于 3D 渲染的 spatial。
  • 用于 2D 渲染的 canvas_item。
  • 用于粒子系统的 particles。
  • 用于渲染 Skies 的 sky。
  • 用于渲染 FogVolumes 的 fog

渲染模式

可以在着色器的第二行,也就是在着色器类型之后,指定渲染模式,类似这样:

c
shader_type spatial;
render_mode unshaded, cull_disabled;

渲染模式会修改 Godot 应用着色器的方式。例如,unshaded 模式会让引擎跳过内置的光线处理器函数。

每种着色器类型都有不同的渲染模式。

类型转换

就像 GLSL ES 3.0 一样, 不允许在标量和相同大小但不同类型的向量之间进行隐式转换. 也不允许铸造不同大小的类型. 转换必须通过构造函数明确完成.

c
float a = 2; // invalid
float a = 2.0; // valid
float a = float(2); // valid

默认整数常量是有符号的, 所以转换为无符号总是需要强制类型转换:

c
int a = 2; // valid
uint a = 2; // invalid
uint a = uint(2); // valid

构建

向量类型的构造必须始终通过:

c
// 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);

混写 Swizzling

只要结果是另一种向量类型(或标量), 就可以以任何顺序获得组件的组合.

c
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.

数组

局部声明

c
void fragment() {
    float arr[3];
}

全局

c
const lowp vec3 v[1] = lowp vec3[1] ( vec3(0, 0, 1) );

void fragment() {
  ALBEDO = v[0];
}

初始化

c
float float_arr[3] = float[3] (1.0, 0.5, 0.0); // first constructor

int int_arr[3] = int[] (2, 1, 0); // second constructor

vec2 vec2_arr[3] = { vec2(1.0, 1.0), vec2(0.5, 0.5), vec2(0.0, 0.0) }; // third constructor

bool bool_arr[] = { true, true, false }; // fourth constructor - size is defined automatically from the element count

内置函数 类型等

直接看官方的吧 godot shading_language

内置变量

全局内置

  • TIME(全局变量):自启动以来的全局时间,以秒为单位(始终为正)。它取决于滚动设置(默认情况下是 3600 秒)。它不受 time_scale 或暂停的影响,但如果需要,您可以定义一个全局着色器统一来添加“缩放”变量。

片段内置

某些节点(例如,Sprite2D)默认显示纹理。然而,当这些节点附加了自定义的片段函数时,需要手动进行纹理查找。Godot 通过 COLOR 内置变量乘以节点的颜色来提供纹理颜色。要单独读取纹理颜色,可以使用以下代码:

c
COLOR = texture(TEXTURE, UV);

类似地,如果在 CanvasTexture 中使用法线贴图,Godot 默认使用它并将其值赋给内置的 normal 变量。如果您使用的是用于 3D 的法线贴图,则它将显示为倒置。为了在你的着色器中使用它,你必须将它分配给 NORMALMAP 属性。Godot 将处理将其转换为在 2D 中使用并覆盖 NORMAL。

c
NORMALMAP = texture(NORMAL_TEXTURE, UV).rgb;
  • TEXTURE: 默认的 2D 纹理.
  • POINT_COORD: 所绘制点的坐标。
  • SCREEN_PIXEL_SIZE: 单个像素的大小. 等于分辨率的倒数.
  • UV : 来自顶点功能的 UV.
  • SCREEN_UV: 屏幕当前像素的 UV 坐标.
  • COLOR: 顶点函数的颜色乘以纹理颜色。也输出颜色值。

内置灯光 略

例子

增加光亮

c
shader_type canvas_item;

uniform float glow_power: hint_range(0.0, 8.0, 0.1) = 2.0;
uniform vec4 glow_color: source_color = vec4(1.0,1.0,1.0,1);
uniform float glow_shift: hint_range(0.0, 10.0, 0.1) = 1.0;
uniform float glow_radius: hint_range(1.0, 10.0, 1.0) = 1.0;

void fragment() {
	vec4 glow = vec4(0.0);
	float count = 0.0;
	for (float x = -glow_radius; x <= glow_radius; x+= 1.0) {
		for (float y = -glow_radius; y <= glow_radius; y+= 1.0){
			vec2 offset = vec2(x,y) * glow_shift * TEXTURE_PIXEL_SIZE;
			glow += texture(TEXTURE,UV+offset) * glow_color;
			count += 1.0;
		}
	}

	glow *= glow_power / count ;
	COLOR = mix(texture(TEXTURE,UV),glow,glow.a);
}