GameTech/URP 渲染实战

案例 11.10调试视图:把光照拆开看入门

目标:写一个与 MyPBR 共用全部 HLSL 的调试 Shader,单独输出 BRDF 的每一项、光照的每一部分。

Assets/Cookbook/Ch11/MyPBRDebug.shader(节选:debug_frag)
half4 DebugFrag(Varyings i) : SV_Target
{
    MySurface s  = MySampleSurface(i.uv);
    InputData id = MyInitInputData(i, s.normalTS);
    MyMaterial m = MyInitMaterial(s.albedo, s.metallic, s.perceptualRoughness);
    int view = (int)_View;

    // 1~3:只看主光方向上的单项,便于和第 9 章的公式图对照
    if (view >= 1 && view <= 3)
    {
        Light L = GetMainLight();
        float3 N = id.normalWS, V = id.viewDirectionWS, H = SafeNormalize(L.direction + V);
        float NoV = abs(dot(N, V)) + 1e-5, NoL = saturate(dot(N, L.direction));
        float NoH = saturate(dot(N, H)), LoH = saturate(dot(L.direction, H));
        if (view == 1)   // 归一化的 D:除以峰值 1/(π α²),范围 0~1
            return MyD_GGX(NoH, m.a2) * PI * m.a2 * (NoL > 0);
        if (view == 2)   // G2 = V * 4 NoL NoV,范围 0~1
            return MyV_SmithGGXCorrelated(NoV, NoL, m.a2) * 4.0 * NoL * NoV;
        return half4(MyF_Schlick(m.f0, 1.0, LoH) * (NoL > 0), 1);
    }

    MyLightingTerms t = MyComputeLighting(s, m, id);
    float3 c;
    switch (view)
    {
        case 4:  c = t.directSpecular; break;
        case 5:  c = t.directDiffuse; break;
        case 6:  c = t.indirectDiffuse; break;
        case 7:  c = t.indirectSpecular; break;
        case 8:  c = t.directDiffuse + t.directSpecular; break;
        case 9:  c = t.indirectDiffuse + t.indirectSpecular; break;
        case 10: c = MySamplePrefilteredEnv(reflect(-id.viewDirectionWS, id.normalWS), m.perceptualRoughness,
                                            id.positionWS, id.normalizedScreenSpaceUV); break;
        case 11:
        {
            float NoV = saturate(dot(id.normalWS, id.viewDirectionWS));
            float2 dfg = MyEnvDFG(NoV, m.perceptualRoughness);
            c = m.f0 * dfg.x + dfg.y; break;
        }
        default: c = t.directDiffuse + t.directSpecular + t.indirectDiffuse + t.indirectSpecular + t.emission; break;
    }
    return half4(c, 1);
}
vec4 DebugFrag()
{
    MySurface s  = MySampleSurface(uv);
    InputData id = MyInitInputData(s.normalTS);
    MyMaterial m = MyInitMaterial(s.albedo, s.metallic, s.perceptualRoughness);
    int view = int(_View);

    // 1~3:只看主光方向上的单项,便于和第 9 章的公式图对照
    if (view >= 1 && view <= 3)
    {
        Light L = GetMainLight();
        vec3 N = id.normalWS, V = id.viewDirectionWS, H = SafeNormalize(L.direction + V);
        float NoV = abs(dot(N, V)) + 1e-5, NoL = saturate(dot(N, L.direction));
        float NoH = saturate(dot(N, H)), LoH = saturate(dot(L.direction, H));
        float lit = NoL > 0.0 ? 1.0 : 0.0;
        if (view == 1)   // 归一化的 D:除以峰值 1/(π α²),范围 0~1
            return vec4(MyD_GGX(NoH, m.a2) * PI * m.a2 * lit);
        if (view == 2)   // G2 = V * 4 NoL NoV,范围 0~1
            return vec4(MyV_SmithGGXCorrelated(NoV, NoL, m.a2) * 4.0 * NoL * NoV);
        return vec4(MyF_Schlick(m.f0, 1.0, LoH) * lit, 1.0);
    }

    MyLightingTerms t = MyComputeLighting(s, m, id);
    vec3 c;
    switch (view)
    {
        case 4:  c = t.directSpecular; break;
        case 5:  c = t.directDiffuse; break;
        case 6:  c = t.indirectDiffuse; break;
        case 7:  c = t.indirectSpecular; break;
        case 8:  c = t.directDiffuse + t.directSpecular; break;
        case 9:  c = t.indirectDiffuse + t.indirectSpecular; break;
        case 10: c = MySamplePrefilteredEnv(reflect(-id.viewDirectionWS, id.normalWS), m.perceptualRoughness,
                                            id.positionWS, id.normalizedScreenSpaceUV); break;
        case 11:
        {
            float NoV = saturate(dot(id.normalWS, id.viewDirectionWS));
            vec2 dfg = MyEnvDFG(NoV, m.perceptualRoughness);
            c = m.f0 * dfg.x + dfg.y; break;
        }
        default: c = t.directDiffuse + t.directSpecular + t.indirectDiffuse + t.indirectSpecular + t.emission; break;
    }
    return vec4(c, 1.0);
}

调试视图用一个 _View 浮点属性加运行时分支切换,不产生变体。前三种模式只看主光方向上的单项(图 11-3 就是用它拍的);之后的模式调用与正式 Shader 完全相同的 MyComputeLighting,只是选择输出哪一部分。这样的调试 Shader 在开发 BRDF 时非常有用:任何一项出错(例如忘了乘 π、NoL 符号反了、环境图 mip 选错),都能在对应视图里一眼看出来。_View 被故意放在 CBUFFER 之外——调试 Shader 不需要 SRP Batcher 兼容,放进去反而要同步修改 MyPBR_Input.hlsl。

图 11-14MyPBRDebug 拆分显示。从左到右:直接光(漫反射 + 镜面,含阴影);间接漫反射(SH,金球因为是金属所以全黑);间接镜面(预滤波环境 × DFG,地面砖块的亮度来自菲涅尔);四项合成的最终结果。

第三格值得多看一眼:砖墙地面是电介质,F0 只有 0.04,但远处掠射角的地面反射出了明显的天空颜色——这是菲涅尔效应,DFG 表里 B 项的贡献。很多“手写 PBR”漏掉环境镜面或者只乘 F0 而不乘 DFG,场景远处的地面就会显得“发干”。