GameTech/URP 渲染实战

案例 22.6让贴花正确受光进阶

目标:贴花在阴影里应该变暗,受到点光时应该被照亮。

屏幕空间贴花画在“已经算完光照的颜色”上。如果直接输出贴图颜色(Unlit),它就像一张自发光的纸:阴影里依然鲜艳,墙角里依然明亮。解决方法有两种。

方法一:正片叠底。污渍、焦痕、脚印这类“让表面变暗”的贴花,用 Blend DstColor Zero:结果 = 背景颜色 × 贴花颜色。背景已经包含了光照和阴影,相乘后贴花自动继承了它们。这是最便宜也最稳妥的办法,但只能变暗、不能改变色相到更亮的颜色——白色喷漆就做不出来。

方法二:在贴花里重新算光照。我们已经有了世界坐标和法线,完全可以像普通物体一样组装 InputData 和 SurfaceData,调用 UniversalFragmentPBR:主光阴影用 TransformWorldToShadowCoord(positionWS) 取得,环境光用 SampleSH,附加光源在 Forward+ 下通过 normalizedScreenSpaceUV 查找分块光源列表。然后用 Alpha 混合盖在背景上。

Assets/Cookbook/Ch22/ScreenSpaceDecal.shader(节选:shade)
    float2 boxUV = positionOS.xy + 0.5;              // 盒子本地 xy → 0..1 的贴花 UV
    float2 uv = TRANSFORM_TEX(boxUV, _BaseMap);
    half4 decal = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv) * _BaseColor;
    half alpha = decal.a * angleFade * depthFade;

    half3 color = decal.rgb;
#if defined(_DECAL_LIT)
    half3 n = normalWS;
    #if defined(_NORMALMAP)
        // 贴花自己的法线贴图:切线空间就是投影器的 X/Y 轴,“上”是 -投影方向
        half3 nTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, uv), _BumpScale);
        float3 T = normalize(TransformObjectToWorldDir(float3(1, 0, 0)));
        float3 B = normalize(TransformObjectToWorldDir(float3(0, 1, 0)));
        // 把法线贴图的扰动量(xy)叠加到重建的表面法线上(UDN 式混合)
        n = normalize(n + (T * nTS.x + B * nTS.y) * decal.a);
    #endif
    InputData inputData = (InputData)0;
    inputData.positionWS = positionWS;
    inputData.normalWS = n;
    inputData.viewDirectionWS = GetWorldSpaceNormalizeViewDir(positionWS);
    inputData.shadowCoord = TransformWorldToShadowCoord(positionWS);
    inputData.bakedGI = SampleSH(n);
    inputData.normalizedScreenSpaceUV = screenUV;
    inputData.shadowMask = half4(1, 1, 1, 1);
    SurfaceData surface = (SurfaceData)0;
    surface.albedo = decal.rgb;
    surface.smoothness = _Smoothness;
    surface.occlusion = 1;
    surface.alpha = alpha;
    color = UniversalFragmentPBR(inputData, surface).rgb;
#endif
    // 正片叠底模式:alpha 折算进颜色(透明处 = 白 = 不改变背景)
    color = lerp(color, lerp(1.0, decal.rgb, alpha), _Modulate);
    return half4(color, alpha);
    vec2 boxUV = positionOS.xy + 0.5;              // 盒子本地 xy → 0..1 的贴花 UV
    vec2 uv = TransformTex(boxUV, _BaseMap_ST);
    vec4 decal = texture(_BaseMap, uv) * _BaseColor;
    float alpha = decal.a * angleFade * depthFade;

    vec3 color = decal.rgb;
#if defined(_DECAL_LIT)
    vec3 n = normalWS;
    #if defined(_NORMALMAP)
        // 贴花自己的法线贴图:切线空间就是投影器的 X/Y 轴,“上”是 -投影方向
        vec3 nTS = UnpackNormalScale(texture(_BumpMap, uv), _BumpScale);
        vec3 T = normalize(TransformObjectToWorldDir(vec3(1, 0, 0)));
        vec3 B = normalize(TransformObjectToWorldDir(vec3(0, 1, 0)));
        // 把法线贴图的扰动量(xy)叠加到重建的表面法线上(UDN 式混合)
        n = normalize(n + (T * nTS.x + B * nTS.y) * decal.a);
    #endif
    InputData inputData = InitInputData();
    inputData.positionWS = positionWS;
    inputData.normalWS = n;
    inputData.viewDirectionWS = GetWorldSpaceNormalizeViewDir(positionWS);
    inputData.shadowCoord = TransformWorldToShadowCoord(positionWS);
    inputData.bakedGI = SampleSH(n);
    inputData.normalizedScreenSpaceUV = screenUV;
    inputData.shadowMask = vec4(1, 1, 1, 1);
    SurfaceData surface = InitSurfaceData();
    surface.albedo = decal.rgb;
    surface.metallic = 0.0;
    surface.smoothness = _Smoothness;
    surface.occlusion = 1.0;
    surface.alpha = alpha;
    color = UniversalFragmentPBR(inputData, surface).rgb;
#endif
    // 正片叠底模式:alpha 折算进颜色(透明处 = 白 = 不改变背景)
    color = mix(color, mix(vec3(1.0), decal.rgb, alpha), _Modulate);
    return vec4(color, alpha);

贴花自带的法线贴图怎么用?它的“切线空间”就是投影盒的本地坐标轴:x 轴是盒子的 right、y 轴是 up。我们把法线贴图的 xy 扰动量沿这两个轴叠加到重建出的表面法线上(与第 20 章 UDN 混合同一思路),并乘上贴花自身的 alpha,让法线扰动只出现在贴花可见的地方。

图 22-8一根柱子的影子斜穿过地面贴花。左:Unlit 贴花,阴影区域里的黄色条纹和阴影外一样亮,像是浮在地面上的发光贴纸;右:在贴花里调用 UniversalFragmentPBR,条纹正确地落入阴影
与 DBuffer 的本质区别方法二让贴花本身受光,但它仍然是“盖”在原表面之上:原表面的高光、反射已经算进了背景颜色,贴花的 alpha 越小,原表面的高光透出得越多,两套光照叠在一起。DBuffer 则是在原表面计算光照之前改写它的属性,只算一次光照,所以能做出“贴花部分变得更光滑”这类效果。这也是为什么 URP 在桌面平台默认选择 DBuffer。