你的第二个 3D 着色器
从宏观层面讲,Godot 为用户提供了一系列可选参数(AO、SSS_Strength、RIM 等)。这些参数对应不同的复杂效果(环境光遮蔽、次表面散射、边缘光等)。当用户未给这些参数赋值时,对应的代码会在编译前被剔除,因此着色器不会产生额外功能带来的性能开销。这使得用户无需编写复杂着色器代码,也能轻松实现符合 PBR 物理渲染的复杂光照效果。当然,Godot 也允许你完全忽略这些参数,直接编写完全自定义的着色器。
有关这些参数的完整列表,请参见 Spatial 着色器 参考文档。
顶点着色器函数与片段着色器函数的一个区别在于:顶点着色器逐顶点运行,并设置诸如 VERTEX(位置)和 NORMAL 等属性,而片段着色器逐像素运行,其最重要的任务是设置 MeshInstance3D 的 ALBEDO 颜色 。
你的第一个 Spatial 片段函数
如本教程前文所述,在 Godot 中,片段函数的标准用法是设置不同的材质属性,其余部分则由 Godot 自动处理。为了提供更大的灵活性,Godot 还提供了名为“渲染模式”的功能。渲染模式在着色器顶部、紧接 shader_type 声明之后进行设置,用于指定你希望着色器内置功能具备的具体处理方式。
例如,如果你不希望物体受到光照影响,可将渲染模式设置为 unshaded:
render_mode unshaded;
你还可以将多个渲染模式堆叠在一起。例如,如果你想使用卡通材质而不是更真实的 PBR 材质,将漫反射模式和镜面反射模式设置为卡通:
render_mode diffuse_toon, specular_toon;
这个内置功能模型允许你通过更改几个参数来编写复杂的自定义着色器。
有关渲染模式的完整列表,请参见 Spatial 着色器参考。
在本教程的这一部分,我们将介绍如何将前一部分的崎岖地形变成海洋。
首先让我们设置水的颜色。我们通过设置 ALBEDO 来实现。
ALBEDO 是一个包含物体的颜色的三维向量 vec3 。
让我们将其设置为一种悦目的蓝色。
void fragment() {
ALBEDO = vec3(0.1, 0.3, 0.5);
}
我们将其设置为深蓝色,因为水的大部分蓝色来自天空的反射。
Godot 采用的 PBR 模型依赖两个主要参数:METALLIC(金属度)与 ROUGHNESS(粗糙度)。
ROUGHNESS 用于定义材质表面的光滑度/粗糙度。较低的 ROUGHNESS 值会使材质呈现闪亮的塑料质感,而较高的值则让材质颜色呈现出更均匀的固有色质感。
METALLIC 用于定义物体的金属质感程度,其取值建议尽量接近 0 或 1。可以将 METALLIC 理解为调节反射率与 ALBEDO 基础色之间平衡的参数:较高的 METALLIC 值会几乎忽略 ALBEDO 颜色,使物体呈现如天空镜面般的反射效果;而较低的 METALLIC 值则会使天空颜色与 ALBEDO 基础色呈现更均衡的混合效果。
ROUGHNESS 从左至右从 0 增加到 1,而 METALLIC 自上而下从 0 增加到 1。
备注
为实现正确的 PBR 着色,METALLIC的取值应接近 0 或 1。仅当需要实现材质过渡混合时,才将其设置为中间值。
水不是金属,所以我们将其 METALLIC 属性设置成 0.0。水的反射性也很高,因此我们将其 ROUGHNESS 属性也设置得非常低。
void fragment() {
METALLIC = 0.0;
ROUGHNESS = 0.01;
ALBEDO = vec3(0.1, 0.3, 0.5);
}
现在,我们有了光滑的塑料外观表面。现在该考虑要模拟的水的某些特定属性了。这里有两种主要的方法可以把诡异的塑料表面变成好看的水。首先是镜面反射(Specular)。镜面反射是那些来自太阳直接反射到你眼里的明亮斑点。第二个是菲涅耳反射(Fresnel)。菲涅尔反射是物体在小角度下更具反射性的属性。这就是为什么你可以看见自己身下的水,却在更远处看见天空倒影的原因。
为了增强镜面反射,我们需要做两件事。首先,由于卡通渲染模式具有更高的镜面反射高光,我们将更改镜面反射为卡通渲染模式。
render_mode specular_toon;
其次,我们将添加边缘光照效果。边缘光照能增强掠射角方向的光照强度。该技术通常用于模拟光线穿过物体边缘织物的视觉效果,但在此处,我们将借助它来实现逼真的水体效果。
void fragment() {
RIM = 0.2;
METALLIC = 0.0;
ROUGHNESS = 0.01;
ALBEDO = vec3(0.1, 0.3, 0.5);
}
为了添加菲涅耳反射效果,我们将在片段着色器中计算菲涅耳项。出于性能考虑,这里不会使用真实的菲涅耳计算公式。作为替代,我们将使用法线向量 NORMAL 与视线向量 VIEW 的点积来近似模拟。NORMAL 向量指向远离网格表面的方向,而 VIEW 向量则是你的眼睛看向表面上某一点的方向。这两者的点积可以有效地判断你是在垂直观察表面还是掠射观察表面。
float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
然后把它混合到 ROUGHNESS 和 ALBEDO 里。这就是 ShaderMaterial 相对于 StandardMaterial3D 的好处。用 StandardMaterial3D,我们只能用纹理或者固定数值来设置这些属性。但用着色器,我们可以用任何能想到的数学函数来设置它们。
void fragment() {
float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
RIM = 0.2;
METALLIC = 0.0;
ROUGHNESS = 0.01 * (1.0 - fresnel);
ALBEDO = vec3(0.1, 0.3, 0.5) + (0.1 * fresnel);
}
现在,仅仅用了5行代码,你就能实现看起来复杂的水面效果。不过现在有了光照,这水面看起来有点太亮了。我们来把它调暗一点。这可以通过减少我们传入 ALBEDO 的 vec3 的值来轻松实现。让我们把它们设置为 vec3(0.01, 0.03, 0.05)。
用 TIME 做动画
回到顶点函数,我们可以使用内置变量 TIME 对波浪进行动画处理。
TIME 是一个内置变量,可从顶点和片段函数访问。
在上一个教程中,我们通过从高度图读取来计算高度。对于本教程,我们将做同样的事情。将高度图代码放在一个名为 height() 的函数中。
float height(vec2 position) {
return texture(noise, position / 10.0).x; // Scaling factor is based on mesh size (this PlaneMesh is 10×10).
}
为了在 height() 函数中使用 TIME,我们需要将其传递进去。
float height(vec2 position, float time) {
}
确保其正确传递到顶点函数中。
void vertex() {
vec2 pos = VERTEX.xz;
float k = height(pos, TIME);
VERTEX.y = k;
}
这次我们不使用法线贴图计算法线。我们将在 vertex() 函数中手动计算它们。为此,请使用以下代码行。
NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, k - height(pos + vec2(0.0, 0.1), TIME)));
我们需要手动计算 NORMAL,因为在下一节中,我们将使用数学来创建外观复杂的波形。
现在,我们要通过使 positon 偏移 TIME 的余弦来使 height() 函数更加复杂。
float height(vec2 position, float time) {
vec2 offset = 0.01 * cos(position + time);
return texture(noise, (position / 10.0) - offset).x;
}
这会实现缓慢移动的波纹效果,但显得有点不自然。下一节将深入探讨,通过加入更多的数学函数来用着色器实现更复杂的效果,比如更加真实的波纹。
进阶效果:水波
着色器的强大之处在于你能通过数学实现复杂效果。为了说明这一点,我们将通过修改 height() 函数并引入一个名为 wave() 的新函数,来把波浪效果提升到新的水平。
wave() 有一个参数,position,和在 height() 中一样。
我们将在 height() 函数中多次调用 wave() 函数,来改变波浪的样子。
float wave(vec2 position){
position += texture(noise, position / 10.0).x * 2.0 - 1.0;
vec2 wv = 1.0 - abs(sin(position));
return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
}
初看可能有点复杂,让我们来逐行解析。
position += texture(noise, position / 10.0).x * 2.0 - 1.0;
通过 noise 纹理对位置进行偏移。这样会让波浪产生弯曲,因此它们不再是与网格完全对齐的直线。
vec2 wv = 1.0 - abs(sin(position));
定义一个使用 sin() 和 position 的波形函数。通常 sin() 产生的是圆滑的波形。我们首先使用 abs() 对其取绝对值(将波形负值部分翻折,形成一串连续的圆顶波),并将数值限制在 0 到 1 之间。然后,用 1.0 减去该值,从而将这些圆顶波反转为尖锐的波形。
return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
将 x 方向的波形与 y 方向的波形相乘,并通过取幂运算使波峰变得更为尖锐。随后用 1.0 减去该结果,使原有的波谷反转为波峰,再次通过取幂运算来锐化这些波峰轮廓。
现在我们可以用 wave() 代替 height() 函数的内容。
float height(vec2 position, float time) {
float h = wave(position);
return h;
}
这样一来,你会得到:
正弦曲线的形状太明显了。所以让我们把波型分散一下。我们通过缩放 position 来实现。
float height(vec2 position, float time) {
float h = wave(position * 0.4);
return h;
}
现在它看起来好多了。
我们可以通过叠加多个不同频率和振幅的波形来实现更好的效果。具体做法是:通过缩放每个波形的位置坐标来调节波纹的疏密(频率),并将其输出值相乘来控制波纹的高低(振幅)。
以下是一个通过叠加四层波形来获得更逼真波浪效果的示例。
float height(vec2 position, float time) {
float d = wave((position + time) * 0.4) * 0.3;
d += wave((position - time) * 0.3) * 0.3;
d += wave((position + time) * 0.5) * 0.2;
d += wave((position - time) * 0.6) * 0.2;
return d;
}
请注意,我们给其中两层波加上时间参数,而从另外两层中减去时间参数。这样会使波浪朝不同方向运动,从而形成更复杂的视觉效果。还要注意,各层波的振幅(即计算结果所乘的系数)总和为 1.0,这样可以确保波浪高度始终保持在 0 到 1 的范围内。
通过这段代码,你应该可以得到更复杂的波浪效果,而你所要做的只是增加一点数学运算!
有关 Spatial 着色器的更多信息,请阅读 着色器语言 文档和 Spatial 着色器 文档。也可以看看 着色器 和 3D 部分的高级教程。