GameTech/URP 渲染实战

案例 10.2URP 高光 vs 标准 GGX:并排看进阶

目标:在同一个 Shader 里,用 URP 的 InitializeBRDFData + DirectBRDFSpecular 和 core 包的 D_GGX、V_SmithJointGGX、F_Schlick 分别计算主光的直接高光,用两排粗糙度递增的球对比。

Assets/Cookbook/Ch10/SpecCompare.shader(节选:frag)
half4 frag(Varyings i) : SV_Target
{
    float3 N = normalize(i.normalWS);
    float3 V = GetWorldSpaceNormalizeViewDir(i.positionWS);
    Light light = GetMainLight();
    float3 L = light.direction;
    float NoL = saturate(dot(N, L));

    // 用 URP 自己的初始化:金属度 1、albedo = F0,于是 brdf.specular = F0、brdf.diffuse = 0
    half alpha = 1;
    BRDFData brdf;
    InitializeBRDFData(_F0.rgb, 1.0, 0, _Smoothness, alpha, brdf);

    float3 spec;   // 已经乘了 π 的“Unity 约定”镜面项,乘 lightColor·NoL 即出射辐射率
    if (_Mode < 0.5)
    {
        spec = brdf.specular * DirectBRDFSpecular(brdf, N, L, V);
    }
    else
    {
        float3 H = SafeNormalize(L + V);
        float NoV = max(dot(N, V), 1e-4), NoH = saturate(dot(N, H)), VoH = saturate(dot(V, H));
        float a = brdf.roughness;                                   // α,与 URP 用同一个值
        float DV = D_GGX(NoH, a) * V_SmithJointGGX(NoL, NoV, a);    // core/BSDF.hlsl
        spec = PI * DV * F_Schlick(_F0.rgb, VoH);
    }

    float3 c = spec * light.color * NoL;
    return half4(1.0 - exp(-c * _Exposure), 1);
}
void main()
{
    vec3 N = normalize(normalWS);
    vec3 V = GetWorldSpaceNormalizeViewDir(positionWS);
    Light light = GetMainLight();
    vec3 L = light.direction;
    float NoL = saturate(dot(N, L));

    // 用 URP 自己的初始化:金属度 1、albedo = F0,于是 brdf.specular = F0、brdf.diffuse = 0
    float alpha = 1.0;
    BRDFData brdf;
    InitializeBRDFData(_F0.rgb, 1.0, vec3(0.0), _Smoothness, alpha, brdf);

    vec3 spec;   // 已经乘了 π 的“Unity 约定”镜面项,乘 lightColor·NoL 即出射辐射率
    if (_Mode < 0.5)
    {
        spec = brdf.specular * DirectBRDFSpecular(brdf, N, L, V);
    }
    else
    {
        vec3 H = SafeNormalize(L + V);
        float NoV = max(dot(N, V), 1e-4), NoH = saturate(dot(N, H)), VoH = saturate(dot(V, H));
        float a = brdf.roughness;                                   // α,与 URP 用同一个值
        float DV = D_GGX(NoH, a) * V_SmithJointGGX(NoL, NoV, a);    // core/BSDF.hlsl
        spec = PI * DV * F_Schlick(_F0.rgb, VoH);
    }

    vec3 c = spec * light.color * NoL;
    fragColor = vec4(1.0 - exp(-c * _Exposure), 1.0);
}

参照组完全使用 core 包现成的标准函数:D_GGX 带 1/π,所以结果再乘 π 与 URP 的约定对齐;V_SmithJointGGX 是 9.7 节的高度相关可见性项。两组用同一个 brdf.roughness,保证 α 相同。为了让暗淡的粗糙高光也看得见,F0 取 0.9,输出用 1−e−x 映射。

图 10-6只输出主光的直接高光。上排:URP DirectBRDFSpecular;下排:标准 GGX · Smith(高度相关)· Schlick。感知粗糙度 0.1 → 0.9(Smoothness 0.9 → 0.1)。

与图 10-5 的数字一致:前两列 URP 的亮核更大更亮;第四、五列下排更亮,而且在右下方明暗交界线附近有一圈明显的亮边——那是标准公式里 V 在掠射时增大、Schlick 的 F 增大共同造成的,而 URP 的 1/(𝐥·𝐡)2 被钳在 10 倍以内,没有这圈亮边。哪一个“更对”?从能量上看是下排(下一个案例验证);从观感上看,粗糙物体边缘这圈亮边在实际场景中常被认为“不干净”,URP 的近似反而更讨美术喜欢。这就是工程上的取舍。