URP 渲染实战 / 第 23 章 阴影进阶 / 案例 23.4
案例 23.4可着色的阴影入门
Assets/Cookbook/Ch23/ShadowColor.shader(节选:frag)
half4 frag(Varyings input) : SV_Target { half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _BaseColor.rgb; half3 normalWS = normalize(input.normalWS); // 逐像素求阴影坐标:同时兼容单级联 / 多级联 / 屏幕空间阴影三种关键字 float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS); // 带 positionWS 的重载会顺带处理“阴影距离淡出”(GetMainLightShadowFade) Light mainLight = GetMainLight(shadowCoord, input.positionWS, half4(1, 1, 1, 1)); half NdotL = saturate(dot(normalWS, mainLight.direction)); half shadow = mainLight.shadowAttenuation; // lit:0 = 完全不受光,1 = 完全受光 // _TintTerminator=1 时背光面(NdotL 小)也算“阴影”,整体风格更统一; // _TintTerminator=0 时只给投射阴影上色,明暗交界线保持 Lambert 的自然过渡 half lit = _TintTerminator > 0.5 ? NdotL * shadow : shadow; half3 shadowTint = lerp(half3(0, 0, 0), _ShadowColor.rgb, _ShadowColor.a); half3 direct = lerp(shadowTint, mainLight.color, lit); if (_TintTerminator < 0.5) direct *= NdotL; // 只染投射阴影时仍需要 N·L half3 ambient = SampleSH(normalWS) * _AmbientScale; half3 color = albedo * (direct + ambient); color = MixFog(color, input.fogFactor); return half4(color, 1); }
vec4 frag() { vec3 albedo = texture(_BaseMap, uv).rgb * _BaseColor.rgb; vec3 n = normalize(normalWS); // 逐像素求阴影坐标:同时兼容单级联 / 多级联 / 屏幕空间阴影 vec4 shadowCoord = TransformWorldToShadowCoord(positionWS); // 带 positionWS 的重载会顺带处理“阴影距离淡出”(GetMainLightShadowFade) Light mainLight = GetMainLight(shadowCoord, positionWS, vec4(1, 1, 1, 1)); float NdotL = saturate(dot(n, mainLight.direction)); float shadow = mainLight.shadowAttenuation; // lit:0 = 完全不受光,1 = 完全受光 // _TintTerminator=1 时背光面(NdotL 小)也算“阴影”,整体风格更统一; // _TintTerminator=0 时只给投射阴影上色,明暗交界线保持 Lambert 的自然过渡 float lit = _TintTerminator > 0.5 ? NdotL * shadow : shadow; vec3 shadowTint = mix(vec3(0, 0, 0), _ShadowColor.rgb, _ShadowColor.a); vec3 direct = mix(shadowTint, mainLight.color, lit); if (_TintTerminator < 0.5) direct *= NdotL; // 只染投射阴影时仍需要 N·L vec3 ambient = SampleSH(n) * _AmbientScale; vec3 color = albedo * (direct + ambient); color = MixFog(color, fogFactor); return vec4(color, 1); }
插值参数 lit 有两种取法。_TintTerminator = 1 时,lit = N·L × shadow:背光面和投射阴影统一用阴影色,适合卡通和插画风格,画面里所有“暗部”色相一致。_TintTerminator = 0 时只用 shadow 插值,再整体乘 N·L:明暗交界线保持 Lambert 的自然过渡,只有投射阴影区域换成阴影色,适合在写实画面里做轻微的色彩倾向。_ShadowColor.a 控制阴影色的强度,a = 0 时退化成普通的 Lambert。
阴影坐标这里在片元阶段用 TransformWorldToShadowCoord(positionWS) 计算,而不是在顶点阶段算好插值。原因是:多级联时相邻顶点可能落在不同级联,插值后的坐标会错;屏幕空间阴影模式下这个函数返回的是屏幕坐标,也是逐像素算才正确。URP Lit 在 _MAIN_LIGHT_SHADOWS(单级联)时会在顶点阶段算(宏 REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR),省一点片元开销,自己写时逐像素算最省心。

这个思路也可以反过来用在“阴影里的物体颜色”上:在 direct 里根据阴影值切换两套色板(受光色、阴影色),就是很多卡通渲染的“双色”做法,与第 14 章的 Ramp 思路相通。