URP 渲染实战 / 第 7 章 法线贴图与视差 / 案例 7.9
案例 7.9POM 自阴影高阶
目标:在案例 7.8 的基础上加入软自阴影,并把结果乘到主光的衰减上。
Assets/Cookbook/Ch07/Parallax.hlsl(节选:shadow)
// 视差自阴影:从命中点朝灯光方向往上走,检查途中有没有更高的凸起挡住光。 // 返回 1 = 不在阴影里,0 = 完全遮挡。用“挡了多少 × 离起点多近”做软化。 // dx/dy 传入原始 UV 的导数:偏移后的 UV 在台阶边缘不连续,用它求导会选错 mip。 half ParallaxSelfShadow(TEXTURE2D_PARAM(heightMap, sampler_heightMap), float2 uv, float2 dx, float2 dy, float hitDepth, half3 lightDirTS, float scale, float steps, float softness) { if (lightDirTS.z <= 0) return 0; // 灯在表面背面 if (hitDepth <= 0.001) return 1; // 已经在最高处,没有东西能挡它 float layerDepth = hitDepth / steps; // 只需要从命中深度走回表面 float2 deltaUV = lightDirTS.xy / max(lightDirTS.z, 0.05) * scale * layerDepth; float rayDepth = hitDepth; float occlusion = 0; [loop] for (int i = 1; i <= (int)steps && i <= CH07_MAX_STEPS; i++) { uv += deltaUV; rayDepth -= layerDepth; float mapDepth = CH07_DEPTH(uv); // mapDepth < rayDepth:这里的表面比光线还高 → 光线被挡 float o = (rayDepth - mapDepth) * softness * (1.0 - (i - 1) / steps); occlusion = max(occlusion, o); } return 1.0 - saturate(occlusion); }
// 视差自阴影:从命中点朝灯光方向往上走,检查途中有没有更高的凸起挡住光。 // 返回 1 = 不在阴影里,0 = 完全遮挡。用“挡了多少 × 离起点多近”做软化。 // dx/dy 传入原始 UV 的导数:偏移后的 UV 在台阶边缘不连续,用它求导会选错 mip。 float ParallaxSelfShadow(sampler2D heightMap, vec2 uv, vec2 dx, vec2 dy, float hitDepth, vec3 lightDirTS, float scale, float steps, float softness) { if (lightDirTS.z <= 0.0) return 0.0; // 灯在表面背面 if (hitDepth <= 0.001) return 1.0; // 已经在最高处,没有东西能挡它 float layerDepth = hitDepth / steps; // 只需要从命中深度走回表面 vec2 deltaUV = lightDirTS.xy / max(lightDirTS.z, 0.05) * scale * layerDepth; float rayDepth = hitDepth; float occlusion = 0.0; for (int i = 1; i <= int(steps) && i <= CH07_MAX_STEPS; i++) { uv += deltaUV; rayDepth -= layerDepth; float mapDepth = CH07_DEPTH(uv); // mapDepth < rayDepth:这里的表面比光线还高 → 光线被挡 float o = (rayDepth - mapDepth) * softness * (1.0 - float(i - 1) / steps); occlusion = max(occlusion, o); } return 1.0 - saturate(occlusion); }
实现要点:
- 光方向要转到切线空间:
mul(tbn, L),因为高度场是在切线空间定义的。片元着色器里我们复用了已经组装好的 TBN。 - 只需要从命中深度走回表面(深度 0),所以每步的深度是
hitDepth / steps,而不是固定的 1/N。命中点本身就在最高处时直接返回“无阴影”。 - 硬阴影只需判断“有没有一步钻进了表面”;这里用“钻进去多深”乘以软化系数、再按步序衰减,近处的遮挡比远处的遮挡更“黑”,得到边缘柔和的阴影,同时减轻了步进带来的条带。
- 结果乘到
Ch07Shade的extraShadow参数上,只影响主光,不影响环境光——环境光的遮挡应该交给 AO(第 8 章)。

自阴影对凹凸感的贡献往往比视差本身还大,尤其在低角度光照(清晨、傍晚、手电筒)下。它的代价是又一轮步进:每像素多 8~32 次采样。移动端可以只在近处开启,或者把步数降到 4~8 并加大软化系数。