高级后期处理
前言
本教程描述了一种在 Godot 中进行后期处理的高级方法。值得注意的是,它将解释如何编写使用深度缓冲区的后期处理着色器。您应该已经熟悉后期处理,特别是使用自定义后期处理教程中介绍的方法。
在前面的后期处理教程中,我们将场景渲染到了 Viewport 中,然后将这个 Viewport 在 ViewportContainer 中渲染到主场景。这个方法存在一个局限,我们无法访问深度缓冲区,因为深度缓冲区只在空间着色器中可用,Viewport 并不维护深度信息。
全屏四边形
在自定义后期处理教程中,我们介绍了如何使用 Viewport 来制作自定义的后期处理特效。使用 Viewport 有两个主要的缺点:
无法访问深度缓冲区
在编辑器中看不到后期处理着色器的效果
要解决使用深度缓冲区的限制,请使用 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 在我们旋转、离开场景中心时对四边形进行剔除。为了防止四边形被剔除,有这么几个选项:
将 QuadMesh 作为子节点添加到相机,这样相机就会始终指向它
在 QuadMesh 中将几何属性
extra_cull_margin
设置得尽可能大
第二个选项会确保四边形在编辑器中可见,而第一个选项能够保证即使摄像机移出剔除边缘也它仍可见。您也可以同时使用这两个选项。
深度纹理
要读取深度纹理,请使用 texture()
和 uniform 变量 DEPTH_TEXTURE
进行纹理查询。
float depth = texture(DEPTH_TEXTURE, SCREEN_UV).x;
备注
与访问屏幕纹理类似,访问深度纹理只有在从当前视区读取时才能进行。深度纹理不能从你已经渲染的另一个视区中访问。
DEPTH_TEXTURE
返回的值介于 0
和 1
之间,并且是非线性的。当直接从 DEPTH_TEXTURE
显示深度时,除非非常接近,否则一切都会看起来几乎是白色的。这是因为深度缓冲区会使用更多的位来存储更靠近相机的对象,因此深度缓冲区中的大部分细节都靠近相机。为了使深度值与世界或模型坐标对齐,我们需要将值线性化,当我们将投影矩阵应用于顶点位置时,Z 值是非线性的,所以为了将其线性化,我们将它乘以投影矩阵的逆矩阵,在 Godot 中可以用变量 INV_PROJECTION_MATRIX
访问。
首先, 取屏幕空间坐标并将其转换为归一化设备坐标(NDC).NDC从 -1
到 1
, 类似于裁剪空间坐标. 使用 SCREEN_UV
来重建NDC的 x
和 y
轴, 以及 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在 x
和 y
方向都从 -1
到 1
运行. 这使得屏幕 2
单位宽, 2
单位高. 为了用一个三角形覆盖整个屏幕, 使用一个 4
单位宽和 4
单位高的三角形, 高度和宽度加倍.
从上面分配相同的顶点着色器, 所有内容应该看起来完全相同.
使用ArrayMesh而不是使用QuadMesh的一个缺点是ArrayMesh在编辑器中不可见, 因为在运行场景之前不会构造三角形. 为了解决这个问题, 在建模程序中构建一个三角形Mesh, 然后在MeshInstance中使用它.