Unity Shader:Linear01Depth 和 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 Depth
Linear01Depth的结果等于观察空间深度除以远裁剪面:
Linear01Depth = EyeDepth f a r \text{Linear01Depth}=\frac{\text{EyeDepth}}{far}Linear01Depth=farEyeDepth
所以它的实际范围是:
Near → near / far Far → 1Near 并不严格等于 0。只是大多数相机的near / far很小,看起来接近 0。
3. Linear Eye Depth
LinearEyeDepth返回观察空间 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.x | 1 − f n 1-\frac{f}{n}1−nf |
_ZBufferParams.y | f n \frac{f}{n}nf |
_ZBufferParams.z | x 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.w | y 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}−1+nf |
_ZBufferParams.y | 1 11 |
_ZBufferParams.z | x 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−1+nf=n1−f1 |
_ZBufferParams.w | y 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)z+nearfar1
1. Near Plane
普通 Z Buffer 中,Near 对应z = 0:
Linear01Depth ( 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 = 1:
Linear01Depth ( 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/near+far/near}=1Linear01Depth(1)=1−far/near+far/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)z+near11
整理后:
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−z1
普通 Z Buffer 的两个边界为:
LinearEyeDepth ( 0 ) = n e a r \text{LinearEyeDepth}(0)=nearLinearEyeDepth(0)=near
LinearEyeDepth ( 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 Depth | Linear01Depth | LinearEyeDepth |
|---|---|---|
| 0 | 0.0003 | 0.3 |
| 0.5 | 约 0.00059982 | 约 0.59982 |
| 1 | 1 | 1000 |
Raw Depth 为 0.5 时,Eye Depth 只有约 0.6,而不是 500。这正是透视深度非线性的表现:大量深度精度集中在相机附近。
在 Reversed-Z 下,Near 和 Far 对应的 Raw Depth 会交换:
| 位置 | Raw Depth | Linear01Depth | LinearEyeDepth |
|---|---|---|---|
| Near | 1 | 0.0003 | 0.3 |
| Far | 0 | 1 | 1000 |
输出结果没有反过来,因为_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 ); }相机需要生成深度纹理:
Cameracamera=GetComponent<Camera>();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/HDRP
URP/HDRP 的深度纹理声明、采样和平台处理接口不同。URP 通常使用SampleSceneDepth等 SRP 函数,不应直接照搬 Built-in 的完整代码。
十二、这两个函数分别适合什么场景
Linear01Depth
适合只关心相对深度比例的效果:
- 深度可视化;
- 按远裁剪面归一化的雾效;
- 简单的深度渐变;
- 调试深度纹理。
LinearEyeDepth
适合需要场景单位的效果:
- 水面和物体交界处的 Depth Fade;
- Soft 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 的深度采样接口不能直接混用。