你的第一个 2D 着色器

前言

着色器是在 GPU 上运行,用来渲染图像的一种特殊程序。现代渲染都是通过着色器实现的。若想了解关于着色器更详细的说明,请查看着色器是什么

本教程将通过带领你逐步完成一个包含顶点函数和片段函数的着色器编写过程,着重介绍着色器程序的实用编写方法。本教程面向着色器的初学者。

备注

如果你在着色器方面有一定的经验,只是想知道着色器在 Godot 中是如何运作的,请参阅着色器参考

场景布置

CanvasItem 着色器在 Godot 中是用来绘制所有 2D 对象的,而 Spatial 着色器则用于绘制所有 3D 对象。

要使用着色器,它必须被附加到一个 Material(材质)上,而这个材质也必须附加到一个对象上。材质是一种 Resource(资源)。若要使用相同的材质绘制多个对象,该材质必须附加到每个对象上。

所有继承自 CanvasItem 的对象都有一个材质属性。包括所有的 GUI elementsSprite2DsTileMapLayersMeshInstance2Ds 等等。它们同时也可以选择性地继承其父类的材质。当你有很多节点都想使用同一个材质,这个特性就可以派上用场。

首先,创建一个 Sprite2D 节点。你可以使用任何 CanvasItem,只要它是在画布上绘制的,因此在本教程中我们将使用 Sprite2D,因为它是最易上手绘制的 CanvasItem。

在“检查器”中,点击“Texture”旁边写着“<空>”的地方然后选择“加载”,再选择“icon.svg”。这个就是新项目中的 Godot 图标。你现在应该能在视口中看到这个图标了。

接下来,在“检查器”下的 CanvasItem 部分中,在“Material”旁点击并选择“新建 ShaderMaterial”。这会创建一个新的材质资源。然后点击新出现的球体。Godot 目前还不知道你是要写 CanvasItem 着色器还是 Spatial 着色器,它显示 Spatial 着色器的输出预览,所以你看到的是默认的 Spatial 着色器的输出。

备注

继承自 Material 资源的材质,例如 StandardMaterial3DParticleProcessMaterial,可以转换为 ShaderMaterial,并且它们现有的属性将被转换为附带的文本着色器。要执行此操作,请在文件系统面板中右键单击材质并选择**转换为 ShaderMaterial**。你也可以通过在检查器中右键单击任何持有材质引用的属性来完成此操作。

点击“Shader”旁边的位置并选择“新建着色器”。最后,双击你刚刚新建的着色器,着色器编辑器将会打开。现在你已准备好开始编写第一个着色器了。

你的第一个 CanvasItem 着色器

在Godot中, 所有的着色器第一行都是指定着色器类型的, 格式如下:

shader_type canvas_item;

因为我们正在编写 CanvasItem 着色器,所以我们在第一行中指定了 canvas_item 。我们所有的代码都会在这个声明下面。

这一行告诉游戏引擎要向你提供哪些内置变量以及函数。

在 Godot 中,你可以重写三个函数来控制着色器的运作,它们是 vertex (顶点函数)、 fragment (片段函数)和 light (光照函数)。本教程会引导你写出一个包含顶点和片段函数的着色器。因为光照函数比另外两个函数要复杂得多,所以在这里不会进行讲解。

你的第一个片段函数

片段函数会为 Sprite2D 的每个像素运行,并决定该像素应显示的颜色。

它们的作用范围仅限于 Sprite2D 所覆盖的像素区域。这意味着,你无法用它来实现诸如在 Sprite2D 周围创建轮廓之类的效果。

最基础的片段函数仅将所有像素设置为单一颜色,不执行其他任何操作。

我们通过向内置变量 COLOR 写入一个 vec4 类型的值来实现此操作。 vec4 是创建包含4个数字的向量的简写。有关向量的更多信息,请参阅 向量数学教程COLOR 既是片段函数的输入变量,也是其最终输出。

void fragment(){
  COLOR = vec4(0.4, 0.6, 0.9, 1.0);
}
../../../_images/blue-box.png

恭喜你!你成功在 Godot 中写出了你的第一个着色器。

现在让我们将事情变得复杂一些。

片段函数有许多可用于计算 COLOR 的输入参数, UV 便是其中之一。UV 坐标已在你的 Sprite2D 中定义(在你不知情的情况下!),它们告诉着色器应从纹理的哪个位置为网格的每个部分读取颜色信息。

在片段函数中,你只能从 UV 读取它的值,但你可以将其用于其他函数的计算,或直接赋值给 COLOR

UV 的取值在 0 到 1 之间,从左至右、从上至下递增。

../../../_images/iconuv.png
void fragment() {
  COLOR = vec4(UV, 0.5, 1.0);
}
../../../_images/UV.png

使用内置变量 TEXTURE

默认的片段函数会读取 Sprite2D 设置的纹理并将其显示出来。

想要调整 Sprite2D 中的颜色时,你可以像下面的代码那样手动修改纹理中的颜色。

void fragment(){
  // This shader will result in a blue-tinted icon
  COLOR.b = 1.0;
}

Sprite2D 等节点存在专门的纹理变量,在着色器中可以通过 TEXTURE 访问。使用 Sprite2D 纹理时如果需要与其他颜色组合,你可以使用 UV 配合 texture 函数来访问这个变量,重绘 Sprite2D 的纹理。

void fragment(){
  COLOR = texture(TEXTURE, UV); // Read from texture again.
  COLOR.b = 1.0; //set blue channel to 1.0
}
../../../_images/blue-tex.png

Uniform 输入

Uniform 输入是用来向着色器传递数据的,这些数据在整个着色器中都是一致的。

你可以像这样通过在着色器顶部定义来使用 Uniform 值:

uniform float size;

用法的更多详情请参见着色语言文档

添加一个 Uniform 值来改变 Sprite2D 中蓝色量。

uniform float blue = 1.0; // you can assign a default value to uniforms

void fragment(){
  COLOR = texture(TEXTURE, UV); // Read from texture
  COLOR.b = blue;
}

现在你可以在编辑器中改变这个 Sprite2D 的蓝色量。回头看看“检查器”中你创建着色器的地方,你应该会看到一个叫做“Shader Parameters”的部分。展开这个部分就会看到你刚刚声明的 uniform。如果在编辑器中改变这个值,就会覆盖你在着色器中提供的默认值。

代码与着色器的交互

在代码中,你可以对该节点的材质资源使用 set_shader_parameter() 函数,从而修改 Uniform。对于 Sprite2D 节点的话,使用下面的代码就可以设置 blue 这个 Uniform。

var blue_value = 1.0
material.set_shader_parameter("blue", blue_value)

注意,uniform 的名称是一个字符串。该字符串必须与着色器中的写法完全一致,包括拼写和大小写。

你的第一个顶点函数

现在我们有了一个片段函数,我们再写一个顶点函数。

顶点函数用于计算每个顶点在屏幕上的最终位置。

顶点函数中最重要的变量是 VERTEX。该变量初始表示模型中的顶点坐标,同时你也可以通过向它赋值来确定这些顶点的最终绘制位置。VERTEX 是一个 vec2 类型的变量,其坐标初始位于局部空间(即与摄像机、视口、父节点无关)。

你可以通过直接调整 VERTEX 来偏移顶点。

void vertex() {
  VERTEX += vec2(10.0, 0.0);
}

结合内置变量 TIME 就可以制作简单的动画。

void vertex() {
  // Animate Sprite2D moving in big circle around its location
  VERTEX += vec2(cos(TIME)*100.0, sin(TIME)*100.0);
}

总结

你目前看到的就是着色器的核心部分了,也就是对 VERTEXCOLOR 的计算。你可以制定更复杂的数学策略来给这些变量赋值。

一些更高级的着色器教程可以给你启发, 如 Shadertoy着色器之书 .