GameTech/URP 渲染实战

案例 6.1多光源 Lambert:同时支持 Forward 与 Forward+入门

目标:在第 5 章 Lambert 的基础上加入附加光循环,写出一个在 Forward(含逐顶点模式)和 Forward+ 下都能正确工作的最小多光源 Shader。

完整代码

Assets/Cookbook/Ch06/MultiLightLambert.shader
// 案例 6.1 多光源 Lambert:主光 + 附加光循环,同时支持 Forward 与 Forward+
Shader "Cookbook/Ch06/MultiLightLambert"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor("Base Color", Color) = (0.8, 0.8, 0.8, 1)
        _AmbientStrength("Ambient Strength", Range(0, 1)) = 0.3
    }

    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;
            half   _AmbientStrength;
        CBUFFER_END
        ENDHLSL

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

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // Forward 路径:URP 按 Asset 的“Additional Lights”设置打开 _ADDITIONAL_LIGHTS(逐像素)
            // 或 _ADDITIONAL_LIGHTS_VERTEX(逐顶点);Forward+ 路径打开 _FORWARD_PLUS,
            // Core.hlsl 会据此自动定义 _ADDITIONAL_LIGHTS 和 USE_FORWARD_PLUS
            #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
            #pragma multi_compile _ _FORWARD_PLUS

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

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float2 uv         : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv         : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                float3 normalWS   : TEXCOORD2;
            #if defined(_ADDITIONAL_LIGHTS_VERTEX)
                half3  vertexLight : TEXCOORD3;
            #endif
            };

            Varyings vert(Attributes input)
            {
                Varyings output;
                VertexPositionInputs pos = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = pos.positionCS;
                output.positionWS = pos.positionWS;
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
            #if defined(_ADDITIONAL_LIGHTS_VERTEX)
                // 逐顶点模式:URP 的 VertexLighting() 在顶点里把所有附加光的 Lambert 累加好
                output.vertexLight = VertexLighting(pos.positionWS, output.normalWS);
            #endif
                return output;
            }

            half3 LambertLight(Light light, half3 normalWS)
            {
                half3 radiance = light.color * (light.distanceAttenuation * light.shadowAttenuation);
                return radiance * saturate(dot(normalWS, light.direction));
            }

            half4 frag(Varyings input) : SV_Target
            {
                half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _BaseColor.rgb;
                half3 normalWS = normalize(input.normalWS);

                // LIGHT_LOOP_BEGIN 在 Forward+ 下会直接读取名为 inputData 的变量
                // (inputData.positionWS 与 inputData.normalizedScreenSpaceUV),所以名字不能改
                InputData inputData = (InputData)0;
                inputData.positionWS = input.positionWS;
                inputData.normalWS = normalWS;
                inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);

                // 主光源
                half3 lighting = LambertLight(GetMainLight(), normalWS);

            #if defined(_ADDITIONAL_LIGHTS)
                uint lightCount = GetAdditionalLightsCount();   // Forward+ 下恒为 0,循环靠 cluster 迭代器

                #if USE_FORWARD_PLUS
                // Forward+:附加的“方向光”不进入分簇结构,排在光源数组最前面,要单独循环
                for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
                {
                    lighting += LambertLight(GetAdditionalLight(lightIndex, inputData.positionWS), normalWS);
                }
                #endif

                // Forward:for (lightIndex = 0; lightIndex < lightCount; ...)
                // Forward+:遍历当前像素所在 tile 与 z-bin 的光源位掩码
                LIGHT_LOOP_BEGIN(lightCount)
                    Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
                    lighting += LambertLight(light, normalWS);
                LIGHT_LOOP_END
            #endif

            #if defined(_ADDITIONAL_LIGHTS_VERTEX)
                lighting += input.vertexLight;
            #endif

                half3 color = albedo * lighting + albedo * SampleSH(normalWS) * _AmbientStrength;
                return half4(color, 1);
            }
            ENDHLSL
        }

        // ---- 投影与深度:见案例 6.4 与 6.8 ----
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On
            ZTest LEqual
            ColorMask 0

            HLSLPROGRAM
            #pragma vertex ShadowCasterVertex
            #pragma fragment ShadowCasterFragment
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            #include "Assets/Cookbook/Ch06/ShadowCasterPass.hlsl"
            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On
            ColorMask R

            HLSLPROGRAM
            #pragma vertex DepthOnlyVertex
            #pragma fragment DepthOnlyFragment
            #include "Assets/Cookbook/Ch06/DepthOnlyPass.hlsl"
            ENDHLSL
        }
    }
}
// 案例 6.1 多光源 Lambert:主光 + 附加光循环,同时支持 Forward 与 Forward+(GLSL 版,对应 Ch06/MultiLightLambert.shader)
Shader "Cookbook/Ch06/MultiLightLambert"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        GLSLINCLUDE
        uniform vec4  _BaseMap_ST;
        uniform vec4  _BaseColor;
        uniform float _AmbientStrength;
        ENDGLSL

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

            GLSLPROGRAM
            // Forward 路径:URP 按 Asset 的“Additional Lights”设置打开 _ADDITIONAL_LIGHTS(逐像素)
            // 或 _ADDITIONAL_LIGHTS_VERTEX(逐顶点);Forward+ 路径打开 _FORWARD_PLUS。
            // 网页版引擎始终定义 _ADDITIONAL_LIGHTS(逐像素),Forward / Forward+ 的区别由引擎在
            // GetAdditionalLightsCount() / GetAdditionalLight() 内部处理(见下面的循环)
            #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS

            uniform sampler2D _BaseMap;

            VARYING vec2 uv;
            VARYING vec3 positionWS;
            VARYING vec3 normalWS;
        #if defined(_ADDITIONAL_LIGHTS_VERTEX)
            VARYING vec3 vertexLight;
        #endif

            #ifdef VERTEX
            void main()
            {
                VertexPositionInputs pos = GetVertexPositionInputs(positionOS.xyz);
                gl_Position = pos.positionCS;
                positionWS = pos.positionWS;
                normalWS = TransformObjectToWorldNormal(normalOS);
                uv = TransformTex(texcoord0.xy, _BaseMap_ST);
            #if defined(_ADDITIONAL_LIGHTS_VERTEX)
                // 逐顶点模式:URP 的 VertexLighting() 在顶点里把所有附加光的 Lambert 累加好
                vertexLight = vec3(0.0);
                for (int i = 0; i < GetAdditionalLightsCount(); i++)
                {
                    Light l = GetAdditionalPerObjectLight(i, pos.positionWS);
                    vertexLight += LightingLambert(l.color * l.distanceAttenuation, l.direction, normalWS);
                }
            #endif
            }
            #endif

            #ifdef FRAGMENT
            vec3 LambertLight(Light light, vec3 normalWS)
            {
                vec3 radiance = light.color * (light.distanceAttenuation * light.shadowAttenuation);
                return radiance * saturate(dot(normalWS, light.direction));
            }

            void main()
            {
                vec3 albedo = texture(_BaseMap, uv).rgb * _BaseColor.rgb;
                vec3 N = normalize(normalWS);

                // HLSL 版里 LIGHT_LOOP_BEGIN 在 Forward+ 下会直接读取名为 inputData 的变量,所以要先填好它;
                // GLSL 版的循环是普通 for,这里保留同样的写法便于对照
                InputData inputData = InitInputData();
                inputData.positionWS = positionWS;
                inputData.normalWS = N;
                inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(gl_FragCoord);

                // 主光源
                vec3 lighting = LambertLight(GetMainLight(), N);

            #if defined(_ADDITIONAL_LIGHTS)
                // Forward:lightCount = 本物体受影响的灯数(Per Object Limit 以内),循环变量是“本物体的第几盏灯”;
                // Forward+:HLSL 版先单独循环附加的方向光,再用 LIGHT_LOOP_BEGIN 遍历当前像素所在 tile / z-bin 的灯。
                // 网页版没有分簇结构,Forward+ 下 GetAdditionalLightsCount() 直接返回全部可见附加光(方向光排在最前面),
                // 所以两种路径都写成同一个普通 for 循环
                int lightCount = GetAdditionalLightsCount();
                for (int lightIndex = 0; lightIndex < lightCount; lightIndex++)
                {
                    Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
                    lighting += LambertLight(light, N);
                }
            #endif

            #if defined(_ADDITIONAL_LIGHTS_VERTEX)
                lighting += vertexLight;
            #endif

                vec3 color = albedo * lighting + albedo * SampleSH(N) * _AmbientStrength;
                fragColor = vec4(color, 1.0);
            }
            #endif
            ENDGLSL
        }

        // ---- 投影与深度:见案例 6.4 与 6.8 ----
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On
            ZTest LEqual
            ColorMask 0

            GLSLPROGRAM
            #include "Ch06/ShadowCasterPass.glsl"
            ENDGLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On
            ColorMask R

            GLSLPROGRAM
            #include "Ch06/DepthOnlyPass.glsl"
            ENDGLSL
        }
    }
}

关键代码讲解

Assets/Cookbook/Ch06/MultiLightLambert.shader(节选:keywords)
// Forward 路径:URP 按 Asset 的“Additional Lights”设置打开 _ADDITIONAL_LIGHTS(逐像素)
// 或 _ADDITIONAL_LIGHTS_VERTEX(逐顶点);Forward+ 路径打开 _FORWARD_PLUS,
// Core.hlsl 会据此自动定义 _ADDITIONAL_LIGHTS 和 USE_FORWARD_PLUS
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile _ _FORWARD_PLUS
// Forward 路径:URP 按 Asset 的“Additional Lights”设置打开 _ADDITIONAL_LIGHTS(逐像素)
// 或 _ADDITIONAL_LIGHTS_VERTEX(逐顶点);Forward+ 路径打开 _FORWARD_PLUS。
// 网页版引擎始终定义 _ADDITIONAL_LIGHTS(逐像素),Forward / Forward+ 的区别由引擎在
// GetAdditionalLightsCount() / GetAdditionalLight() 内部处理(见下面的循环)
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS

两行 multi_compile 组合出 3 × 2 = 6 个变体。注意 _ADDITIONAL_LIGHTS_VERTEX 与 _FORWARD_PLUS 同时开启的组合实际上不会出现(Core.hlsl 会在 Forward+ 下取消前者),URP 的构建期变体剥离也会删掉它们。

Assets/Cookbook/Ch06/MultiLightLambert.shader(节选:frag)
half4 frag(Varyings input) : SV_Target
{
    half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _BaseColor.rgb;
    half3 normalWS = normalize(input.normalWS);

    // LIGHT_LOOP_BEGIN 在 Forward+ 下会直接读取名为 inputData 的变量
    // (inputData.positionWS 与 inputData.normalizedScreenSpaceUV),所以名字不能改
    InputData inputData = (InputData)0;
    inputData.positionWS = input.positionWS;
    inputData.normalWS = normalWS;
    inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);

    // 主光源
    half3 lighting = LambertLight(GetMainLight(), normalWS);

#if defined(_ADDITIONAL_LIGHTS)
    uint lightCount = GetAdditionalLightsCount();   // Forward+ 下恒为 0,循环靠 cluster 迭代器

    #if USE_FORWARD_PLUS
    // Forward+:附加的“方向光”不进入分簇结构,排在光源数组最前面,要单独循环
    for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
    {
        lighting += LambertLight(GetAdditionalLight(lightIndex, inputData.positionWS), normalWS);
    }
    #endif

    // Forward:for (lightIndex = 0; lightIndex < lightCount; ...)
    // Forward+:遍历当前像素所在 tile 与 z-bin 的光源位掩码
    LIGHT_LOOP_BEGIN(lightCount)
        Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
        lighting += LambertLight(light, normalWS);
    LIGHT_LOOP_END
#endif

#if defined(_ADDITIONAL_LIGHTS_VERTEX)
    lighting += input.vertexLight;
#endif

    half3 color = albedo * lighting + albedo * SampleSH(normalWS) * _AmbientStrength;
    return half4(color, 1);
}
void main()
{
    vec3 albedo = texture(_BaseMap, uv).rgb * _BaseColor.rgb;
    vec3 N = normalize(normalWS);

    // HLSL 版里 LIGHT_LOOP_BEGIN 在 Forward+ 下会直接读取名为 inputData 的变量,所以要先填好它;
    // GLSL 版的循环是普通 for,这里保留同样的写法便于对照
    InputData inputData = InitInputData();
    inputData.positionWS = positionWS;
    inputData.normalWS = N;
    inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(gl_FragCoord);

    // 主光源
    vec3 lighting = LambertLight(GetMainLight(), N);

#if defined(_ADDITIONAL_LIGHTS)
    // Forward:lightCount = 本物体受影响的灯数(Per Object Limit 以内),循环变量是“本物体的第几盏灯”;
    // Forward+:HLSL 版先单独循环附加的方向光,再用 LIGHT_LOOP_BEGIN 遍历当前像素所在 tile / z-bin 的灯。
    // 网页版没有分簇结构,Forward+ 下 GetAdditionalLightsCount() 直接返回全部可见附加光(方向光排在最前面),
    // 所以两种路径都写成同一个普通 for 循环
    int lightCount = GetAdditionalLightsCount();
    for (int lightIndex = 0; lightIndex < lightCount; lightIndex++)
    {
        Light light = GetAdditionalLight(lightIndex, inputData.positionWS);
        lighting += LambertLight(light, N);
    }
#endif

#if defined(_ADDITIONAL_LIGHTS_VERTEX)
    lighting += vertexLight;
#endif

    vec3 color = albedo * lighting + albedo * SampleSH(N) * _AmbientStrength;
    fragColor = vec4(color, 1.0);
}
  1. 构造 inputData。只填循环需要的两个字段:世界坐标,以及用 GetNormalizedScreenSpaceUV(input.positionCS) 算出的屏幕 UV。片元着色器里 SV_POSITION 的 xy 是像素坐标,这个函数除以屏幕尺寸并处理了平台的 UV 翻转。
  2. 一盏灯的贡献写成函数 LambertLight(Light, N)。主光源、附加方向光、分簇里的点光/聚光都调用它。distanceAttenuation × shadowAttenuation 一起乘进去,即使现在还没有阴影,将来换成带阴影的 GetAdditionalLight 重载也不用改这里。
  3. 两个循环。#if USE_FORWARD_PLUS 里的 for 循环只处理附加方向光;LIGHT_LOOP_BEGIN/END 之间处理其余光源,宏内部提供了循环变量 lightIndex。
  4. 逐顶点模式。Varyings 里多一个 vertexLight,在顶点着色器调用 URP 的 VertexLighting() 累加所有附加光的 Lambert,片元里加回去即可。
图 6-28 盏彩色点光源围成一圈,地面和三个球都使用本案例的 Shader。左:Forward,Per Object Limit = 4,地面是一个物体,只分到 4 盏灯,另外 4 盏在地面上“消失”了;中:Forward,Per Object Limit = 8;右:Forward+,没有每物体上限,结果与中图一致
坑如果你的自定义 Shader 在切换到 Forward+ 后附加光全没了,先检查三件事:是否声明了 _FORWARD_PLUS;inputData.normalizedScreenSpaceUV 和 positionWS 是否赋值(漏赋值时编译能过,但所有像素都落在同一个 tile 上,光照会错乱成块状);是否漏掉了附加方向光的那个单独循环。