您的第一个空间着色器:第2部分

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

有关这些参数的完整列表,请参见:ref:`空间着色器<doc_spatial_shader>`参考文档。

A difference between the vertex function and a fragment function is that the vertex function runs per vertex and sets properties such as VERTEX (position) and NORMAL, while the fragment shader runs per pixel and, most importantly, sets the ALBEDO color of the Mesh.

第一个空间片段函数

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

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

  1. render_mode unshaded;

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

  1. render_mode diffuse_toon, specular_toon;

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

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

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

首先让我们设置水的颜色,我们通过设置“反射率”来做到这一点。

“反射率”是一种与物体颜色相关的“三维数组”。

我们把它调成蓝色。

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

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

我们把它设置为非常深的蓝色,因为大部分的蓝色的水将来自于从天空的反射。

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

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

METALLIC specifies how much like a metal the object is. It is better set close to 0 or 1. Think of METALLIC as changing the balance between the reflection and the ALBEDO color. A high METALLIC almost ignores ALBEDO altogether, and looks like a mirror of the sky. While a low METALLIC has a more equal representation of sky color and ALBEDO color.

ROUGHNESS increases from 0 to 1 from left to right while METALLIC increase from 0 to 1 from top to bottom.

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

注解

METALLIC should be close to 0 or 1 for proper PBR shading. Only set it between them for blending between materials.

Water is not a metal, so we will set its METALLIC property to 0.0. Water is also highly reflective, so we will set its ROUGHNESS property to be quite low as well.

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

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

Now we have a smooth plastic looking surface. It is time to think about some particular properties of water that we want to emulate. There are two main ones that will take this from a weird plastic surface to nice stylized water. The first is specular reflections. Specular reflections are those bright spots you see from where the sun reflects directly into your eye. The second is fresnel reflectance. Fresnel reflectance is the property of objects to become more reflective at shallow angles. It is the reason why you can see into water below you, but farther away it reflects the sky.

In order to increase the specular reflections, we will do two things. First, we will change the render mode for specular to toon because the toon render mode has larger specular highlights.

  1. render_mode specular_toon;

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

Second we will add rim lighting. Rim lighting increases the effect of light at glancing angles. Usually it is used to emulate the way light passes through fabric on the edges of an object, but we will use it here to help achieve a nice watery effect.

  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

In order to add fresnal reflectance, we will compute a fresnel term in our fragment shader. Here, we aren’t going to use a real fresnel term for performance reasons. Instead, we’ll approximate it using the dot product of the NORMAL and VIEW vectors. The NORMAL vector points away from the mesh’s surface, while the VIEW vector is the direction between your eye and that point on the surface. The dot product between them is a handy way to tell when you are looking at the surface head-on or at a glancing angle.

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

And mix it into both ROUGHNESS and ALBEDO. This is the benefit of ShaderMaterials over SpatialMaterials. With SpatialMaterials, we could set these properties with a texture, or to a flat number. But with shaders we can set them based on any mathematical function that we can dream up.

  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

And now, with only 5 lines of code, you can have complex looking water. Now that we have lighting, this water is looking too bright. Let’s darken it. This is done easily by decreasing the values of the vec3 we pass into ALBEDO. Let’s set them to vec3(0.01, 0.03, 0.05).

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

Animating with TIME

Going back to the vertex function, we can animate the waves using the built-in variable TIME.

TIME is a built-in variable that is accessible from the vertex and fragment functions.

In the last tutorial we calculated height by reading from a heightmap. For this tutorial, we will do the same. Put the heightmap code in a function called 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. }

In order to use TIME in the height() function, we need to pass it in.

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

And make sure to correctly pass it in inside the vertex function.

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

Instead of using a normalmap to calculate normals. We are going to compute them manually in the vertex() function. To do so use the following line of code.

  1. NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, k - height(pos + vec2(0.0, 0.1), TIME)));

We need to compute NORMAL manually because in the next section we will be using math to create complex-looking waves.

Now, we are going to make the height() function a little more complicated by offsetting position by the cosine of TIME.

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

This results in waves that move slowly, but not in a very natural way. The next section will dig deeper into using shaders to create more complex effects, in this case realistic waves, by adding a few more mathematical functions.

Advanced effects: waves

What makes shaders so powerful is that you can achieve complex effects by using math. To illustrate this, we are going to take our waves to the next level by modifying the height() function and by introducing a new function called wave().

wave() has one parameter, position, which is the same as it is in height().

We are going to call wave() multiple times in height() in order to fake the way waves look.

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

At first this looks complicated. So let’s go through it line-by-line.

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

Offset the position by the noise texture. This will make the waves curve, so they won’t be straight lines completely aligned with the grid.

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

Define a wave-like function using sin() and position. Normally sin() waves are very round. We use abs() to absolute to give them a sharp ridge and constrain them to the 0-1 range. And then we subtract it from 1.0 to put the peak on top.

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

Multiply the x-directional wave by the y-directional wave and raise it to a power to sharpen the peaks. Then subtract that from 1.0 so that the ridges become peaks and raise that to a power to sharpen the ridges.

We can now replace the contents of our height() function with wave().

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

Using this you get:

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

The shape of the sin wave is too obvious. So let’s spread the waves out a bit. We do this by scaling position.

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

现在它看起来好多了。

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

We can do even better if we layer multiple waves on top of each other at varying frequencies and amplitudes. What this means is that we are going to scale position for each one to make the waves thinner or wider (frequency). And we are going to multiply the output of the wave to make them shorter or taller (amplitude).

Here is an example for how you could layer the four waves to achieve nicer looking waves.

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

Note that we add time to two and subtract it from the other two. This makes the waves move in different directions creating a complex effect. Also note that the amplitudes (the number the result is multiplied by) all add up to 1.0. This keeps the wave in the 0-1 range.

With this code you should end up with more complex looking waves and all you had to do was add a bit of math!

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

For more information about Spatial shaders read the Shading Language doc and the Spatial Shaders doc. Also look at more advanced tutorials in the Shading section and the 3D sections.