Unity ShaderLinear01Depth 和 LinearEyeDepth在屏幕后处理、水面交界、软粒子、雾效和 SSAO 里经常需要读取_CameraDepthTexture。采样出来的值一般不能直接当作场景距离使用因为透视投影写入 Z Buffer 的深度不是线性分布的。Unity Built-in Render Pipeline 提供了两个常用函数Linear01Depth把原始深度转换为相对远裁剪面的线性深度LinearEyeDepth把原始深度转换为观察空间中的正向深度。本文对照 Unity 2022.3.62f3 的UnityCG.cginc和UnityShaderVariables.cginc重点说明这两个函数的输入、输出和使用边界。一、先分清几种深度讨论函数之前先把几个容易混淆的概念分开。1. Raw Depth从深度纹理采样得到的原始硬件深度float rawDepth SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);透视相机下这个值通常位于 0 到 1但它不是线性距离。Raw Depth 等于 0.5并不表示物体位于 Near 和 Far 的正中间。普通 Z Buffer 中Near → 0 Far → 1Reversed-Z 中正好相反Near → 1 Far → 02. Linear01 DepthLinear01Depth的结果等于观察空间深度除以远裁剪面Linear01Depth EyeDepth f a r \text{Linear01Depth}\frac{\text{EyeDepth}}{far}Linear01DepthfarEyeDepth​所以它的实际范围是Near → near / far Far → 1Near 并不严格等于 0。只是大多数相机的near / far很小看起来接近 0。3. Linear Eye DepthLinearEyeDepth返回观察空间 View Z 方向上的正数深度Near → near Far → far如果 Unity 场景中一个单位表示一米那么返回 10 可以理解为物体沿相机观察轴方向约 10 米深。它不是相机到该像素位置的欧氏距离LinearEyeDepth(rawDepth) ! distance(_WorldSpaceCameraPos, worldPos);画面边缘的点尤其要注意这个区别。二、Unity 官方函数Unity 2022.3.62f3 的UnityCG.cginc中定义为// Z buffer to linear 0..1 depth inline float Linear01Depth(float z) { return 1.0 / (_ZBufferParams.x * z _ZBufferParams.y); } // Z buffer to linear depth inline float LinearEyeDepth(float z) { return 1.0 / (_ZBufferParams.z * z _ZBufferParams.w); }这里的z应该是从深度纹理采样得到的 Raw Depth不是 Clip Space Z、View Space Z也不是已经线性化过的深度。三、_ZBufferParams的含义Unity 会根据相机的 Near、Far 和当前图形 API 自动填写_ZBufferParams。设n near f far1. 普通 Z Buffer分量数值_ZBufferParams.x1 − f n 1-\frac{f}{n}1−nf​_ZBufferParams.yf n \frac{f}{n}nf​_ZBufferParams.zx f 1 − f n f 1 f − 1 n \frac{x}{f}\frac{1-\frac{f}{n}}{f}\frac{1}{f}-\frac{1}{n}fx​f1−nf​​f1​−n1​_ZBufferParams.wy f f n f 1 n \frac{y}{f}\frac{\frac{f}{n}}{f}\frac{1}{n}fy​fnf​​n1​2. Reversed-Z在 D3D11、Metal、Vulkan 等常见平台上Unity 通常使用 Reversed-Z分量数值_ZBufferParams.x− 1 f n -1\frac{f}{n}−1nf​_ZBufferParams.y1 11_ZBufferParams.zx f − 1 f n f 1 n − 1 f \frac{x}{f}\frac{-1\frac{f}{n}}{f}\frac{1}{n}-\frac{1}{f}fx​f−1nf​​n1​−f1​_ZBufferParams.wy f 1 f \frac{y}{f}\frac{1}{f}fy​f1​官网写的z x / far和w y / far中x、y指的是同一个_ZBufferParams当前已经确定的 x、y 分量。开启 Reversed-Z 后先把 x、y 换成 Reversed-Z 的值再计算 z、w并不是继续沿用普通 Z 的 x、y。Unity 已经把普通 Z 和 Reversed-Z 的差异编码进_ZBufferParams所以把正确采样到的 Raw Depth 直接传给这两个函数即可。不要在调用前又手动执行一次1 - rawDepth否则会重复反转。官方变量说明https://docs.unity3d.com/2022.3/Documentation/Manual/SL-UnityShaderVariables.html四、Linear01Depth 的推导官方实现inline float Linear01Depth(float z) { return 1.0 / (_ZBufferParams.x * z _ZBufferParams.y); }先看普通 Z Buffer_ZBufferParams.x 1 - far / near _ZBufferParams.y far / near代入可得Linear01Depth ( z ) 1 ( 1 − f a r n e a r ) z f a r n e a r \text{Linear01Depth}(z)\frac{1}{\left(1-\frac{far}{near}\right)z\frac{far}{near}}Linear01Depth(z)(1−nearfar​)znearfar​1​1. Near Plane普通 Z Buffer 中Near 对应z 0Linear01Depth ( 0 ) 1 f a r / n e a r n e a r f a r \text{Linear01Depth}(0)\frac{1}{far/near}\frac{near}{far}Linear01Depth(0)far/near1​farnear​2. Far Plane普通 Z Buffer 中Far 对应z 1Linear01Depth ( 1 ) 1 1 − f a r / n e a r f a r / n e a r 1 \text{Linear01Depth}(1)\frac{1}{1-far/nearfar/near}1Linear01Depth(1)1−far/nearfar/near1​1因此它不是严格的[0, 1]映射而是[ n e a r f a r , 1 ] \left[\frac{near}{far},1\right][farnear​,1]3. 如果需要 Near 严格等于 0可以先得到 Eye Depth再重新归一化float eyeDepth LinearEyeDepth(rawDepth); float strictDepth01 saturate( (eyeDepth - _ProjectionParams.y) / (_ProjectionParams.z - _ProjectionParams.y) );其中_ProjectionParams.y near _ProjectionParams.z far五、LinearEyeDepth 的推导官方实现inline float LinearEyeDepth(float z) { return 1.0 / (_ZBufferParams.z * z _ZBufferParams.w); }普通 Z Buffer 中_ZBufferParams.z 1 / far - 1 / near _ZBufferParams.w 1 / near代入可得LinearEyeDepth ( z ) 1 ( 1 f a r − 1 n e a r ) z 1 n e a r \text{LinearEyeDepth}(z)\frac{1}{\left(\frac{1}{far}-\frac{1}{near}\right)z\frac{1}{near}}LinearEyeDepth(z)(far1​−near1​)znear1​1​整理后LinearEyeDepth ( z ) 1 z f a r 1 − z n e a r \text{LinearEyeDepth}(z)\frac{1}{\frac{z}{far}\frac{1-z}{near}}LinearEyeDepth(z)farz​near1−z​1​普通 Z Buffer 的两个边界为LinearEyeDepth ( 0 ) n e a r \text{LinearEyeDepth}(0)nearLinearEyeDepth(0)nearLinearEyeDepth ( 1 ) f a r \text{LinearEyeDepth}(1)farLinearEyeDepth(1)far这里返回的是观察空间中的正向深度不是“世界空间深度”。Unity 相机观察空间通常朝-Z而该函数返回的是正数深度大小。六、两个函数之间的关系因为_ZBufferParams.z _ZBufferParams.x / far _ZBufferParams.w _ZBufferParams.y / far所以LinearEyeDepth ( z ) f a r ⋅ Linear01Depth ( z ) \text{LinearEyeDepth}(z)far\cdot\text{Linear01Depth}(z)LinearEyeDepth(z)far⋅Linear01Depth(z)反过来Linear01Depth ( z ) LinearEyeDepth ( z ) f a r \text{Linear01Depth}(z)\frac{\text{LinearEyeDepth}(z)}{far}Linear01Depth(z)farLinearEyeDepth(z)​这也解释了为什么Linear01Depth在 Near Plane 返回的是near / far而不是严格的 0。七、数值例子假设相机参数为near 0.3 far 1000普通 Z Buffer 下_ZBufferParams.x ≈ -3332.333333 _ZBufferParams.y ≈ 3333.333333 _ZBufferParams.z ≈ -3.332333 _ZBufferParams.w ≈ 3.333333计算结果Raw DepthLinear01DepthLinearEyeDepth00.00030.30.5约 0.00059982约 0.59982111000Raw Depth 为 0.5 时Eye Depth 只有约 0.6而不是 500。这正是透视深度非线性的表现大量深度精度集中在相机附近。在 Reversed-Z 下Near 和 Far 对应的 Raw Depth 会交换位置Raw DepthLinear01DepthLinearEyeDepthNear10.00030.3Far011000输出结果没有反过来因为_ZBufferParams已经同步变化。八、为什么透视深度不是线性的透视投影需要让远处物体看起来更小。完成透视除法后写入深度缓冲的值大致与1 / EyeDepth相关而不是与 Eye Depth 成正比。这样做会把更多深度精度留给 Near Plane 附近相机附近Raw Depth 变化快精度高 远处区域Raw Depth 逐渐挤在一起精度低Near 设置得过小会浪费大量深度精度也更容易出现 Z-Fighting。Reversed-Z 配合浮点深度缓冲可以改善远距离精度但它不会让 Raw Depth 变成线性值。九、正交相机需要单独处理前面的倒数公式是按透视投影推导的。正交相机没有透视除法Raw Depth 本身就是线性变化的不应直接套用透视相机的解释。可以使用unity_OrthoParams.w判断相机是否为正交相机unity_OrthoParams.w 1正交相机 unity_OrthoParams.w 0透视相机正交相机的 Eye Depth 可以这样计算float OrthographicEyeDepth(float rawDepth) { #if defined(UNITY_REVERSED_Z) rawDepth 1.0 - rawDepth; #endif return lerp( _ProjectionParams.y, _ProjectionParams.z, rawDepth ); }同时支持两种相机时可以分开处理float GetEyeDepth(float rawDepth) { if (unity_OrthoParams.w 0.5) { return OrthographicEyeDepth(rawDepth); } return LinearEyeDepth(rawDepth); }十、Built-in 中的深度采样示例下面给出顶点到片元阶段的核心代码。顶点阶段用ComputeScreenPos生成屏幕坐标片元阶段用投影采样宏读取深度#include UnityCG.cginc UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture); struct v2f { float4 pos : SV_POSITION; float4 screenPos : TEXCOORD0; }; v2f vert(appdata_base v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.screenPos ComputeScreenPos(o.pos); return o; } float4 frag(v2f i) : SV_Target { float rawDepth SAMPLE_DEPTH_TEXTURE_PROJ( _CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos) ); float eyeDepth; if (unity_OrthoParams.w 0.5) { float orthoDepth rawDepth; #if defined(UNITY_REVERSED_Z) orthoDepth 1.0 - orthoDepth; #endif eyeDepth lerp( _ProjectionParams.y, _ProjectionParams.z, orthoDepth ); } else { eyeDepth LinearEyeDepth(rawDepth); } float strictDepth01 saturate( (eyeDepth - _ProjectionParams.y) / (_ProjectionParams.z - _ProjectionParams.y) ); return float4( strictDepth01, strictDepth01, strictDepth01, 1 ); }相机需要生成深度纹理CameracameraGetComponentCamera();camera.depthTextureMode|DepthTextureMode.Depth;Built-in 的_CameraDepthTexture通常只包含 Render Queue 小于或等于 2500 的不透明物体。物体使用的 Shader 还需要存在可用的ShadowCasterPass 或能从 Fallback 中取得该 Pass否则它可能不会写入相机深度纹理。十一、常见错误1. 把 Linear01Depth 当成严格的 0 到 1它实际是Near → near / far Far → 1只有在near / far很小时Near 才近似为 0。2. 把 LinearEyeDepth 写成世界空间距离它是沿 View Z 方向的深度不是distance(_WorldSpaceCameraPos, worldPos)3. 对已经线性化的深度再次线性化下面这种写法是错误的float eyeDepth LinearEyeDepth(rawDepth); float wrong LinearEyeDepth(eyeDepth);4. 认为 Raw Depth 为 0.5 就是距离中点透视相机下Raw Depth 和实际距离不是线性关系。5. 手动反转后又调用 Unity 函数对于透视相机Unity 已通过_ZBufferParams处理 Reversed-Z。不要先执行rawDepth 1.0 - rawDepth;再调用Linear01Depth或LinearEyeDepth。6. 忽略正交相机正交相机的深度本身是线性的应按 Near 和 Far 插值。7. 忘记开启深度纹理Built-in 相机需要启用camera.depthTextureMode|DepthTextureMode.Depth;8. 把 Built-in 代码直接复制到 URP/HDRPURP/HDRP 的深度纹理声明、采样和平台处理接口不同。URP 通常使用SampleSceneDepth等 SRP 函数不应直接照搬 Built-in 的完整代码。十二、这两个函数分别适合什么场景Linear01Depth适合只关心相对深度比例的效果深度可视化按远裁剪面归一化的雾效简单的深度渐变调试深度纹理。LinearEyeDepth适合需要场景单位的效果水面和物体交界处的 Depth FadeSoft Particles按米计算的雾效屏幕空间位置重建SSAO、SSR 等屏幕空间算法。十三、总结Raw Depth 是硬件深度透视相机下不是线性的Linear01Depth返回EyeDepth / far范围为[near / far, 1]LinearEyeDepth返回观察空间 View Z 方向上的正数深度LinearEyeDepth不是相机到像素的欧氏距离Unity 通过_ZBufferParams自动适配普通 Z 和 Reversed-Z透视相机不要手动反转 Raw Depth 后再调用这两个函数正交相机需要按 Near、Far 单独线性插值Built-in、URP 和 HDRP 的深度采样接口不能直接混用。