当前位置: 首页 > news >正文

UnityShader Linear01Depth和LinearEyeDepth函数

Unity Shader:Linear01Depth 和 LinearEyeDepth

在屏幕后处理、水面交界、软粒子、雾效和 SSAO 里,经常需要读取_CameraDepthTexture。采样出来的值一般不能直接当作场景距离使用,因为透视投影写入 Z Buffer 的深度不是线性分布的。

Unity Built-in Render Pipeline 提供了两个常用函数:

  • Linear01Depth:把原始深度转换为相对远裁剪面的线性深度;
  • LinearEyeDepth:把原始深度转换为观察空间中的正向深度。

本文对照 Unity 2022.3.62f3 的UnityCG.cgincUnityShaderVariables.cginc,重点说明这两个函数的输入、输出和使用边界。


一、先分清几种深度

讨论函数之前,先把几个容易混淆的概念分开。

1. Raw Depth

从深度纹理采样得到的原始硬件深度:

float rawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);

透视相机下,这个值通常位于 0 到 1,但它不是线性距离。Raw Depth 等于 0.5,并不表示物体位于 Near 和 Far 的正中间。

普通 Z Buffer 中:

Near → 0 Far → 1

Reversed-Z 中正好相反:

Near → 1 Far → 0

2. Linear01 Depth

Linear01Depth的结果等于观察空间深度除以远裁剪面:

Linear01Depth = EyeDepth f a r \text{Linear01Depth}=\frac{\text{EyeDepth}}{far}Linear01Depth=farEyeDepth

所以它的实际范围是:

Near → near / far Far → 1

Near 并不严格等于 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 = far

1. 普通 Z Buffer

分量数值
_ZBufferParams.x1 − f n 1-\frac{f}{n}1nf
_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=f1nf=f1n1
_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}1+nf
_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=f1+nf=n1f1
_ZBufferParams.wy f = 1 f \frac{y}{f}=\frac{1}{f}fy=f1

官网写的z = x / farw = y / far中,xy指的是同一个_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)=(1nearfar)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)=1far/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)=(far1near1)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+near1z1

普通 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)=farLinear01Depth(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 DepthLinear01DepthLinearEyeDepth
00.00030.3
0.5约 0.00059982约 0.59982
111000

Raw Depth 为 0.5 时,Eye Depth 只有约 0.6,而不是 500。这正是透视深度非线性的表现:大量深度精度集中在相机附近。

在 Reversed-Z 下,Near 和 Far 对应的 Raw Depth 会交换:

位置Raw DepthLinear01DepthLinearEyeDepth
Near10.00030.3
Far011000

输出结果没有反过来,因为_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;

再调用Linear01DepthLinearEyeDepth

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 的深度采样接口不能直接混用。
http://www.jsqmd.com/news/1302033/

相关文章:

  • 5分钟学会CartoonGAN-Test-Pytorch-Torch:简单命令行实现图片卡通化
  • 跨境电商指纹浏览器选型:AdsPower替代方案如何比较功能、成本与迁移条件
  • 众岸公考客观测评|打破地域误区:不止适配山东,全国考生均可选择 - 互联网科技品牌测评
  • 苏州买猫避坑指南!实测多家黑店,认准这家正规连锁猫舍 - 资讯报道
  • 六安热门的瓷砖店铺哪家好 - 甄选测评馆
  • 武汉尚瑞装饰:透明报价背后的整装服务体系解析 - 精彩城市
  • 计算机毕业设计之宠物用品商城的设计与实现
  • Laravel Timezone核心功能解析:自动检测用户时区的实现原理
  • 写小说卡文怎么办?2026年10款写小说软件真实测评与优缺点对比
  • Balena Etcher终极指南:3步实现高效安全的系统镜像烧录
  • zotero翻译文献每行开头有奇怪数字
  • 【单片机课程设计/毕业设计】基于 STM32 的洗衣多流程时序控制方案研究 基于单片机继电器驱动洗衣模拟装置开发(015701)
  • 从“打卡式管理”到“意图感知办公”:AI混合办公管理的范式跃迁(含2023-2024全球TOP10案例拆解)
  • 安能物流40公斤怎么收费?2026年寄大件行李避坑指南,这样寄更省钱 - 快递物流资讯
  • LLVM Pass
  • 如何轻松保存全网小说?novel-downloader完整使用指南
  • 老天奶!NAS竟然这么玩?《罪恶都市》部署在NAS中,网页直接玩
  • SwitchEmuModDownloader高级使用技巧:清除缓存、删除安装包与自定义Mod存放路径
  • 户外电源设备为什么优先选用燊桐启元导热矽胶布?
  • 第一次参加海外食品展|90%老板必踩的5个坑
  • 一站式企业管家是什么?瑞祥信息 环保+工控 双轮服务模式 - 精彩城市
  • 计算机毕业设计之宠物用品商城系统的设计与实现
  • 5步打造你的GTA5终极防护盾:YimMenu完整使用指南
  • Wu.CommTool:工业通信协议调试工具的模块化架构设计与技术实现
  • Spring Cloud Gateway 分布式 IP 限流方案
  • NestJS-Prisma开发常见问题解答:从入门到精通
  • logback-spring.xml文件的一些记录
  • 华北猎头公司测评:北京猎头公司、青岛猎头公司交付能力分析 - 榜单推荐
  • Arcanist高级配置:自定义工作流提升团队协作效率
  • 终极指南:如何用OfficeCLI实现AI驱动的Office自动化革命