GameTech/URP 渲染实战

案例 11.2第一步:只有主光的最小 PBR入门

目标:在加任何复杂功能之前,用最少的代码验证 BRDF 是对的。这个 Shader 只读主光方向和颜色,没有阴影、没有环境光、没有贴图。

Assets/Cookbook/Ch11/Step1_DirectPBR.shader
// 第一步:只有主光、没有阴影、没有环境光的最小 PBR。
// 目的:先单独验证 BRDF(D、V、F、Lambert 与 π 的约定)是否正确,再往上加东西。
Shader "Cookbook/Ch11/Step1_DirectPBR"
{
    Properties
    {
        _BaseColor ("Base Color", Color) = (1, 1, 1, 1)
        _Metallic ("Metallic", Range(0, 1)) = 0
        _Smoothness ("Smoothness", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
        Pass
        {
            Tags { "LightMode"="UniversalForward" }
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            CBUFFER_START(UnityPerMaterial)
                half4 _BaseColor;
                half  _Metallic;
                half  _Smoothness;
            CBUFFER_END
            // MyPBR_BRDF.hlsl 里的 LUT 分支要用到这张贴图的声明(本步不启用)
            TEXTURE2D(_DFGLut);
            #include "Assets/Cookbook/Ch11/MyPBR_BRDF.hlsl"

            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;
                o.positionWS = TransformObjectToWorld(v.positionOS.xyz);
                o.positionCS = TransformWorldToHClip(o.positionWS);
                o.normalWS   = TransformObjectToWorldNormal(v.normalOS);
                return o;
            }

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

                MyMaterial m = MyInitMaterial(_BaseColor.rgb, _Metallic, 1.0 - _Smoothness);
                float3 fd, fs; float NoL;
                MyDirectBRDF(m, N, V, light.direction, fd, fs, NoL);

                // 渲染方程只剩一项:Lo = f(l,v) · E⊥ · NoL,E⊥ = π · light.color
                float3 Lo = (fd + fs) * PI * light.color * NoL;
                return half4(Lo, 1);
            }
            ENDHLSL
        }
    }
}
// 第一步:只有主光、没有阴影、没有环境光的最小 PBR。(GLSL 版,对应 Ch11/Step1_DirectPBR.shader)
// 目的:先单独验证 BRDF(D、V、F、Lambert 与 π 的约定)是否正确,再往上加东西。
Shader "Cookbook/Ch11/Step1_DirectPBR"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
        Pass
        {
            Tags { "LightMode"="UniversalForward" }
            GLSLPROGRAM
            uniform vec4  _BaseColor;
            uniform float _Metallic;
            uniform float _Smoothness;
            // MyPBR_BRDF.glsl 里的 LUT 分支要用到这张贴图的声明(本步不启用)
            uniform sampler2D _DFGLut;
            #include "Ch11/MyPBR_BRDF.glsl"

            VARYING vec3 positionWS;
            VARYING vec3 normalWS;

            #ifdef VERTEX
            void main()
            {
                positionWS  = TransformObjectToWorld(positionOS.xyz);
                gl_Position = TransformWorldToHClip(positionWS);
                normalWS    = TransformObjectToWorldNormal(normalOS);
            }
            #endif

            #ifdef FRAGMENT
            void main()
            {
                vec3 N = normalize(normalWS);
                vec3 V = GetWorldSpaceNormalizeViewDir(positionWS);
                Light light = GetMainLight();

                MyMaterial m = MyInitMaterial(_BaseColor.rgb, _Metallic, 1.0 - _Smoothness);
                vec3 fd, fs; float NoL;
                MyDirectBRDF(m, N, V, light.direction, fd, fs, NoL);

                // 渲染方程只剩一项:Lo = f(l,v) · E⊥ · NoL,E⊥ = π · light.color
                vec3 Lo = (fd + fs) * PI * light.color * NoL;
                fragColor = vec4(Lo, 1.0);
            }
            #endif
            ENDGLSL
        }
    }
}

片元函数就是渲染方程的一项:(fd + fs) * PI * light.color * NoL。GetMainLight() 的无参版本只返回方向、颜色和距离衰减(方向光恒为 1,在 Forward 下它还兼做“灯光是否被 Culling Mask 剔除”的开关),没有阴影。TEXTURE2D(_DFGLut) 这一行是因为 MyPBR_BRDF.hlsl 里的 LUT 分支引用了它——即使本步不启用 _DFG_LUT,这张贴图的声明也必须存在,否则预处理后的代码在语法上不完整。

图 11-4Step1_DirectPBR 的结果。上排为橙色电介质,下排为金色金属,光滑度从左到右 0.1 → 0.95。没有环境光时背光面全黑,光滑的金属几乎全黑——它只反射“环境”,而此时环境不存在。

这张图已经能说明很多事情:电介质的漫反射亮度不随光滑度变化(能量分配给了漫反射),高光越来越小越来越亮;金属没有漫反射,随着光滑度升高,球体的大部分区域反而变黑,只剩一个刺眼的光点。很多初学者会在这一步觉得“金属不对”,其实是对的——金属的外观几乎完全由它反射的环境决定,这正是 11.4 节 IBL 要补上的部分。

提示验证 BRDF 时,最好关掉后处理(本书配图默认不开 Tonemapping)、用纯色背景、用 0.5 左右的中性灰材质做一个对照球。高光是否在光滑度变化时保持“总量守恒”,可以截图后用取色器比较高光区域的平均亮度。