GameTech/URP 渲染实战

案例 9.1BRDF 单项可视化 Shader进阶

目标:写一个可以单独输出 D、G、F、镜面项、漫反射项或完整直接光照的 Shader,用一排粗糙度递增的球把每一项“拍”出来。它是本章后面所有案例的工具。

思路:BRDF 的各项写成独立的 HLSL 函数,全部保留教科书形式(带 π、不合并常数),放在 BRDFLib.hlsl;Shader 本身只取主光方向,算出 𝐧·𝐥、𝐧·𝐡 等点积,再按 _Mode 选择输出哪一项。为了避免和 core 包 BSDF.hlsl 里已有的 D_GGX、F_Schlick 重名(Lighting.hlsl 会间接 include 它),函数名统一加 C9_ 前缀。

先看 D 的三种实现:

Assets/Cookbook/Ch09/BRDFLib.hlsl(节选:ndf)
// ---------- 法线分布函数 D(h) ----------
// GGX / Trowbridge-Reitz:D = α² / (π · ((n·h)²(α²-1) + 1)²)
float C9_D_GGX(float NoH, float a)
{
    float a2 = a * a;
    float d = NoH * NoH * (a2 - 1.0) + 1.0;
    return a2 / (PI * d * d);
}

// Beckmann:D = exp(-tan²θh / α²) / (π α² cos⁴θh)
float C9_D_Beckmann(float NoH, float a)
{
    float a2 = a * a;
    float c2 = max(NoH * NoH, 1e-6);
    float t2 = (1.0 - c2) / c2;                 // tan²θh
    return exp(-t2 / a2) / (PI * a2 * c2 * c2);
}

// 归一化的 Blinn-Phong NDF:D = (n+2)/(2π) · cosⁿθh,指数 n 由 α 换算:n = 2/α² - 2
float C9_D_BlinnPhong(float NoH, float a)
{
    float n = 2.0 / max(a * a, 1e-4) - 2.0;
    return (n + 2.0) / (2.0 * PI) * pow(max(NoH, 0.0), n);
}
// ---------- 法线分布函数 D(h) ----------
// GGX / Trowbridge-Reitz:D = α² / (π · ((n·h)²(α²-1) + 1)²)
float C9_D_GGX(float NoH, float a)
{
    float a2 = a * a;
    float d = NoH * NoH * (a2 - 1.0) + 1.0;
    return a2 / (PI * d * d);
}

// Beckmann:D = exp(-tan²θh / α²) / (π α² cos⁴θh)
float C9_D_Beckmann(float NoH, float a)
{
    float a2 = a * a;
    float c2 = max(NoH * NoH, 1e-6);
    float t2 = (1.0 - c2) / c2;                 // tan²θh
    return exp(-t2 / a2) / (PI * a2 * c2 * c2);
}

// 归一化的 Blinn-Phong NDF:D = (n+2)/(2π) · cosⁿθh,指数 n 由 α 换算:n = 2/α² - 2
float C9_D_BlinnPhong(float NoH, float a)
{
    float n = 2.0 / max(a * a, 1e-4) - 2.0;
    return (n + 2.0) / (2.0 * PI) * pow(max(NoH, 0.0), n);
}

然后是 Shader 主体。_F0 特意声明成 Vector 而不是 Color:在线性色彩空间下,Unity 会把材质的 Color 属性当成 sRGB 值,传进 Shader 前转换成线性值,而 F0 的参考数据(0.04、0.02 等)本身就是线性的,用 Vector 可以原样传入。_Albedo 是美术习惯在拾色器里挑的颜色,保留 Color 类型。

Assets/Cookbook/Ch09/BRDFTerms.shader
// 第 9 章:BRDF 单项可视化。
// 用一个 _Mode 参数切换输出 D、G、F、镜面项、漫反射项或完整的直接光照,
// 配合一排粗糙度递增的小球,就能“看见”每一项随粗糙度怎么变。
Shader "Cookbook/Ch09/BRDFTerms"
{
    Properties
    {
        [Enum(D,0,G,1,F_NoV,2,F_VoH,3,Specular,4,Diffuse,5,Full,6)] _Mode("Output", Float) = 6
        [Enum(GGX,0,Beckmann,1,BlinnPhong,2)] _NDF("NDF", Float) = 0
        [Enum(SmithSeparable,0,SmithHeightCorrelated,1,SchlickGGX,2,DiffX10,3)] _GModel("Geometry", Float) = 1
        [Enum(Lambert,0,Disney,1)] _DiffuseModel("Diffuse", Float) = 0
        _Roughness("Perceptual Roughness", Range(0.02, 1)) = 0.5
        _F0("F0 (linear)", Color) = (0.04, 0.04, 0.04, 1)
        _Albedo("Diffuse Albedo", Color) = (0.8, 0.8, 0.8, 1)
        [Enum(Raw,0,Exp,1)] _Display("Display", Float) = 0
        _Exposure("Exposure", Float) = 1
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Assets/Cookbook/Ch09/BRDFLib.hlsl"

            CBUFFER_START(UnityPerMaterial)
                float _Mode, _NDF, _GModel, _DiffuseModel;
                float _Roughness;
                float4 _F0, _Albedo;
                float _Display, _Exposure;
            CBUFFER_END

            struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; };
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float3 positionWS : TEXCOORD0;
                float3 normalWS   : TEXCOORD1;
            };

            Varyings vert(Attributes v)
            {
                Varyings o;
                VertexPositionInputs p = GetVertexPositionInputs(v.positionOS.xyz);
                o.positionCS = p.positionCS;
                o.positionWS = p.positionWS;
                o.normalWS = TransformObjectToWorldNormal(v.normalOS);
                return o;
            }

            float EvalD(float NoH, float a)
            {
                if (_NDF < 0.5) return C9_D_GGX(NoH, a);
                if (_NDF < 1.5) return C9_D_Beckmann(NoH, a);
                return C9_D_BlinnPhong(NoH, a);
            }

            float EvalG(float NoL, float NoV, float a)
            {
                if (_GModel < 0.5) return C9_G_SmithSeparable(NoL, NoV, a);
                if (_GModel < 1.5) return C9_G_SmithHeightCorrelated(NoL, NoV, a);
                if (_GModel < 2.5) return C9_G_SchlickGGX(NoL, NoV, a);
                // 两种 Smith 形式之差放大 10 倍,看它们在哪儿不一样
                return abs(C9_G_SmithHeightCorrelated(NoL, NoV, a) - C9_G_SmithSeparable(NoL, NoV, a)) * 10.0;
            }

            half4 frag(Varyings i) : SV_Target
            {
                float3 N = normalize(i.normalWS);
                float3 V = GetWorldSpaceNormalizeViewDir(i.positionWS);
                Light light = GetMainLight();
                float3 L = light.direction;
                float3 H = SafeNormalize(L + V);

                float NoL = saturate(dot(N, L));
                float NoV = max(dot(N, V), 1e-4);     // 防止边缘处除零
                float NoH = saturate(dot(N, H));
                float VoH = saturate(dot(V, H));
                float LoH = VoH;                       // h 是 l、v 的角平分线,两者相等

                float a = _Roughness * _Roughness;     // α = perceptualRoughness²

                float  D = EvalD(NoH, a);
                float  G = EvalG(NoL, NoV, a);
                float3 F = C9_F_Schlick(_F0.rgb, VoH);

                // Cook-Torrance 镜面 BRDF:D·G·F / (4 (n·l)(n·v))
                float3 fSpec = D * G * F / max(4.0 * NoL * NoV, 1e-4);
                float3 fDiff = (_DiffuseModel < 0.5)
                    ? C9_Lambert(_Albedo.rgb)
                    : C9_DisneyDiffuse(_Albedo.rgb, NoL, NoV, LoH, _Roughness);

                // Unity 的灯光“颜色×强度”约定:白色 Lambert 面正对强度 1 的灯时输出 1,
                // 相当于入射辐照度 E = π · lightColor,所以出射辐射率 = f · π · lightColor · (n·l)。
                float3 E = PI * light.color * NoL;

                float3 c;
                if      (_Mode < 0.5) c = D.xxx;
                else if (_Mode < 1.5) c = (NoL > 0.0 ? G : 0.0).xxx;
                else if (_Mode < 2.5) c = C9_F_Schlick(_F0.rgb, NoV);
                else if (_Mode < 3.5) c = F;
                else if (_Mode < 4.5) c = fSpec * E;
                else if (_Mode < 5.5) c = fDiff * E;
                else                  c = ((1.0 - F) * fDiff + fSpec) * E;   // 漫反射只拿“没被镜面反射走”的那部分

                // 显示映射:Raw 直接输出(超过 1 会被截断);Exp 用 1 - e^(-x·k) 把大范围数值压进 0..1
                if (_Display > 0.5) c = 1.0 - exp(-c * _Exposure);
                else                c *= _Exposure;
                return half4(c, 1);
            }
            ENDHLSL
        }
    }
}
// 第 9 章:BRDF 单项可视化。(GLSL 版,对应 Ch09/BRDFTerms.shader)
// 用一个 _Mode 参数切换输出 D、G、F、镜面项、漫反射项或完整的直接光照,
// 配合一排粗糙度递增的小球,就能“看见”每一项随粗糙度怎么变。
Shader "Cookbook/Ch09/BRDFTerms"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }

            GLSLPROGRAM
            #include "Ch09/BRDFLib.glsl"

            uniform float _Mode, _NDF, _GModel, _DiffuseModel;
            uniform float _Roughness;
            uniform vec4 _F0, _Albedo;
            uniform float _Display, _Exposure;

            VARYING vec3 positionWS;
            VARYING vec3 normalWS;

            #ifdef VERTEX
            void main()
            {
                VertexPositionInputs p = GetVertexPositionInputs(positionOS.xyz);
                gl_Position = p.positionCS;
                positionWS = p.positionWS;
                normalWS = TransformObjectToWorldNormal(normalOS);
            }
            #endif

            #ifdef FRAGMENT
            float EvalD(float NoH, float a)
            {
                if (_NDF < 0.5) return C9_D_GGX(NoH, a);
                if (_NDF < 1.5) return C9_D_Beckmann(NoH, a);
                return C9_D_BlinnPhong(NoH, a);
            }

            float EvalG(float NoL, float NoV, float a)
            {
                if (_GModel < 0.5) return C9_G_SmithSeparable(NoL, NoV, a);
                if (_GModel < 1.5) return C9_G_SmithHeightCorrelated(NoL, NoV, a);
                if (_GModel < 2.5) return C9_G_SchlickGGX(NoL, NoV, a);
                // 两种 Smith 形式之差放大 10 倍,看它们在哪儿不一样
                return abs(C9_G_SmithHeightCorrelated(NoL, NoV, a) - C9_G_SmithSeparable(NoL, NoV, a)) * 10.0;
            }

            void main()
            {
                vec3 N = normalize(normalWS);
                vec3 V = GetWorldSpaceNormalizeViewDir(positionWS);
                Light light = GetMainLight();
                vec3 L = light.direction;
                vec3 H = SafeNormalize(L + V);

                float NoL = saturate(dot(N, L));
                float NoV = max(dot(N, V), 1e-4);     // 防止边缘处除零
                float NoH = saturate(dot(N, H));
                float VoH = saturate(dot(V, H));
                float LoH = VoH;                       // h 是 l、v 的角平分线,两者相等

                float a = _Roughness * _Roughness;     // α = perceptualRoughness²

                float D = EvalD(NoH, a);
                float G = EvalG(NoL, NoV, a);
                vec3  F = C9_F_Schlick(_F0.rgb, VoH);

                // Cook-Torrance 镜面 BRDF:D·G·F / (4 (n·l)(n·v))
                vec3 fSpec = D * G * F / max(4.0 * NoL * NoV, 1e-4);
                vec3 fDiff = (_DiffuseModel < 0.5)
                    ? C9_Lambert(_Albedo.rgb)
                    : C9_DisneyDiffuse(_Albedo.rgb, NoL, NoV, LoH, _Roughness);

                // Unity 的灯光“颜色×强度”约定:白色 Lambert 面正对强度 1 的灯时输出 1,
                // 相当于入射辐照度 E = π · lightColor,所以出射辐射率 = f · π · lightColor · (n·l)。
                vec3 E = PI * light.color * NoL;

                vec3 c;
                if      (_Mode < 0.5) c = vec3(D);
                else if (_Mode < 1.5) c = vec3(NoL > 0.0 ? G : 0.0);
                else if (_Mode < 2.5) c = C9_F_Schlick(_F0.rgb, NoV);
                else if (_Mode < 3.5) c = F;
                else if (_Mode < 4.5) c = fSpec * E;
                else if (_Mode < 5.5) c = fDiff * E;
                else                  c = ((1.0 - F) * fDiff + fSpec) * E;   // 漫反射只拿“没被镜面反射走”的那部分

                // 显示映射:Raw 直接输出(超过 1 会被截断);Exp 用 1 - e^(-x·k) 把大范围数值压进 0..1
                if (_Display > 0.5) c = 1.0 - exp(-c * _Exposure);
                else                c *= _Exposure;
                fragColor = vec4(c, 1.0);
            }
            #endif
            ENDGLSL
        }
    }
}

几个关键处:

图 9-7只输出 GGX 的 D 项(Exp 显示),从左到右感知粗糙度为 0.1、0.3、0.5、0.7、0.9。光从左上方射来,亮斑位于 n = h 处。粗糙度越大,分布越宽、峰值越低。

注意最右边的球几乎被均匀照亮:粗糙度 0.9 时 α=0.81,D 在整个半球上变化不大,“高光”已经和漫反射分不清了。这正是粗糙金属看起来“发灰发闷”的原因。