GameTech/URP 渲染实战

案例 6.6级联阴影可视化进阶

目标:写一个调试 Shader,按像素所在的级联染色,直观检查级联划分;再对比 1 个和 4 个级联的阴影精度。

Assets/Cookbook/Ch06/CascadeDebug.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);

    Light mainLight = GetMainLight(TransformWorldToShadowCoord(input.positionWS));
    half3 color = albedo * mainLight.color * mainLight.shadowAttenuation * saturate(dot(normalWS, mainLight.direction));
    color += albedo * SampleSH(normalWS) * 0.4;

    // ComputeCascadeIndex:依次判断像素是否落在 4 个“级联包围球”里,返回第一个命中的序号;
    // 4 表示超出了所有级联(阴影距离之外)
#if defined(_MAIN_LIGHT_SHADOWS_CASCADE)
    half cascade = ComputeCascadeIndex(input.positionWS);
#else
    half cascade = 0;   // 只有一个级联时 URP 不会更新包围球数据,直接当作第 0 级
#endif
    const half3 kCascadeColors[5] =
    {
        half3(1.0, 0.25, 0.2), half3(0.25, 0.85, 0.3), half3(0.25, 0.45, 1.0), half3(1.0, 0.85, 0.2), half3(1, 1, 1)
    };
    half3 tint = kCascadeColors[(uint)cascade];
    color = lerp(color, color * tint * 1.6, _TintStrength);
    return half4(color, 1);
}
void main()
{
    vec3 albedo = texture(_BaseMap, uv).rgb * _BaseColor.rgb;
    vec3 N = normalize(normalWS);

    Light mainLight = GetMainLight(TransformWorldToShadowCoord(positionWS));
    vec3 color = albedo * mainLight.color * mainLight.shadowAttenuation * saturate(dot(N, mainLight.direction));
    color += albedo * SampleSH(N) * 0.4;

    // ComputeCascadeIndex:依次判断像素是否落在 4 个“级联包围球”里,返回第一个命中的序号;
    // 4 表示超出了所有级联(阴影距离之外)。
    // HLSL 版用 _MAIN_LIGHT_SHADOWS_CASCADE 关键字区分;GLSL 版里它是 uniform:_MainLightShadowsCascade > 0.5
    float cascade = 0.0;   // 只有一个级联时 URP 不会更新包围球数据,直接当作第 0 级
    if (_MainLightShadowsCascade > 0.5) cascade = ComputeCascadeIndex(positionWS);

    const vec3 kCascadeColors[5] = vec3[5](
        vec3(1.0, 0.25, 0.2), vec3(0.25, 0.85, 0.3), vec3(0.25, 0.45, 1.0), vec3(1.0, 0.85, 0.2), vec3(1, 1, 1)
    );
    vec3 tint = kCascadeColors[int(cascade)];
    color = mix(color, color * tint * 1.6, _TintStrength);
    fragColor = vec4(color, 1.0);
}

ComputeCascadeIndex 是 Shadows.hlsl 中的公开函数。只有一个级联时,URP 不会更新包围球数据,这时读出来的可能是残留值,所以要用 _MAIN_LIGHT_SHADOWS_CASCADE 关键字保护。返回值 4 表示超出了所有级联,用白色表示。

图 6-17阴影距离 60 米,分别使用 1、2、4 个级联(4 级联的分割为 10%、25%、50%)。红、绿、蓝、黄依次为级联 0~3。可以看到级联边界是以相机为中心的圆弧,这正是“包围球”的形状
图 6-18阴影距离 80 米、阴影图 1024、硬阴影,镜头靠近前排立方体。左:1 个级联,一张 1024 的图要覆盖 80 米,近处的影子边缘模糊、呈块状;右:4 个级联,第一级只覆盖前几米,虽然每块只有 512,近处阴影依然清晰
调参建议级联划分的原则是让每一级在屏幕上的“texel 密度”接近。透视投影下远处缩小得很快,所以分割点应该向近处集中(默认的 6.7%、20%、46.7% 就是这个思路)。把 Shadow Distance 设得刚好覆盖需要阴影的范围,往往比加级联更有效:阴影距离减半,所有级联的精度都翻倍。移动端常用 1~2 个级联,配合较短的阴影距离。