您的第二个 3D 着色器

从高级设置开始,Godot所做的是为用户提供一组可选设置的参数(“环境光遮蔽” , “次表面散射强度” , “边缘” 等等)这些参数对应不同的复杂效应(环境遮挡, 次表面散射, 边缘照明等等)如果没有写入, 代码在编译之前被抛出, 因此着色器不会产生额外特性的成本. 这使得用户很容易拥有复杂的支持PBR着色, 而不需要编写复杂的着色器. 当然,Godot还允许您忽略所有这些参数, 并编写一个完全定制的着色器.

有关这些参数的完整列表, 请参见 空间着色器 参考文档.

顶点函数和片段函数的区别在于, 顶点函数是按顶点运行的, 并设置诸如 VERTEX (坐标)和 NORMAL 等属性, 而片段着色器是按像素运行的, 最重要的是设置 MeshALBEDO 颜色.

第一个空间片段函数

如本教程前一部分所述. 在Godot中, 片段函数的标准用法是设置不同的材质属性, 然后让Godot处理剩下的部分. 为了提供更大的灵活性,Godot还提供了渲染模式. 渲染模式设置在着色器的顶部, 直接在 “着色_方式” 下面, 它们指定了你想要着色器的内置方面具有什么样的功能.

例如, 如果你不想让灯光影响一个物体, 设置渲染模式为 “无阴影”:

  1. render_mode unshaded;

您还可以将多个渲染模式堆叠在一起。例如,如果你想使用卡通材质而不是更真实的 PBR 材质,将漫反射模式和镜面反射模式设置为卡通:

  1. render_mode diffuse_toon, specular_toon;

这个内置功能模型允许您通过更改几个参数来编写复杂的自定义着色器.

有关渲染模式的完整列表, 请参见空间着色器参考 Spatial shader reference.

在本教程的这一部分中, 我们将介绍如何将前一部分的崎岖地形变成海洋.

首先让我们设置水的颜色. 我们通过设置 ALBEDO 来做到这一点.

ALBEDO 是一个 vec3 , 包含物体的颜色.

我们把它调成蓝色.

  1. void fragment() {
  2. ALBEDO = vec3(0.1, 0.3, 0.5);
  3. }

../../../_images/albedo.png

我们将其设置为深蓝色, 因为水的大部分蓝色来自天空的反射.

PBR模型的Godot用户两个主要参数:”金属度” 和 “粗糙度”.

粗糙度是指材料表面的光滑程度. 低 “粗糙度” 会使材料看起来像闪亮的塑料, 而高粗糙度使材料在颜色上看起来更坚实.

METALLIC 指定该物体有多像金属, 它最好设置为接近 01 . 把 METALLIC 看作是改变反射和 ALBEDO 颜色之间的平衡. 高的 METALLIC 几乎完全忽略了 ALBEDO , 看起来像天空的镜子. 而低的 METALLIC 对天空的颜色和 ALBEDO 的颜色有一个更平实的表现.

“粗糙度” 从左到右从0增加到1, 而 “金属度” 从上到下从0增加到1.

../../../_images/PBR.png

备注

对恰当的PBR阴影,”金属度” 应当接近0或者1. 为了混合不同的材料, 只有将其设置在0和1之间.

水不是金属,所以我们将其 METALLIC 属性设置成 0.0。水的反射性也很高,因此我们将其``ROUGHNESS`` 属性也设置得非常低。

  1. void fragment() {
  2. METALLIC = 0.0;
  3. ROUGHNESS = 0.01;
  4. ALBEDO = vec3(0.1, 0.3, 0.5);
  5. }

../../../_images/plastic.png

现在,我们有了光滑的塑料外观表面。现在该考虑要模拟的水的某些特定属性了。这里有两种主要的方法可以把诡异的塑料表面变成好看的水。首先是镜面反射(Specular)。镜面反射是那些来自太阳直接反射到你眼里的明亮斑点。第二个是菲涅耳反射(Fresnel)。菲涅尔反射是物体在小角度下更具反射性的属性。这就是为什么你可以看见自己身下的水,却在更远处看见天空倒影的原因。

为了增强镜面反射,我们需要做两件事。首先,由于卡通渲染模式具有更高的镜面反射高光,我们将更改镜面反射为卡通渲染模式。

  1. render_mode specular_toon;

../../../_images/specular-toon.png

其次, 我们将添加边缘照明. 边缘照明增加了掠射角度的光线效果. 通常, 它用于模拟光线穿过对象边缘上的织物的路径, 但是我们将在此处使用它来帮助实现良好的水润效果.

  1. void fragment() {
  2. RIM = 0.2;
  3. METALLIC = 0.0;
  4. ROUGHNESS = 0.01;
  5. ALBEDO = vec3(0.1, 0.3, 0.5);
  6. }

../../../_images/rim.png

为了增加菲涅耳反射率,我们将在片段着色器中计算菲涅耳项。在这里,出于性能方面的考虑,我们将不使用真正的菲涅耳术语。取而代之的是,我们将使用 NORMALVIEW 向量的点积对其进行近似。NORMAL 向量指向远离网格物体表面的位置,而 VIEW 向量则是您的眼睛与该表面上的点之间的方向。它们之间的点积是一种可以告诉您何时正视或掠过某个角度的方便方法。

  1. float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));

并将其混合到 ROUGHNESSALBEDO。这是 ShaderMaterial 比 SpatialMaterial 的好处。使用 SpatialMaterial,我们可以用纹理来设置这些属性,或者设置成一个统一的数字。但是用着色器,我们可以根据我们能想到的任何数学函数来设置它们。

  1. void fragment() {
  2. float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
  3. RIM = 0.2;
  4. METALLIC = 0.0;
  5. ROUGHNESS = 0.01 * (1.0 - fresnel);
  6. ALBEDO = vec3(0.1, 0.3, 0.5) + (0.1 * fresnel);
  7. }

../../../_images/fresnel.png

而现在, 只需要5行代码, 你就可以拥有看起来很复杂的水. 现在, 我们有了照明, 这个水看起来太亮了. 让我们把它变暗. 这可以通过减少我们传入 ALBEDOvec3 的值来轻松实现. 让我们把它们设置为 vec3(0.01, 0.03, 0.05) .

../../../_images/dark-water.png

TIME 做动画

回到顶点功能,我们可以使用内置变量 TIME 对波浪进行动画处理。

TIME 是一个内置变量,可从顶点和片段函数访问。

在上一个教程中,我们通过从高度图读取来计算高度。对于本教程,我们将做同样的事情。将高度图代码放在一个名为 height() 的函数中。

  1. float height(vec2 position) {
  2. return texture(noise, position / 10.0).x; // Scaling factor is based on mesh size (this PlaneMesh is 10×10).
  3. }

为了在 height() 函数中使用 TIME,我们需要将其传递进去。

  1. float height(vec2 position, float time) {
  2. }

确保其正确传递到顶点函数中.

  1. void vertex() {
  2. vec2 pos = VERTEX.xz;
  3. float k = height(pos, TIME);
  4. VERTEX.y = k;
  5. }

而不是使用法线贴图来计算法线。我们将在 vertex() 函数中手动计算它们。为此,请使用以下代码行。

  1. 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() 函数更加复杂。

  1. float height(vec2 position, float time) {
  2. vec2 offset = 0.01 * cos(position + time);
  3. return texture(noise, (position / 10.0) - offset).x;
  4. }

这会实现缓慢移动的波纹效果, 但显得有点不自然. 下一节将深入探讨, 通过加入更多的数学函数, 来用着色器实现更复杂的效果, 比如更加真实的波纹.

进阶效果:水波

利用数学, 着色器可以实现复杂的效果, 这是着色器的强大之处. 为阐述这一点, 我们将修改 height() 函数和引入新函数 wave() , 来让波纹效果更进一层.

wave() 有一个参数, position, 和在 height() 中一样.

我们将在 height() 函数中多次调用 wave() 函数, 来改变波纹的样子.

  1. float wave(vec2 position){
  2. position += texture(noise, position / 10.0).x * 2.0 - 1.0;
  3. vec2 wv = 1.0 - abs(sin(position));
  4. return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
  5. }

这在一开始会让人觉得很复杂, 所以我们一行一行地来实现.

  1. position += texture(noise, position / 10.0).x * 2.0 - 1.0;

通过 noise 纹理来偏移位置. 这将会使波浪成为曲线, 所以它们将不会是与网格所对齐的直线.

  1. vec2 wv = 1.0 - abs(sin(position));

sin()position 定义一个类似波浪的函数. 通常 sin() 波是很圆的. 我们使用 abs() 去将其绝对化, 让它有一个尖锐波峰, 并将其约束于0-1的范围内. 然后我们再从 1.0 中减去, 将峰值放在上方.

  1. return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);

将x方向的波乘以y方向的波, 并将其提高到使峰值变得尖锐的幂. 然后从 1.0 中减去它, 使山脊成为山峰, 并提高山脊锐化的能力.

现在我们可以用 wave() 代替 height() 函数的内容.

  1. float height(vec2 position, float time) {
  2. float h = wave(position);
  3. return h;
  4. }

这样一来, 你会得到:

../../../_images/wave1.png

正弦曲线的形状太明显了. 所以让我们把波型分散一下. 我们通过缩放 位置 来实现.

  1. float height(vec2 position, float time) {
  2. float h = wave(position * 0.4);
  3. return h;
  4. }

现在它看起来好多了.

../../../_images/wave2.png

如果我们将多个波以不同的频率和幅度彼此叠加, 则可以做得更好. 这意味着我们将按比例缩放每个位置, 以使波形更细或更宽(频率). 我们将乘以波的输出, 以使它们变低或变高(振幅).

下面以四种波形为例, 说明如何将四种波形分层, 以达到更漂亮的波形效果.

  1. float height(vec2 position, float time) {
  2. float d = wave((position + time) * 0.4) * 0.3;
  3. d += wave((position - time) * 0.3) * 0.3;
  4. d += wave((position + time) * 0.5) * 0.2;
  5. d += wave((position - time) * 0.6) * 0.2;
  6. return d;
  7. }

请注意, 我们把时间加到两个上, 再从另外两个上减去. 这使得波在不同的方向上移动, 产生了复杂的效果. 还要注意, 振幅(结果乘以的数字)全部加起来是 1.0. 这使波浪保持在0-1的范围内.

有了这段代码, 你应该可以得到更复杂的波形, 而你所要做的只是增加一点数学运算!

../../../_images/wave3.png

有关空间着色器的更多信息, 请阅读 Shading Language 文档和 Spatial Shaders 文档. 也可以看看 Shading 部分3D 部分的高级教程.