如何在运行时动态加载Additive烘焙场景
解读
国内项目普遍采用**“大地图 + 子场景”**的流式加载方案,以控制包体、降低内存峰值并支持大世界无缝漫游。Additive烘焙场景(Lighting Data Asset 已提前烘焙)的动态加载,核心矛盾是:光照贴图、Light Probe、反射探针、NavMesh、OcclusionCulling 等烘焙数据必须在子场景挂载到主场景后,立即生效且不能破坏主场景已有光照。面试时,面试官想确认你是否真正踩过坑:
- 直接 SceneManager.LoadSceneAsync(path, LoadSceneMode.Additive) 后,为什么光照全黑或全爆?
- 如何在不重启播放器的前提下,让多套烘焙数据共存?
- 如何兼容 AssetBundle、Addressable、HybridCLR 热更新?
- 如何量化验证加载后光照正确、GPU 内存无泄漏?
知识点
- Unity 场景烘焙文件结构:LightingData.asset、Lightmaps、Light Probe、Reflection Probe、NavMesh、OcclusionCullingData
- SceneManager 与 LightingSettings 生命周期:Editor 与 Player 中 LightingSettings.AmbientMode、LightingSettings.Lightmapper 的差异
- LightmapSettings 运行时 API:LightmapSettings.lightmaps、LightmapSettings.lightmapsMode、LightmapSettings.lightProbes
- Additive 场景光照合并策略:Unity 原生仅支持单套 Global Lightmap,多套烘焙数据需手动合并贴图数组或切换光照贴图索引
- AssetBundle/Addressable 打包规则:.lighting 文件必须与生成的贴图打入同一包,且使用 LoadAssetAsync<Texture2D>() 显式加载,否则出现 pink 贴图
- GPU 内存采样:Profiler.BeginSample("AppendAdditiveLightmap") + Recorder.Get("GPU Memory.Textures")
- 移动设备兼容性:GLES3 对 4K 以上 lightmap 采样会降级为双线性,需在烘焙面板勾选 Android PVRTC 分离 Alpha
- 代码热更新:HybridCLR 下反射调用 LightmapSettings 需在 link.xml 中保留字段,否则 il2cpp 裁剪导致空指针
答案
分五步落地,给出可直接写进简历的“生产级方案”:
-
烘焙阶段
在 Editor 脚本中为每个子场景勾选 “Auto Generate Lighting”=false,使用 Baked GI 模式,烘焙输出到 SceneName_LightingData 文件夹,确保 LightmapIndex 从 0 开始连续编号;同时把 Reflection Probe 的 Baked Type 改为“Realtime”,避免合并冲突。 -
打包阶段
写 BuildPipeline.BuildAssetBundles 后处理,把 .lighting、_LightingData.asset、_Lightmap-.png* 打入同一 AssetBundle,命名规则 sceneName_lighting_平台,并在 manifest 中记录 lightmapCount、lightmapDir 供运行时解析。 -
加载阶段
先 SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive),回调完成后:
a. 通过 AssetBundle.LoadAssetAsync<LightingDataAsset>(“LightingData.asset”) 拿到数据;
b. 按记录数量循环 LightmapSettings.lightmaps.Add(new LightmapData() { lightmapColor = texColor, lightmapDir = texDir });
c. 若子场景使用 Shadowmask,需把 LightmapSettings.lightmapsMode 临时设为 Shadowmask,合并完再恢复;
d. 把 LightmapSettings.lightProbes 合并到主场景 ProbeAnchor,使用 SphericalHarmonicsL2 追加写入;
e. 调用 DynamicGI.UpdateEnvironment() 刷新反射探针,最后 UnloadAssetBundle(false) 释放头文件,贴图常驻内存。 -
卸载阶段
当玩家离开区域,先 SceneManager.UnloadSceneAsync,再 LightmapSettings.lightmaps.RemoveRange(startIndex, count),并 Resources.UnloadUnusedAssets() 确认 GPU 内存回落;若使用 Addressable,需 Addressables.Release(lightmapHandle) 防止引用计数泄漏。 -
验证阶段
真机 Profiler 抓帧,确认 Lightmap 纹理内存 = lightmapCount × 单张大小,且 Shader.PropertyToID(“unity_LightmapST”) 采样结果与编辑器一致;写 EditorUnitTest 用 ImageAssert.PixelColor 比对关键物体光照,CI 流水线自动拒绝 ΔE>3 的提交。
拓展思考
- Unity 2022.1+ 的 Streaming Scene 与 Scene Prefab 已支持 “Scene-in-Scene” 烘焙,但实验性 API 在国内大厂尚未落地,可调研 ScriptableRenderContext.DrawScene 自定义合并方案,提前布局。
- GPU Resident Drawer + DOTS 场景下,Lightmap 需要转成 GPU Uploadable Texture3D,用 Custom BatchRendererGroup 传入 MaterialPropertyBlock.SetTexture,可做到 256 张 1K lightmap 合并成一张 3D 纹理,内存降低 70%,但仅支持 Vulkan/Metal,需要写 Platform #ifdef 做回退。
- 数字孪生项目常要求 24h 昼夜变化,烘焙数据无法动态改方向光,可引入 Baked Lightmap + Realtime Shadowmask 混合方案:静态物体用烘焙,动态人物用 Screen Space Shadow,在加载 Additive 场景后,通过 LightmapSettings.SetLightmapIndex 把 Shadowmask Texture 绑定到 Custom Shader Keyword,实现 一套烘焙、多套实时阴影,既省电又保证视觉效果。