案例 22.4屏幕空间贴花:深度重建与盒内裁剪进阶
目标:实现上面五步,得到一个可以贴在任意几何体上的贴花。
渲染状态
Cull Front // 画盒子的“背面”:相机钻进盒子里也不会丢 ZTest Greater // 背面比场景表面“更远”才通过 = 场景表面在盒子里(或盒子前方) ZWrite Off Blend [_SrcBlend] [_DstBlend]
为什么画背面、用 ZTest Greater?画正面看似更自然,但相机一旦进入盒子(贴花很大、或者贴在相机脚下的地面上),正面全在相机后方被裁掉,贴花瞬间消失。背面则只要盒子还在视野里就一定有片元。ZTest Greater 的意思是“盒子背面比深度缓冲里的场景表面更远时才通过”——也就是场景表面在背面的前方,它要么在盒子里、要么在盒子前面(后者会被第 4 步的范围判断丢弃)。这样盒子后面完全没有表面的像素(比如天空)连片元着色器都不会执行,省掉了不少开销。Unity 会根据平台是否使用 Reversed-Z 自动翻转比较方向,Shader 里按“Greater = 更远”写即可。
队列设为 Transparent-100:在不透明物体之后(此时深度图已经生成),在普通透明物体之前(贴花不应该出现在玻璃、粒子上面)。
重建世界坐标
// 1) 屏幕 UV → 场景深度 → 世界坐标 float2 screenUV = GetNormalizedScreenSpaceUV(i.positionCS); float rawDepth = SampleSceneDepth(screenUV); #if !UNITY_REVERSED_Z rawDepth = lerp(UNITY_NEAR_CLIP_VALUE, 1, rawDepth); // OpenGL 类平台:[0,1] → [-1,1] #endif float3 positionWS = ComputeWorldSpacePosition(screenUV, rawDepth, UNITY_MATRIX_I_VP); // 2) 世界坐标 → 贴花盒子的本地空间;盒子外的像素直接丢掉 float3 positionOS = TransformWorldToObject(positionWS); clip(0.5 - abs(positionOS));
// 1) 屏幕 UV → 场景深度 → 世界坐标 vec2 screenUV = GetNormalizedScreenSpaceUV(gl_FragCoord); float rawDepth = SampleSceneDepth(screenUV); // HLSL 版在 OpenGL 类平台要先把 [0,1] 深度换成 [-1,1];GLSL 版的 ComputeWorldSpacePosition 内部已经做了 vec3 positionWS = ComputeWorldSpacePosition(screenUV, rawDepth, UNITY_MATRIX_I_VP); // 2) 世界坐标 → 贴花盒子的本地空间;盒子外的像素直接丢掉 vec3 positionOS = TransformWorldToObject(positionWS); if (any(lessThan(0.5 - abs(positionOS), vec3(0.0)))) discard;
ComputeWorldSpacePosition(core 库 Common.hlsl)做的是透视除法的逆运算:屏幕 UV 和设备深度组成 NDC 坐标,乘以 UNITY_MATRIX_I_VP(视图投影矩阵的逆),再除以 w。需要注意的平台差异只有一处:D3D、Metal、Vulkan 的 NDC 深度范围是 [0, 1](Unity 在这些平台上还使用 Reversed-Z),OpenGL 是 [−1, 1],所以在 UNITY_REVERSED_Z 未定义时要先把采样到的 0..1 深度映射到 −1..1——这也是 URP 文档中重建世界坐标示例的写法。屏幕 UV 用 GetNormalizedScreenSpaceUV(positionCS) 从 SV_POSITION 换算,它会处理动态分辨率缩放。
得到世界坐标后,TransformWorldToObject 用当前物体(就是盒子)的逆矩阵变到本地空间,clip(0.5 − abs(positionOS)) 一行同时检查三个轴:任何一个分量的绝对值超过 0.5,结果为负,像素被丢弃。
/// 屏幕空间贴花就是一个挂着贴花材质的单位立方体:位置/旋转/缩放 = 投影盒 static GameObject SSD(Shot s, Material m, Vector3 pos, Vector3 euler, Vector3 size) { var go = s.Cube(m, pos); go.name = "Screen Space Decal"; go.transform.rotation = Quaternion.Euler(euler); go.transform.localScale = size; var r = go.GetComponent<MeshRenderer>(); r.shadowCastingMode = ShadowCastingMode.Off; // 投影盒本身不投影、不接收 r.receiveShadows = false; return go; }
在 C# 一侧,屏幕空间贴花就是一个普通的立方体:位置、旋转、缩放就是投影盒。它关闭了投射阴影和接收阴影;Shader 的 Tags 里还加了 "DisableBatching"="True"——静态/动态合批会把多个立方体合成一个网格、改写顶点坐标,物体矩阵就不再代表盒子了。GPU Instancing 则不受影响,因为每个实例仍有自己的矩阵。

depthFade),如果地面恰好位于盒子的 ±Z 端面上,贴花会完全透明——本章拍图时就踩到过:盒子中心放在 0.6 m、深度 1.2 m,地面正好在端面上,地面上的贴花全部消失。让目标表面落在盒子深度的中间部分。