UE5.8 灯光类型与移动性:一次把灯放明白
小林入职第三周,主美扔给他一个废弃地铁站的关卡。要求很明确:天花板上的日光灯要微微闪烁,站台尽头的阳光得随时间移动,墙上的应急灯要能映出人物影子。小林打开 Outliner,把几十盏灯全勾成了 Movable。他心想,这样最省事,想怎么动就怎么动。
按下调 Play 的那一瞬间,帧数只剩 18。老周端着咖啡走过来,只看了一眼 Light Complexity 视图就笑了。灯光移动性不是简单的开关,它是一份预算表。Static、Stationary、Movable 三条路,每一条路都有代价。UE5.8 在这一点上没变,只是 Lumen 和 Virtual Shadow Maps 让某些选择变得宽容了一点。
一、Static / Stationary / Movable 到底是什么
Static 灯会把光照和阴影全部烘焙进 Lightmap。构建一次之后,运行时不消耗动态光照开销。代价也明显:灯不能移动,不能改颜色,不能开关,投下的影子也不会动。远处根本不会走近的廊灯、背景里的街灯、永远不变的天光,适合这条路。
Stationary 灯是折中。它的间接光照会烘焙到 Lightmap,直接光和阴影走实时计算。颜色、强度可以在运行时调,但位置不能变。一个场景里重叠的 Stationary 阴影灯最多四盏,超过这个数就会触发 Overlap 规则。多数室内主光、太阳光、路灯用 Stationary 最顺手。
Movable 灯完全实时。位置、颜色、方向、强度都能每帧变化,阴影也实时绘制。代价按灯的数量和复杂度线性增长。闪烁的霓虹、跟着玩家走的火把、舞台上的追光灯,才是它该去的地方。
下面这张图把三条数据流拆开来看:
flowchart TD
subgraph source ["灯光源"]
static["Static Light"]
stationary["Stationary Light"]
movable["Movable Light"]
end
subgraph compute ["光照计算"]
lightmass["Lightmass 烘焙"]
indirect["间接光 Lightmap"]
direct["实时直接光"]
shadow["动态阴影"]
end
subgraph runtime ["运行时"]
gpu["GPU 绘制"]
lumen["Lumen GI"]
end
static --> lightmass
lightmass --> indirect
stationary --> indirect
stationary --> direct
movable --> direct
direct --> shadow
indirect --> gpu
direct --> gpu
shadow --> gpu三条路径分别对应不同的成本结构。Static 把大部分工作前置到构建阶段;Stationary 把间接光前置,直接光后置;Movable 把所有压力都堆到 GPU 上。
Static 灯的位置、旋转、颜色、强度都不能改。如果你把它 intensity 设为 0,运行时不会真正关闭,烘焙结果里它仍然占着 Lightmap 空间。想实现开关效果,得把影响范围移走或用 Stationary/Movable。Stationary 灯虽然允许运行时改颜色和强度,但移动位置后它会被强制重新计算阴影,效果等同一次破坏性的切换。Movable 灯最贵的地方在于它每次都会被加入光照列表,GPU 要逐像素判断它是否影响当前像素,半径越大,这个判断涉及的像素越多。
二、烘焙与动态阴影的权衡
烘焙的核心思路是把重复计算提前做完。Lightmass 或 GPU Lightmass 会求解辐射度方程,把反弹光压成 Lightmap 纹素。运行时只需要采样这张贴图,成本几乎可以忽略。问题是,只要灯光或几何体发生任何变化,烘焙结果就失效,必须重新构建。
动态阴影则相反。每帧根据当前光源和遮挡物重新生成阴影贴图。灵活性高,但 Draw Call、阴影贴图分辨率、几何处理都会计入 GPU 时间。Shadow Map 分辨率不够,影子边缘会锯齿;分辨率太高,显存和带宽又吃不消。
一盏点光源在距离 处的强度可以近似写成:
其中 是光源初始强度, 是为了避免除零的小偏置。这个式子告诉我们,灯光影响范围越大,单位像素分摊到的能量越低,衰减也越狠。想让远处还亮,要么提高 ,要么把半径拉大。半径拉大意味着更多像素进入光照计算,性能也跟着涨。
选择策略很直接:能不动的灯就烘焙;必须动但位置不变的灯用 Stationary;非得每帧变的灯再用 Movable。
flowchart TD
A["这盏灯需要每帧变化吗"] -->|"是"| B[Movable]
A -->|"否"| C["位置会变化吗"]
C -->|"是"| D["改成 Stationary 或重新设计"]
C -->|"否"| E["需要实时阴影吗"]
E -->|"是"| F[Stationary]
E -->|"否"| G[Static]烘焙时间也是成本。复杂场景里,CPU Lightmass 可能需要几十分钟,GPU Lightmass 能快一个数量级,但对显存有要求。Stationary 灯在构建时只烘焙间接光,所以构建时间通常比全 Static 场景短,但比全 Movable 场景长。Movable 灯不参与烘焙,构建阶段很快,运行时却把账一次还清。所以项目初期全 Movable 看起来开发快,到了优化阶段就会发现自己欠了一屁股债。反过来看,Static 和 Stationary 需要在前期多花点时间布局,但运行时的回报是稳定的帧率。
Stationary 灯还有一个常被忽略的优化:静态物体的阴影只渲染一次并缓存到 Shadow Map 中,只有动态物体才会每帧重绘。这意味着在一个以静态几何为主的室内场景里,Stationary 阴影的实际开销比你想象的小。一旦动态物体变多,或者灯光颜色频繁变化,缓存失效变多,开销才会接近 Movable。
三、四种常用灯光类型
UE5.8 里灯光类型不算多,但每一种都有自己的脾气。
方向光用来模拟太阳或月亮这种远距离光源。它的光线平行,不受位置影响,只看旋转。方向光的阴影通常用 Cascaded Shadow Maps 或 Virtual Shadow Maps。Cascades 把视锥切成几段,近处阴影分辨率高,远处低。PC 上 Virtual Shadow Maps 可以提供更高质量的远距离阴影,但对几何体剔除和缓存管理有要求。
点光源向四面八方发光,适合灯泡、蜡烛、火把。它的衰减由半径控制,阴影需要渲染六次或一次立方体贴图。点光源的代价对半径很敏感,半径加倍,覆盖的像素数大致变成四倍。
聚光灯有一个锥形影响区域,可以设置内角和外角,适合手电筒、舞台灯、车头灯。它的阴影只需要一次透视投影,开销通常比同半径的点光源低。配合 IES 光度学文件,可以模拟真实灯具的光强分布。
区域光在 UE 里主要用 Rect Light 表示。它有面积,发光面是一个矩形,能产生柔和的镜面高光和更真实的反射。对 Lumen 和反射来说,区域光是提升质感的捷径。窗户、灯箱、柔光箱常用它。
下面这张图用简单几何示意四种灯的覆盖范围:
方向光和区域光负责氛围,点光源和聚光灯负责戏剧性的局部照明。把这四类灯混在一起,才能得到有层次的画面。
实际搭景时,我习惯先用方向光定整体时间,再用 Rect Light 补窗户进光,最后用点光源和聚光灯勾出高光和剪影。霓虹招牌这种需要颜色快速变化的元素,直接上 Movable 点光源或聚光灯,配合 Light Function 做出闪烁。远处的街灯和天花板日光灯用 Stationary,靠近玩家的火把用手持 Movable。每个灯各司其职,画面才不会糊成一片。
四、Stationary Light Overlap
Stationary 灯有个硬规则:在同一个重叠体积内,最多只能有四盏 Stationary 灯投射实时阴影。第五盏要么变成全动态阴影,要么被编辑器标红警告,具体行为取决于你的项目设置和版本。
这个限制来自阴影图集。UE 把 Stationary 灯的阴影打包进一张 Atlas,每个灯分配一块固定区域。四盏是目前阴影图集布局的一个折中。超过四盏后,要么动态切换,要么需要额外图集,性能会突然跳变。
用下面这张图感受一下:
graph TD
A["区域 A"] --> B["Stationary 1"]
A --> C["Stationary 2"]
A --> D["Stationary 3"]
A --> E["Stationary 4"]
A --> F["Stationary 5"]
F --> G{Overlap > 4}
G --> H["切换为动态阴影"]
G --> I["编辑器标红"]实际工作中,我会打开 Show > Visualize > Stationary Light Overlap 来看哪些区域触发了限制。红色区域就是需要拆灯或改移动性的地方。常见做法是:保留最重要的四盏阴影灯,把其余改成 Static 或 Movable。Static 不投实时阴影,自然不算数;Movable 自己管理阴影缓存,也不占 Stationary 图集。
Overlap 只看投射阴影的 Stationary 灯。你把 Cast Shadows 关掉,或者把 Mobility 改成 Static/Movable,它就不占那四个名额。很多新手看到一片红,第一反应是删灯,其实更合理的做法是分层:重要的角色照明保留阴影,背景装饰灯关闭阴影或改成 Static。Unreal 的 Show > Visualize > Stationary Light Overlap 会把重叠区域染成红色,重叠数量越深越红。红线通常出现在走廊、小房间、吊灯密集的地方。
五、阴影质量与性能
阴影是灯光开销里最容易失控的部分。UE5.8 默认使用 Virtual Shadow Maps,方向光、点光源、聚光灯都能用。VSM 用虚拟页表管理高分辨率阴影,只在需要的地方分配显存。听起来很美,但 Nanite 几何体、动态物体、大量灯光同时变化时,页表更新和缓存失效会让 GPU 时间飙升。
方向光的阴影质量由 Cascade 数量和分辨率决定。设视锥被切为 段,最近一段的阴影分辨率是 ,那么 Cascade 覆盖距离 时的有效分辨率大致满足:
越小,近处阴影越清晰,但远处的分辨率会被摊薄。如果想远近都清楚,就得加 Cascade 或加分辨率,两者都会让成本上升。
Shadow Bias 和 Contact Shadows 也需要调。Bias 太大会产生 Peter-Panning,影子和脚分离;Bias 太小又会出现 Shadow Acne。Contact Shadows 可以补近距离的细节,但只在屏幕空间生效,对远处大影子没帮助。
VSM 的页表大小由控制台变量 r.Shadow.Virtual.MaxPhysicalPages 控制。默认值在多数场景够用,但当灯光数量多、每帧移动范围大时,页表会频繁换页,出现阴影闪烁或分辨率骤降。调大页表可以缓解,但会占用更多显存。移动端不支持 VSM,所以移动项目的阴影策略必须回到传统 Shadow Map。
调试时我常用这组控制台命令:
# 查看当前 Virtual Shadow Maps 状态
stat shadowrendering
# 显示灯光复杂度
viewmode lightcomplexity
# 可视化 Stationary Light Overlap
ShowFlag.VisualizeStationaryLightOverlap 1这些命令在编辑器运行时和独立游戏里都能用。重点是不只看单一指标,Shadow、Light Complexity 和 GPU 时间要一起看。
六、Light Function
Light Function 是一张材质,灯光会把它投射到被照亮的表面上。用它可以做云影、树影、百叶窗条纹、舞台图案,甚至简单的水下焦散。它的本质是给灯光加一个调制图案。
在内容浏览器里新建一个 Material,把 Material Domain 改成 Light Function,就能往里面连节点。常用的是 Time、Sine、Cloud 噪声或一张灰度纹理。复杂程度会直接影响每盏灯的像素开销。Mobile 平台上尤其要小心,因为 Light Function 每像素采样都会计入 Fragment Shader。
Static 灯使用 Light Function 时,结果会一起烘焙进 Lightmap。Stationary 灯会把它应用到实时直接光上。Movable 灯则每帧重新采样。换句话说,Light Function 不会替你省掉移动性本身的成本,它只是让同一盏灯看起来更丰富。
下面是一个最简 Light Function 材质的节点描述:
Material Domain: Light Function
Emissive Color:
Texture Sample (CloudMask) * Sine(Time * Speed)实际做的时候,CloudMask 用灰度图,Sine 控制云的漂移。把这个材质拖到灯光的 Light Function 槽里就能生效。
Light Function 材质的 UV 会跟随灯光的投射方向。聚光灯的投射方向就是锥形方向;方向光的 UV 沿着光线方向展开;点光源则会被投影到六个面。你可以用 World Position、Camera Position 或 Time 节点让图案动起来。警车灯、摇曳的树影、云遮太阳,都是 Light Function 的典型应用。唯一要记住的是,越复杂的材质,每像素开销越高,Mobile 上尽量少用带噪声采样的 Light Function。
七、与 Lumen 的结合
Lumen 在 UE5 里负责动态全局光照和反射。它让小场景里少烘焙几盏灯成为可能。但 Lumen 不直接替灯光投阴影,也不改变 Static 灯的性质。
一盏 Movable 灯配合 Lumen 时,它的直接光走常规阴影贴图,间接漫反射由 Lumen 估算。Lumen 用 Surface Cache 记录最近表面的光照信息,再用屏幕空间追踪和硬件光追去收集反射。最终像素颜色由直接光和间接光共同决定。
Stationary 灯的间接光已经烘焙到 Lightmap,所以 Lumen 主要补动态物体的间接光。比如一个 Movable 角色走进 Stationary 台灯旁边,Lumen 会估算角色从墙面和地面接收到的二次反射。Static 灯则完全不参与 Lumen,因为所有信息都在烘焙时写死了。
Lumen 的反射和 GI 是分开的两条线。反射通过 Lumen Reflections 计算,依赖场景中的 Emissive 材质和灯光亮度。区域光和自发光面在 Lumen 反射中表现很好,这也是 Rect Light 在 UE5 里地位上升的原因之一。Lumen 的 Software Ray Tracing 需要生成 Mesh Distance Fields,对大型开放世界来说,DF 的分辨率和精度会直接影响反射质量。Hardware Ray Tracing 则依赖显卡 RT Core,在支持的平台上有更稳定的质量。
sequenceDiagram
participant 像素
participant 直接光
participant 阴影
participant Lumen
participant Lightmap
像素->>直接光: 请求 Movable 灯颜色
直接光->>阴影: 检查可见性
阴影-->>直接光: 返回 shadow factor
直接光-->>像素: 返回直接光照
像素->>Lumen: 请求间接光
Lumen->>Lumen: Surface Cache + Screen/RT Traces
Lumen-->>像素: 返回漫反射 GI
像素->>Lightmap: 请求烘焙 GI
Lightmap-->>像素: 返回 Static/Stationary 间接光
像素->>像素: 合成最终颜色在高端 PC 和主机上,Lumen 可以开启 Hardware Ray Tracing,质量更高。中低端 PC 或移动平台通常用 Software Ray Tracing,依赖 Screen Space 和 Mesh Signed Distance Fields,对场景复杂度更敏感。
八、平台差异
同样的灯光设置,在不同平台上表现可能差很远。
高端 PC 是最宽容的。Lumen、Virtual Shadow Maps、Hardware Ray Tracing 都能开。Movable 灯数量可以多一点,阴影分辨率也可以拉高。但宽容不代表无限,帧时间预算 still 是硬道理。我通常把 PC 上的 Movable 灯控制在影响范围内的数量,避免一盏半径巨大的点光源照亮半个关卡。
主机平台(PS5、Xbox Series X/S)硬件和高端 PC 接近,但内存和带宽是固定预算。主机上更倾向于 Stationary + 烘焙 GI,把动态部分留给必须动的物体。Virtual Shadow Maps 在主机上可用,但页表大小和缓存策略需要专门调。
移动平台完全是另一套规则。默认 Forward Rendering,没有 Lumen,也不支持 Virtual Shadow Maps。Movable 灯走 Tile 或 Cluster Deferred,数量受严格限制,且投射阴影的开销极高。移动端的阴影常用 Modulated Shadows 或 Cascade Shadow Maps,精度低、消耗小。做移动端项目时,我的习惯是:主光 Stationary 或 Static,补光用 Static,只有玩家手持光源或特效用 Movable。
VR 平台额外关注光照一致性。左右眼都需要稳定的光照和阴影,动态灯光过多容易造成抖动或帧率不稳定。VR 里我优先烘焙静态环境光,动态光源只保留少数几个。
跨平台项目建议把灯光分组并做平台开关。PC 上开 Lumen 和 VSM,主机上保留 Stationary 烘焙,移动端尽量全 Static。通过 Device Profiles 和 CVar 可以针对不同平台自动切换阴影质量和灯光数量。
结尾
小林按照老周的建议改了一遍。地铁站的日光改成 Stationary,日光灯改成 Static 但用一个 Movable 的 Rect Light 模拟闪烁,火把和应急灯保持 Movable。帧率回到了 55 到 60 之间。他再打开 Stationary Light Overlap 视图,红色区域只剩一小块,稍微挪了一盏补光就干净了。
灯光移动性的选择,说到底就是算账。Static 把成本搬到构建时,Stationary 把大部分间接光也搬走,Movable 则要每帧都买单。UE5.8 给你了 Lumen 和 Virtual Shadow Maps 这样的好工具,但工具不会替你决定哪盏灯值得花钱。这个决定得靠你对场景、平台和预算的理解。
先把灯分组,再给每一组选一条合适的路。别把所有灯都设成 Movable,除非你真的付得起。