如何在运行时动态加载Additive烘焙场景

解读

国内项目普遍采用**“大地图 + 子场景”**的流式加载方案,以控制包体、降低内存峰值并支持大世界无缝漫游。Additive烘焙场景(Lighting Data Asset 已提前烘焙)的动态加载,核心矛盾是:光照贴图、Light Probe、反射探针、NavMesh、OcclusionCulling 等烘焙数据必须在子场景挂载到主场景后,立即生效且不能破坏主场景已有光照。面试时,面试官想确认你是否真正踩过坑:

  1. 直接 SceneManager.LoadSceneAsync(path, LoadSceneMode.Additive) 后,为什么光照全黑或全爆?
  2. 如何在不重启播放器的前提下,让多套烘焙数据共存?
  3. 如何兼容 AssetBundle、Addressable、HybridCLR 热更新?
  4. 如何量化验证加载后光照正确、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 裁剪导致空指针

答案

分五步落地,给出可直接写进简历的“生产级方案”:

  1. 烘焙阶段
    在 Editor 脚本中为每个子场景勾选 “Auto Generate Lighting”=false,使用 Baked GI 模式,烘焙输出到 SceneName_LightingData 文件夹,确保 LightmapIndex 从 0 开始连续编号;同时把 Reflection Probe 的 Baked Type 改为“Realtime”,避免合并冲突。

  2. 打包阶段
    BuildPipeline.BuildAssetBundles 后处理,把 .lighting_LightingData.asset_Lightmap-.png* 打入同一 AssetBundle,命名规则 sceneName_lighting_平台,并在 manifest 中记录 lightmapCount、lightmapDir 供运行时解析。

  3. 加载阶段
    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) 释放头文件,贴图常驻内存。

  4. 卸载阶段
    当玩家离开区域,先 SceneManager.UnloadSceneAsync,再 LightmapSettings.lightmaps.RemoveRange(startIndex, count),并 Resources.UnloadUnusedAssets() 确认 GPU 内存回落;若使用 Addressable,需 Addressables.Release(lightmapHandle) 防止引用计数泄漏。

  5. 验证阶段
    真机 Profiler 抓帧,确认 Lightmap 纹理内存 = lightmapCount × 单张大小,且 Shader.PropertyToID(“unity_LightmapST”) 采样结果与编辑器一致;写 EditorUnitTestImageAssert.PixelColor 比对关键物体光照,CI 流水线自动拒绝 ΔE>3 的提交。

拓展思考

  1. Unity 2022.1+ 的 Streaming Scene 与 Scene Prefab 已支持 “Scene-in-Scene” 烘焙,但实验性 API 在国内大厂尚未落地,可调研 ScriptableRenderContext.DrawScene 自定义合并方案,提前布局。
  2. GPU Resident Drawer + DOTS 场景下,Lightmap 需要转成 GPU Uploadable Texture3D,用 Custom BatchRendererGroup 传入 MaterialPropertyBlock.SetTexture,可做到 256 张 1K lightmap 合并成一张 3D 纹理,内存降低 70%,但仅支持 Vulkan/Metal,需要写 Platform #ifdef 做回退。
  3. 数字孪生项目常要求 24h 昼夜变化,烘焙数据无法动态改方向光,可引入 Baked Lightmap + Realtime Shadowmask 混合方案:静态物体用烘焙,动态人物用 Screen Space Shadow,在加载 Additive 场景后,通过 LightmapSettings.SetLightmapIndexShadowmask Texture 绑定到 Custom Shader Keyword,实现 一套烘焙、多套实时阴影,既省电又保证视觉效果。