GameTech/URP 渲染实战

案例 8.9GILit:自写 Shader 接入 SH、光照贴图与反射探针高阶

目标:写出与 URP Lit 在间接光上完全一致的自写 Shader,并能拆分显示各个分量。完整代码如下(较长,后面分段讲解):

Assets/Cookbook/Ch08/GILit.shader
// 一个“全局光照就绪”的自写 Shader:球谐 / 光照贴图 / 动态光照贴图 / Shadowmask / 反射探针(含混合与盒体投影)/
// SSAO / 材质 AO / 自发光,并提供 Meta Pass 参与烘焙。直接光沿用 URP 的 BRDF,便于与 URP Lit 对照。
Shader "Cookbook/Ch08/GILit"
{
    Properties
    {
        [MainTexture] _BaseMap("Albedo", 2D) = "white" {}
        [MainColor] _BaseColor("Color", Color) = (1, 1, 1, 1)
        _Metallic("Metallic", Range(0, 1)) = 0
        _Smoothness("Smoothness", Range(0, 1)) = 0.5
        [Toggle(_NORMALMAP)] _UseNormalMap("Use Normal Map", Float) = 0
        [NoScaleOffset][Normal] _BumpMap("Normal Map", 2D) = "bump" {}
        _BumpScale("Normal Scale", Float) = 1
        [Toggle(_OCCLUSIONMAP)] _UseOcclusionMap("Use Occlusion Map", Float) = 0
        [NoScaleOffset] _OcclusionMap("Occlusion (G)", 2D) = "white" {}
        _OcclusionStrength("Occlusion Strength", Range(0, 1)) = 1
        [Toggle(_EMISSION)] _UseEmission("Emission", Float) = 0
        [HDR] _EmissionColor("Emission Color", Color) = (0, 0, 0, 1)
        [NoScaleOffset] _EmissionMap("Emission Map", 2D) = "white" {}
        [ToggleOff(_ENVIRONMENTREFLECTIONS_OFF)] _EnvironmentReflections("Environment Reflections", Float) = 1
        [Enum(Full,0,IndirectDiffuse,1,IndirectSpecular,2,Direct,3,AOOnDirectToo,4)] _DebugView("Debug View", Float) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        CBUFFER_START(UnityPerMaterial)
            float4 _BaseMap_ST;
            half4 _BaseColor;
            half4 _EmissionColor;
            half _Metallic;
            half _Smoothness;
            half _BumpScale;
            half _OcclusionStrength;
            half _DebugView;
        CBUFFER_END
        ENDHLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForwardOnly" }
            HLSLPROGRAM
            #pragma target 2.0
            #pragma vertex vert
            #pragma fragment frag

            // 材质开关
            #pragma shader_feature_local _NORMALMAP
            #pragma shader_feature_local_fragment _OCCLUSIONMAP
            #pragma shader_feature_local_fragment _EMISSION
            #pragma shader_feature_local_fragment _ENVIRONMENTREFLECTIONS_OFF
            // URP 管线关键字(与 URP 14 Lit.shader 一致的子集)
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile _ EVALUATE_SH_MIXED EVALUATE_SH_VERTEX
            #pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING
            #pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION
            #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
            #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
            #pragma multi_compile _ _FORWARD_PLUS
            // Unity 引擎根据烘焙结果自动打开的关键字
            #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
            #pragma multi_compile _ SHADOWS_SHADOWMASK
            #pragma multi_compile _ DIRLIGHTMAP_COMBINED
            #pragma multi_compile _ LIGHTMAP_ON
            #pragma multi_compile _ DYNAMICLIGHTMAP_ON
            #pragma multi_compile_fog

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            TEXTURE2D(_BaseMap);      SAMPLER(sampler_BaseMap);
            TEXTURE2D(_BumpMap);      SAMPLER(sampler_BumpMap);
            TEXTURE2D(_OcclusionMap); SAMPLER(sampler_OcclusionMap);
            TEXTURE2D(_EmissionMap);  SAMPLER(sampler_EmissionMap);

            struct Attributes
            {
                float4 positionOS        : POSITION;
                float3 normalOS          : NORMAL;
                float4 tangentOS         : TANGENT;
                float2 uv                : TEXCOORD0;
                float2 staticLightmapUV  : TEXCOORD1;   // 光照贴图 UV(导入设置 Generate Lightmap UVs 生成的 UV2)
                float2 dynamicLightmapUV : TEXCOORD2;   // Enlighten 实时 GI 用的 UV3
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv         : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                half3  normalWS   : TEXCOORD2;
            #if defined(_NORMALMAP)
                half4  tangentWS  : TEXCOORD3;
            #endif
                // 有光照贴图时声明 float2 staticLightmapUV,否则声明 half3 vertexSH(逐顶点 SH 结果)
                DECLARE_LIGHTMAP_OR_SH(staticLightmapUV, vertexSH, 4);
            #ifdef DYNAMICLIGHTMAP_ON
                float2 dynamicLightmapUV : TEXCOORD5;
            #endif
                half fogFactor : TEXCOORD6;
            };

            Varyings vert(Attributes input)
            {
                Varyings o = (Varyings)0;
                VertexPositionInputs pos = GetVertexPositionInputs(input.positionOS.xyz);
                VertexNormalInputs nrm = GetVertexNormalInputs(input.normalOS, input.tangentOS);
                o.positionCS = pos.positionCS;
                o.positionWS = pos.positionWS;
                o.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                o.normalWS = nrm.normalWS;
            #if defined(_NORMALMAP)
                o.tangentWS = half4(nrm.tangentWS, input.tangentOS.w * GetOddNegativeScale());
            #endif
                // 光照贴图 UV:乘上 unity_LightmapST(每个物体在图集里的缩放与偏移)
                OUTPUT_LIGHTMAP_UV(input.staticLightmapUV, unity_LightmapST, o.staticLightmapUV);
            #ifdef DYNAMICLIGHTMAP_ON
                o.dynamicLightmapUV = input.dynamicLightmapUV * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
            #endif
                // 没有光照贴图时:EVALUATE_SH_VERTEX 在这里算完整 SH,EVALUATE_SH_MIXED 只算 L2,逐像素模式什么都不算
                OUTPUT_SH(o.normalWS, o.vertexSH);
                o.fogFactor = ComputeFogFactor(pos.positionCS.z);
                return o;
            }

            half4 frag(Varyings i) : SV_Target
            {
                // ---------- 表面属性 ----------
                half4 baseSample = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv) * _BaseColor;
                half3 albedo = baseSample.rgb;
                half alpha = 1;
            #if defined(_OCCLUSIONMAP)
                half occlusion = LerpWhiteTo(SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, i.uv).g, _OcclusionStrength);
            #else
                half occlusion = 1;
            #endif
            #if defined(_EMISSION)
                half3 emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, i.uv).rgb * _EmissionColor.rgb;
            #else
                half3 emission = 0;
            #endif

            #if defined(_NORMALMAP)
                half3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uv), _BumpScale);
                float3 bitangent = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz);
                half3 normalWS = TransformTangentToWorld(normalTS, half3x3(i.tangentWS.xyz, bitangent, i.normalWS));
            #else
                half3 normalWS = i.normalWS;
            #endif
                normalWS = NormalizeNormalPerPixel(normalWS);
                half3 viewDirWS = GetWorldSpaceNormalizeViewDir(i.positionWS);
                float2 screenUV = GetNormalizedScreenSpaceUV(i.positionCS);

                BRDFData brdf;
                InitializeBRDFData(albedo, _Metallic, half3(0, 0, 0), _Smoothness, alpha, brdf);

                // ---------- ① 间接漫反射:光照贴图 或 球谐 ----------
            #if defined(DYNAMICLIGHTMAP_ON)
                half3 bakedGI = SAMPLE_GI(i.staticLightmapUV, i.dynamicLightmapUV, i.vertexSH, normalWS);
            #else
                half3 bakedGI = SAMPLE_GI(i.staticLightmapUV, i.vertexSH, normalWS);
            #endif

                // ---------- ② 主光 + 阴影(含 Shadowmask / 光照探针遮挡) ----------
                half4 shadowMask = SAMPLE_SHADOWMASK(i.staticLightmapUV);
                float4 shadowCoord = TransformWorldToShadowCoord(i.positionWS);
                Light mainLight = GetMainLight(shadowCoord, i.positionWS, shadowMask);

                // ---------- ③ AO:材质 AO 与 SSAO 取较小值,只压间接光;SSAO 还按 Direct Lighting Strength 压一点直接光 ----------
                AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(screenUV, occlusion);
            #if defined(_SCREEN_SPACE_OCCLUSION)
                mainLight.color *= aoFactor.directAmbientOcclusion;
            #endif

                // Subtractive 模式下,从光照贴图里“减掉”实时阴影覆盖区域的主光贡献
                MixRealtimeAndBakedGI(mainLight, normalWS, bakedGI);

                // ---------- ④ 间接高光:反射探针(Forward+ 下走探针图集与聚类) ----------
                half3 reflectVector = reflect(-viewDirWS, normalWS);
                half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, i.positionWS,
                                                                     brdf.perceptualRoughness, half(1.0), screenUV);

                // ---------- ⑤ 组合:与 URP 的 GlobalIllumination() 相同 ----------
                half NoV = saturate(dot(normalWS, viewDirWS));
                half fresnelTerm = Pow4(1.0 - NoV);
                half3 giDiffuse  = bakedGI * brdf.diffuse;
                half3 giSpecular = indirectSpecular * EnvironmentBRDFSpecular(brdf, fresnelTerm);
                half3 gi = (giDiffuse + giSpecular) * aoFactor.indirectAmbientOcclusion;

                half3 direct = LightingPhysicallyBased(brdf, mainLight, normalWS, viewDirWS);

                half3 color = gi + direct + emission;
                // 调试视图:分别看各个分量(书中配图用)
                if (_DebugView > 0.5 && _DebugView < 1.5) color = giDiffuse * aoFactor.indirectAmbientOcclusion;
                else if (_DebugView > 1.5 && _DebugView < 2.5) color = giSpecular * aoFactor.indirectAmbientOcclusion;
                else if (_DebugView > 2.5 && _DebugView < 3.5) color = direct;
                else if (_DebugView > 3.5) color = gi + direct * occlusion + emission;   // 反例:AO 也乘到直接光上

                color = MixFog(color, i.fogFactor);
                return half4(color, 1);
            }
            ENDHLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            HLSLPROGRAM
            #pragma vertex Ch08ShadowVert
            #pragma fragment Ch08NullFrag
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            #include "Assets/Cookbook/Ch08/Ch08Passes.hlsl"
            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            HLSLPROGRAM
            #pragma vertex Ch08DepthVert
            #pragma fragment Ch08NullFrag
            #include "Assets/Cookbook/Ch08/Ch08Passes.hlsl"
            ENDHLSL
        }

        Pass
        {
            Name "DepthNormals"
            Tags { "LightMode"="DepthNormals" }
            ZWrite On
            HLSLPROGRAM
            #pragma vertex Ch08DepthNormalsVert
            #pragma fragment Ch08DepthNormalsFrag
            #include "Assets/Cookbook/Ch08/Ch08Passes.hlsl"
            ENDHLSL
        }

        // Deferred 渲染路径下,只有 UniversalForwardOnly 的材质由这个 Pass 提供法线(与 URP Unlit 的做法相同)
        Pass
        {
            Name "DepthNormalsOnly"
            Tags { "LightMode"="DepthNormalsOnly" }
            ZWrite On
            HLSLPROGRAM
            #pragma vertex Ch08DepthNormalsVert
            #pragma fragment Ch08DepthNormalsFrag
            #include "Assets/Cookbook/Ch08/Ch08Passes.hlsl"
            ENDHLSL
        }

        // Meta Pass:只在烘焙光照贴图时由 Unity 调用,把物体“摊平”到光照贴图 UV 空间,
        // 输出 Albedo(决定反弹光的颜色)和 Emission(自发光作为光源参与烘焙)。
        Pass
        {
            Name "Meta"
            Tags { "LightMode"="Meta" }
            Cull Off
            HLSLPROGRAM
            #pragma vertex MetaVert
            #pragma fragment MetaFrag
            #pragma shader_feature_local_fragment _EMISSION
            #pragma shader_feature EDITOR_VISUALIZATION
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/MetaInput.hlsl"

            TEXTURE2D(_BaseMap);     SAMPLER(sampler_BaseMap);
            TEXTURE2D(_EmissionMap); SAMPLER(sampler_EmissionMap);

            struct MetaAttributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float2 uv0        : TEXCOORD0;
                float2 uv1        : TEXCOORD1;
                float2 uv2        : TEXCOORD2;
            };

            struct MetaVaryings
            {
                float4 positionCS : SV_POSITION;
                float2 uv         : TEXCOORD0;
            #ifdef EDITOR_VISUALIZATION
                float2 VizUV      : TEXCOORD1;
                float4 LightCoord : TEXCOORD2;
            #endif
            };

            MetaVaryings MetaVert(MetaAttributes input)
            {
                MetaVaryings o = (MetaVaryings)0;
                // unity_MetaVertexControl.x/y 决定用 uv1(静态光照贴图)还是 uv2(动态光照贴图)当作光栅化位置
                o.positionCS = UnityMetaVertexPosition(input.positionOS.xyz, input.uv1, input.uv2);
                o.uv = TRANSFORM_TEX(input.uv0, _BaseMap);
            #ifdef EDITOR_VISUALIZATION
                UnityEditorVizData(input.positionOS.xyz, input.uv0, input.uv1, input.uv2, o.VizUV, o.LightCoord);
            #endif
                return o;
            }

            half4 MetaFrag(MetaVaryings i) : SV_Target
            {
                half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb * _BaseColor.rgb;
                half alpha = 1;
                BRDFData brdf;
                InitializeBRDFData(albedo, _Metallic, half3(0, 0, 0), _Smoothness, alpha, brdf);

                MetaInput meta = (MetaInput)0;
                // 与 URP Lit 相同:漫反射色 + 粗糙金属的一部分高光色,近似“这块表面把光反弹成什么颜色”
                meta.Albedo = brdf.diffuse + brdf.specular * brdf.roughness * 0.5;
            #if defined(_EMISSION)
                meta.Emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, i.uv).rgb * _EmissionColor.rgb;
            #endif
            #ifdef EDITOR_VISUALIZATION
                meta.VizUV = i.VizUV;
                meta.LightCoord = i.LightCoord;
            #endif
                // unity_MetaFragmentControl.x 要 Albedo,.y 要 Emission,由烘焙器分两次调用
                return UnityMetaFragment(meta);
            }
            ENDHLSL
        }
    }
}
// 一个“全局光照就绪”的自写 Shader:球谐 / 光照贴图 / 动态光照贴图 / Shadowmask / 反射探针(含混合与盒体投影)/
// SSAO / 材质 AO / 自发光,并提供 Meta Pass 参与烘焙。直接光沿用 URP 的 BRDF,便于与 URP Lit 对照。
// (GLSL 版,对应 Ch08/GILit.shader)
// 网页版没有光照烘焙:光照贴图、动态光照贴图、Shadowmask、Subtractive 混合这几条分支在浏览器里永远不会打开,
// 所以下面只保留“无光照贴图”的路径(SAMPLE_GI 退化为逐像素 SampleSH),Meta Pass 也不移植。
Shader "Cookbook/Ch08/GILit"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        GLSLINCLUDE
        uniform vec4  _BaseMap_ST;
        uniform vec4  _BaseColor;
        uniform vec4  _EmissionColor;
        uniform float _Metallic;
        uniform float _Smoothness;
        uniform float _BumpScale;
        uniform float _OcclusionStrength;
        uniform float _DebugView;
        ENDGLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForwardOnly" }
            GLSLPROGRAM
            // 材质开关
            #pragma shader_feature_local _NORMALMAP
            #pragma shader_feature_local_fragment _OCCLUSIONMAP
            #pragma shader_feature_local_fragment _EMISSION
            #pragma shader_feature_local_fragment _ENVIRONMENTREFLECTIONS_OFF
            // URP 管线关键字(主光阴影、软阴影、SSAO、雾)在网页版由引擎统一处理,不用声明;
            // LIGHTMAP_ON / DYNAMICLIGHTMAP_ON / SHADOWS_SHADOWMASK 等烘焙关键字网页版没有

            uniform sampler2D _BaseMap;
            uniform sampler2D _BumpMap;
            uniform sampler2D _OcclusionMap;
            uniform sampler2D _EmissionMap;

            VARYING vec2 uv;
            VARYING vec3 positionWS;
            VARYING vec3 normalWS;
            VARYING vec4 tangentWS;    // 只在 _NORMALMAP 时有意义
            // HLSL 版:有光照贴图时声明 staticLightmapUV,否则声明 vertexSH(逐顶点 SH)。网页版走逐像素 SH,两者都不需要
            VARYING float fogFactor;

            #ifdef VERTEX
            void main()
            {
                VertexPositionInputs pos = GetVertexPositionInputs(positionOS.xyz);
                VertexNormalInputs nrm = GetVertexNormalInputs(normalOS, tangentOS);
                gl_Position = pos.positionCS;
                positionWS = pos.positionWS;
                uv = TransformTex(texcoord0.xy, _BaseMap_ST);
                normalWS = nrm.normalWS;
                tangentWS = vec4(nrm.tangentWS, tangentOS.w * GetOddNegativeScale());
                fogFactor = ComputeFogFactor(pos.positionCS.z);
            }
            #endif

            #ifdef FRAGMENT
            void main()
            {
                // ---------- 表面属性 ----------
                vec4 baseSample = texture(_BaseMap, uv) * _BaseColor;
                vec3 albedo = baseSample.rgb;
                float alpha = 1.0;
            #if defined(_OCCLUSIONMAP)
                float occlusion = LerpWhiteTo(texture(_OcclusionMap, uv).g, _OcclusionStrength);
            #else
                float occlusion = 1.0;
            #endif
            #if defined(_EMISSION)
                vec3 emission = texture(_EmissionMap, uv).rgb * _EmissionColor.rgb;
            #else
                vec3 emission = vec3(0.0);
            #endif

            #if defined(_NORMALMAP)
                vec3 normalTS = UnpackNormalScale(texture(_BumpMap, uv), _BumpScale);
                vec3 bitangent = tangentWS.w * cross(normalWS, tangentWS.xyz);
                vec3 N = TransformTangentToWorld(normalTS, mat3(tangentWS.xyz, bitangent, normalWS));
            #else
                vec3 N = normalWS;
            #endif
                N = NormalizeNormalPerPixel(N);
                vec3 viewDirWS = GetWorldSpaceNormalizeViewDir(positionWS);
                vec2 screenUV = GetNormalizedScreenSpaceUV(gl_FragCoord);

                BRDFData brdf;
                InitializeBRDFData(albedo, _Metallic, vec3(0.0), _Smoothness, alpha, brdf);

                // ---------- ① 间接漫反射:光照贴图 或 球谐 ----------
                // 网页版没有光照贴图,SAMPLE_GI 就是逐像素 SampleSH
                vec3 bakedGI = SampleSH(N);

                // ---------- ② 主光 + 阴影(含 Shadowmask / 光照探针遮挡) ----------
                vec4 shadowMask = vec4(1.0);     // SAMPLE_SHADOWMASK:没有 Shadowmask 时是 1
                vec4 shadowCoord = TransformWorldToShadowCoord(positionWS);
                Light mainLight = GetMainLight(shadowCoord, positionWS, shadowMask);

                // ---------- ③ AO:材质 AO 与 SSAO 取较小值,只压间接光;SSAO 还按 Direct Lighting Strength 压一点直接光 ----------
                AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(screenUV);
                aoFactor.indirectAmbientOcclusion = min(aoFactor.indirectAmbientOcclusion, occlusion);
                // 没开 SSAO 时 directAmbientOcclusion 恒为 1(HLSL 版用 _SCREEN_SPACE_OCCLUSION 关键字包住这一行)
                mainLight.color *= aoFactor.directAmbientOcclusion;

                // MixRealtimeAndBakedGI:Subtractive 模式下从光照贴图里“减掉”实时阴影区域的主光——没有光照贴图时什么都不做

                // ---------- ④ 间接高光:反射探针(网页版只有全局环境反射 unity_SpecCube0) ----------
                vec3 reflectVector = reflect(-viewDirWS, N);
                vec3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, brdf.perceptualRoughness, 1.0);

                // ---------- ⑤ 组合:与 URP 的 GlobalIllumination() 相同 ----------
                float NoV = saturate(dot(N, viewDirWS));
                float fresnelTerm = Pow4(1.0 - NoV);
                vec3 giDiffuse  = bakedGI * brdf.diffuse;
                vec3 giSpecular = indirectSpecular * EnvironmentBRDFSpecular(brdf, fresnelTerm);
                vec3 gi = (giDiffuse + giSpecular) * aoFactor.indirectAmbientOcclusion;

                vec3 direct = LightingPhysicallyBased(brdf, mainLight, N, viewDirWS);

                vec3 color = gi + direct + emission;
                // 调试视图:分别看各个分量(书中配图用)
                if (_DebugView > 0.5 && _DebugView < 1.5) color = giDiffuse * aoFactor.indirectAmbientOcclusion;
                else if (_DebugView > 1.5 && _DebugView < 2.5) color = giSpecular * aoFactor.indirectAmbientOcclusion;
                else if (_DebugView > 2.5 && _DebugView < 3.5) color = direct;
                else if (_DebugView > 3.5) color = gi + direct * occlusion + emission;   // 反例:AO 也乘到直接光上

                color = MixFog(color, fogFactor);
                fragColor = vec4(color, 1.0);
            }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            GLSLPROGRAM
            #include "Ch08/Ch08Passes.glsl"
            #ifdef VERTEX
            void main() { gl_Position = Ch08ShadowVert(); }
            #endif
            #ifdef FRAGMENT
            void main() { fragColor = Ch08NullFrag(); }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            GLSLPROGRAM
            #include "Ch08/Ch08Passes.glsl"
            #ifdef VERTEX
            void main() { gl_Position = Ch08DepthVert(); }
            #endif
            #ifdef FRAGMENT
            void main() { fragColor = Ch08NullFrag(); }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "DepthNormals"
            Tags { "LightMode"="DepthNormals" }
            ZWrite On
            GLSLPROGRAM
            #define CH08_DEPTH_NORMALS
            #include "Ch08/Ch08Passes.glsl"
            #ifdef VERTEX
            void main() { gl_Position = Ch08DepthNormalsVert(); }
            #endif
            #ifdef FRAGMENT
            void main() { fragColor = Ch08DepthNormalsFrag(); }
            #endif
            ENDGLSL
        }

        // DepthNormalsOnly(Deferred 专用)与 Meta(烘焙光照贴图专用)两个 Pass 网页版用不到,没有移植
    }
}

关键字

Assets/Cookbook/Ch08/GILit.shader(节选:keywords)
// 材质开关
#pragma shader_feature_local _NORMALMAP
#pragma shader_feature_local_fragment _OCCLUSIONMAP
#pragma shader_feature_local_fragment _EMISSION
#pragma shader_feature_local_fragment _ENVIRONMENTREFLECTIONS_OFF
// URP 管线关键字(与 URP 14 Lit.shader 一致的子集)
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ EVALUATE_SH_MIXED EVALUATE_SH_VERTEX
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION
#pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
#pragma multi_compile _ _FORWARD_PLUS
// Unity 引擎根据烘焙结果自动打开的关键字
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile _ DYNAMICLIGHTMAP_ON
#pragma multi_compile_fog
// 材质开关
#pragma shader_feature_local _NORMALMAP
#pragma shader_feature_local_fragment _OCCLUSIONMAP
#pragma shader_feature_local_fragment _EMISSION
#pragma shader_feature_local_fragment _ENVIRONMENTREFLECTIONS_OFF
// URP 管线关键字(主光阴影、软阴影、SSAO、雾)在网页版由引擎统一处理,不用声明;
// LIGHTMAP_ON / DYNAMICLIGHTMAP_ON / SHADOWS_SHADOWMASK 等烘焙关键字网页版没有

关键字分三类。材质开关用 shader_feature_local,只编译材质实际用到的组合;URP 管线关键字(阴影、SH 模式、探针、SSAO、Forward+)用 multi_compile,由 URP 在运行时根据 Asset 和 Renderer 设置全局开关;最后一组由引擎根据物体的烘焙状态逐物体打开。列表以 URP 14 的 Lit.shader 为准,我们只保留了与 GI 相关的子集(省去了附加光、光照层、贴花、光照 Cookie 等),实际项目可以按需补齐。变体数量在这里会迅速膨胀,第 29 章会讲怎样剥离不需要的变体。

插值器与顶点

Assets/Cookbook/Ch08/GILit.shader(节选:structs)
struct Attributes
{
    float4 positionOS        : POSITION;
    float3 normalOS          : NORMAL;
    float4 tangentOS         : TANGENT;
    float2 uv                : TEXCOORD0;
    float2 staticLightmapUV  : TEXCOORD1;   // 光照贴图 UV(导入设置 Generate Lightmap UVs 生成的 UV2)
    float2 dynamicLightmapUV : TEXCOORD2;   // Enlighten 实时 GI 用的 UV3
};

struct Varyings
{
    float4 positionCS : SV_POSITION;
    float2 uv         : TEXCOORD0;
    float3 positionWS : TEXCOORD1;
    half3  normalWS   : TEXCOORD2;
#if defined(_NORMALMAP)
    half4  tangentWS  : TEXCOORD3;
#endif
    // 有光照贴图时声明 float2 staticLightmapUV,否则声明 half3 vertexSH(逐顶点 SH 结果)
    DECLARE_LIGHTMAP_OR_SH(staticLightmapUV, vertexSH, 4);
#ifdef DYNAMICLIGHTMAP_ON
    float2 dynamicLightmapUV : TEXCOORD5;
#endif
    half fogFactor : TEXCOORD6;
};
VARYING vec2 uv;
VARYING vec3 positionWS;
VARYING vec3 normalWS;
VARYING vec4 tangentWS;    // 只在 _NORMALMAP 时有意义
// HLSL 版:有光照贴图时声明 staticLightmapUV,否则声明 vertexSH(逐顶点 SH)。网页版走逐像素 SH,两者都不需要
VARYING float fogFactor;
Assets/Cookbook/Ch08/GILit.shader(节选:vert)
Varyings vert(Attributes input)
{
    Varyings o = (Varyings)0;
    VertexPositionInputs pos = GetVertexPositionInputs(input.positionOS.xyz);
    VertexNormalInputs nrm = GetVertexNormalInputs(input.normalOS, input.tangentOS);
    o.positionCS = pos.positionCS;
    o.positionWS = pos.positionWS;
    o.uv = TRANSFORM_TEX(input.uv, _BaseMap);
    o.normalWS = nrm.normalWS;
#if defined(_NORMALMAP)
    o.tangentWS = half4(nrm.tangentWS, input.tangentOS.w * GetOddNegativeScale());
#endif
    // 光照贴图 UV:乘上 unity_LightmapST(每个物体在图集里的缩放与偏移)
    OUTPUT_LIGHTMAP_UV(input.staticLightmapUV, unity_LightmapST, o.staticLightmapUV);
#ifdef DYNAMICLIGHTMAP_ON
    o.dynamicLightmapUV = input.dynamicLightmapUV * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#endif
    // 没有光照贴图时:EVALUATE_SH_VERTEX 在这里算完整 SH,EVALUATE_SH_MIXED 只算 L2,逐像素模式什么都不算
    OUTPUT_SH(o.normalWS, o.vertexSH);
    o.fogFactor = ComputeFogFactor(pos.positionCS.z);
    return o;
}
void main()
{
    VertexPositionInputs pos = GetVertexPositionInputs(positionOS.xyz);
    VertexNormalInputs nrm = GetVertexNormalInputs(normalOS, tangentOS);
    gl_Position = pos.positionCS;
    positionWS = pos.positionWS;
    uv = TransformTex(texcoord0.xy, _BaseMap_ST);
    normalWS = nrm.normalWS;
    tangentWS = vec4(nrm.tangentWS, tangentOS.w * GetOddNegativeScale());
    fogFactor = ComputeFogFactor(pos.positionCS.z);
}

Attributes 里多了 TEXCOORD1 和 TEXCOORD2:分别是静态光照贴图 UV 与动态光照贴图 UV。即使你的模型现在没有烘焙,也应该声明它们,否则一旦场景开始烘焙,物体会因为缺少 UV 而拿到错误的光照贴图坐标。顶点着色器里的 OUTPUT_LIGHTMAP_UV 与 OUTPUT_SH 会根据关键字二选一。

片元:五步走

Assets/Cookbook/Ch08/GILit.shader(节选:frag)
half4 frag(Varyings i) : SV_Target
{
    // ---------- 表面属性 ----------
    half4 baseSample = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv) * _BaseColor;
    half3 albedo = baseSample.rgb;
    half alpha = 1;
#if defined(_OCCLUSIONMAP)
    half occlusion = LerpWhiteTo(SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, i.uv).g, _OcclusionStrength);
#else
    half occlusion = 1;
#endif
#if defined(_EMISSION)
    half3 emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, i.uv).rgb * _EmissionColor.rgb;
#else
    half3 emission = 0;
#endif

#if defined(_NORMALMAP)
    half3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uv), _BumpScale);
    float3 bitangent = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz);
    half3 normalWS = TransformTangentToWorld(normalTS, half3x3(i.tangentWS.xyz, bitangent, i.normalWS));
#else
    half3 normalWS = i.normalWS;
#endif
    normalWS = NormalizeNormalPerPixel(normalWS);
    half3 viewDirWS = GetWorldSpaceNormalizeViewDir(i.positionWS);
    float2 screenUV = GetNormalizedScreenSpaceUV(i.positionCS);

    BRDFData brdf;
    InitializeBRDFData(albedo, _Metallic, half3(0, 0, 0), _Smoothness, alpha, brdf);

    // ---------- ① 间接漫反射:光照贴图 或 球谐 ----------
#if defined(DYNAMICLIGHTMAP_ON)
    half3 bakedGI = SAMPLE_GI(i.staticLightmapUV, i.dynamicLightmapUV, i.vertexSH, normalWS);
#else
    half3 bakedGI = SAMPLE_GI(i.staticLightmapUV, i.vertexSH, normalWS);
#endif

    // ---------- ② 主光 + 阴影(含 Shadowmask / 光照探针遮挡) ----------
    half4 shadowMask = SAMPLE_SHADOWMASK(i.staticLightmapUV);
    float4 shadowCoord = TransformWorldToShadowCoord(i.positionWS);
    Light mainLight = GetMainLight(shadowCoord, i.positionWS, shadowMask);

    // ---------- ③ AO:材质 AO 与 SSAO 取较小值,只压间接光;SSAO 还按 Direct Lighting Strength 压一点直接光 ----------
    AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(screenUV, occlusion);
#if defined(_SCREEN_SPACE_OCCLUSION)
    mainLight.color *= aoFactor.directAmbientOcclusion;
#endif

    // Subtractive 模式下,从光照贴图里“减掉”实时阴影覆盖区域的主光贡献
    MixRealtimeAndBakedGI(mainLight, normalWS, bakedGI);

    // ---------- ④ 间接高光:反射探针(Forward+ 下走探针图集与聚类) ----------
    half3 reflectVector = reflect(-viewDirWS, normalWS);
    half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, i.positionWS,
                                                         brdf.perceptualRoughness, half(1.0), screenUV);

    // ---------- ⑤ 组合:与 URP 的 GlobalIllumination() 相同 ----------
    half NoV = saturate(dot(normalWS, viewDirWS));
    half fresnelTerm = Pow4(1.0 - NoV);
    half3 giDiffuse  = bakedGI * brdf.diffuse;
    half3 giSpecular = indirectSpecular * EnvironmentBRDFSpecular(brdf, fresnelTerm);
    half3 gi = (giDiffuse + giSpecular) * aoFactor.indirectAmbientOcclusion;

    half3 direct = LightingPhysicallyBased(brdf, mainLight, normalWS, viewDirWS);

    half3 color = gi + direct + emission;
    // 调试视图:分别看各个分量(书中配图用)
    if (_DebugView > 0.5 && _DebugView < 1.5) color = giDiffuse * aoFactor.indirectAmbientOcclusion;
    else if (_DebugView > 1.5 && _DebugView < 2.5) color = giSpecular * aoFactor.indirectAmbientOcclusion;
    else if (_DebugView > 2.5 && _DebugView < 3.5) color = direct;
    else if (_DebugView > 3.5) color = gi + direct * occlusion + emission;   // 反例:AO 也乘到直接光上

    color = MixFog(color, i.fogFactor);
    return half4(color, 1);
}
void main()
{
    // ---------- 表面属性 ----------
    vec4 baseSample = texture(_BaseMap, uv) * _BaseColor;
    vec3 albedo = baseSample.rgb;
    float alpha = 1.0;
#if defined(_OCCLUSIONMAP)
    float occlusion = LerpWhiteTo(texture(_OcclusionMap, uv).g, _OcclusionStrength);
#else
    float occlusion = 1.0;
#endif
#if defined(_EMISSION)
    vec3 emission = texture(_EmissionMap, uv).rgb * _EmissionColor.rgb;
#else
    vec3 emission = vec3(0.0);
#endif

#if defined(_NORMALMAP)
    vec3 normalTS = UnpackNormalScale(texture(_BumpMap, uv), _BumpScale);
    vec3 bitangent = tangentWS.w * cross(normalWS, tangentWS.xyz);
    vec3 N = TransformTangentToWorld(normalTS, mat3(tangentWS.xyz, bitangent, normalWS));
#else
    vec3 N = normalWS;
#endif
    N = NormalizeNormalPerPixel(N);
    vec3 viewDirWS = GetWorldSpaceNormalizeViewDir(positionWS);
    vec2 screenUV = GetNormalizedScreenSpaceUV(gl_FragCoord);

    BRDFData brdf;
    InitializeBRDFData(albedo, _Metallic, vec3(0.0), _Smoothness, alpha, brdf);

    // ---------- ① 间接漫反射:光照贴图 或 球谐 ----------
    // 网页版没有光照贴图,SAMPLE_GI 就是逐像素 SampleSH
    vec3 bakedGI = SampleSH(N);

    // ---------- ② 主光 + 阴影(含 Shadowmask / 光照探针遮挡) ----------
    vec4 shadowMask = vec4(1.0);     // SAMPLE_SHADOWMASK:没有 Shadowmask 时是 1
    vec4 shadowCoord = TransformWorldToShadowCoord(positionWS);
    Light mainLight = GetMainLight(shadowCoord, positionWS, shadowMask);

    // ---------- ③ AO:材质 AO 与 SSAO 取较小值,只压间接光;SSAO 还按 Direct Lighting Strength 压一点直接光 ----------
    AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(screenUV);
    aoFactor.indirectAmbientOcclusion = min(aoFactor.indirectAmbientOcclusion, occlusion);
    // 没开 SSAO 时 directAmbientOcclusion 恒为 1(HLSL 版用 _SCREEN_SPACE_OCCLUSION 关键字包住这一行)
    mainLight.color *= aoFactor.directAmbientOcclusion;

    // MixRealtimeAndBakedGI:Subtractive 模式下从光照贴图里“减掉”实时阴影区域的主光——没有光照贴图时什么都不做

    // ---------- ④ 间接高光:反射探针(网页版只有全局环境反射 unity_SpecCube0) ----------
    vec3 reflectVector = reflect(-viewDirWS, N);
    vec3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, positionWS, brdf.perceptualRoughness, 1.0);

    // ---------- ⑤ 组合:与 URP 的 GlobalIllumination() 相同 ----------
    float NoV = saturate(dot(N, viewDirWS));
    float fresnelTerm = Pow4(1.0 - NoV);
    vec3 giDiffuse  = bakedGI * brdf.diffuse;
    vec3 giSpecular = indirectSpecular * EnvironmentBRDFSpecular(brdf, fresnelTerm);
    vec3 gi = (giDiffuse + giSpecular) * aoFactor.indirectAmbientOcclusion;

    vec3 direct = LightingPhysicallyBased(brdf, mainLight, N, viewDirWS);

    vec3 color = gi + direct + emission;
    // 调试视图:分别看各个分量(书中配图用)
    if (_DebugView > 0.5 && _DebugView < 1.5) color = giDiffuse * aoFactor.indirectAmbientOcclusion;
    else if (_DebugView > 1.5 && _DebugView < 2.5) color = giSpecular * aoFactor.indirectAmbientOcclusion;
    else if (_DebugView > 2.5 && _DebugView < 3.5) color = direct;
    else if (_DebugView > 3.5) color = gi + direct * occlusion + emission;   // 反例:AO 也乘到直接光上

    color = MixFog(color, fogFactor);
    fragColor = vec4(color, 1.0);
}
  1. 间接漫反射:SAMPLE_GI 得到 bakedGI,来自光照贴图或 SH,无需关心是哪一个。
  2. 主光:SAMPLE_SHADOWMASK 取得烘焙阴影信息,传给 GetMainLight,由它混合实时阴影与 Shadowmask。
  3. AO:CreateAmbientOcclusionFactor 合并材质 AO 与 SSAO(详见 8.7 节);开启 SSAO 时按 Direct Lighting Strength 衰减主光。
  4. Subtractive 修正:MixRealtimeAndBakedGI,只在 Subtractive 模式下生效。
  5. 间接高光与组合:GlossyEnvironmentReflection 采样探针;漫反射部分乘 brdf.diffuse,高光部分乘 EnvironmentBRDFSpecular,两者之和再乘间接 AO。这几行与 URP 的 GlobalIllumination() 函数逐项相同,我们把它展开写,是为了能单独输出每个分量。

EnvironmentBRDFSpecular 的内容只有两行:surfaceReduction = 1 / (roughness² + 1),以及在 F0(brdf.specular)和掠射颜色(grazingTerm)之间按 fresnelTerm = (1 − N·V)⁴ 插值。它是对 Split Sum 中“BRDF 积分”那一项的解析近似,第 10 章逐行读 URP Lit 时会再展开。

图 8-14左:URP Lit;右:GILit。五个球依次为红色粗糙塑料、蓝色中等光滑塑料、白色光滑塑料、金色金属、镜面银色金属,参数相同。两张图在间接漫反射、间接高光、阴影上一致,说明我们复刻的 GI 路径与 URP Lit 等价。
图 8-15用 _DebugView 拆分 GILit 的光照。从左到右:仅直接光(金属球除高光外全黑)、仅间接漫反射(金属球全黑,因为金属没有漫反射)、仅间接高光(金属球完整映出天空与地面)、全部合成。

图 8-15 是理解 PBR 外观的好材料:橙色纽结(非金属)的颜色主要来自直接光和间接漫反射,间接高光只在掠射角给它添了一层淡淡的天光;银色球(金属)在直接光下几乎只有一个高光点,它的外观几乎完全由环境反射决定。如果你的金属材质看起来又黑又假,先检查环境反射,而不是调灯光。