GameTech/URP 渲染实战

案例 10.4把 Lit 的光照拆成四项进阶

目标:按 UniversalFragmentPBR 的流程重算一遍,但把直接漫反射、直接高光、间接漫反射、间接高光分开累加,可以单独输出任何一项;再验证四项之和与原函数完全一致。

思路:三个 Pass 的 CBUFFER 放进 SubShader 级的 HLSLINCLUDE 块,它会被拼接到每个 Pass 的最前面,比单独的 include 文件更紧凑。直接光部分抄写 LightingPhysicallyBased,只是把 brdf.diffuse 和 brdf.specular * DirectBRDFSpecular(...) 分别乘 radiance:

Assets/Cookbook/Ch10/LitBreakdown.shader(节选:direct)
// 与 LightingPhysicallyBased 相同的计算,只是把漫反射和高光分开累加
void AccumulateDirect(BRDFData brdf, Light light, InputData inputData, inout half3 diffuse, inout half3 specular)
{
    half NdotL = saturate(dot(inputData.normalWS, light.direction));
    half3 radiance = light.color * (light.distanceAttenuation * light.shadowAttenuation * NdotL);
    diffuse  += brdf.diffuse * radiance;
    specular += brdf.specular * DirectBRDFSpecular(brdf, inputData.normalWS, light.direction, inputData.viewDirectionWS) * radiance;
}
// 与 LightingPhysicallyBased 相同的计算,只是把漫反射和高光分开累加
void AccumulateDirect(BRDFData brdf, Light light, InputData inputData, inout vec3 diffuse, inout vec3 specular)
{
    float NdotL = saturate(dot(inputData.normalWS, light.direction));
    vec3 radiance = light.color * (light.distanceAttenuation * light.shadowAttenuation * NdotL);
    diffuse  += brdf.diffuse * radiance;
    specular += brdf.specular * DirectBRDFSpecular(brdf, inputData.normalWS, light.direction, inputData.viewDirectionWS) * radiance;
}

主体照着 UniversalFragmentPBR 与 GlobalIllumination 的顺序写,函数全部来自 URP:

Assets/Cookbook/Ch10/LitBreakdown.shader(节选:breakdown)
    BRDFData brdf;
    InitializeBRDFData(surfaceData, brdf);
    half4 shadowMask = CalculateShadowMask(inputData);
    AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
    Light mainLight = GetMainLight(inputData, shadowMask, aoFactor);
    MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);

    // ---- 间接光:与 GlobalIllumination() 相同,只是拆成两项 ----
    half3 reflectVector = reflect(-inputData.viewDirectionWS, inputData.normalWS);
    half NoV = saturate(dot(inputData.normalWS, inputData.viewDirectionWS));
    half fresnelTerm = Pow4(1.0 - NoV);
    half3 envSpecular = GlossyEnvironmentReflection(reflectVector, inputData.positionWS,
                            brdf.perceptualRoughness, 1.0h, inputData.normalizedScreenSpaceUV);
    half3 indirectDiffuse  = inputData.bakedGI * brdf.diffuse * aoFactor.indirectAmbientOcclusion;
    half3 indirectSpecular = envSpecular * EnvironmentBRDFSpecular(brdf, fresnelTerm) * aoFactor.indirectAmbientOcclusion;

    // ---- 直接光:主光 + 附加光 ----
    half3 directDiffuse = 0, directSpecular = 0;
    AccumulateDirect(brdf, mainLight, inputData, directDiffuse, directSpecular);

#if defined(_ADDITIONAL_LIGHTS)
    uint pixelLightCount = GetAdditionalLightsCount();
#if USE_FORWARD_PLUS
    // Forward+ 下,附加的“方向光”不进分簇列表,要单独循环
    for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
    {
        Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
        AccumulateDirect(brdf, light, inputData, directDiffuse, directSpecular);
    }
#endif
    // 注意:Forward+ 版本的 LIGHT_LOOP_BEGIN 宏内部直接引用名为 inputData 的变量
    LIGHT_LOOP_BEGIN(pixelLightCount)
        Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
        AccumulateDirect(brdf, light, inputData, directDiffuse, directSpecular);
    LIGHT_LOOP_END
#endif
BRDFData brdf;
InitializeBRDFData(surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.alpha, brdf);
vec4 shadowMask = vec4(1.0);   // CalculateShadowMask:没有 Shadowmask 时是 1
AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
Light mainLight = GetMainLight(inputData, shadowMask, aoFactor);
// MixRealtimeAndBakedGI:没有光照贴图时什么都不做

// ---- 间接光:与 GlobalIllumination() 相同,只是拆成两项 ----
vec3 reflectVector = reflect(-inputData.viewDirectionWS, inputData.normalWS);
float NoV = saturate(dot(inputData.normalWS, inputData.viewDirectionWS));
float fresnelTerm = Pow4(1.0 - NoV);
vec3 envSpecular = GlossyEnvironmentReflection(reflectVector, inputData.positionWS,
                        brdf.perceptualRoughness, 1.0);
vec3 indirectDiffuse  = inputData.bakedGI * brdf.diffuse * aoFactor.indirectAmbientOcclusion;
vec3 indirectSpecular = envSpecular * EnvironmentBRDFSpecular(brdf, fresnelTerm) * aoFactor.indirectAmbientOcclusion;

// ---- 直接光:主光 + 附加光 ----
vec3 directDiffuse = vec3(0.0), directSpecular = vec3(0.0);
AccumulateDirect(brdf, mainLight, inputData, directDiffuse, directSpecular);

// 网页版只有 Forward:附加光就是普通循环(HLSL 版的 Forward+ 分支与 LIGHT_LOOP_BEGIN 宏不需要)
int pixelLightCount = GetAdditionalLightsCount();
for (int lightIndex = 0; lightIndex < pixelLightCount; lightIndex++)
{
    Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
    AccumulateDirect(brdf, light, inputData, directDiffuse, directSpecular);
}
图 10-11从左到右:直接漫反射、直接高光、间接漫反射(SH)、间接高光(反射探针 × EnvironmentBRDF)、URP 原函数的完整结果。场景同图 10-3。

这组图把第 9 章的理论“摊开”在眼前:

图 10-12左:_Component = 0,调用 URP 原版 UniversalFragmentPBR;右:_Component = 5,四项分量之和加自发光。两者一致,说明拆分没有遗漏。

四张分量图还可以拿来做定量判断。例如想知道“环境光占了多大比重”,把间接两项和直接两项分别输出,用 Frame Debugger 或截图取色比较同一位置的数值;想知道法线贴图对高光的影响,对比砖块在直接高光与直接漫反射中的细节——前者对法线扰动敏感得多,因为 GGX 的波瓣比余弦函数窄得多。这些判断在调光照、做材质规范时都很常用。

调试技巧URP 自带的 Rendering Debugger(Window › Analysis › Rendering Debugger)的 Lighting 页也能单独关闭 GI、主光、附加光等(对应 CalculateLightingColor 里的 IsLightingFeatureEnabled),但它不区分漫反射和高光。本案例的 Shader 粒度更细,排查“为什么这个金属这么暗”“高光是灯给的还是环境给的”时很好用。在 Forward+ 下注意保留 inputData 这个变量名。