Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

高级后期处理

前言

本教程描述了一种在 Godot 中进行后期处理的高级方法。值得注意的是,它将解释如何编写使用深度缓冲区的后期处理着色器。你应该已经熟悉后期处理,特别是使用自定义后期处理教程中介绍的方法。

In the previous post-processing tutorial, we rendered the scene to a Viewport and then rendered the Viewport in a SubViewportContainer to the main scene. One limitation of this method is that we could not access the depth buffer because the depth buffer is only available in shaders and Viewports do not maintain depth information.

全屏四边形

自定义后期处理教程中,我们介绍了如何使用 Viewport 来制作自定义的后期处理特效。使用 Viewport 有两个主要的缺点:

  1. 无法访问深度缓冲区

  2. 在编辑器中看不到后期处理着色器的效果

To get around the limitation on using the depth buffer, use a MeshInstance3D with a QuadMesh primitive. This allows us to use a shader and to access the depth texture of the scene. Next, use a vertex shader to make the quad cover the screen at all times so that the post-processing effect will be applied at all times, including in the editor.

First, create a new MeshInstance3D and set its mesh to a QuadMesh. This creates a quad centered at position (0, 0, 0) with a width and height of 1. Set the width and height to 2 and enable Flip Faces. Right now, the quad occupies a position in world space at the origin. However, we want it to move with the camera so that it always covers the entire screen. To do this, we will bypass the coordinate transforms that translate the vertex positions through the difference coordinate spaces and treat the vertices as if they were already in clip space.

顶点着色器希望在裁剪空间中输出坐标,即从屏幕左侧和底部的 -1 到屏幕顶部和右侧的 1 的坐标。这就是为什么 QuadMesh 的高度和宽度需要是 2。Godot 会在幕后处理从模型到视图空间再到剪辑空间的转换,所以我们需要使 Godot 的转换效果无效。我们通过设置内置 POSITION 到我们想要的坐标来做到这一点。POSITION 会绕过内置变换,直接设置顶点坐标。

shader_type spatial;

void vertex() {
  POSITION = vec4(VERTEX, 1.0);
}

即使有了这样的顶点着色器,这个四边形仍会消失。这是因为视锥剔除的缘故,是在 CPU 上完成的。视锥剔除使用摄像机矩阵和 Mesh 的 AABB 来确定 Mesh 是否可见,然后再传递给 GPU。CPU 不知道我们对顶点做了什么,所以它认为指定的坐标指的是世界坐标,而不是裁剪空间的坐标,这就导致了 Godot 在我们旋转、离开场景中心时对四边形进行剔除。为了防止四边形被剔除,有这么几个选项:

  1. 将 QuadMesh 作为子节点添加到相机,这样相机就会始终指向它

  2. 在 QuadMesh 中将几何属性 extra_cull_margin 设置得尽可能大

第二个选项会确保四边形在编辑器中可见,而第一个选项能够保证即使摄像机移出剔除边缘也它仍可见。你也可以同时使用这两个选项。

深度纹理

To read from the depth texture, we first need to create a texture uniform set to the depth buffer by using hint_depth_texture.

uniform sampler2D depth_texture : source_color, hint_depth_texture;

Once defined, the depth texture can be read with the texture() function.

float depth = texture(depth_texture, SCREEN_UV).x;

备注

与访问屏幕纹理类似,访问深度纹理只有在从当前视口读取时才能进行。深度纹理不能从你已经渲染的另一个视口中访问。

The values returned by depth_texture are between 0.0 and 1.0 and are nonlinear. When displaying depth directly from the depth_texture, everything will look almost white unless it is very close. This is because the depth buffer stores objects closer to the camera using more bits than those further, so most of the detail in depth buffer is found close to the camera. In order to make the depth value align with world or model coordinates, we need to linearize the value. When we apply the projection matrix to the vertex position, the z value is made nonlinear, so to linearize it, we multiply it by the inverse of the projection matrix, which in Godot, is accessible with the variable INV_PROJECTION_MATRIX.

Firstly, take the screen space coordinates and transform them into normalized device coordinates (NDC). NDC run -1.0 to 1.0 in x and y directions and from 0.0 to 1.0 in the z direction when using the Vulkan backend. Reconstruct the NDC using SCREEN_UV for the x and y axis, and the depth value for z.

备注

This tutorial assumes the use of the Vulkan renderer, which uses NDCs with a Z-range of [0.0, 1.0]. In contrast, OpenGL uses NDCs with a Z-range of [-1.0, 1.0].

void fragment() {
  float depth = texture(depth_texture, SCREEN_UV).x;
  vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
}

通过将NDC乘以 INV_PROJECTION_MATRIX , 将NDC转换成视图空间. 回顾一下, 视图空间给出了相对于相机的位置, 所以 z 值将给我们提供到该点的距离.

void fragment() {
  ...
  vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  view.xyz /= view.w;
  float linear_depth = -view.z;
}

因为摄像机是朝向负的 z 方向的, 所以坐标会有一个负的 z 值. 为了得到一个可用的深度值, 我们必须否定 view.z .

The world position can be constructed from the depth buffer using the following code. Note that the INV_VIEW_MATRIX is needed to transform the position from view space into world space, so it needs to be passed to the fragment shader with a varying.

varying mat4 CAMERA;

void vertex() {
  CAMERA = INV_VIEW_MATRIX;
}

void fragment() {
  ...
  vec4 world = CAMERA * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  vec3 world_position = world.xyz / world.w;
}

优化

你可以使用单个大三角形而不是使用全屏四边形. 解释的原因在 这里 . 但是, 这种好处非常小, 只有在运行特别复杂的片段着色器时才有用.

Set the Mesh in the MeshInstance3D to an ArrayMesh. An ArrayMesh is a tool that allows you to easily construct a Mesh from Arrays for vertices, normals, colors, etc.

Now, attach a script to the MeshInstance3D and use the following code:

extends MeshInstance3D

func _ready():
  # Create a single triangle out of vertices:
  var verts = PackedVector3Array()
  verts.append(Vector3(-1.0, -1.0, 0.0))
  verts.append(Vector3(-1.0, 3.0, 0.0))
  verts.append(Vector3(3.0, -1.0, 0.0))

  # Create an array of arrays.
  # This could contain normals, colors, UVs, etc.
  var mesh_array = []
  mesh_array.resize(Mesh.ARRAY_MAX) #required size for ArrayMesh Array
  mesh_array[Mesh.ARRAY_VERTEX] = verts #position of vertex array in ArrayMesh Array

  # Create mesh from mesh_array:
  mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_array)

备注

The triangle is specified in normalized device coordinates. Recall, NDC run from -1.0 to 1.0 in both the x and y directions. This makes the screen 2 units wide and 2 units tall. In order to cover the entire screen with a single triangle, use a triangle that is 4 units wide and 4 units tall, double its height and width.

从上面分配相同的顶点着色器, 所有内容应该看起来完全相同.

The one drawback to using an ArrayMesh over using a QuadMesh is that the ArrayMesh is not visible in the editor because the triangle is not constructed until the scene is run. To get around that, construct a single triangle Mesh in a modeling program and use that in the MeshInstance3D instead.