Unity Shader 光照衰减:点光源与聚光灯的衰减计算技巧

深入 URP 渲染管线内核,解析不同光源类型的距离衰减与角度衰减算法,掌握自定义 Shader 中精确控制光照强度的方法。

01 · 基础概念

什么是光照衰减?

在物理世界中,光从光源发出后,随着传播距离的增加,其照亮表面的能力会逐渐减弱,这就是光照衰减(Light Attenuation)。正确模拟衰减效果是实现真实感渲染的关键步骤之一。

在传统渲染中,最常见的做法是使用平方反比定律:光照强度与距离的平方成反比。然而,在实时渲染中,纯粹的物理公式往往需要被修改,以便在保持视觉真实性的同时控制计算成本和行为边界。

💡 注意

URP 并不直接使用上述三项衰减公式,而是采用了一套精心设计的近似算法,在保证视觉质量的同时大幅降低 GPU 指令数。了解其底层逻辑是自定义高性能 Shader 的前提。

02 · 架构解析

URP 的光照衰减架构

URP 将光照数据以 Light 结构体的形式提供给 Shader。核心帮助函数位于 Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl 中。

整个衰减流程可以拆分为两个正交维度:

维度适用光源控制参数URP 内置函数
距离衰减点光源 · 聚光灯Range(灯光范围)DistanceAttenuation()
角度衰减聚光灯Inner / Outer AngleAngleAttenuation()
阴影衰减所有投影光源Shadow MapGetMainLight() / GetAdditionalLight()
方向光方向光无距离概念(无衰减,始终 = 1)

URP 将附加光源(Additional Lights)的参数打包到一个常量缓冲区数组中,Shader 通过 GetAdditionalLight(lightIndex, worldPos) 一次性获取已经计算好衰减系数的 Light 结构体。但如果想精细控制,就需要理解它内部的计算方式。

03 · 点光源

点光源距离衰减

URP 中点光源的距离衰减不使用原始平方反比,而是使用一个经过精心设计的平滑截断函数,确保在光源范围边界处平滑降为零,避免硬截边。

核心算法

其本质是将物理正确的平方反比与一个基于 Range 的平滑窗口函数相乘:

查看 URP 源码实现

以下是 URP ShaderLibrary 中 DistanceAttenuation 的核心实现,逐行解读:

// 点光源/聚光灯 距离衰减(URP 14 核心实现)
real DistanceAttenuation(real distanceSqr, half2 distanceAttenuation) {
    // distanceAttenuation.x = 1 / range²  (由 CPU 预计算传入)
    // distanceAttenuation.y = 1.0 - (1/range²)^2  (平滑系数)
    // 步骤1:计算归一化平方距离
    real lightAtten = 1.0 / max(distanceSqr, 0.0001);
    // 步骤2:计算平滑窗口因子(防止超出 range 时出现硬截边)
    real factor = distanceSqr * distanceAttenuation.x;
    real smoothFactor = saturate(1.0 - factor * factor);
    // 步骤3:二次平方使曲线更平滑(Frostbite 技巧)
    smoothFactor = smoothFactor * smoothFactor;
    // 步骤4:最终结果 = 物理衰减 × 平滑截断
    return lightAtten * smoothFactor;
}

⚡ 关键洞察

distanceAttenuation.x 的值 1/range² 由 CPU 端的 LightData.cs 预先计算并上传 GPU,避免在 Shader 中进行除法。这是 URP 的典型性能优化策略。

04 · 聚光灯

聚光灯角度衰减

聚光灯在点光源距离衰减的基础上,额外施加一个角度衰减(Angle/Spot Attenuation)。它控制从聚光灯轴线方向向外扩展时光照强度的过渡,分为内角(完整亮度)和外角(完全黑暗)两个区域。

角度衰减的数学推导

URP 使用向量点积计算当前像素方向与聚光灯轴线方向的夹角余弦值,再利用线性映射将余弦值转换到 [0,1] 范围,最后应用 smoothstep 过渡:

CPU 端预计算参数(C# 侧)

// CPU 端将 Spot 参数打包为 Shader 可直接使用的形式
static void GetLightAttenuationAndSpotDirection(
    out float lightAttenuation,
    out Vector4 lightSpotDir)
{
    // 外角余弦 cosOuter = cos(outerAngle * 0.5)
    float cosOuterAngle = Mathf.Cos(Mathf.Deg2Rad * outerAngle * 0.5f);
    float cosInnerAngle = Mathf.Cos(Mathf.Deg2Rad * innerAngle * 0.5f);
    // 线性映射系数:将 [cosOuter, cosInner] 映射到 [0, 1]
    float delta = cosInnerAngle - cosOuterAngle;
    float sScale = 1.0f / Mathf.Max(delta, 0.001f);
    float sOffset = -cosOuterAngle * sScale;
    // 打包到 lightSpotDir.w (x = scale, y = offset)
    lightSpotDir = new Vector4(dir.x, dir.y, dir.z, sScale);
    lightAttenuation = sOffset; // 在其他 Vector4 中存 offset
}

Shader 端角度衰减实现

// 聚光灯角度衰减函数
real AngleAttenuation(half3 spotDirection,
                      half3 lightDirection,
                      half2 spotAttenuation)
{
    // 计算像素方向与聚光轴的点积(即夹角余弦)
    real SdotL = dot(spotDirection, lightDirection);
    // 线性映射:spotAtten.x = scale, spotAtten.y = offset
    real atten = saturate(SdotL * spotAttenuation.x + spotAttenuation.y);
    // 二次平方:使外边缘过渡更柔和
    return atten * atten;
}

🎯 关键技巧

角度衰减使用的是余弦空间的线性插值而非角度空间的线性插值。由于余弦是非线性的,实际视觉效果上外圈会比内圈有更平滑的过渡——这正是真实聚光灯的物理特性。

05 · 自定义实现

在自定义 Shader 中完整实现

了解了内部机制后,下面给出一个完整的、可在 URP 自定义 Lit Shader 中使用的点光源 + 聚光灯衰减实现,包含手动计算衰减的完整流程:

// ━━━ 头文件引入 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
// ━━━ 自定义衰减计算函数 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
half3 CustomAdditionalLighting(float3 worldPos,
                               float3 worldNormal,
                               half3  albedo)
{
    half3 result = 0;
    int count = GetAdditionalLightsCount();
    for (int i = 0; i < count; i++) {
        // ① 获取附加光源数据(已包含衰减预计算)
        Light light = GetAdditionalLight(i, worldPos);
        // ② 提取距离衰减(已融合角度衰减)
        half distAtten   = light.distanceAttenuation;
        half shadowAtten = light.shadowAttenuation;
        half totalAtten  = distAtten * shadowAtten;
        // ③ Lambert 漫反射 NdotL
        half NdotL = saturate(dot(worldNormal, light.direction));
        // ④ 合并贡献:颜色 × NdotL × 总衰减 × 反照率
        result += light.color * NdotL * totalAtten * albedo;
    }
    return result;
}

手动计算距离衰减(不依赖内置函数)

当你需要完全自定义衰减曲线(如游戏特效中使用非物理的线性衰减),可以绕过 URP 内置计算:

// ─── 自定义点光源衰减(线性 + 平滑截断)───
half CustomPointAttenuation(float3 worldPos,
                             float3 lightPos,
                             half   lightRange)
{
    float3 diff = lightPos - worldPos;
    float  dist = length(diff);
    // 使用物理平方反比
    half physicalAtten = 1.0 / (1.0 + dist * dist);
    // 平滑截断窗口(Frostbite 风格)
    half normalizedDist = dist / lightRange;
    half window = saturate(1.0 - normalizedDist * normalizedDist);
    window = window * window;  // 二次平方使截断更顺滑
    return physicalAtten * window;
}
// ─── 自定义聚光灯衰减(距离 × 角度)──────────────
half CustomSpotAttenuation(float3 worldPos,
                            float3 lightPos,
                            float3 spotDir,
                            half   lightRange,
                            half   cosInner,
                            half   cosOuter)
{
    // ① 距离衰减(复用点光源函数)
    half distAtten = CustomPointAttenuation(worldPos, lightPos, lightRange);
    // ② 计算像素到光源方向(归一化)
    float3 L = normalize(lightPos - worldPos);
    // ③ 角度余弦:L 与 spotDir 点积
    half cosAngle = dot(spotDir, L);
    // ④ 线性映射 [cosOuter, cosInner] → [0, 1]
    half angleAtten = saturate((cosAngle - cosOuter) /
                             max(cosInner - cosOuter, 0.001));
    angleAtten = angleAtten * angleAtten;
    // ⑤ 最终衰减 = 距离衰减 × 角度衰减
    return distAtten * angleAtten;
}

06 · 性能优化

性能优化技巧

⚠️ 移动端注意

在移动端 GPU 上,length() 函数(含 sqrt)开销较高。应尽可能使用 lengthSq = dot(diff, diff)(平方距离)延迟开根号,直到必须使用时再计算。

  • 使用平方距离 — 衰减计算全程使用 distanceSqr 而非 distance,最终才开根号,节省 GPU 开销

  • CPU 端预计算 — 将 1/range²cosInner/cosOuter 的线性映射系数在 C# 端算好,以常量形式传入,避免 Shader 中重复计算

  • half 精度 — 衰减值最终是 [0,1] 范围,使用 half 而非 float,在移动端 GPU 可获得 2x 吞吐量提升

  • Forward+ / Deferred 路径 — 在 URP 14+ 中启用 Forward+,通过 Light Tile Binning 减少无效光源循环迭代次数

  • 合理设置 Range — 点光源 Range 设置过大会导致更多 Tile 被该光源影响,尽量精确设置可视范围

  • Shader Variant 管理 — 启用 _ADDITIONAL_LIGHTS keyword 才会编译附加光源循环,关闭可显著减少 Shader 变体体积

  • UNITY_LOOP 提示 — 在 PC 平台为附加光源 for 循环加 [loop] 属性,防止展开;在移动端谨慎使用动态分支

光源类型衰减对比总结

光源类型距离衰减角度衰减阴影性能开销
方向光无(始终 1)级联阴影最低
点光源平方衰减 + 平滑截断Cube 阴影图中等
聚光灯平方衰减 + 平滑截断余弦线性映射 + 二次平方Spot 阴影图较高
区域光距离衰减(烘焙)形状遮挡(烘焙)仅 Lightmap极低(仅烘焙)

🚀 最佳实践建议

对于大多数场景,直接使用 GetAdditionalLight(i, worldPos) 获取包含完整衰减的 Light 结构体是最优选择。只有当你需要非物理风格的特效光照(如卡通渲染、赛璐璐等)或需要在衰减之上叠加自定义效果时,才需要手动实现本文中的底层计算。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值