GameTech/URP 渲染实战

案例 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),省一点片元开销,自己写时逐像素算最省心。

图 23-14左:URP Lit,阴影色由天空环境光决定,本来就偏蓝灰;中:阴影色设为饱和的蓝,影子更通透;右:紫色阴影并降低环境光,画面接近插画风格。三张图的光照方向、强度完全相同

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