2. 深度图获取:从G-Buffer中提取深度、从深度纹理采样、线性深度与非线性深度转换、平台差异(DX/OpenGL)
好,咱们正式开始聊深度图。这一节是后处理的基础,也是很多坑的源头。我见过不少项目,后处理效果花里胡哨,结果一上深度就穿帮——说白了,就是没搞懂深度是怎么来的。
2.1 深度从哪来?两个主要渠道
在Unity或Unreal里,获取深度通常有两个路子。我个人习惯把这两个路子记清楚,因为选错了,后面全是坑。
- 从G-Buffer提取:延迟渲染管线里,G-Buffer中通常有一个专门的深度通道。你直接采样这个RT就行,精度高,而且已经是线性空间了(取决于你的编码方式)。
- 从深度纹理采样:前向渲染或者某些后处理特效里,我们用的是_CameraDepthTexture(Unity)或者SceneDepth(Unreal)。这玩意儿是硬件直接输出的,是非线性的。
嗯,这里要注意:G-Buffer里的深度和深度纹理里的深度,虽然都是深度,但含义完全不同。我在项目中遇到过有人直接把G-Buffer深度拿去当线性深度用,结果边缘闪烁得没法看。
2.2 非线性深度 vs 线性深度
为什么硬件要输出非线性深度?说白了,是为了精度。你想想看,近处的物体需要高精度,远处的可以糙一点。硬件用了一个z/w的映射,把近处的精度拉高了。
但后处理里,我们通常需要线性深度。比如做雾效、做扫描线、做视差映射——这些都需要真实的世界距离,而不是那个被压缩过的值。
核心公式(DX平台):
// 非线性深度 -> 线性深度
float linearDepth = _ProjectionParams.z * depth / (_ProjectionParams.z - depth * _ProjectionParams.y);
// 或者更常见的写法
float linearEyeDepth = LinearEyeDepth(depth); // Unity内置函数
OpenGL那边呢?它的NDC z范围是[-1, 1],而DX是[0, 1]。所以转换公式要调一下。我记得有一次帮朋友调一个跨平台的Shader,他死活想不通为什么在PC上正常,到了手机上就全黑了——其实就是这个平台差异没处理。
| 平台 | NDC Z范围 | 深度纹理值范围 | 线性化公式差异 |
|---|---|---|---|
| DirectX (PC/Xbox) | [0, 1] | [0, 1] | 标准公式 |
| OpenGL (PC/Mac/Linux) | [-1, 1] | [0, 1] (经过映射) | 需要先还原到[-1,1] |
| Vulkan | [0, 1] | [0, 1] | 同DX |
| Metal | [0, 1] | [0, 1] | 同DX |
我曾经踩过的坑:在Unreal里用CustomDepth做描边,结果在Mac上死活不生效。查了两天,发现是OpenGL的深度范围问题。Unreal虽然封装了大部分差异,但CustomDepth的采样坐标在某些平台上需要手动翻转Y轴。
2.3 平台差异:DX vs OpenGL
除了深度范围,还有一个要命的差异:纹理坐标的Y轴方向。
- DX:左上角是(0,0),向下是Y正方向
- OpenGL:左下角是(0,0),向上是Y正方向
这就导致了一个经典问题:当你从深度纹理采样时,如果直接使用屏幕坐标的UV,在OpenGL平台上会上下颠倒。Unity和Unreal内部都做了处理,但如果你自己写Compute Shader或者用Graphics.Blit,就得小心了。
我的习惯做法:在Shader开头加一个宏判断,统一处理平台差异。
#if UNITY_UV_STARTS_AT_TOP
// DX平台,UV原点在左上
float2 depthUV = screenUV;
#else
// OpenGL平台,需要翻转Y
float2 depthUV = float2(screenUV.x, 1.0 - screenUV.y);
#endif
2.4 实战:从深度纹理到世界坐标
很多后处理效果需要从深度反推世界坐标。比如做体积光、做视差遮蔽。这里我给一个我常用的模板:
// 从深度纹理重建世界坐标
float3 ReconstructWorldPos(float2 uv, float depth)
{
// 1. 获取线性深度
float linearDepth = LinearEyeDepth(depth);
// 2. 计算NDC坐标
float4 clipPos = float4(uv * 2.0 - 1.0, depth, 1.0);
#if UNITY_UV_STARTS_AT_TOP
clipPos.y = -clipPos.y;
#endif
// 3. 通过逆VP矩阵得到世界坐标
float4 worldPos = mul(_InvViewProjMatrix, clipPos);
return worldPos.xyz / worldPos.w;
}
这段代码我用了好几年,基本没出过问题。但要注意:_InvViewProjMatrix 在Unity里需要手动传递,Unreal里可以直接用 GetWorldToViewMatrix() 的逆矩阵。
2.5 知识体系总览
下面这张图是我自己整理的深度图获取全流程。你看一眼,心里就有谱了。
你看,整个流程其实不复杂。关键就三步:拿到深度 → 处理平台差异 → 转成线性。每一步都有坑,但只要你把上面的图记在脑子里,遇到问题就知道该查哪了。
好了,这一节就到这。下一节我们会聊怎么用深度图做具体的后处理效果,到时候我会拿几个实际案例出来,包括我在项目里踩过的那些坑。