Unity Shader URP 光照数据传递机制

从 _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 循环遍历所有额外光源。当场景中有大量光源时,这会成为明显的性能瓶颈,尤其在移动端。

优化策略清单

  1. 控制光源数量

    在 URP Settings 中设置 Render Pipeline Asset → Main Light / Additional Lights 的最大数量上限。移动端建议不超过 4 盏额外光源。

  2. 使用 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;

}
  1. 利用 Light Layer / Rendering Layer Mask
    让特定光源只影响特定物体,减少不必要的 Buffer 遍历。
  2. Reflection Probe 替代实时点光源
    静态环境可用反射探针或烘焙光照贴图替代实时点光源。
  3. SRP Batcher 兼容
    确保你的 Shader 中光照相关的 CBuffer 变量名与 URP 一致,否则会打断 SRP Batcher 合批。

08总结

一句话总结

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值