高级后期处理

前言

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

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

全屏四边形

custom post-processing tutorial 中, 我们介绍了如何使用视窗来制作自定义的后期处理效果. 使用视窗有两个主要缺点:

  1. 无法访问深度缓冲区

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

要解决使用深度缓冲区的限制, 请使用 MeshInstance 并使用 QuadMesh 原语. 这允许我们使用空间着色器并访问场景的深度纹理. 接下来, 使用顶点着色器使四边形始终覆盖屏幕, 以便始终应用后期处理效果, 包括在编辑器中.

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

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

  1. shader_type spatial;
  2. void vertex() {
  3. POSITION = vec4(VERTEX, 1.0);
  4. }

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

  1. 将QuadMesh作为子节点添加到相机, 因此相机始终指向它

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

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

深度纹理

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

  1. 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 的深度值.

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

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

  1. void fragment() {
  2. ...
  3. vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  4. view.xyz /= view.w;
  5. float linear_depth = -view.z;
  6. }

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

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

  1. varying mat4 CAMERA;
  2. void vertex() {
  3. CAMERA = CAMERA_MATRIX;
  4. }
  5. void fragment() {
  6. ...
  7. vec4 world = CAMERA * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
  8. vec3 world_position = world.xyz / world.w;
  9. }

优化

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

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

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

  1. extends MeshInstance
  2. func _ready():
  3. # Create a single triangle out of vertices:
  4. var verts = PoolVector3Array()
  5. verts.append(Vector3(-1.0, -1.0, 0.0))
  6. verts.append(Vector3(-1.0, 3.0, 0.0))
  7. verts.append(Vector3(3.0, -1.0, 0.0))
  8. # Create an array of arrays.
  9. # This could contain normals, colors, UVs, etc.
  10. var mesh_array = []
  11. mesh_array.resize(Mesh.ARRAY_MAX) #required size for ArrayMesh Array
  12. mesh_array[Mesh.ARRAY_VERTEX] = verts #position of vertex array in ArrayMesh Array
  13. # Create mesh from mesh_array:
  14. mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_array)

注解

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

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

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