从透视投影的数学本质出发,拆解深度缓冲区的非线性编码原理,掌握 LinearEyeDepth 的完整变换链路与实战技巧。
一、Camera DepthTexture:它是什么
在 Unity 中,启用 DepthTextureMode.Depth 后,渲染管线会在不透明物体绘制完毕后、透明物体绘制之前,额外生成一张全屏的深度纹理------这就是 _CameraDepthTexture。它本质上是将当前帧的深度缓冲区(Depth Buffer)内容拷贝到一张可被 Shader 采样的纹理中。

💡 关键要点
半透明物体的 Shader 可以直接采样 _CameraDepthTexture ------这是水面、玻璃等效果的基础。URP 中不透明物体队列为 RenderQueue ≤ 2500,因此任何在 Transparent 队列(3000)渲染的材质都能读到底层场景的深度。
二、为什么深度是非线性的
GPU 写入深度缓冲区的值并不是世界空间中的真实距离 ,而是经过透视投影矩阵变换后的非线性值。理解这一点,是掌握 LinearEyeDepth 的前提。
2.1 透视投影的本质
透视投影将视锥体(Frustum)映射到 NDC 立方体 [-1, 1]3。在这个变换中,z 分量经历了非线性压缩:近平面附近的深度精度极高,远平面附近的精度则急剧下降。

2.2 深度缓冲区中的值
采样 _CameraDepthTexture 得到的是一个 0 到 1 的浮点数,它遵循以下公式(OpenGL / Unity 约定):
Depthbuffer = ( far · (z − near) ) / ( z · (far − near) )
其中 z 是观察空间中的深度(相机前方的实际距离),near 和 far 是裁剪面距离。这个曲线在近平面附近陡峭,在远平面附近平坦------这就是 z-fighting 更常出现在远处的根本原因。

三、LinearEyeDepth:从非线性到线性
3.1 它做了什么
LinearEyeDepth 是 URP 中 Common.hlsl 提供的核心函数,它将采样到的非线性深度值还原为观察空间中的线性距离。这意味着转换后的值可以直接用于计算世界空间位置、做距离比较、驱动雾效等。
cs
// URP / Core RP Library 中的实现
float LinearEyeDepth(float rawDepth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.x * rawDepth + zBufferParam.y);
}
其中 zBufferParam 由引擎在每帧计算:
zBufferParam.x = (far − near) / far
zBufferParam.y = near / far
3.2 数学推导
将原始非线性公式取倒数即可还原:
原始: rawDepth = far · (z − near) / (z · (far − near))
取倒数: 1 / rawDepth = z · (far − near) / (far · (z − near))
整理后得到: z = 1 / ( ((far−near)/far) · rawDepth + near/far )
这就是 LinearEyeDepth 的完整数学本质------一个有理函数的倒数。
📐 与 Linear01Depth 的区别
Linear01Depth(rawDepth, zBufferParam) 返回的是 0, 1 归一化深度(0 = near, 1 = far),适合做深度比较。
LinearEyeDepth(rawDepth, zBufferParam) 返回的是 观察空间的实际距离(米),适合做世界空间重建或距离相关的计算。
四、如何正确采样深度纹理
4.1 声明纹理与采样器
cs
// URP 中推荐使用宏声明(自动处理平台兼容性)
TEXTURE2D_FLOAT(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
4.2 在 Shader 中采样与转换
cs
// 1. 计算屏幕空间 UV
float4 screenPos = ComputeScreenPos(positionCS);
float2 screenUV = screenPos.xy / screenPos.w; // 透视除法
// 2. 采样深度纹理(R 通道即为深度值)
float rawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV);
// 3. 转换为观察空间线性深度
float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);
// 4. (可选)转换为 01 深度
float linear01 = Linear01Depth(rawDepth, _ZBufferParams);
4.3 关键注意事项
⚠️ 常见陷阱
- 记得透视除法:
ComputeScreenPos返回的是齐次坐标,必须除以.w才是正确的 UV。 - Vulkan / Metal 平台: 这些 API 下深度值已经在线性空间中(如果启用了 Reverse-Z),
_ZBufferParams会自动适配。 - Scene Depth vs Camera Depth:
_CameraDepthTexture不包含透明物体;若需包含透明物体深度,使用_CameraDepthAttachment(URP 14+)。 - **深度纹理精度:**大部分移动平台为 16位或24位深度格式,远处精度有限,避免在远平面附近做高精度深度比较。
五、实战场景分析
5.1 场景一:水面边缘的软过渡(深度差)
水面 Shader 的经典写法:比较水面像素深度与场景深度,在物体与水面交界处产生泡沫或边缘效果。

cs
float sceneEyeDepth = LinearEyeDepth(
SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV),
_ZBufferParams
);
float waterEyeDepth = screenPos.w; // 水面像素在观察空间的深度
float depthDiff = sceneEyeDepth - waterEyeDepth;
// 深度差越小 = 物体越靠近水面 = 泡沫强度越高
float foamFactor = saturate(1.0 - depthDiff / _FoamDistance);
5.2 场景二:从深度重建世界空间坐标
结合 LinearEyeDepth 与 NDC 坐标,可以精确重建每个像素的世界位置------这是屏幕空间效果(SSR、SSAO、贴花)的核心技术。
cs
float3 ReconstructWorldPos(float2 screenUV, float rawDepth)
{
// 1. 得到观察空间线性深度
float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);
// 2. 构建 NDC 坐标(xy 映射到 [-1,1])
float3 ndcPos = float3(screenUV * 2.0 - 1.0, 1.0);
// 3. NDC → 观察空间(乘以深度)
float3 viewPos = mul(unity_CameraInvProjection, float4(ndcPos, 1.0)).xyz * eyeDepth;
// 4. 观察空间 → 世界空间
float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0)).xyz;
return worldPos;
}
🔧 ASE 节点映射
如果你使用 Amplify Shader Editor,以上操作可以通过以下节点实现:
Depth Texture → Eye Depth 节点(自动调用 LinearEyeDepth)→ 配合 Screen Position 和 Inverse View Projection Matrix 重建世界坐标。
5.3 场景三:水下角色渲染修正
这是一个经典的深度排序问题:当角色半身浸入水中时,水面 Shader 需要区分"水面到场景"和"水面到角色"的深度差,避免在不正确的深度层产生扭曲。

关键修正逻辑------在深度比较时取 min,确保角色后方物体不会干扰水面的折射计算:
cs
// 采样场景不透明深度
float rawSceneDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV);
float sceneEyeDepth = LinearEyeDepth(rawSceneDepth, _ZBufferParams);
// 水面像素自身的观察空间深度
float waterEyeDepth = screenPos.w;
// 关键修正:取水面和场景深度中的较近者
// 避免角色背后的冰块等物体产生错误扭曲
float effectiveSceneDepth = min(sceneEyeDepth, waterEyeDepth);
// 正确的水面-场景深度差
float correctedDiff = effectiveSceneDepth - waterEyeDepth;
// 若差值很小或为负 → 有物体在水面附近或上方 → 减少扭曲
float distortionMask = saturate(correctedDiff / _MaxDistortionDepth);
六、函数速查表
| 函数 / 宏 | 输入 | 输出 | 用途 |
|---|---|---|---|
SAMPLE_DEPTH_TEXTURE |
纹理 + UV | raw depth 0,1 | 从 _CameraDepthTexture 采样 |
LinearEyeDepth |
rawDepth + _ZBufferParams | 观察空间距离(米) | 世界坐标重建、真实距离计算 |
Linear01Depth |
rawDepth + _ZBufferParams | 0,1 线性深度 | 深度比较、软粒子、雾效 |
ComputeScreenPos |
positionCS | 齐次屏幕坐标 | 计算采样 UV(需除 .w) |
_ZBufferParams |
--- | float4 (x,y,z,w) | URP 自动传入的深度反算参数 |
七、小结
LinearEyeDepth 的代码虽然只有一行,但它背后承载的是透视投影的完整数学链路:
- 深度缓冲区存储的是非线性值------近处精度高、远处精度低,这是透视投影矩阵的必然结果。
LinearEyeDepth用一个有理函数的倒数将非线性深度还原为观察空间线性距离。_ZBufferParams由引擎根据near/far自动计算,适配不同图形 API(OpenGL / D3D / Vulkan / Metal)的深度范围约定。- 实战中最常见的错误------忘记透视除法、没处理 Reverse-Z 平台差异、角色背后物体干扰深度比较------都可以通过理解上述原理来避免。
📚 延伸阅读
如果你想进一步深入,推荐阅读 URP 源码中的 Common.hlsl、DepthOnlyPass.hlsl,以及 Unity 官方的 Frame Debugger 来验证每个 Pass 的深度写入行为。配合 ASE 的 Eye Depth 节点,你可以不写一行代码就完成大部分深度相关效果。