深度图重建世界空间是从深度信息计算出对应像素的世界位置坐标,对于实现如Raymarching、阴影映射、环境遮挡和后处理效果(例如景深和运动模糊)等高级图形效果至关重要。
注意
Unreal Engine 不需要进行深度图重建世界空间,它提供的深度值本身就是世界空间下的
NDC变换法
在该方法中,需要先构建出NDC坐标,再通过VP矩阵(视图矩阵和透视矩阵)将NDC坐标变换到世界空间。

- 首先将屏幕UV转到NDC空间(映射到[-1, 1])得: $$ \text{NDC}_{XY} = 2 \cdot \text{UV}_\text{screen} - 1 $$
- 构建NDC空间下的Z值,并补全W分量: $$ \begin{align*} \text{NDC}_{Z} &= \begin{cases} \text{Depth}, & \text{DirectX} \\ 2 \cdot \text{Depth} + 1, & \text{OpenGL} \end{cases} \\[10pt] \text{NDC}_{W} &= 1.0 \end{align*} $$
- NDC空间转世界空间的齐次坐标(视图矩阵 & 透视矩阵 & 齐次除法): $$ \begin{align*} \text{Position}_\text{WSHC} &= M_{\text{View}}^{-1}\cdot M_{\text{Project}}^{-1}\cdot\text{NDC} \\[10pt] \text{Position}_{XYZ} &= \frac{(\text{Position}_\text{WSHC})_{XYZ}}{(\text{Position}_\text{WSHC})_{W}} \end{align*} $$
Unity示例如下:
half4 frag(Varyings IN) : SV_Target {
// 采样深度图
real rawDepth = SampleSceneDepth(IN.uv);
// 转为平台规范的 NDC Z
#if UNITY_REVERSED_Z
// D3D/Vulkan: Reverse-Z,rawDepth [0,1] 对应 [远, 近]
// NDC Z 需要映射到 [0,1]
float ndcZ = rawDepth;
#else
// OpenGL: 传统 Z,rawDepth [0,1] 对应 [近, 远]
// NDC Z 需要映射到 [-1,1]
float ndcZ = lerp(UNITY_NEAR_CLIP_VALUE, 1.0, rawDepth);
#endif
// 重建世界空间位置
float3 worldPos = ComputeWorldSpacePosition(IN.uv, ndcZ, UNITY_MATRIX_I_VP);
}大部分逻辑已经被封装成函数ComputeWorldSpacePosition。
远裁剪平面法(相似三角形法)

- 首先将屏幕UV转到NDC空间(映射到[-1, 1])得: $$ \text{NDC}_{XY} = 2 \cdot \text{UV}_\text{screen} - 1 $$
- 假设点在远裁剪平面上,构建NDC空间下的Z值,并补全W分量: $$ \begin{align*} \text{NDC}_{Z} &= \begin{cases} 0, & \text{DirectX} \\ 1, & \text{OpenGL} \end{cases} \\[10pt] \text{NDC}_{W} &= 1.0 \end{align*} $$
- 求该假设的远裁剪平面上的点的世界坐标: $$ \begin{align*} \text{Position}_{\text{远}_\text{WSHC}} &= M_{\text{View}}^{-1}\cdot M_{\text{Project}}^{-1}\cdot\text{NDC} \\[10pt] \text{Position}_{\text{远}_{XYZ}} &= \frac{(\text{Position}_{\text{远}_\text{WSHC}})_{XYZ}}{(\text{Position}_{\text{远}_\text{WSHC}})_{W}} \end{align*} $$
- 根据深度插值出最终坐标: $$ \text{Position} = \text{Lerp}(\text{Position}_{\text{相机}_{XYZ}}, \; \text{Position}_{\text{远}_{XYZ}}, \; \text{Linear01Depth}) $$
解释:当深度为
0时,说明最终坐标正好在相机的位置;当深度为1时,说明最终坐标在远裁剪平面上。
Unity示例如下:
Varyings vert(Attributes IN) {
Varyings OUT;
// 调用 URP 内置函数,根据 ID 计算裁剪空间坐标和 UV
OUT.positionCS = GetFullScreenTriangleVertexPosition(IN.vertexID);
OUT.uv = GetFullScreenTriangleTexCoord(IN.vertexID);
// ---- 核心:在 VS 构造射线 ----
// 获取远裁剪面的 NDC Z
#if UNITY_REVERSED_Z
// D3D/Vulkan: Reverse-Z,rawDepth [0,1] 对应 [远, 近]
float farZ = 0.0;
#else
// OpenGL: 传统 Z,rawDepth [0,1] 对应 [近, 远]
float farZ = 1.0;
#endif
/*
// 从 UV 还原 NDC 的 XY 坐标 [-1, 1]
float2 ndcXY = OUT.uv * 2.0 - 1.0;
#if UNITY_UV_STARTS_AT_TOP
ndcXY.y = -ndcXY.y;
#endif
// 构建远裁剪面上的点
float4 farClipPos = float4(ndcXY, farZ, 1.0);
// 裁剪空间转观察齐次空间
float4 farClipPosWS = mul(UNITY_MATRIX_I_VP, farClipPos);
// 顶点数越少,这里的透视除法执行得也就越少,极赚!
farClipPosWS.xyz /= farClipPosWS.w;
OUT.farClipPosWS = farClipPosWS.xyz;
*/
// 运算逻辑一样
OUT.farClipPosWS = ComputeWorldSpacePosition(OUT.uv, farZ, UNITY_MATRIX_I_VP);
return OUT;
}
half4 frag(Varyings IN) : SV_Target {
// 采样深度图
real rawDepth = SampleSceneDepth(IN.uv);
float linear01 = Linear01Depth(rawDepth, _ZBufferParams);
// 根据深度插值出最终坐标
float3 worldPos = lerp(_WorldSpaceCameraPos, IN.farClipPosWS, linear01);
}小优化
lerp(_WorldSpaceCameraPos, IN.farClipPosWS, linear01)
等价于
_WorldSpaceCameraPos + (IN.farClipPosWS - _WorldSpaceCameraPos) * linear01
故可以将其中的IN.farClipPosWS - _WorldSpaceCameraPos提取出来,在顶点着色器中计算。
两种方法的对比
这两种方法的运算逻辑实际上基本上是一样的,远裁剪平面法更像是NDC变换法的一种优化手段。
计算量对比
- 在NDC变换法中,所有的计算均在片元着色器中进行,故其计算压力与像素数量正相关;
- 而远裁剪平面法的大部分计算都在顶点着色器中进行,故其计算压力与顶点数量正相关。 深度图重建世界空间的大多数应用所使用Mesh的顶点并不多,如后处理所使用顶点就极少,在这种情况下远裁剪平面法的计算量比NDC变换法的少了不是一星半点。
精度问题
远裁剪平面法是通过计算顶点上的数据,再通过插值传到片元。在后处理场景理论上不会有什么损失,但其他场景可能会产生损失。