Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
您的第一个 3D 着色器¶
You have decided to start writing your own custom Spatial shader. Maybe you saw a cool trick online that was done with shaders, or you have found that the StandardMaterial3D isn't quite meeting your needs. Either way, you have decided to write your own and now you need to figure out where to start.
这个教程将说明如何编写空间着色器, 并将涵盖比 CanvasItem 更多的主题.
空间着色器比CanvasItem着色器有更多的内置功能. 对空间着色器的期望是:Godot为常见的用例提供了功能, 用户仅需在着色器中设置适当的参数. 这对于PBR(基于物理的渲染)工作流来说尤其如此.
This is a two-part tutorial. In this first part we will create terrain using vertex displacement from a heightmap in the vertex function. In the second part we will take the concepts from this tutorial and set up custom materials in a fragment shader by writing an ocean water shader.
备注
这个教程假定你对着色器有一些基本的了解, 例如类型( vec2
, float
, sampler2D
), 和函数. 如果你对这些概念摸不着头脑, 那么你在完成这个教程之前, 最好先从 着色器之书 <https://thebookofshaders.com/?lan=ch> 获取一些基本知识.
在何处设定材质¶
在3D中, 对象是使用 Meshes 绘制的.Mesh是一种资源类型, 它以 "表面(surface)" 为单位存储几何体(对象的形状)和材质(对象的颜色和对光线的反应). 一个Mesh可以有多个表面, 也可以只有一个. 通常情况下, 你会从另一个程序(如Blender)导入一个Mesh. 但是Godot也有一些 PrimitiveMeshes 允许你在不导入Mesh的情况下为场景添加基本几何体.
There are multiple node types that you can use to draw a mesh. The main one is MeshInstance3D, but you can also use GPUParticles3D, MultiMeshes (with a MultiMeshInstance3D), or others.
Typically, a material is associated with a given surface in a mesh, but some nodes, like MeshInstance3D, allow you to override the material for a specific surface, or for all surfaces.
If you set a material on the surface or mesh itself, then all MeshInstance3Ds that share that mesh will share that material. However, if you want to reuse the same mesh across multiple mesh instances, but have different materials for each instance then you should set the material on the MeshInstance3D.
For this tutorial we will set our material on the mesh itself rather than taking advantage of the MeshInstance3D's ability to override materials.
设置¶
Add a new MeshInstance3D node to your scene.
在检查器选项卡中,点击“Mesh”旁边的“[空]”,然后选择“新建 PlaneMesh”。然后点击出现的平面的图像。
这会在场景中添加一个 PlaneMesh .
然后,在视图中,单击左上角的“透视”按钮。会出现一个菜单,在菜单中间找到如何显示场景的选项。选择“显示线框”。
这将允许您查看构成平面的三角形.
Now set Subdivide Width
and Subdivide Depth
of the PlaneMesh to 32
.
You can see that there are now many more triangles in the MeshInstance3D. This will give us more vertices to work with and thus allow us to add more detail.
PrimitiveMeshes, like PlaneMesh, only have one surface, so instead of an array of materials there is only one. Click beside "Material" where it says "[empty]" and select "New ShaderMaterial". Then click the sphere that appears.
现在点击“Shader”旁边写着“[空]”的地方,选择“新建 Shader”。
现在将弹出一个着色器编辑器, 你已经准备好编写你的第一个空间着色器了!
着色器魔术¶
The new shader is already generated with a shader_type
variable and the fragment()
function.
The first thing Godot shaders need is a declaration
of what type of shader they are.
In this case the shader_type
is set to spatial
because this is a spatial shader.
shader_type spatial;
For now ignore the fragment()
function
and define the vertex()
function. The vertex()
function
determines where the vertices of your MeshInstance3D appear in
the final scene. We will be using it to offset the height of each vertex and
make our flat plane appear like a little terrain.
我们像这样定义顶点着色器:
void vertex() {
}
在 vertex()
函数中没有任何内容,Godot将使用其默认的顶点着色器. 我们可以简单地通过添加一行进行更改:
void vertex() {
VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}
添加此行后, 你应该会得到类似下方的图像.
好, 我们来解读一下. VERTEX
的 y
值正在增加. 我们将 VERTEX
的 x
和 z
分量作为参数传递给 cos
和 sin
;这样就得到了在 x
和 z
轴上呈现出波浪状的图像.
我们想要实现的是小山丘的外观. 而 cos
和 sin
已经有点像山丘了. 我们便可以通过缩放 cos
和 sin
函数的输入来实现.
void vertex() {
VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
看起来效果好了一些, 但它仍然过于尖锐和重复, 让我们把它变得更有趣一点.
噪声高度图¶
噪声是一种非常流行的伪造地形的工具. 可以认为它和余弦函数一样生成重复的小山, 只是在噪声的影响下每个小山都拥有不同的高度.
Godot provides the NoiseTexture2D resource for generating a noise texture that can be accessed from a shader.
要在着色器中访问纹理,请在着色器顶部附近、vertex()
函数外部添加以下代码。
uniform sampler2D noise;
你可以用它将噪声纹理发送给着色器。现在看看检查器中的材质。你应该会看到一个名为“Shader Params”(着色器参数)的区域。如果展开该区域,就会看到一个叫“noise”的部分。
Click beside it where it says "[empty]" and select "New NoiseTexture2D". Then in your NoiseTexture2D click beside where it says "Noise" and select "New FastNoiseLite".
备注
FastNoiseLite is used by the NoiseTexture2D to generate a heightmap.
设置好后, 看起来应该像这样.
Now, access the noise texture using the texture()
function. texture()
takes a texture as the first argument and a vec2
for the position on the
texture as the second argument. We use the x
and z
channels of
VERTEX
to determine where on the texture to look up. Note that the PlaneMesh
coordinates are within the [-1,1] range (for a size of 2), while the texture
coordinates are within [0,1], so to normalize we divide by the size of the
PlaneMesh by 2.0 and add 0.5. texture()
returns a vec4
of the r, g, b,
a
channels at the position. Since the noise texture is grayscale, all of the
values are the same, so we can use any one of the channels as the height. In
this case we'll use the r
, or x
channel.
void vertex() {
float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
VERTEX.y += height;
}
注意: xyzw
和GLSL中的 rgba
是相同的, 所以我们可以用 texture().x
代替上面的 texture().r
. 详情请参见 OpenGL 文档 .
使用此代码后, 你可以看到纹理创建了随机外观的山峰.
目前它还很尖锐, 我们需要稍微柔化一下山峰. 这将用到uniform值. 你在之前已经使用了uniform 值来传递噪声纹理, 现在让我们来学习一下其中的工作原理.
Uniform¶
uniform值变量允许你把游戏的变量传递到着色器. 它们对于控制着色器效果非常有用. 几乎所有在着色器中使用的数据类型都可以作为uniform值. 要使用uniform值, 请在 Shader 中使用关键字 uniform
声明它.
让我们做一个改变地形高度的uniform.
uniform float height_scale = 0.5;
Godot lets you initialize a uniform with a value; here, height_scale
is set
to 0.5
. You can set uniforms from GDScript by calling the function
set_shader_parameter()
on the material corresponding to the shader. The value
passed from GDScript takes precedence over the value used to initialize it in
the shader.
# called from the MeshInstance3D
mesh.material.set_shader_parameter("height_scale", 0.5)
备注
Changing uniforms in Spatial-based nodes is different from
CanvasItem-based nodes. Here, we set the material inside the PlaneMesh
resource. In other mesh resources you may need to first access the
material by calling surface_get_material()
. While in the
MeshInstance3D you would access the material using
get_surface_material()
or material_override
.
Remember that the string passed into set_shader_parameter()
must match the name
of the uniform variable in the Shader. You can use the
uniform variable anywhere inside your Shader. Here, we will
use it to set the height value instead of arbitrarily multiplying by 0.5
.
VERTEX.y += height * height_scale;
现在它看起来好多了.
Using uniforms, we can even change the value every frame to animate the height of the terrain. Combined with Tweens, this can be especially useful for animations.
与光交互¶
首先关闭线框显示。再次点击视口左上角的“透视”字样,选择“显示标准”。
注意网格颜色是如何变得平滑的. 这是因为它的光线是平滑的. 让我们加一盏灯吧!
First, we will add an OmniLight3D to the scene.
你会看到光线影响了地形, 但这看起来很奇怪. 问题是光线对地形的影响就像在平面上一样. 这是因为光着色器使用 网格 中的法线来计算光.
法线存储在网格中, 但是我们在着色器中改变网格的形状, 所以法线不再正确. 为了解决这个问题, 我们可以在着色器中重新计算法线, 或者使用与我们的噪声相对应的法线纹理.Godot让这一切变得很简单.
您可以在顶点函数中手动计算新的法线,然后只需设置法线 NORMAL
。设置好 NORMAL
后,Godot 将为我们完成所有困难的光照计算。我们将在本教程的下一部分介绍这种方法,现在我们将从纹理中读取法线。
相反, 我们将再次依靠噪声来计算法线. 我们通过传入第二个噪声纹理来做到这一点.
uniform sampler2D normalmap;
Set this second uniform texture to another NoiseTexture2D with another FastNoiseLite. But this time, check As Normalmap.
现在, 因为这是一个法线贴图, 而不是每个顶点的法线, 我们将在 fragment()
函数中分配它. fragment()
函数将在本教程的下一部分中详细解释.
void fragment() {
}
When we have normals that correspond to a specific vertex we set NORMAL
, but
if you have a normalmap that comes from a texture, set the normal using
NORMAL_MAP
. This way Godot will handle the wrapping of texture around the
mesh automatically.
最后, 为了确保我们从噪声纹理和法线图纹理的相同位置读取数据, 我们将把 vertex()
函数中的 VERTEX.xz
坐标传递给 fragment()
函数. 我们用variings来做这个.
在 vertex()
上面定义一个 vec2
叫做 tex_position
. 在 vertex()
函数中, 将 VERTEX.xz
分配给 tex_position
.
varying vec2 tex_position;
void vertex() {
...
tex_position = VERTEX.xz / 2.0 + 0.5;
float height = texture(noise, tex_position).x;
...
}
现在我们可以从 fragment()
函数中访问 tex_position
.
void fragment() {
NORMAL_MAP = texture(normalmap, tex_position).xyz;
}
法线就位后, 光线就会对网格的高度做出动态反应.
我们甚至可以把灯拖来拖去, 灯光会自动更新.
以下是本教程的完整代码. 您可以看到,Godot会为您处理大多数繁琐的事情, 本教程篇幅不会太长.
shader_type spatial;
uniform float height_scale = 0.5;
uniform sampler2D noise;
uniform sampler2D normalmap;
varying vec2 tex_position;
void vertex() {
tex_position = VERTEX.xz / 2.0 + 0.5;
float height = texture(noise, tex_position).x;
VERTEX.y += height * height_scale;
}
void fragment() {
NORMAL_MAP = texture(normalmap, tex_position).xyz;
}
这就是这部分的全部内容. 希望您现在已了解Godot中顶点着色器的基本知识. 在本教程的下一部分中, 我们将编写一个片段函数来配合这个顶点函数, 并且我们将介绍一种更高级的技术来将这个地形转换成一个移动的波浪海洋.