URP 渲染实战 / 第 6 章 多光源与阴影 / 案例 6.1
案例 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); }
- 构造
inputData。只填循环需要的两个字段:世界坐标,以及用GetNormalizedScreenSpaceUV(input.positionCS)算出的屏幕 UV。片元着色器里SV_POSITION的 xy 是像素坐标,这个函数除以屏幕尺寸并处理了平台的 UV 翻转。 - 一盏灯的贡献写成函数
LambertLight(Light, N)。主光源、附加方向光、分簇里的点光/聚光都调用它。distanceAttenuation × shadowAttenuation一起乘进去,即使现在还没有阴影,将来换成带阴影的GetAdditionalLight重载也不用改这里。 - 两个循环。
#if USE_FORWARD_PLUS里的 for 循环只处理附加方向光;LIGHT_LOOP_BEGIN/END之间处理其余光源,宏内部提供了循环变量lightIndex。 - 逐顶点模式。Varyings 里多一个
vertexLight,在顶点着色器调用 URP 的VertexLighting()累加所有附加光的 Lambert,片元里加回去即可。

坑如果你的自定义 Shader 在切换到 Forward+ 后附加光全没了,先检查三件事:是否声明了
_FORWARD_PLUS;inputData.normalizedScreenSpaceUV 和 positionWS 是否赋值(漏赋值时编译能过,但所有像素都落在同一个 tile 上,光照会错乱成块状);是否漏掉了附加方向光的那个单独循环。