Your first Spatial shader

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 SpatialMaterial isn't quite meeting your needs. Either way, you have decided to write your own and now you need figure out where to start.

This tutorial will explain how to write a Spatial shader and will cover more topics than the CanvasItem tutorial.

Spatial shaders have more built-in functionality than CanvasItem shaders. The expectation with spatial shaders is that Godot has already provided the functionality for common use cases and all the user needs to do in the shader is set the proper parameters. This is especially true for a PBR (physically based rendering) workflow.

This is a two-part tutorial. In this first part we are going to go through how to make a simple terrain using vertex displacement from a heightmap in the vertex function. In the second part we are going to take the concepts from this tutorial and walk through how to set up custom materials in a fragment shader by writing an ocean water shader.

注解

这个教程假定你对着色器有一些基本的了解,例如类型( vec2floatsampler2D ),和函数。如果你对这些概念摸不着头脑,那么你在完成这个教程之前,最好先从 The Book of Shaders <https://thebookofshaders.com> 获取一些基本知识。

Where to assign my material

In 3D, objects are drawn using Meshes. Meshes are a resource type that store geometry (the shape of your object) and materials (the color and how the object reacts to light) in units called "surfaces". A Mesh can have multiple surfaces, or just one. Typically, you would import a mesh from another program (e.g. Blender). But Godot also has a few PrimitiveMeshes that allow you to add basic geometry to a scene without importing Meshes.

There are multiple node types that you can use to draw a mesh. The main one is MeshInstance, but you can also use Particles, MultiMeshes (with a MultiMeshInstance), or others.

Typically, a material is associated with a given surface in a mesh, but some nodes, like MeshInstance, 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 MeshInstances 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 Meshinstance.

For this tutorial we will set our material on the mesh itself rather than taking advantage of the MeshInstance's ability to override materials.

设置

Add a new MeshInstance node to your scene.

In the inspector tab beside "Mesh" click "[empty]" and select "New PlaneMesh". Then click on the image of a plane that appears.

This adds a PlaneMesh to our scene.

Then, in the viewport, click in the upper left corner on the button that says "Perspective". A menu will appear. In the middle of the menu are options for how to display the scene. Select 'Display Wireframe'.

这将允许您查看构成平面的三角形。

../../../_images/plane.png

Now set Subdivide Width and Subdivide Depth to 32.

../../../_images/plane-sub-set.png

You can see that there are now many more triangles in the Mesh. This will give us more vertices to work with and thus allow us to add more detail.

../../../_images/plane-sub.png

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.

Now click beside "Shader" where it says "[empty]" and select "New Shader".

The shader editor should now pop up and you are ready to begin writing your first Spatial shader!

着色器魔术

../../../_images/shader-error.png

Notice how there is already error? This is because the shader editor reloads shaders on the fly. The first thing Godot shaders need is a declaration of what type of shader they are. We set the variable shader_type to spatial because this is a spatial shader.

shader_type spatial;

Next we will define the vertex() function. The vertex() function determines where the vertices of your Mesh 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() {

}

With nothing in the vertex() function, Godot will use its default vertex shader. We can easily start to make changes by adding a single line:

void vertex() {
  VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}

Adding this line, you should get an image like the one below.

../../../_images/cos.png

Okay, let's unpack this. The y value of the VERTEX is being increased. And we are passing the x and z components of the VERTEX as arguments to cos and sin; that gives us a wave-like appearance across the x and z axes.

What we want to achieve is the look of little hills; after all. cos and sin already look kind of like hills. We do so by scaling the inputs to the cos and sin functions.

void vertex() {
  VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
../../../_images/cos4.png

看起来效果好了一些,但它仍然过于尖锐和重复,我们把它变得更有趣一点。

Noise heightmap

Noise is a very popular tool for faking the look of terrain. Think of it as similar to the cosine function where you have repeating hills except, with noise, each hill has a different height.

Godot提供了:ref:噪声纹理<class_noisetexture> 资源,可以生成从着色器访问的噪声纹理。

要在着色器中访问纹理,请在着色器顶部附近,“vertex()”函数外部添加以下代码。

uniform sampler2D noise;

This will allow you to send a noise texture to the shader. Now look in the inspecter under your material. You should see a section called "Shader Params". If you open it up, you'll see a section called "noise".

点击旁边写着“[空]”的地方,选择“新建噪声纹理”。在你的噪声纹理中,点击旁边的“噪声”,然后选择“新建开放式简单噪声”。

开放式简单噪声 可从噪声纹理生成高度图。

Once you set it up and should look like this.

../../../_images/noise-set.png

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

float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
VERTEX.y += height;

Note: xyzw is the same as rgba in GLSL, so instead of texture().x above, we could use texture().r. See the OpenGL documentation for more details.

使用此代码,您可以看到纹理创建了随机外观的山峰。

../../../_images/noise.png

Right now it is too spiky, we want to soften the hills a bit. To do that, we will use a uniform. You already used a uniform above to pass in the noise texture, now let's learn how they work.

制服

Uniform variables allow you to pass data from the game into the shader. They are very useful for controlling shader effects. Uniforms can be almost any datatype that can be used in the shader. To use a uniform, you declare it in your Shader using the keyword uniform.

Let's make a uniform that changes the height of the terrain.

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_param() 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 MeshInstance
mesh.material.set_shader_param("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 MeshInstance you would access the material using get_surface_material() or material_override.

Remember that the string passed into set_shader_param() 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;

现在看起来好多了。

../../../_images/noise-low.png

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 simple animations.

与光交互

首先,关闭线框显示。若恢复显示,再次点击视窗左上方的“透视”按钮,选择“正常显示”。

../../../_images/normal.png

注意网格颜色是如何变得平滑的。这是因为它的光线是平滑的。让我们加一盏灯吧!

First, we will add an OmniLight to the scene.

../../../_images/light.png

你会看到光线影响了地形,但这看起来很奇怪。问题是光线对地形的影响就像在平面上一样。这是因为光着色器使用:ref:`网格 <class_mesh>`中的法线来计算光。

法线存储在网格中,但是我们在着色器中改变网格的形状,所以法线不再正确。为了解决这个问题,我们可以在着色器中重新计算法线,或者使用与我们的噪声相对应的法线纹理。Godot让这一切变得很简单。

您可以在顶点函数中手动计算新的法线,然后只需设置“法线”。设置“法线”后,Godot将为我们完成所有困难的光照计算。我们将在本教程的下一部分介绍这种方法,现在我们将从纹理中读取法线。

相反,我们将再次依靠噪声来计算法线。我们通过传入第二个噪声纹理来做到这一点。

uniform sampler2D normalmap;

使用另一个开放式简单噪声将第二个均匀纹理设置为另一个噪声纹理。但这一次,勾选“作为法线贴图”。

../../../_images/normal-set.png

Now, because this is a normalmap and not a per-vertex normal, we are going to assign it in the fragment() function. The fragment() function will be explained in more detail in the next part of this tutorial.

void fragment() {
}

当我们有法线对应于一个特定顶点时,我们设置“法线”,但如果你有一个来自纹理的法线贴图,要使用“法线贴图”设置法线。这样,Godot将自动处理环绕网格的纹理。

最后,为了确保我们从相同的地方读取噪声纹理和法线贴图纹理,我们要通过“VERTEX.xz”从“vertex()”函数到“fragment()”函数的位置。我们用偏差值来做。

Above the vertex() define a vec2 called tex_position. And inside the vertex() function assign VERTEX.xz to tex_position.

varying vec2 tex_position;

void vertex() {
  ...
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  ...
}

现在我们可以从“fragment()”函数中访问“顶点_位置”。

void fragment() {
  NORMALMAP = texture(normalmap, vertex_position).xyz;
}

法线就位后,光线就会对网格的高度做出动态反应。

../../../_images/normalmap.png

We can even drag the light around and the lighting will update automatically.

../../../_images/normalmap2.png

以下是本教程的完整代码。您可以看到,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() {
  NORMALMAP = texture(normalmap, tex_position).xyz;
}

这就是这部分的全部内容。希望您现在已了解Godot中顶点着色器的基本知识。在本教程的下一部分中,我们将编写一个片段函数来配合这个顶点函数,并且我们将介绍一种更高级的技术来将这个地形转换成一个移动的波浪海洋。