从”普通程序员能听懂”到”能改 UE 光照源码”——基于源码逐行考证,六条主线全覆盖:
烘焙流程 / 自动化展开 UV / 压缩 Lightmap / 提高填充率 / 把 UE 当烘焙器 / Unity 场景导入
(材质、灯光、地表对齐)
- 文档版本:1.0(2026-08-26)
- 源码快照:本地仓库
d:\Project\GameDevelop\UnrealEngine,UE 5.9 移动优化 fork,HEAD6cea9bd20f8f - 写作原则:文中所有”源码考古”块引用的文件路径、行号、函数名均在本仓库中实际验证过;行号只用于定位,请以
路径:函数名为准 - 姊妹文档:本文是《Nanite》《VSM》《UE 网络架构》《空间音频》之后的第五篇——前四篇是纯 GPU/纯实时主题,本篇是第一条离线管线主线(CPU 构建 + GPU 渲染消费 + 跨引擎工作流)
- 符号说明:
Lightmap=光照贴图,Bake=烘焙,Texel Density=纹素密度,Atlas=图集,Chart=UV 岛,Padding=边距,Packing=装箱,Tile=瓦片;HQ/LQ/Scale/Add/SH不翻译
第 0 章 导读:这份文档怎么读
TL;DR|这份文档把 UE Lightmap 拆成六条主线:烘焙流程(GPU Lightmass 怎么把场景
“烤”成光照贴图)、自动化展开 UV(让引擎替你摊橘子皮)、压缩 Lightmap(内存瘦身)、
提高填充率(拼图利用率)、把 UE 当烘焙器(离线光照工厂)、Unity 场景导入
(材质/灯光/地表对齐)。心智模型:Lightmap = 给场景”烤”一层光照贴图——离线预计算,
运行时廉价读取。三个深度档位:30 秒看懂、半天学会烘焙、1 天读源码。
0.1 目标读者与前置知识
目标读者是普通程序员:会摆场景调灯光、知道 UV 与纹理贴图是什么、见过编辑器里”Build Lighting”转圈。每个术语第一次出现都配一句直觉解释。
前置知识三个:
- UV 与纹理:模型表面的”贴图坐标”——光照贴图也是贴图,同样需要 UV;
- 场景灯光与移动性:Static/Stationary/Movable 三态(第 1 章细讲);
- 烘焙(Bake):离线预计算光照,把结果存成贴图——本篇的一切围绕它展开。
与前三篇的关系:Nanite/VSM 讲实时渲染,空间音频讲听觉,网络讲多机——本篇讲**”光照怎么提前算好”**,与实时 GI(Lumen)是互补关系(第 1 章对比)。
0.2 三条阅读路径
| 通道 | 读什么 | 耗时 | 读完能做什么 |
|---|---|---|---|
| 🚀 30 秒通道 | 每章 TL;DR + 第 2 章概念类比 + 图 F1 | 10 分钟 | 跟人讲清 Lightmap 六条主线与”烘焙=拍照片”心智模型 |
| 📚 标准通道 | TL;DR + 正文 + 第 7~12 章(六专题,占全文 54%) | 6~8 小时 | 会完整烘焙关卡、会开自动 UV、会压内存、会查填充率、能搭离线烘焙流水线、能把 Unity 场景迁进 UE |
| 🔬 源码考古通道 | 全部 + 源码考古块 + 附录 A 对照 | 1~2 天 | 敢打开 GPULightmass/ 与 LightMap.cpp 对照着读 |
0.3 块标记约定
与前四篇一致的五种块:
- **
TL;DR**(每章开头):30 秒读完本章,3~5 句、必须含类比名、不含源码路径; - **
类比实验室**(正文小节):用普通程序员已懂的东西讲陌生概念; - **
源码考古**(每章末尾,折叠块):贴真实源码(≤25 行/段),逐行翻译; - **
避坑**(正文穿插):版本差异、已删除项、公开资料不一致; - **
动手实验室**(第 7~12 章):配方型内容——步骤|资产/设置|验收标准表 + 检查清单。
0.4 一张图看懂全局
读图方法:场景设置好了 → GPU Lightmass 逐 tile 路径追踪”烤”出光照 → 编码成 8bit 系数 → 装箱进 Atlas → 压缩存储 → 运行时 BasePass 廉价采样 → 显示。①~⑤ 是离线(构建期),⑥⑦ 是运行时——这就是”预计算”的全部含义。
0.5 章节地图
| 篇 | 章节 | 一句话 |
|---|---|---|
| 开篇 | 第 0~1 章 | 怎么读 + 静态光照的”预计算”哲学(Lumen vs Lightmap) |
| 概念篇 | 第 2 章 | 六大概念速览(每个一个类比) |
| 机制篇 | 第 3~6 章 | 烘焙管线 → 编码 → 装箱 → 运行时采样与流送 |
| 专题篇 | 第 7~12 章 | 六条主线(用户重点,占 54%) |
| 调优篇 | 第 13~14 章 | CVar 速查与常见坑 / 局限与展望 |
| 附录 | A~D | 源码地图、术语表、参考资料、自测题 |
第 1 章 Lightmap 是什么:静态光照的”预计算”哲学
TL;DR|Lightmap 是”给场景烤一层光照贴图“:把静态光照离线算好存成纹理,
运行时每像素读一下就行——把昂贵的实时光照计算换成廉价的贴图采样。Lumen 是
实时全局光照(贵但动态),Lightmap 是离线预烘焙(便宜但静态),Mobility 三态
(Static/Stationary/Movable)决定了灯光参与哪种方案。
1.1 烘焙 = 拍照片
类比实验室:拍照片
烘焙就像给场景”拍照片”:相机(光照求解器)花很长时间拍(计算),洗出来的照片
(Lightmap 贴图)被贴到模型表面,运行时”看照片”(采样贴图)——不重新拍。
照片是死的:灯移动了照片不会变;但看照片几乎零成本。这就是预计算的全部哲学:
把贵的计算挪到离线,把廉价的结果留给运行时。
1.2 为什么需要预计算
一个静态场景(建筑、地形、道具)的光照其实不会变——灯不动、物体不动,光照结果就是确定的。实时重算它是浪费:每帧算全局光照 = 每帧都要昂贵的路径追踪/辐射度。预计算让”确定的结果”只算一次:
- 质量:离线可以跑几百采样/像素(GPU Lightmass 默认 GISamples=512),实时 GI 只能给几十;
- 成本:运行时每像素一次贴图采样(几十纳秒)vs 实时 GI 每像素几十微秒;
- 代价:静态(灯动/物体动都需要重新烘焙)、存储(贴图占空间)。
1.3 Lumen vs Lightmap:实时 GI vs 离线烘焙
| Lumen(实时 GI) | Lightmap(离线烘焙) | |
|---|---|---|
| 计算时机 | 每帧实时 | 构建期离线 |
| 质量 | 好(但受帧预算限制) | 极高(数百采样/像素) |
| 动态物体 | ✅ 支持 | ❌ 静态 |
| 移动端 | 重(通常关) | 轻(贴图采样) |
| 存储 | 无额外 | 贴图占用 |
| 适用 | 动态光照游戏 | 静态场景/移动端 |
决策树:移动端/静态场景 → Lightmap;动态光照/高端 PC → Lumen;混合(大多数游戏)→ 静态部分 Lightmap + 动态物体 Lumen/动态光。
1.4 Mobility 三态:灯光的三档开关
ENetRole 之外,灯光还有 Mobility(Engine/Classes/Engine/EngineTypes.h 的 EComponentMobility):
| Mobility | 行为 | Lightmap 参与 |
|---|---|---|
| Static | 完全静态(烘焙全量) | ✅ 全烘焙(阴影/间接光都进 Lightmap) |
| Stationary | 位置固定但亮度可变 | ✅ 烘焙间接光 + 动态阴影遮罩(ShadowMapChannel) |
| Movable | 完全动态 | ❌ 不烘焙(Lumen/动态阴影) |
烘焙的本质:只有 Static/Stationary 灯光参与 Lightmap 烘焙;Movable 灯完全走实时。场景里全是 Movable 灯 = 烘焙了个寂寞(第 7 章检查清单第一条)。
1.5 体积光照图(Volumetric Lightmap)
VolumeLightingMethod(WorldSettings.h:123)——除了表面贴图(Lightmap),烘焙还产出体积光照图:稀疏 3D 网格(brick)存 SH 光照,供动态物体(角色/粒子)在静态光照环境中采样——“静态场景里的动态角色也有正确光照”。VolumetricLightmapDetailCellSize=200(:159)控制网格密度。
1.6 一点历史
- UE4:CPU Lightmass(UnrealLightmass,独立进程多线程求解,还有 Swarm 分布式);
- UE5:GPU Lightmass(
Engine/Plugins/Experimental/GPULightmass/)转正为默认——用 GPU 路径追踪逐 tile 收敛,FStaticLightingSystemInterface::GetPreferredImplementation()(StaticLightingSystemInterface.cpp:113)硬编码优先 GPULightmass; - 本 fork:无 Lightmap/Lightmass 改动(git 验证),移动端 one-pass 管线沿用现有 lightmap 采样路径。
1.7 小结
这一章记住三句话:
- Lightmap = 离线预计算的光照贴图(拍照片哲学);
- Lumen vs Lightmap = 实时 vs 离线——移动端/静态场景选 Lightmap;
- Mobility 三态决定参与度——只有 Static/Stationary 灯进 Lightmap。
下一章,六大概念各配一个类比。
第 2 章 六大核心概念速览(每个概念 = 一个类比)
TL;DR|Lightmap 的六个核心概念,用六个你早就懂的东西类比:纹素密度 = 光照分辨率、
UV 展开 = 把橘子皮摊平、Atlas 装箱 = 拼图游戏、填充率 = 拼图的利用率、
编码 = 把亮度塞进 8bit 行李箱、Padding = 拼图块之间的缝隙。这一章只立类比不给代码。
2.1 纹素密度(Texel Density)= 光照的”分辨率”
定义:Lightmap 上每个纹素(texel)覆盖多少世界面积。LightMapResolution(StaticMesh.h:1194-1196)是纹理边长(texel 数),实际密度 = 分辨率 ÷ 网格世界尺寸。
类比实验室:分辨率
同样的照片,4K 屏看比 720p 屏看清晰——Lightmap 同理:大模型配小分辨率 =
光照糊成一团(”光斑”);小模型配大分辨率 = 浪费显存。**纹素密度是光照的
“每平方米像素数”**,全场景统一密度(如 4 texel/cm)是烘焙质量的第一要务。
2.2 UV 展开 = 把橘子皮摊平
定义:模型表面(3D)→ UV 平面(2D)的映射。Lightmap 需要第二套 UV(LightMapCoordinateIndex,StaticMesh.h:1221,默认 UV1)——UV 岛(Chart)不得重叠、必须落在 0-1 空间,因为光照贴图是按 UV 位置写的。
类比实验室:橘子皮
把橘子皮完整摊平(不撕裂、不重叠)就是 UV 展开。Lightmap UV 要求摊得均匀
(每块面积与世界面积成比例——否则纹素密度不均)且留缝(岛与岛之间有
边距,否则采样会串色)。引擎能自动摊(第 8 章),但摊得好的关键参数
(MinLightmapResolution/通道选择)需要人懂。
2.3 Atlas 装箱 = 拼图游戏
定义:多个 Actor 的 Lightmap 打包进共享纹理(Atlas)。编辑器侧:FLightMapPendingTexture 用 skyline 装箱(4px 对齐,LightMap.cpp:457);网格侧:FLayoutUV 的 FindBestPacking(LayoutUV.h:67)做 UV 岛装箱。
类比实验室:拼图游戏
所有光照贴图拼进一张大画布(默认 1024×512,PackedLightAndShadowMapTextureSize):
每块光照贴图是拼图片,拼图器(装箱算法)负责把片放进去。片太多/太大 → 换更大的
画布或开新画布——这是显存与烘焙时间的权衡。
2.4 填充率 = 拼图的利用率
定义:Atlas 中被光照贴图实际占用的面积比例。填充率低 = 拼图有大量空白 = 浪费显存。引擎的 FindBestPackingEfficiency(LayoutUV.h:86)专门统计装箱效率。
类比实验室:拼图利用率
一堆形状怪异的拼图片(细长条、异形)放进画布,空隙多 → 利用率低。
填充率优化 = 让拼图片更”方正”(UV 岛长宽比)、让片大小更匹配
(分辨率统一)、留缝更小(padding 恰到好处)。第 10 章专讲。
2.5 编码 = 把亮度塞进 8bit 行李箱
定义:烘焙结果是高动态范围(RGBA16F),要存进 8bit 贴图——QuantizeLightSamples(LightmapEncoding.cpp:35)用 LogScale=11.5 的对数量化 + 每图 Scale/Add(min/max 归一化)把亮度”塞进行李箱”。
类比实验室:8bit 行李箱
亮度范围从 0 到几百(阳光下的墙 vs 阴影),8bit 只能存 0-255——直接存会爆。
解法:先取对数(LogScale,把大范围压缩进小范围),再整体缩放平移
(Scale/Add,让 0-255 被用满)。**”行李箱装不下的亮度,先对数压缩再装箱”**。
2.6 Padding(边距)= 拼图块之间的缝隙
定义:UV 岛之间/光照贴图之间留的空隙(纹素)。引擎装箱 4px 对齐(LightMap.cpp:491),官方建议约 4 纹素——**缝隙防”串色”**(双线性采样/纹理压缩时相邻岛互相污染)。
类比实验室:拼图缝隙
拼图块之间完全无缝 → 双线性插值会”抄”邻居的颜色(串色/漏光)。
留缝(padding)防串色,但缝隙太宽浪费面积(填充率↓)——缝隙是质量与
效率的平衡。漏光(Light Bleeding)90% 是 padding 不足。
2.7 概念 → 源码落点预告表
| 概念 | 类比 | 核心类/函数 | 章节 |
|---|---|---|---|
| 纹素密度 | 分辨率 | LightMapResolution / GetUVDensity | Ch8 |
| UV 展开 | 橘子皮 | FLayoutUV / CreateLightMapUVLayout | Ch5/Ch8 |
| Atlas 装箱 | 拼图 | FLightMapPendingTexture / FindBestPacking | Ch5/Ch10 |
| 填充率 | 拼图利用率 | FindBestPackingEfficiency / FAllocator2D | Ch10 |
| 编码 | 8bit 行李箱 | QuantizeLightSamples / LogScale=11.5 | Ch4/Ch9 |
| Padding | 拼图缝隙 | FTextureLayout 4px / GLightmassDebugOptions | Ch5/Ch10 |
第 3 章 烘焙管线:GPU Lightmass 全流程
TL;DR|UE5 的默认烘焙器是 GPU Lightmass(
Engine/Plugins/Experimental/GPULightmass/):
Build Lighting 触发 →UGPULightmassSubsystem::Launch→FGPULightmass注册场景 →
后台每帧推进(BackgroundTick)→FLightmapRenderer逐 tile 路径追踪(默认 512 采样/
像素 + 辐照度缓存 + OIDN 去噪)→ 渐进收敛。烘焙不是一次性的,是”洗衣机转桶”——
每转一圈(每帧)处理一批 tile,边转边看效果。
3.1 插件结构
1 | Engine/Plugins/Experimental/GPULightmass/ |
避坑:路径是
Experimental/GPULightmass/(实验性插件目录)——不是Plugins/GPULightmass。
老资料讲 UE4 的 Lightmass 是独立进程 CPU 求解器(UnrealLightmass),与 GPU Lightmass
完全不同:GPU 版在引擎进程内、用 GPU 路径追踪、逐 tile 渐进。
3.2 触发链
1 | 编辑器 Build Lighting 按钮 / 控制台 GPULM.BuildLighting |
为什么 GPU Lightmass 是默认:FStaticLightingSystemInterface::GetPreferredImplementation()(StaticLightingSystemInterface.cpp:113)硬编码优先取 "GPULightmass" 实现——写死优先,无需配置。
3.3 关键参数:两张大表
世界设置(FLightmassWorldInfoSettings,WorldSettings.h:55-239,挂载 :672):
| 参数 | 默认 | 作用 |
|---|---|---|
StaticLightingLevelScale |
1 | 世界缩放补偿(注释”1 UU = 1 cm,大关卡设 2-4 可大幅缩短构建”) |
NumIndirectLightingBounces |
3 | 间接光反弹次数 |
IndirectLightingQuality |
1 | GI 求解采样数倍率(调高更慢更干净) |
IndirectLightingSmoothness |
1 | 间接光平滑(调低细节多噪点多) |
bUseAmbientOcclusion |
false | 环境光遮蔽 |
bCompressLightmaps |
true | 压缩光照贴图(注释”关闭后内存/体积 ×4”) |
VolumetricLightmapDetailCellSize |
200 | 体积光照图网格密度 |
VolumeLightingMethod |
VLM_VolumetricLightmap | 体积光照法 |
GPU Lightmass 面板(UGPULightmassSettings,GPULightmassSettings.h:33):
| 参数 | 默认 | 作用 |
|---|---|---|
GISamples |
512 | 每像素 GI 采样数(质量核心旋钮) |
StationaryLightShadowSamples |
128 | Stationary 灯阴影采样 |
bUseIrradianceCaching |
true | 辐照度缓存(复用相邻点 GI,大幅加速) |
IrradianceCacheQuality |
128 | 缓存质量 |
TilePassesInSlowMode/FullSpeedMode |
1/8 | 慢速/全速模式每 tile 的 pass 数 |
LightmapTilePoolSize |
55 | tile 池大小(显存预算) |
bCompressLightmaps |
true | 压缩 |
3.4 与 CPU Lightmass 的差异
| CPU Lightmass(UE4) | GPU Lightmass(UE5) | |
|---|---|---|
| 计算设备 | CPU 多线程(可 Swarm 分布式) | GPU(DX12 + 硬件光追/DXR) |
| 方式 | 一次性求解 | 渐进式(边烘边看,可中断续烘) |
| 采样 | 静态配置 | GISamples 可实时调 |
| 去噪 | 无(靠采样数) | Intel OIDN 内置 |
| 分布式 | ✅(多机) | ❌ |
3.5 源码考古:Launch 触发链
源码考古:想看真东西的人进。
GPULightmassSettings.cpp:195(要点):
1
2
3
4
5
6
7
8
9 void UGPULightmassSubsystem::Launch()
{
// 创建烘焙系统实例
GPULightmass = FGPULightmassModule::CreateGPULightmassForWorld(World.Get());
// 注册为当前世界的静态光照系统(GetPreferredImplementation 优先 GPULightmass)
FStaticLightingSystemInterface::SetPreferredImplementation(FName("GPULightmass"));
...
// 标记场景需要烘焙(编辑器勾选"Build Lighting"后进入后台推进)
}这段代码在干什么:Launch 是烘焙的”点火”——创建 FGPULightmass 并显式把 GPULightmass
设为首选实现(SetPreferredImplementation)。之后烘焙不再是一次性阻塞调用,而是FScene::BackgroundTick每帧推进的渐进过程——这就是”边烘边看、可中断”的来源。
第 4 章 编码与数据结构:FLightMap2D 的行李箱
TL;DR|烘焙结果是高动态范围(RGBA16F),存进 8bit 贴图要靠量化 + Scale/Add:
QuantizeLightSamples用 LogScale=11.5 对数量化(HQ=LogLUVW 亮度+方向、Simple=LogRGB),
每张图按 min/max 算 Scale/Add 归一化。运行时FLightMap2D持有纹理数组与解码参数,
shader 用LightMapScale/LightMapAdd还原亮度。类比:8bit 行李箱——对数压缩 +
缩放平移,把大范围亮度装进小行李箱。
4.1 从 RGBA16F 到 8bit:量化
GPU Lightmass 的 tile 渲染目标是 FLinearColor(RGBA16F),最终 QuantizeLightSamples(LightmapEncoding.cpp:35)量化成 8bit 系数:
1 | // LightmapEncoding.cpp:44-47 |
为什么取对数:亮度范围从 0.01(阴影)到 500+(阳光),线性量化会丢失暗部细节——对数压缩让暗部与亮部都有足够的 8bit 档位。
4.2 两种编码:HQ 与 LQ
| 编码 | 内容 | 用途 |
|---|---|---|
| HQ(复杂) | LogLUVW(亮度+色度 UVW)+ SH 方向(Dx/Dy/Dz+残差) | 高质量(默认) |
| LQ(简单) | LogRGB(三分量对数) | 低质量/移动端 |
HQ 的方向性:不只是”这个像素多亮”,还有”光从哪来”(SH 球谐)——材质法线不同,同一 Lightmap 像素的间接光方向不同(法线贴图配合真实感的关键)。解码时 Directionality = max(0, dot(SH, WorldNormal))(LightmapCommon.ush:166)。
4.3 Scale/Add:整图归一化
量化后还要按全图 min/max算 ScaleVectors/AddVectors(LightMap.h:338-341):最终亮度 = 量化值 * Scale + Add——让 8bit 的 0-255 被全图用满(不然整张图只用了一半档位,暗部更糊)。
4.4 FLightMap2D:纹理布局
FLightMap2D(LightMap.h:198):
1 | // The textures containing the light-map data. |
运行时纹理:ULightMapTexture2D(LightMapTexture2D.h:24,LODGroup=TEXTUREGROUP_Lightmap)——源数据 FColor(8bit),压缩经 SetModernSettingsForNewOrChangedTexture(LightMap.cpp:673)决定格式。
4.5 运行时解码
FPrecomputedLightingUniformParameters(LightmapUniformShaderParameters.h:16-25)携带 LightMapCoordinateScaleBias/LightMapScale[2]/LightMapAdd[2];shader 解码(LightmapCommon.ush:91-159):
1 | LogL = Lightmap0.w |
4.6 源码考古:PostEncode 的 atlas 收尾
源码考古:想看真东西的人进。
LightMap.cpp:685-723(PostEncode,要点):
1
2
3
4
5
6
7 // 计算光照贴图在 atlas 内的 UV 缩放/偏移:
// 每个实例的光照贴图被装箱进共享纹理后,它的 UV 要乘以 Scale 加上 Bias
// 才能指向 atlas 中自己的位置
FLightMap2D::CoordinateScale = PaddedSize / GetSizeX();
FLightMap2D::CoordinateBias = Base / GetSizeX();
...
// 写入 ScaleVectors/AddVectors(解码用)这段代码在干什么:装箱的”坐标翻译”——光照贴图在 atlas 里的位置(Base/Size)被
换算成 UV 的 Scale/Bias,运行时每个实例的 lightmap UV 先乘 Scale 加 Bias 再采样,
就精确指向 atlas 中自己的区域。这是第 10 章”填充率”的收尾动作:装箱完成后,
每个实例都知道”我的光照贴图在 atlas 的哪个角落”。
第 5 章 打包装箱:Atlas 拼图游戏
TL;DR|装箱发生在两个层面:网格侧(
FLayoutUV:UV 岛 FindCharts → FindBestPacking
→ CommitPackedUVs,把岛装进网格的 lightmap UV 空间)与编辑器侧(FLightMapPendingTexture
的 skyline 装箱:把多个 Actor 的光照贴图装进共享 1024×512 atlas)。填充率 = 装箱的
利用率,第 10 章专讲优化。类比:拼图游戏——岛装进网格,网格装进图集。
5.1 一条链两套实现
1 | 网格侧(每网格): |
为什么两套:网格侧解决”这个网格的 UV 岛怎么摆“(每网格的 lightmap UV 空间),编辑器侧解决”每个实例的光照贴图怎么拼“(共享 atlas)——两层拼图。
5.2 FAllocator2D:段式 best-fit
FAllocator2D(Allocator2D.h:19-80):段式空闲空间分配器,FindWithSegments(:74)按”最合适矩形”准则(best-fit)找空位——装箱质量的核心算法。FAllocator2DFlipFix 是 ELightmapUVVersion 的一个修复版本(翻转修复)。
5.3 ELightmapUVVersion:装箱算法的演进
MeshUtilitiesCommon.h:7-21:BitByBit=0 → ... → ConsiderLightmapPadding=6 → ForceLightmapPadding=7 → Segments2D=8 → OptimalSurfaceArea=9 → ScaleByEdgesLength=10(=Latest)——每个版本改进了装箱质量(padding 处理、段分配、按边长缩放岛)。
避坑:
LightmapUVVersion(StaticMesh.h:772)是”UV 打包算法版本”——改了引擎的装箱
算法后,旧资产的 UV 布局不会自动重打(除非重新生成)。改装箱质量 = 旧资产可能要
重新”Apply Changes”。
5.4 GPU Lightmass 侧:tile 池
GPU Lightmass 不共享 atlas——每实例独立 FLightmap(LightmapStorage.h:66),渲染目标是 tile 池(FLightmapTilePool,LightmapTilePool.h:28-72,LightmapTilePoolSize=55 个 tile,FreeHeap 线性分配);每实例 lightmap 尺寸向上取整到 128 tile(GetPaddedSizeInTiles,LightmapStorage.h:73-79)。
ISM 实例打包(InstancedStaticMesh.cpp:108-119):TotalLightmapRes = 宽 × ceil(sqrt(实例数))——所有实例排进一个正方形大图。
5.5 源码考古:FindBestPacking 装箱循环
源码考古:想看真东西的人进。
LayoutUV.h:67-86(要点):
1
2
3
4
5
6 /** 找到把全部 Chart 装进指定分辨率纹理的最佳排列 */
bool FindBestPacking(uint32 InTextureResolution);
/** 提交装箱结果(把 PackingScale/Bias 写回 UV) */
void CommitPackedUVs();
/** 装箱效率统计(填充率!)——每帧统计,LogStats() 输出 */
uint32 FindBestPackingEfficiency() const;这段代码在干什么:
FindBestPacking尝试把全部 UV 岛装进给定分辨率的纹理——
装不下就返回失败(调用方提高分辨率重试);FindBestPackingEfficiency量化
“装得满不满”(填充率)。这就是”自动生成 lightmap UV 时分辨率怎么定”的机制:
从 MinLightmapResolution 开始试,装不下就翻倍,直到装下或达上限。
第 6 章 运行时渲染与流送:烘焙结果怎么被画出来
TL;DR|运行时 BasePass 读 Lightmap:
FUniformLightMapPolicy绑定 uniform(Scale/Add/
CoordinateScaleBias)→ shader 里GetLightMapColorHQ解码 → 与直接光合并。UV 从
顶点流的LightMapCoordinate通道(默认 UV1)来,两个系数纹理上下打包进一张
(float2(1,0.5)切分)。流送:GAllowStreamingLightmaps+LightmapStreamingFactor=0.2
按距离流送 mip。类比:运行时是”看照片”——读贴图,不重算。
6.1 BasePass 采样链
1 | BasePass 像素着色器(BasePassPixelShader.usf:717-731): |
LightMapPolicyImpl::ModifyCompilationEnvironment(LightMapRendering.cpp:42-49)设置 HQ_TEXTURE_LIGHTMAP/LQ_TEXTURE_LIGHTMAP 宏;FUniformLightMapPolicy::GetPixelShaderBindings(:562-577)绑定 PrecomputedLightingBuffer(含 Scale/Add 等)。
6.2 HQ/LQ 策略选择
| 宏 | 触发 | 用途 |
|---|---|---|
HQ_TEXTURE_LIGHTMAP |
r.HighQualityLightMaps=1(UnrealEngine.cpp:18478) |
高质量(默认) |
LQ_TEXTURE_LIGHTMAP |
r.SupportLowQualityLightmaps=1(BasePassRendering.cpp:139) |
低质量/移动端 |
HQ 需要两个系数纹理(方向 SH),LQ 只需一个(LogRGB)。
6.3 顶点流双纹理打包
LocalVertexFactoryCommon.ush:118-128(LocalVertexFactoryCommon.ush):
1 | // lightmap UV 来自顶点流的 LightMapCoordinate 通道(默认 UV1,LightMapCoordinateIndex) |
关键技巧:两个系数纹理上下打包进一张贴图(UV.y 乘 0.5 切分上下半区)——省一次纹理绑定与采样。
6.4 VT Lightmap 与流送
- VT Lightmap:
r.VirtualTexturedLightmaps=1(LightMap.cpp:95)时生成虚拟纹理光照贴图(每 tile 64×64 + 2px 边界)——按可视性流送,大世界省显存; - 传统流送:
GAllowStreamingLightmaps(LightMap.cpp:53,ini[TextureStreaming] AllowStreamingLightmaps)+LightmapStreamingFactor=0.2(BaseEngine.ini:2040)——lightmap mip 按距离流送; - 体积光照图流送:
FVolumetricLightmapStreamingManager(PrecomputedVolumetricLightmapStreaming.cpp)。
避坑:老资料(UE4)的
FLightmapStreamingManager/r.LightmapStreamingCVar 在
本 fork 已不存在(UE4 遗留已删)——流送只走上面的三条路径。
6.5 源码考古:GetPixelShaderBindings
源码考古:想看真东西的人进。
LightMapRendering.cpp:562-577(要点):
1
2
3
4
5
6
7
8 void FUniformLightMapPolicy::GetPixelShaderBindings(...)
{
// 绑定预计算光照 uniform(Scale/Add/CoordinateScaleBias/纹理引用)
FPrecomputedLightingUniformParameters LCI;
SetupLCIUniformBuffers(FeatureLevel, Mesh, ...);
PixelShader->SetUniformBufferParameter(..., PrecomputedLightingBuffer);
...
}这段代码在干什么:把烘焙结果(纹理 + 解码参数)打包进 uniform 绑给像素着色器——
运行时”看照片”的全部准备。烘焙阶段算好的 Scale/Add/CoordinateScaleBias 在这里
变成 shader 可读的参数,GetLightMapColorHQ用它们把 8bit 系数还原成亮度。
第 7 章 专题一:烘焙流程实操——从世界设置到贴图落地
TL;DR|烘焙实操四步:检查(Mobility 归位/UV 检查/漏光预防)→ 设参(世界设置
Lightmass + GPU Lightmass 面板)→ 触发(Build Lighting / GPULM.BuildLighting,渐进
烘烤边看边等)→ 验证(视口/Lightmap Density 视图/对比)。类比:**相机的快门与
ISO——参数决定”拍多久、多干净”**。
7.1 烘焙前检查清单
| 检查项 | 为什么 | 怎么查 |
|---|---|---|
| 灯光 Mobility | 只有 Static/Stationary 灯进 Lightmap(Movable 白烘) | 选中灯光看 Mobility 面板 |
| Lightmap UV | 没有合法第二 UV 的网格会静默降级(错位/发黑) | 视口 → Lightmap Density 视图 |
| 漏光预防 | UV padding 不足 → 串色 | 烘焙后检查墙角/岛边缘 |
| 网格 LightMapResolution | 密度统一(全场景同 texel 密度) | 静态网格资产面板 |
| 世界规模 | 大关卡设 StaticLightingLevelScale 2-4 缩短构建 |
世界设置 |
7.2 世界设置参数(逐项)
第 3.3 章的两张表是参考——实操要点:
- **
StaticLightingLevelScale**:大关卡(数 km)设 2-4——注释原话”可大幅缩短构建时间”(尺度相关参数全部缩放); - **
NumIndirectLightingBounces**:室内 3 足够;洞穴/密闭空间可 4-5;室外 2 足够; IndirectLightingQuality:正式烘焙 1;预览烘焙 0.5(快 4 倍,验证布局用);- **
bUseAmbientOcclusion**:美术风格需要接触阴影时开; - **
bCompressLightmaps**:默认开(第 9 章详述,调试质量问题时可临时关)。
7.3 GPU Lightmass 面板(编辑器)
UGPULightmassSettings(GPULightmassSettings.h:33):
- GISamples:512 是质量/速度平衡;预览 64、最终 1024+;
- bUseIrradianceCaching:开(默认)——相邻点复用 GI,大幅加速;
- TilePassesInSlowMode/FullSpeedMode:慢速模式(1 pass/tile)用于”烘一会儿看效果”,全速模式(8 pass)用于定稿;
- LightmapTilePoolSize:显存预算(默认 55 tile,爆显存就调小,会慢)。
7.4 触发与监控
- 触发:工具栏 Build → Build Lighting;或控制台
GPULM.BuildLighting(GPULightmassEditorModule.cpp:44); - 监控:烘焙是渐进的——视口里光照逐帧变好(这是 GPU Lightmass 的标志性体验);输出日志有进度;
- 中断:可以随时停止(渐进式 = 已烘的部分保留)。
7.5 验证三件套
- 视口目检:漏光(墙角发亮)、光斑(纹素密度不足)、噪点(采样不够);
- Lightmap Density 视图(编辑器视图模式):红=密度过高、蓝=过低——全场景尽量同一色;
- 构建对比:改参数前先记录(截图/时长),每次只动一个旋钮。
7.6 避坑清单
- 漏光(Light Bleeding):90% 是 UV padding 不足——装箱 4px 对齐是默认,官方建议约 4 纹素边距;
- Stationary 灯阴影噪点:
StationaryLightShadowSamples=128不够就调高; - 大关卡烘焙慢:先
StaticLightingLevelScale,再调 IndirectLightingQuality 预览; - 烘焙结果与实机不一致:检查压缩(bCompressLightmaps)与分辨率差异。
7.7 动手实验室:从零烘焙一个室内关卡
| 步骤 | 操作 | 验收标准 |
|---|---|---|
| ① 场景准备 | 建室内关卡(四面墙+地板),放 1 Static 定向光 + 1 Static 点光;所有网格确认有 Lightmap UV(自动生成已开) | 视口 Lightmap Density 视图无红色/蓝色块 |
| ② 参数 | 世界设置:Bounces=3、Quality=1;GPU Lightmass:GISamples=128(预览) | 参数面板正确 |
| ③ 烘焙 | Build Lighting → 观察渐进收敛 | 视口光照逐步变好,日志无 error |
| ④ 验证 | 目检 + Density 视图 + 检查墙角漏光 | 无漏光/无光斑/无噪点 |
| ⑤ 迭代 | 有问题 → 单旋钮调整重烘 | 每次只改一个参数,记录对比 |
检查清单:Mobility 全对 ✓、UV 全绿 ✓、无漏光 ✓、无噪点 ✓、烘焙时长记录 ✓。
第 8 章 专题二:自动化展开 UV——让引擎替你摊开橘子皮
TL;DR|引擎能自动生成 Lightmap UV:FMeshBuildSettings 的
bGenerateLightmapUVs=true
默认开启——构建时CreateLightMapUVLayout(或旧路径 FLayoutUV Packer)自动把 UV 岛
装箱进第二通道(DstLightmapIndex=1)。缺第二 UV 不会报错,而是静默降级钳制。
FBX 导入还会自动识别名为LightMapUV的 UV 集。类比:橘子皮自动摊平机。
8.1 两条生成路径
1 | 新 MeshDescription 路径(默认): |
默认设置(FMeshBuildSettings,EngineTypes.h:2947):bGenerateLightmapUVs=true、MinLightmapResolution=64、SrcLightmapIndex=0、DstLightmapIndex=1(:2988-3007,默认值 :3051-3056)——默认就从 UV0 复制并重新装箱到 UV1。
8.2 缺第二 UV 的”静默降级”
UStaticMesh::EnforceLightmapRestrictions(StaticMesh.cpp:9857-9941):计算实际 UV 通道数后把 LightMapCoordinateIndex 钳制到合法范围——缺第二 UV 不报错,静默用 UV0 采样(lightmap 会错位/发黑)。CheckLightMapUVs(:9952)区分 Missing/Bad/Valid 供审计。
避坑:**”没报错 = 没问题”是错的**。烘焙后某个物体光照错位/全黑,第一件事就是
查它的 Lightmap UV(视口 Lightmap Density 视图 + 资产 CheckLightMapUVs 工具)。
8.3 分辨率与密度
- **
LightMapResolution(StaticMesh.h:1194-1196):纹理边长 texel 数(ClampMax=4096)——不是”每 texel 世界单位”**,实际密度 = 分辨率 ÷ 网格尺寸; - **
LightmapUVDensity**(:1176):按 Section 加权平均的密度(流送用),GetUVDensity(StaticMesh.cpp:5194-5216); - GPU Lightmass 逐 LOD 减半(
StaticMesh.cpp:34-82):LODIndex > 0 ? max(基础分辨率 / 2^(LOD-1), 32) : 基础——LOD 越高光照越粗(合理)。
实践:全场景统一纹素密度(如 4 texel/cm)——大模型(建筑)分辨率 512-1024,小道具 64-128,按世界尺寸而不是感觉调。
8.4 FBX 导入的自动处理
FbxStaticMeshImport.cpp:
- :400-403:FBX 里有名为
LightMapUV的 UV 集 → 自动设为 LightMapCoordinateIndex(DCC 导出时命名为 LightMapUV 就会被识别); - :1865-1867:默认
SetLightMapResolution(64)、SetLightMapCoordinateIndex(1); - :2065-2069:
bGenerateLightmapUVs时DstLightmapIndex = FirstOpenUVChannel(自动找空通道)。
8.5 Datasmith 自动展 UV
Datasmith 自带展平工具(UVTools/):UUVGenerationFlattenMapping::GenerateUVs(UVGenerationFlattenMapping.cpp:1218,角度阈值 + 面积权重展平);SetupGeneratedLightmapUVResolution(UVGenerationUtils.cpp:48:由 chart 数算最小分辨率)——Datasmith 导入无 UV 的模型会自动摊。
8.6 引擎级自动 UV 接口
- **
IGeometryProcessing_MeshAutoUV**(MeshAutoUV.h:17-72):EAutoUVMethod{PatchBuilder/UVAtlas/XAtlas}三种算法——程序化批量摊 UV 的官方接口(建模工具/程序化生成用); - UVEditor 插件:
UVEditorLayoutTool(自动排版)/UVEditorRecomputeUVsTool(重算)/UVEditorTexelDensityTool(纹素密度可视化); - 蓝图:
UStaticMeshEditorSubsystem::SetGenerateLightmapUVs(StaticMeshEditorSubsystem.cpp:1970)。
8.7 动手实验室:自动生成 + 检查配方
| 步骤 | 操作 | 验收标准 |
|---|---|---|
| ① 开启 | 静态网格资产 → Build Settings → Generate Lightmap UVs 勾选(默认已勾) | 复选框状态正确 |
| ② 通道 | SrcLightmapIndex=0、DstLightmapIndex=1、MinLightmapResolution=64 | 参数默认 |
| ③ 重建 | 资产上右键 Reimport 或 Apply Changes | 构建日志无 UV 错误 |
| ④ 检查 | 视口 Lightmap Density 视图 + CheckLightMapUVs(资产工具) | 无 Missing/Bad UV |
| ⑤ 密度微调 | LightMapResolution 按世界尺寸调(大模型 512+,小道具 64) | Density 视图全场景同色 |
检查清单:UV 集 Valid ✓、通道=UV1 ✓、密度统一 ✓、无静默降级(烘焙后无错位物体)✓。
第 9 章 专题三:压缩 Lightmap——内存瘦身
TL;DR|Lightmap 压缩三把刀:bCompressLightmaps(默认开,关掉内存×4——调试质量
才关)、纹理压缩格式(BC7 桌面/ASTC 移动端)、流送(LightmapStreamingFactor=0.2
按距离流送 mip)。移动端还有编码级优化(社区实测:跳 SkyOcclusion、AO 写 A 通道,
包体减 150MB——非本仓库实测)。类比:行李箱抽真空——质量与体积的权衡。
9.1 压缩链全貌
1 | 烘焙(RGBA16F tile)→ 量化 8bit(FColor)→ SetModernSettingsForNewOrChangedTexture |
bCompressLightmaps(WorldSettings.h:151):默认 true——注释原话”关闭后内存/体积 ×4”。
9.2 什么时候不该关压缩
| 场景 | 建议 |
|---|---|
| 正常发布 | 开(默认) |
| 调试漏光/串色 | 临时关(压缩是串色的来源之一) |
| 移动端 | 开(内存是移动端生命线) |
9.3 移动端格式与编码级优化(公开资料,非本仓库实测)
社区对移动端 Lightmap 的编码优化(知乎,写作时未能访问核实):
- 编码格式:HQ=RGBLogL 8:8:8:8、LQ=移动端专属 24bit RGB8、SimpleLogScale=16.0 对数量化、cook 后重映射 ASTC;
- 自定义优化(源码级改
LightMap.cpp):跳过 SkyOcclusionTexture 分配、把全局 AO 写进系数纹理 A 通道、半尺寸 LQ Lightmap——实测包体减约 150MB。
避坑:这些文章基于 UE4——编码常量与本 fork 的 GPU Lightmass(LogScale=11.5)
不同;思路可借鉴(AO 合并、跳纹理、半尺寸),数值别照抄。
9.4 VT 有损压缩
r.VT.EnableLossyCompressLightmaps=1(LightMap.cpp:111,默认 0):VT Lightmap 路径下有损压缩——体积更小,质量略降(阴影过渡变糊)。
9.5 流送与内存账本
GAllowStreamingLightmaps(LightMap.cpp:53,ini[TextureStreaming] AllowStreamingLightmaps);LightmapStreamingFactor=0.2(BaseEngine.ini:2040):lightmap 流送权重(0.2 = 比其他纹理更早降 mip——光照糊一点可接受);- 内存估算:单张 1024×512 RGBA8 = 2MB(未压缩)→ BC7 约 0.5MB。
9.6 动手实验室:内存测量与压缩对照
| 步骤 | 操作 | 验收标准 |
|---|---|---|
| ① 基线 | 烘焙完成,stat memory 记录 Lightmap 内存 |
记录基线数字 |
| ② 对照 | 世界设置关 bCompressLightmaps → 重烘 → 对比内存 | 内存约 ×4(验证注释) |
| ③ 恢复 | 开回压缩,对比画质差异(墙角阴影过渡) | 无可见差异 = 压缩无损观感 |
| ④ 流送 | 确认 ini AllowStreamingLightmaps=true | 远距离光照糊、靠近变清晰 |
| ⑤ 定案 | 记录最终内存与画质 | 达标 |
检查清单:压缩开 ✓、流送生效 ✓、画质无可见损失 ✓、内存记录 ✓。
第 10 章 专题四:提高填充率——让拼图每一块都被用到
TL;DR|填充率 = Atlas 中光照贴图的实际占用率。四把刀:统一纹素密度(片大小匹配)、
岛形状方正(ELightmapUVVersion 的 ScaleByEdgesLength 减小细长岛)、合理 padding
(缝隙够用就行)、分组装箱(编辑器按距离分组,PackedLightAndShadowMapTextureSize)。
测量:FindBestPackingEfficiency(引擎统计)+ Lightmap Density 视图。类比:拼图
利用率——片越方正、缝隙越小、利用率越高。
10.1 填充率为什么重要
填充率低 = Atlas 里有大片空白 = 显存浪费 + 烘焙时间浪费(空白区域也占 tile)。高填充率 = 同样的显存装更多光照信息。
10.2 网格侧装箱:岛的”方正化”
ELightmapUVVersion::ScaleByEdgesLength=10(MeshUtilitiesCommon.h:7-21,=Latest):按边长缩放岛——细长岛(长宽比极端)装箱时浪费大,按边长缩放让岛更”方正”,填充率提升。FAllocator2D 的 best-fit 段分配(Allocator2D.h:19-80)负责找空位。
10.3 编辑器侧分组装箱
LightMap.cpp:
- 分组(
FLightMapAllocationGroup:426):按 Outer(package)、LightmapFlags、空间距离分组——GMaxLightmapRadius=5000(:56)限制同图最大包围球半径; - 装箱(
FLightMapPendingTexture:457):FTextureLayout 4px 对齐 skyline 装箱(:491); - atlas 尺寸:
PackedLightAndShadowMapTextureSize=1024(WorldSettings.cpp:137),2:1 长宽比(:2478-2479); - 降序装箱(:2525):大的先放(大件先占位,小件填空——经典贪心)。
10.4 影响填充率的五因素
| 因素 | 影响 | 对策 |
|---|---|---|
| 岛大小分布 | 大小悬殊 → 空隙多 | 统一分辨率/拆分超大网格 |
| 岛长宽比 | 细长岛浪费 | ScaleByEdgesLength(默认已开) |
| Padding | 缝隙宽 = 浪费 | 保持默认 4px,别乱加 |
| 分辨率匹配 | 大网格小分辨率 = 岛小 | 按世界尺寸定分辨率 |
| LOD 差异 | LOD 间分辨率不一致 | GPU Lightmass 已按 LOD 减半(合理) |
10.5 测量手段
- 引擎统计:
FindBestPackingEfficiency(LayoutUV.h:86)——装箱效率原子统计,LogStats()输出; - Lightmap Density 视图:红=过密(浪费)、蓝=过疏(糊)——全场景同色 = 填充率健康;
- AverageTexelDensity(UnrealLightmass,
TextureMapping.cpp:1892-1927):lightmap 面积 ÷ 世界面积的平均密度。
10.6 动手实验室:填充率测量与优化
| 步骤 | 操作 | 验收标准 |
|---|---|---|
| ① 统计 | 烘焙后看 Density 视图 + 引擎装箱效率日志 | 记录填充率基线 |
| ② 归因 | 找出红色/蓝色区域对应的网格 | 定位密度不均来源 |
| ③ 修复 | 统一分辨率(大网格 512、小道具 64)+ 拆超大网格 | 密度图趋于同色 |
| ④ 复测 | 重烘 → 对比填充率与显存 | 填充率提升、显存下降 |
| ⑤ 定案 | 记录最终数据 | 达标 |
检查清单:Density 视图同色 ✓、无细长岛(ScaleByEdgesLength 已开)✓、padding 默认 ✓、显存记录 ✓。
第 11 章 专题五:把 UE 当烘焙器——离线光照工厂
TL;DR|把 UE 当纯烘焙器 = 让烘焙与关卡制作解耦:命令行/脚本触发烘焙(无人值守)、
分层烘焙(直接光/间接光分离)、产物导出(给其他引擎/流程用)。三个配方:① 命令行
烘焙 ② 分层烘焙 ③ 产物导出。类比:离线光照工厂——输入场景,输出贴图。
11.1 定位:UE 作为烘焙器的价值
- 质量:GPU Lightmass 的 512 采样/像素 + OIDN 去噪 = 顶级离线光照质量;
- 可脚本化:
GPULM.BuildLighting控制台命令可进批处理(无人值守烘焙); - 产物可导出:GLTFExporter 等把带 lightmap 的网格导出(烘焙结果”带走”)。
11.2 管线架构
1 | 资产规范(分辨率/UV 规范)→ 自动 UV(第 8 章)→ GPU Lightmass(第 3 章) |
11.3 动手实验室配方 ×3
配方 A:命令行/批处理烘焙
| 步骤 | 操作 | 验收标准 |
|---|---|---|
| ① 打开 | UnrealEditor.exe 项目 -game -run=...(或编辑器内执行 GPULM.BuildLighting) |
命令可执行 |
| ② 触发 | 控制台 GPULM.BuildLighting(GPULightmassEditorModule.cpp:44) |
日志出现烘焙进度 |
| ③ 等待 | 渐进收敛完成(观察视口/日志) | 无 error |
| ④ 保存 | 保存关卡(烘焙数据进 MapBuildDataRegistry) | 重新打开光照仍在 |
配方 B:分层烘焙(引用公开资料《命运扳机》经验,非本仓库实测)
| 步骤 | 操作 | 验收标准 |
|---|---|---|
| ① 分层 | 直接光/间接光分离(不同光照场景) | 两套烘焙可独立出 |
| ② 地形 | RVT 地形需先 Scene Capture 预渲染(公开资料) | 地形光照正确 |
| ③ 材质 | 特殊材质需重写 Lightmass 材质导出模块(公开资料:顶点 Position 换 Lightmap UV 的 trick) | 材质光照正确 |
配方 C:产物导出(把烘焙结果带出 UE)
| 步骤 | 操作 | 验收标准 |
|---|---|---|
| ① 导出 | 启用 GLTFExporter(GLTFExporter)导出网格 |
导出成功 |
| ② 检查 | 确认 Lightmap 纹理随网格导出 | 其他引擎可读 |
| ③ 复用 | 回导 Unity 等(第 12 章迁移流程) | 光照贴图可显示 |
11.4 避坑
- 烘焙与实机差异:分辨率/去噪/压缩在实机上观感不同——导出前用实机设置验收;
- 多场景烘焙:增量烘焙(只烘改动的场景)省时间;
- 版本管理:烘焙产物(MapBuildDataRegistry)进版本库——团队协作不重烘。
第 12 章 专题六:Unity 场景导入 UE——材质、灯光、地表对齐
TL;DR|Unity→UE 迁移四关:坐标单位(100 倍缩放:UE 1UU=1cm vs Unity 1 单位=1m;
Y-up→Z-up 换手)、材质对齐(FBX 导入器自动映射 sDiffuse→BaseColor 等,多数需手动
修 Metallic/Roughness)、灯光对齐(单位换算:UE 厘米制 vs Unity 米制,物理公式
在GetUnitsConversionFactor注释里)、地表对齐(Heightmap 导入 Landscape)。
导入通道:FBX(官方主线)/ USD / Datasmith(清单无 Unity)/ GLTF(本 fork 经
Interchange 可用)。类比:度量衡转换 + 坐标系换手 + 材质翻译。
12.1 导入器现状对比
| 通道 | 本 fork 状态 | Unity 支持 | 适用 |
|---|---|---|---|
| FBX | ✅ 经典 UnrealEd 管线 | ✅ 官方推荐(FBX Exporter 包) | 迁移主线 |
| USD | ✅ Importers/USDImporter/ | ✅ Unity 官方 USD 包 | 复杂场景/协同 |
| Datasmith | ✅ Enterprise 插件 | ❌ 官方清单无 Unity | 3dsMax/Revit 等 |
| GLTF | ✅ 经 Interchange 可用(实测 InterchangeGltfTranslator.cpp 存在,InterchangeImportModule.cpp:111 注册) |
✅ Unity 可导出 | 开源中间格式 |
避坑:此前资料说”本 fork 无 GLTF 导入器”——实测有(Interchange 的 GLTFCore/
InterchangeImport 模块 + InterchangeGltfTranslator)。GLTF 可用,但 FBX 仍是 Unity
迁移官方主线(材质映射最成熟)。
12.2 坐标与单位:100 倍缩放 + 换手
- 100 倍:UE 1 UU = 1 cm,Unity 1 单位 = 1 m——
WorldToMeters(WorldSettings.h:595,本 fork 用旧名;注释 “1uu = 1cm would be 100.0”)。FBX 导入勾 Convert Scene 自动处理; - 换手:Unity Y-up → UE Z-up——FBX Import Options 勾 Force Front X Axis(官方迁移文档);
- 佐证:UE 默认碰撞盒 32 单位 = 32cm(
BoxComponent.cpp构造BoxExtent=32)。
12.3 材质对齐:自动映射 + 手动修复
FBX 导入器自动映射(FbxMaterialImport.cpp:817-834):
| FBX 属性 | UE 属性 |
|---|---|
sDiffuse |
BaseColor |
sEmissive |
EmissiveColor |
sSpecular |
Specular |
sSpecularFactor |
Roughness |
sShininess |
Metallic |
sNormalMap / sBump |
Normal |
sTransparentColor |
Opacity(+BlendMode=Translucent) |
手动修复清单(自动映射的经典坑):
- Metallic/Roughness 反了:Unity 的 Smoothness = 1 - Roughness——FBX 导出器一般已处理,但检查”金属质感全无”;
- 纹理颜色空间:Unity 的 sRGB 标记可能丢失——检查 BaseColor 的 sRGB 开关;
- 透明度:Unity Transparent → UE 要手动设 BlendMode(自动映射设了但通常要微调)。
12.4 灯光对齐:单位换算
UE 灯光单位(LocalLightComponent.h 的 IntensityUnits 注释):点光/聚光默认 Candelas(cd)或 Lumens(lm);方向光用 lux(ELightUnits,LightComponent.h)。
换算公式(GetUnitsConversionFactor 注释原文):
1 | Flux = Lm = cd·sr (光通量 = 坎德拉 × 立体角) |
与 Unity 的换算:Unity 用流明(Lumen)或 cd(Candela,米制)——迁入 UE 时:
| Unity 值 | UE 值(单位换算后) |
|---|---|
| 点光 100 lm | UE Lumens 单位直接填 100(同单位) |
| 点光 100 cd | UE Candelas 直接填 100 |
| 方向光(lux) | 直接填(同 lux 单位) |
关键:单位一致时数值直接搬;但 UE 的 100 倍缩放让”物理正确”的光强在厘米制下显得暗——光源面积/距离都 ×100,GetUnitsConversionFactor 的 100*100 就是物理换算;实战中更多靠目测微调(方向光 10 lux 量级、点光按半径试)。
项目设置:DefaultLightUnits(RendererSettings.h,ConsoleVariable r.DefaultFeature.LightUnits)——新建灯光的默认单位。
12.5 地表对齐:Heightmap 导入 Landscape
LandscapeImportHelper(LandscapeImportHelper.cpp):GetHeightmapImportDescriptor/GetHeightmapImportData/TransformHeightmapImportData——支持 MyHeightmap_x0_y0.png 分块命名。Unity Terrain 导出 Heightmap(Raw/PNG)→ UE Landscape 导入 → 材质重新指定(地形材质是 UE 的 Landscape 材质体系,Unity Terrain 材质不能直接搬)。
12.6 动手实验室:Unity 场景端到端迁移
| 步骤 | 操作 | 验收标准 |
|---|---|---|
| ① Unity 导出 | 装 FBX Exporter 包,导出场景 FBX(勾 Embed Textures) | FBX 含材质贴图 |
| ② 坐标核对 | UE 侧 FBX Import Options:Convert Scene ✓、Force Front X Axis ✓ | 模型方向/大小正确(人物 180cm 左右) |
| ③ 材质修复 | 导入后检查 Metallic/Roughness/透明度(12.3 清单) | 材质观感接近 Unity |
| ④ 灯光换算 | 灯光按 12.4 表换算(单位一致直接搬 + 目测微调) | 光照氛围接近 |
| ⑤ 地表导入 | Terrain Heightmap → Landscape 导入 → 指定地形材质 | 地形轮廓正确 |
| ⑥ 烘焙 | 第 7 章流程烘焙(Lightmap 替代 Unity 的烘焙) | 光照质量达标 |
检查清单:缩放正确 ✓、轴向正确 ✓、材质观感一致 ✓、灯光氛围接近 ✓、地形轮廓正确 ✓、烘焙完成 ✓。
第 13 章 平台支持与性能调优
TL;DR|Lightmap 调优三板斧:内存(压缩/分辨率/流送)→ 烘焙时间(采样数/
LevelScale/辐照度缓存)→ 质量(漏光/光斑/噪点)。CVar 十一项速查 + 症状定位表。
13.1 CVar 速查表
| CVar/参数 | 默认 | 作用 | 位置 |
|---|---|---|---|
r.HighQualityLightMaps |
1 | HQ 编码开关(ReadOnly) | UnrealEngine.cpp:18478 |
r.SupportLowQualityLightmaps |
1 | LQ 编码编译开关 | BasePassRendering.cpp:139 |
r.VirtualTexturedLightmaps |
0 | VT Lightmap | LightMap.cpp:95 |
r.IncludeNonVirtualTexturedLightmaps |
0 | 非 VT 光照贴图也保留 | LightMap.cpp:103 |
r.VT.EnableLossyCompressLightmaps |
0 | VT 有损压缩 | LightMap.cpp:111 |
r.MultithreadedLightmapEncode |
1 | 多线程编码 | StaticLightingSystem.cpp:157 |
GPULM.GISamples |
512 | GI 采样数(编辑器面板参数) | GPULightmassSettings |
GPULM.LightmapTilePoolSize |
55 | tile 池(显存预算) | GPULightmassSettings |
GPULM.TilePassesInSlowMode |
1 | 慢速模式 pass 数 | GPULightmassSettings |
GPULM.TilePassesInFullSpeedMode |
8 | 全速模式 pass 数 | GPULightmassSettings |
LightmapStreamingFactor(ini) |
0.2 | 流送权重 | BaseEngine.ini:2040 |
13.2 调优工作流:三象限决策
1 | 内存问题(显存涨/包体大): |
13.3 症状 → 定位表
| 症状 | 大概率原因 | 检查/对策 |
|---|---|---|
| 烘焙慢 | 采样太高/关卡太大/辐照度缓存关 | LevelScale → Quality 预览 → GISamples |
| 显存涨 | 压缩关/分辨率过高/流送关 | 压缩开 → 统一密度 → 流送确认 |
| 漏光(Light Bleeding) | UV padding 不足/压缩串色 | padding 检查 → 临时关压缩排查 |
| 光斑(糊) | 纹素密度不足 | Lightmap Density 视图定位 → 分辨率 |
| 噪点 | 采样不足 | GISamples/IndirectLightingQuality |
| 物体光照错位/发黑 | Lightmap UV 缺失(静默降级) | CheckLightMapUVs → 重新生成 UV |
| 烘焙结果与实机不同 | 压缩/分辨率差异 | 实机设置验收 |
第 14 章 局限、替代方案与展望
TL;DR|Lightmap 的局限:静态(灯动/物体动要重烘)、存储占用、迭代慢。
替代:Lumen(实时)、CPU Lightmass(分布式)、第三方烘焙器。方向:GPU Lightmass
成熟化、VT Lightmap 大世界标配。
14.1 局限与对策
| 局限 | 表现 | 对策 |
|---|---|---|
| 静态 | 动态物体/灯光无光照 | 体积光照图给动态物体 + Lumen 补动态 |
| 存储 | 贴图占用显存/包体 | 压缩 + 流送 + VT(第 9 章) |
| 迭代慢 | 改灯重烘 | 预览质量烘焙 + 单旋钮调参(第 7 章) |
| 移动端纹理格式 | 不同平台格式差异 | ASTC(Android)/BC7(桌面) |
14.2 替代路径
| 方案 | 定位 |
|---|---|
| Lumen | 实时 GI(动态场景) |
| CPU Lightmass(UnrealLightmass) | 分布式/无 GPU 环境 |
| 第三方烘焙器 | 特殊需求(更高采样/特殊 GI) |
14.3 演进时间线与阅读进阶
| 时间 | 里程碑 |
|---|---|
| UE4 | CPU Lightmass(Swarm 分布式) |
| UE5.0~5.2 | GPU Lightmass 实验性 → 默认 |
| 5.3~5.9 | VT Lightmap、OIDN 去噪、辐照度缓存成熟 |
| 未来 | GPU Lightmass 转正式、大世界 VT Lightmap 标配 |
进阶路线:官方文档(Lighting the Environment → GPU Lightmass → Understanding Lightmapping → Generating Lightmap UVs → Migrating assets from Unity to Unreal)→ 中文深挖(附录 C)→ 本仓库源码(附录 A 指路)。
附录 A 源码地图
全部路径基于本仓库快照(2026-08-26,HEAD
6cea9bd20f8f)。行号只用于定位;代码演进后以函数名为准。
阅读顺序建议:先GPULightmassSettings.cpp(烘焙入口)→LightMap.cpp(装箱/编码)→LightmapCommon.ush(解码)→ 按专题深入。
A.1 GPU Lightmass 插件(Plugins/Experimental/GPULightmass/)
| 文件 | 关键内容 |
|---|---|
Source/GPULightmass/Private/GPULightmassSettings.cpp |
Launch :195(烘焙触发链入口)、UGPULightmassSubsystem |
Source/GPULightmass/Public/GPULightmassSettings.h |
UGPULightmassSettings :33(GISamples=512/StationaryLightShadowSamples=128/bUseIrradianceCaching=true/IrradianceCacheQuality=128/TilePassesInSlowMode=1/FullSpeedMode=8/LightmapTilePoolSize=55/bCompressLightmaps) |
Source/GPULightmass/Private/GPULightmass.cpp |
FGPULightmass :15(场景注册)、GetPrimitiveMeshMapBuildData :278 |
Source/GPULightmass/Private/LightmapRenderer.cpp |
FLightmapRenderer(4032 行,逐 tile 路径追踪、IrradianceCaching 组合 :230/:1287-1305) |
Source/GPULightmass/Private/LightmapEncoding.cpp |
QuantizeLightSamples :35、LogScale=11.5/LogBlackPoint :44-47 |
Source/GPULightmass/Private/LightmapTilePool.cpp |
FLightmapTilePoolGPU :111(FreeHeap 线性分配) |
Source/GPULightmass/Private/LightmapStorage.h |
FLightmap :66、GetPaddedSizeInTiles :73-79 |
Source/GPULightmass/Private/IrradianceCaching.h/.cpp |
FIrradianceCache(辐照度缓存,上限 4194304 条) |
Source/GPULightmass/Private/Scene/StaticMesh.cpp |
AllocateLightmaps :34-82(逐 LOD 减半下限 32) |
Source/GPULightmass/Private/Scene/InstancedStaticMesh.cpp |
ISM 打包 :108-119(宽×ceil(sqrt(N))) |
Source/GPULightmass/Private/Scene/Landscape.cpp |
Landscape 烘焙(复用 GetTerrainExpandPatchCount) |
Source/GPULightmassEditor/Private/GPULightmassEditorModule.cpp |
GPULM.BuildLighting 命令 :44、r.RayTracing 判定 :110 |
A.2 引擎核心(Runtime/Engine)
| 文件 | 关键内容 |
|---|---|
Public/LightMap.h |
FLightMap2D :198(Textures[2] :326/SkyOcclusionTexture :328/ScaleVectors :338/AddVectors :341/CoordinateScale :344/CoordinateBias :347) |
Private/LightMap.cpp |
FLightMapAllocationGroup :426、FLightMapPendingTexture :457(FTextureLayout 4px :491)、GMaxLightmapRadius :56、GAllowStreamingLightmaps :53、r.VirtualTexturedLightmaps :95、r.VT.EnableLossyCompressLightmaps :111、PostEncode :685-723、PackedLightAndShadowMapTextureSize 装箱 :2469-2585、SetModernSettingsForNewOrChangedTexture :673 |
Classes/Engine/LightMapTexture2D.h |
ULightMapTexture2D :24(LODGroup=TEXTUREGROUP_Lightmap) |
Public/LightmapUniformShaderParameters.h |
FPrecomputedLightingUniformParameters :16-25 |
Renderer/Private/LightMapRendering.cpp |
LightMapPolicyImpl :42-49、FUniformLightMapPolicy::GetPixelShaderBindings :562-577、GEmptyPrecomputedLightingUniformBuffer :723 |
Shaders/Private/LightmapCommon.ush |
GetLightMapColorLQ :78、解码 :91-159、GetLightMapColorHQ :132、SH 方向性 :166 |
Shaders/Private/LocalVertexFactoryCommon.ush |
GetLightMapCoordinates :118-128(UV0=Coord*float2(1,0.5)/UV1=UV0+float2(0,0.5)) |
Shaders/Private/BasePassPixelShader.usf |
HQ_TEXTURE_LIGHTMAP 采样 :717-731 |
Classes/GameFramework/WorldSettings.h |
FLightmassWorldInfoSettings :55-239(StaticLightingLevelScale :67/NumIndirectLightingBounces :76/IndirectLightingQuality :92/bCompressLightmaps :151/VolumetricLightmapDetailCellSize :159)、WorldToMeters :595 |
Classes/Engine/StaticMesh.h |
LightMapCoordinateIndex :1221-1224、LightMapResolution :1194-1196、LightmapUVDensity :1176、LightmapUVVersion :772 |
Private/StaticMesh.cpp |
GetUVDensity :5194-5216、EnforceLightmapRestrictions :9857-9941、CheckLightMapUVs :9952 |
Classes/Engine/EngineTypes.h |
FMeshBuildSettings :2947(bGenerateLightmapUVs :2988/MinLightmapResolution :3001/SrcLightmapIndex :3004/DstLightmapIndex :3007) |
Classes/Engine/MapBuildDataRegistry.h |
UMapBuildDataRegistry :294(MeshBuildData :426/LightmapResourceClusters :434)、FMeshMapBuildData :55 |
Private/PrecomputedVolumetricLightmapStreaming.cpp |
FVolumetricLightmapStreamingManager |
MeshUtilitiesCommon/Public/LayoutUV.h |
FLayoutUV :40-87(FindCharts :66/FindBestPacking :67/CommitPackedUVs :68/FindBestPackingEfficiency :86) |
MeshUtilitiesCommon/Public/Allocator2D.h |
FAllocator2D :19-80(FindWithSegments :74) |
MeshUtilitiesCommon/Public/MeshUtilitiesCommon.h |
ELightmapUVVersion :7-21(ScaleByEdgesLength=10=Latest) |
StaticMeshOperations.cpp |
CreateLightMapUVLayout :1866-1973 |
MeshBuilder/Private/MeshDescriptionHelper.cpp |
自动 UV 生成 :101-129 |
UnrealLightmass/Private/Lighting/TextureMapping.cpp |
AverageTexelDensity :1892-1927 |
A.3 导入器与编辑器
| 文件 | 关键内容 |
|---|---|
Editor/UnrealEd/Private/Fbx/FbxMaterialImport.cpp |
材质属性映射 :817-834(sDiffuse→BaseColor 等) |
Editor/UnrealEd/Private/Fbx/FbxStaticMeshImport.cpp |
LightMapUV 命名识别 :400-403、默认 64/Index=1 :1865-1867、DstLightmapIndex=FirstOpenUVChannel :2065-2069、EnforceLightmapRestrictions :2395 |
Plugins/Interchange/Runtime/Source/Import/Private/Gltf/ |
InterchangeGltfTranslator.cpp(GLTF 导入,本 fork 存在) |
Plugins/Interchange/Runtime/Source/Import/Private/InterchangeImportModule.cpp |
UInterchangeGLTFTranslator 注册 :111 |
Plugins/Enterprise/DatasmithImporter/Private/UVTools/ |
UUVGenerationFlattenMapping::GenerateUVs :1218、SetupGeneratedLightmapUVResolution :48-69 |
Plugins/Enterprise/DatasmithImporter/Private/DatasmithStaticMeshImporter.cpp |
GetLightmapSize :58-70 |
Plugins/Enterprise/DatasmithImporter/Private/DatasmithLightImporter.cpp |
灯光单位映射(Candelas/Lumens/EV/Unitless) |
Editor/LandscapeEditor/Private/LandscapeImportHelper.cpp |
Heightmap 导入(GetHeightmapImportDescriptor 等) |
Classes/Components/LocalLightComponent.h |
IntensityUnits(cd/流明注释) |
Private/Components/LocalLightComponent.cpp |
GetUnitsConversionFactor(”UE unit is in centimeters…100*100”) |
Classes/Engine/RendererSettings.h |
DefaultLightUnits(r.DefaultFeature.LightUnits) |
GeometryProcessingInterfaces/Public/MeshAutoUV.h |
IGeometryProcessing_MeshAutoUV :17-72(PatchBuilder/UVAtlas/XAtlas) |
StaticMeshEditorSubsystem.cpp |
SetGenerateLightmapUVs :1970 |
附录 B 术语表(中英对照)
| 英文 | 中文(本文用词) | 一句话解释 | 首次出现 |
|---|---|---|---|
| Lightmap | 光照贴图 | 离线烘焙的光照纹理 | Ch1 |
| Bake | 烘焙 | 离线预计算光照 | Ch1 |
| Texel Density | 纹素密度 | 每纹素覆盖的世界面积 | Ch2 |
| Atlas | 图集 | 多张光照贴图拼成的共享纹理 | Ch2 |
| Chart | UV 岛 | UV 展开后的连通区域 | Ch2 |
| Padding | 边距 | UV 岛/贴图之间的缝隙 | Ch2 |
| Packing | 装箱 | 把片装进画布 | Ch2 |
| Tile | 瓦片 | GPU Lightmass 的处理单元(128×128) | Ch3 |
| HQ / LQ | (不译) | 高质量(LogLUVW+SH)/低质量(LogRGB)编码 | Ch4 |
| LogScale | (不译) | 对数量化尺度(11.5) | Ch4 |
| Scale / Add | (不译) | 每图归一化的缩放/平移 | Ch4 |
| CoordinateScale / Bias | (不译) | atlas 内 UV 的缩放/偏移 | Ch4 |
| SH | (不译) | 球谐(方向性光照编码) | Ch4 |
| Mobility | 移动性 | Static/Stationary/Movable 三态 | Ch1 |
| Stationary | (不译) | 固定位置可变亮度的灯 | Ch1 |
| Bounce | 反弹 | 间接光反弹次数 | Ch7 |
| Irradiance Caching | 辐照度缓存 | 复用相邻点 GI(加速) | Ch3 |
| OIDN | (不译) | Intel 开源降噪器 | Ch3 |
| MinLightmapResolution | (不译) | 自动 UV 的最小分辨率 | Ch8 |
| LightMapCoordinateIndex | (不译) | Lightmap UV 通道号(默认 1) | Ch2 |
| ELightmapUVVersion | (不译) | UV 打包算法版本 | Ch5 |
| FAllocator2D | (不译) | 段式 best-fit 装箱分配器 | Ch5 |
| PackedLightAndShadowMapTextureSize | (不译) | Atlas 最大尺寸(默认 1024) | Ch5 |
| LightmapTilePoolSize | (不译) | GPU Lightmass tile 池大小 | Ch3 |
| ISM | (不译) | 实例化静态网格 | Ch5 |
| VT | (不译) | 虚拟纹理 | Ch6 |
| LightmapStreamingFactor | (不译) | 流送权重(0.2) | Ch6 |
| ELightUnits | (不译) | 灯光单位(Candela/Lumen/Lux/EV) | Ch12 |
| WorldToMeters | (不译) | 世界单位换算(1UU=1cm → 100) | Ch12 |
| Convert Scene | (不译) | FBX 导入的单位/轴向转换 | Ch12 |
| Force Front X Axis | (不译) | FBX 导入的轴向修正 | Ch12 |
| Heightmap | 高度图 | 地形高度数据(导入用) | Ch12 |
| Interchange | (不译) | UE 新导入框架(GLTF 经此) | Ch12 |
| Datasmith | (不译) | 场景导入插件(无 Unity) | Ch12 |
| Light Bleeding | 漏光 | UV 边距不足导致的串色 | Ch7 |
| Lightmap Density 视图 | (不译) | 编辑器密度可视化(红/蓝) | Ch7 |
附录 C 参考资料
Epic 官方(dev.epicgames.com):
- Lighting the Environment in Unreal Engine(5.0+)—— 光照总览(光源 Mobility/Lumen/VSM 导航);
- GPU Lightmass Global Illumination(含官方中文版)—— GPU 烘焙文档:DX12+DXR、Full Bake/Bake What You See 两模式、面板参数、已知限制;
- Understanding Lightmapping(5.2+)—— Lightmap UV 硬性要求:UV 岛不重叠、0-1 空间、约 4 纹素 padding;
- Generating Lightmap UVs(5.3)—— Build Settings 自动生成(Generate Lightmap UVs/Min Resolution/Source→Destination);
- Datasmith Supported Software and File Types(5.6)—— 官方清单无 Unity;
- Importing glTF Files Into Unreal Engine(经 Interchange)—— 本 fork 已实测可用;
- Migrating assets from Unity to Unreal Engine(官方迁移文档)—— FBX Exporter 包、Convert Scene + Force Front X Axis、100 倍缩放、Y-up→Z-up。
中文社区(标注:以下基于 UE4 或写作时未能访问核实,非本仓库实测):
- 知乎《UE5 光照烘焙—CPU Lightmass》(p/660381167)—— 烘焙全流程(关 Lumen/VSM、Lightmass 参数、Lightmap Density 视图);
- 知乎《UE4 LightMap 编码格式(移动端)》(p/539598416)—— HQ=RGBLogL 8:8:8:8、LQ=24bit RGB8、SimpleLogScale=16、cook 后 ASTC;
- 知乎《UNREAL 移动端 LightMap 自定义优化修改(六)》(p/261297862)—— 跳 SkyOcclusion、AO 写 A 通道、包体减 150MB;
- ldpk《Unity 光照贴图 UV 优化实战》—— UV 空间浪费与 packing 优化(跨引擎对照);
- masterTOB《From Unity to Unreal》(Epic 论坛)—— 一键导出整个 Unity 场景为 FBX;
- 知乎《全局光照引擎—Lightmap 烘焙器构件》(p/384470030)—— 自研烘焙器视角的 Lightmap 管线拆解;
- 知乎《命运扳机》的光与影(UFSH2025)—— UE 当烘焙器的坑(RVT 地形预渲染、Lightmass 材质导出模块重写)。
附录 D 自测练习
入门级:
- 用三句话 + 一个类比向同事解释”烘焙”(Lightmap)是什么。
- Lumen 和 Lightmap 的区别?什么场景选哪个?
LightMapResolution是”每 texel 多少世界单位”吗?它实际是什么?
进阶级:
- 画出一张光照贴图从烘焙到显示的七段流程(图 F1 复述)。
- 缺第二 UV 时引擎会怎样?为什么”没报错 = 没问题”是错的?
- Unity 场景导入 UE 的 100 倍缩放是哪来的?灯光单位怎么换算?
- 漏光(Light Bleeding)的成因与排查顺序是什么?
源码级:
- 在
LightmapEncoding.cpp:35(QuantizeLightSamples)找到 LogScale 常量,解释为什么要取对数。 - 在
LightMap.cpp:685(PostEncode)找到 CoordinateScale/Bias 的计算,说明它们解决什么问题。 - 在
LocalVertexFactoryCommon.ush:118找到双纹理打包的 UV 变换,推导 UV1 为什么是UV0 + float2(0,0.5)。 - 在
FbxMaterialImport.cpp:817找到材质属性映射表,说出 Unity Smoothness 应该映射到 UE 的什么。 - 实验:建一个室内关卡烘焙,用 Lightmap Density 视图找密度不均的物体,调整分辨率后对比显存占用。