从 _LightColor0 到 _AdditionalLightsBuffer
01为什么需要了解这个?
如果你在 Unity URP 中手写过自定义 Shader,一定遇到过这样的困惑:
- 明明场景里放了多个光源,Shader 却只响应主光源(Main Light)
- 内置管线中熟悉的
_LightColor0、_WorldSpaceLightPos0变量去哪了? - 官方的
Lit.shader能正确渲染多光源,自己写的却不行
核心原因只有一个:URP 的光照数据传递方式与内置管线完全不同。

⚠️
常见陷阱
如果在 Fragment Shader 中只采样了主光源数据(MainLight()),而没有循环处理 Additional Lights,那么场景中的点光源、聚光灯将全部"消失",只有方向光生效。
02数据传递架构对比
下图直观展示了两种管线在光照数据传递上的本质差异:

💡
关键洞察
URP 的设计哲学是:数据与逻辑分离。引擎负责把所有光源打包进 Buffer,Shader 负责按需遍历。这种模式支持动态数量的光源,且不会浪费未使用的 Uniform 插槽。
03核心代码详解
3.1 正确的多光照处理流程
以下是一个完整的 URP Fragment Shader 多光照处理示例。每一行都附有详细注释:
// ============================================================
// URP 多光源 Fragment Shader 完整示例
// 需要包含 URP 的 Lighting.hlsl 来获取光照相关函数
// ============================================================
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
half4 frag(Varyings input) : SV_Target
{
// ---- ① 获取世界空间位置和法线 ----
float3 worldPos = TransformObjectToWorld(input.positionOS);
float3 normal = normalize(TransformObjectToWorldNormal(input.normalOS));
// ---- ② 处理【主光源】(Main Light)----
// 主光源通常是场景的方向光(Directional Light)
Light mainLight = GetMainLight();
// 计算主光源贡献:漫反射 + 镜面反射
half3 mainDiffuse = max(0, dot(normal, mainLight.direction))
* mainLight.color * mainLight.shadowAttenuation;
half3 lightingResult = mainDiffuse;
// ---- ③ 【关键!】循环处理所有额外光源 ----
// _AdditionalLightsCount 由 URP 引擎自动设置
for (uint i = 0; i < _AdditionalLightsCount; i++)
{
// 从 Buffer 中取出第 i 盏额外光源的数据
Light addLight = GetAdditionalLight(i, worldPos);
// 计算该光源的 N·L 漫反射
half3 addDiffuse = max(0, dot(normal, addLight.direction))
* addLight.color
* addLight.distanceAttenuation // 距离衰减
* addLight.shadowAttenuation; // 阴影衰减
// 【累积】叠加到最终光照结果
lightingResult += addDiffuse;
}
// ---- ④ 组合材质颜色并输出 ----
half3 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb;
half3 finalColor = baseColor * lightingResult;
return half4(finalColor, 1.0);
}
04关键 API 逐行拆解
4.1 GetMainLight() —— 主光源
// Lighting.hlsl 中的定义(简化版)
Light GetMainLight()
{
Light light;
light.direction = _MainLightPosition.xyz; // 方向
light.color = _MainLightColor.rgb; // 颜色
light.distanceAttenuation = 1.0; // 主光源无距离衰减
light.shadowAttenuation = SampleMainlightShadow(); // 采样阴影贴图
return light;
}
注意
GetMainLight() 不需要传入参数,因为它始终对应场景中的主方向光。返回的 Light 结构体包含四个关键字段。
4.2 GetAdditionalLight(i, worldPos) —— 额外光源
这是整个机制的核心函数。它接收两个参数:
Light GetAdditionalLight(uint index, float3 positionWS) 2 ↑ ↑ 3 第几盏光 世界坐标 4 (0~N-1) (用于计算衰减)
为什么需要传 worldPos?因为点光源和聚光灯的 distanceAttenuation(距离衰减)依赖于表面点到光源的距离计算——这是方向光不需要的。
4.3 Light 结构体的完整字段
struct Light
{
half3 direction; // 光照方向(指向光源)
half3 color; // 光源颜色(已包含强度)
half distanceAttenuation; // 距离衰减系数 [0,1](点光/聚光有效)
half shadowAttenuation; // 阴影衰减系数 [0,1]
uint layerMask; // 光照层遮罩
};
05❌ 常见错误写法 vs ✅ 正确写法

06底层原理:_AdditionalLightsBuffer 是什么?
_AdditionalLightsBuffer 并非一个简单的数组,它的实际类型取决于目标平台:
06
底层原理:_AdditionalLightsBuffer 是什么?
_AdditionalLightsBuffer 并非一个简单的数组,它的实际类型取决于目标平台:
平台自适应 Buffer 类型定义
// ============================================================
// URP 内部根据着色器目标平台,条件编译选择 Buffer 类型
// 文件路径: RealtimeLights.hlsl
// ============================================================
#if defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)
// ── OpenGL ES 平台:使用 CBuffer(常量缓冲区)──
// GLES 不支持 StructuredBuffer,退回到固定大小的 CBuffer
// 通常限制为 URP_MAX_LIGHTS(默认 8~16)个光源
CBUFFER_START(AdditionalLights)
float4 _AdditionalLightsPosition[URP_MAX_LIGHTS];
half4 _AdditionalLightsColor[URP_MAX_LIGHTS];
half4 _AdditionalLightsAttenuation[URP_MAX_LIGHTS];
half4 _AdditionalLightsSpotDir[URP_MAX_LIGHTS];
half4 _AdditionalLightsOcclusionProbes[URP_MAX_LIGHTS];
CBUFFER_END
#else
// ── 现代 GPU 平台:使用 StructuredBuffer ──
// 支持 DX11/Vulkan/Metal,可变长度,更高效
STRUCTURED_BUFFER(LightData) _AdditionalLightsBuffer;
// 每个 LightData 包含:position, color, attenuation,
// spotDirection, layerMask 等紧凑打包数据
#endif
两种方案对比
📱 CBuffer(移动端/GLES)
- 兼容性最好,所有 GPU 都支持
- 固定大小上限(通常 8~16 灯)
- 占用 Uniform 内存,受限于常量缓冲区大小
- 数据通过 CPU→GPU 每次 Draw Call 更新
🖥️ StructuredBuffer(PC/主机)
- 需要 Shader Model 4.0+(DX11+)
- 动态大小,无硬性数量上限
- 存储在显存专用区域,不影响 Uniform 限制
- 通过 ComputeBuffer.SetData() 批量上传
07性能优化实践建议
🔥
性能敏感点
每个像素都要执行 for 循环遍历所有额外光源。当场景中有大量光源时,这会成为明显的性能瓶颈,尤其在移动端。
优化策略清单
- 控制光源数量
在 URP Settings 中设置
Render Pipeline Asset → Main Light / Additional Lights的最大数量上限。移动端建议不超过 4 盏额外光源。 - 使用 Per-Vertex 代替 Per-Pixel 额外光照
// 在 Vertex Shader 中预先计算 Additional Lights
// 大幅减少 Fragment Shader 的循环开销
Varyings vert(Attributes input)
{
Varyings output;
float3 worldPos = TransformObjectToWorld(input.positionOS);
// 在顶点阶段就把额外光照算好!
half3 vertexLight = 0;
for (uint i = 0; i < _AdditionalLightsCount; i++)
{
Light light = GetAdditionalLight(i, worldPos);
vertexLight += light.color * light.distanceAttenuation
* max(0, dot(normal, light.direction));
}
output.vertexLighting = vertexLight; // 传给片元
return output;
}
- 利用 Light Layer / Rendering Layer Mask
让特定光源只影响特定物体,减少不必要的 Buffer 遍历。 - Reflection Probe 替代实时点光源
静态环境可用反射探针或烘焙光照贴图替代实时点光源。 - SRP Batcher 兼容
确保你的 Shader 中光照相关的 CBuffer 变量名与 URP 一致,否则会打断 SRP Batcher 合批。
08总结

✨
一句话总结
URP 把「多光源数据」装进 _AdditionalLightsBuffer,Shader 必须主动循环遍历才能获取每一盏灯的贡献。这不是可选操作,而是手写 Lit Shader 的必要步骤。
4551

被折叠的 条评论
为什么被折叠?



