Unity Shader 深入理解 LinearEyeDepth 与 DepthTexture
从透视投影的数学本质出发拆解深度缓冲区的非线性编码原理掌握 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提供的核心函数它将采样到的非线性深度值还原为观察空间中的线性距离。这意味着转换后的值可以直接用于计算世界空间位置、做距离比较、驱动雾效等。// URP / Core RP Library 中的实现 float LinearEyeDepth(float rawDepth, float4 zBufferParam) { return 1.0 / (zBufferParam.x * rawDepth zBufferParam.y); }其中zBufferParam由引擎在每帧计算zBufferParam.x (far − near) / farzBufferParam.y near / far3.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 声明纹理与采样器// URP 中推荐使用宏声明自动处理平台兼容性 TEXTURE2D_FLOAT(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture);4.2 在 Shader 中采样与转换// 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不包含透明物体若需包含透明物体深度使用_CameraDepthAttachmentURP 14。深度纹理精度大部分移动平台为 16位或24位深度格式远处精度有限避免在远平面附近做高精度深度比较。五、实战场景分析5.1 场景一水面边缘的软过渡深度差水面 Shader 的经典写法比较水面像素深度与场景深度在物体与水面交界处产生泡沫或边缘效果。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、贴花的核心技术。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确保角色后方物体不会干扰水面的折射计算// 采样场景不透明深度 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纹理 UVraw depth [0,1]从 _CameraDepthTexture 采样LinearEyeDepthrawDepth _ZBufferParams观察空间距离米世界坐标重建、真实距离计算Linear01DepthrawDepth _ZBufferParams[0,1] 线性深度深度比较、软粒子、雾效ComputeScreenPospositionCS齐次屏幕坐标计算采样 UV需除 .w_ZBufferParams—float4 (x,y,z,w)URP 自动传入的深度反算参数七、小结LinearEyeDepth的代码虽然只有一行但它背后承载的是透视投影的完整数学链路深度缓冲区存储的是非线性值——近处精度高、远处精度低这是透视投影矩阵的必然结果。LinearEyeDepth用一个有理函数的倒数将非线性深度还原为观察空间线性距离。_ZBufferParams由引擎根据near/far自动计算适配不同图形 APIOpenGL / D3D / Vulkan / Metal的深度范围约定。实战中最常见的错误——忘记透视除法、没处理 Reverse-Z 平台差异、角色背后物体干扰深度比较——都可以通过理解上述原理来避免。 延伸阅读如果你想进一步深入推荐阅读 URP 源码中的Common.hlsl、DepthOnlyPass.hlsl以及 Unity 官方的Frame Debugger来验证每个 Pass 的深度写入行为。配合 ASE 的Eye Depth节点你可以不写一行代码就完成大部分深度相关效果。