高级后期处理

前言

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

在前面的后期处理教程中,我们将场景渲染到了 Viewport 中,然后将这个 Viewport 在 ViewportContainer 中渲染到主场景。这个方法存在一个局限,我们无法访问深度缓冲区,因为深度缓冲区只在空间着色器中可用,Viewport 并不维护深度信息。

全屏四边形

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

  1. 无法访问深度缓冲区

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

要解决使用深度缓冲区的限制,请使用 MeshInstance 并设置 QuadMesh 图元。这样我们就可以使用空间着色器,并且可以访问该场景的深度纹理。接下来,请使用顶点着色器让这个四边形始终覆盖屏幕,以便始终应用后期处理效果,包括在编辑器中。

首先,新建一个 MeshInstance,并将其网格设置为 QuadMesh。这将创建一个以坐标 (0, 0, 0) 为中心的四边形,宽度和高度均为 1。请将其宽度和高度设置为 2。现在,这个四边形在世界空间中占据了原点的位置;但是,我们希望它能随着摄像机的移动而移动,这样它就能始终覆盖整个屏幕。为此,我们将绕过坐标转换,该转换通过不同的坐标空间转换顶点位置,并将顶点视为已位于裁剪空间中。

顶点着色器希望在裁剪空间中输出坐标,即从屏幕左侧和底部的 -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 设置得尽可能大

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

深度纹理

要读取深度纹理,请使用 texture() 和 uniform 变量 DEPTH_TEXTURE 进行纹理查询。

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

备注

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

DEPTH_TEXTURE 返回的值介于 01 之间,并且是非线性的。当直接从 DEPTH_TEXTURE 显示深度时,除非非常接近,否则一切都会看起来几乎是白色的。这是因为深度缓冲区会使用更多的位来存储更靠近相机的对象,因此深度缓冲区中的大部分细节都靠近相机。为了使深度值与世界或模型坐标对齐,我们需要将值线性化,当我们将投影矩阵应用于顶点位置时,Z 值是非线性的,所以为了将其线性化,我们将它乘以投影矩阵的逆矩阵,在 Godot 中可以用变量 INV_PROJECTION_MATRIX 访问。

首先, 取屏幕空间坐标并将其转换为归一化设备坐标(NDC).NDC从 -11 , 类似于裁剪空间坐标. 使用 SCREEN_UV 来重建NDC的 xy 轴, 以及 z 的深度值.

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

通过将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 .

世界坐标可以通过以下代码从深度缓冲区构建. 注意 CAMERA_MATRIX 需要将坐标从视图空间转换到世界空间, 所以它需要以varying的方式传递给片段着色器.

varying mat4 CAMERA;

void vertex() {
  CAMERA = CAMERA_MATRIX;
}

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

优化

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

将MeshInstance中的Mesh设置为 ArrayMesh. ArrayMesh是一个工具, 允许您从顶点, 法线, 颜色等方便地从数组构造网格.

现在, 将脚本附加到MeshInstance并使用以下代码:

extends MeshInstance

func _ready():
  # Create a single triangle out of vertices:
  var verts = PoolVector3Array()
  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)

备注

三角形在标准化设备坐标中指定. 回想一下,NDC在 xy 方向都从 -11 运行. 这使得屏幕 2 单位宽, 2 单位高. 为了用一个三角形覆盖整个屏幕, 使用一个 4 单位宽和 4 单位高的三角形, 高度和宽度加倍.

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

使用ArrayMesh而不是使用QuadMesh的一个缺点是ArrayMesh在编辑器中不可见, 因为在运行场景之前不会构造三角形. 为了解决这个问题, 在建模程序中构建一个三角形Mesh, 然后在MeshInstance中使用它.