案例 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,让法线扰动只出现在贴花可见的地方。

与 DBuffer 的本质区别方法二让贴花本身受光,但它仍然是“盖”在原表面之上:原表面的高光、反射已经算进了背景颜色,贴花的 alpha 越小,原表面的高光透出得越多,两套光照叠在一起。DBuffer 则是在原表面计算光照之前改写它的属性,只算一次光照,所以能做出“贴花部分变得更光滑”这类效果。这也是为什么 URP 在桌面平台默认选择 DBuffer。