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 知识体系总览

下面这张图是我自己整理的深度图获取全流程。你看一眼,心里就有谱了。

深度图获取全流程 渲染管线输出 管线类型? G-Buffer深度通道 直接采样,线性空间 深度纹理采样 硬件输出,非线性 平台差异处理 线性深度转换

你看,整个流程其实不复杂。关键就三步:拿到深度 → 处理平台差异 → 转成线性。每一步都有坑,但只要你把上面的图记在脑子里,遇到问题就知道该查哪了。

好了,这一节就到这。下一节我们会聊怎么用深度图做具体的后处理效果,到时候我会拿几个实际案例出来,包括我在项目里踩过的那些坑。


专注资料整理