URP 渲染实战 / 第 10 章 逐行读懂 URP Lit / 案例 10.4
案例 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); }

这组图把第 9 章的理论“摊开”在眼前:
- 直接漫反射里金球是纯黑的——金属没有漫反射;地面上可以看到橙色点光的光斑和清晰的阴影。
- 直接高光非常稀疏:两盏灯各在球上留下一个小亮点,粗糙的砖块和地面几乎没有高光。对于点状光源,镜面反射只是“灯的倒影”。
- 间接漫反射是天空 SH 给出的柔和蓝灰色,没有阴影——这正是“环境光没有方向性遮挡”的问题,需要 AO 或烘焙来弥补。
- 间接高光里金球映出了天空与地平线,红色塑料球只在边缘有淡淡的反射(F0 = 0.04,掠射时增大)。金属的“质感”几乎全部来自这一项:没有反射探针(或天空盒),金属就会发黑。

_Component = 0,调用 URP 原版 UniversalFragmentPBR;右:_Component = 5,四项分量之和加自发光。两者一致,说明拆分没有遗漏。四张分量图还可以拿来做定量判断。例如想知道“环境光占了多大比重”,把间接两项和直接两项分别输出,用 Frame Debugger 或截图取色比较同一位置的数值;想知道法线贴图对高光的影响,对比砖块在直接高光与直接漫反射中的细节——前者对法线扰动敏感得多,因为 GGX 的波瓣比余弦函数窄得多。这些判断在调光照、做材质规范时都很常用。
调试技巧URP 自带的 Rendering Debugger()的 Lighting 页也能单独关闭 GI、主光、附加光等(对应
CalculateLightingColor 里的 IsLightingFeatureEnabled),但它不区分漫反射和高光。本案例的 Shader 粒度更细,排查“为什么这个金属这么暗”“高光是灯给的还是环境给的”时很好用。在 Forward+ 下注意保留 inputData 这个变量名。