Unity场景烘焙Cubemap全攻略:从原理到批量处理实践 1. 项目概述从HDR天空盒到场景烘焙的思维跃迁在Unity开发中尤其是涉及到环境反射、天空盒或者全景图预览时我们最常接触到的资源就是Cubemap立方体贴图。无论是内置的Skybox材质还是Reflection Probe反射探针其核心都是基于Cubemap。过去我们获取Cubemap的途径非常有限要么从Asset Store购买现成的HDR天空盒资源包要么使用Photoshop等工具手动拼接六张图过程繁琐且效果难以把控。你有没有想过其实我们手头精心搭建的每一个游戏场景本身就是一个绝佳的、独一无二的Cubemap来源与其依赖千篇一律的第三方资源不如将你自己的场景“烘焙”成专属的Cubemap资产。这就是今天要分享的核心技巧使用Unity脚本将任意场景的任意视角一键烘焙成可复用的Cubemap资源文件。这不仅仅是调用一个API那么简单它涉及到烘焙策略的选择、性能的权衡、资源的管理以及最终效果的优化。我将手把手带你从原理到实践从简单的编辑器工具到可集成到生产管线的自动化脚本彻底掌握这项能将你的场景资产价值最大化的技能。无论你是技术美术、图形程序员还是希望提升场景表现力的开发者这套方法都能为你打开一扇新的大门。2. 核心原理与方案选型为什么是Camera.RenderToCubemap在深入代码之前我们必须理解Unity为我们提供的核心武器Camera.RenderToCubemap方法。根据官方API文档这个方法有三个重载但它们的本质都是从一个摄像机Camera的视角渲染出它周围360度的环境并将其保存为立方体贴图的六个面。2.1 静态烘焙 vs. 实时渲染这是第一个关键决策点直接决定了我们脚本的设计目标和应用场景。静态烘焙这是我们本次重点讨论的方向。它的目标是在编辑阶段或游戏初始化时生成一个.cubemap或.exr格式的资产文件存储在项目中。这个资源是静态的不会在运行时改变。它的优点是零运行时开销烘焙完成后使用它就像使用任何一张图片一样对性能没有影响。高质量可以设置极高的分辨率进行多次采样或光照烘焙得到非常精致的细节。可复用与批处理生成的资源可以跨场景使用也可以作为天空盒、反射贴图甚至用于离线渲染或宣传图制作。实时渲染使用RenderTexture作为目标并且将RenderTexture.dimension设置为TextureDimension.Cube。这通常用于实现动态的反射效果比如行驶中汽车的漆面反射、角色武器表面的环境映射。它的代价很高因为每一帧都可能需要渲染最多6次每个面一次对性能冲击很大。除非有强烈的动态反射需求否则在移动平台或性能敏感的项目中应谨慎使用。我们的脚本将专注于静态烘焙因为它更通用学习价值更高且能直接产出可管理的项目资产。2.2 关键参数深度解析理解RenderToCubemap的参数是写出健壮脚本的基础。目标Cubemap / RenderTexture对于静态烘焙我们传入一个Cubemap类型的对象。这个对象需要提前在内存中创建好并指定其尺寸如512, 1024, 2048。尺寸越大细节越丰富但烘焙时间和内存占用也呈平方增长。faceMask参数这是一个按位掩码bitmask用于控制渲染哪些面。立方体贴图有六个面X, -X, Y, -Y, Z, -Z对应CubemapFace枚举。默认值63二进制00111111表示渲染所有六个面。你可以通过1 (int)CubemapFace.PositiveX这样的方式来生成只渲染特定面的掩码。在静态烘焙中我们几乎总是渲染全部六个面。摄像机设置位置Position这是烘焙的“视点”。所有光线都从这个点出发捕获周围环境。对于天空盒这个点通常放在场景中心或玩家出生点对于物体表面的反射贴图类似于反射探针这个点则放在物体中心。裁剪平面Clipping PlanesnearClipPlane和farClipPlane至关重要。farClipPlane决定了你能“看”多远。如果设置太小远处的天空或山脉可能被裁剪掉如果设置太大可能会包含一些你不希望出现在Cubemap中的远处无关物体甚至影响深度精度。通常你需要根据场景的边界来合理设置。清除标志Clear Flags通常设置为Solid Color并选择一个纯色如天空蓝或透明黑作为无法被几何体填充的区域的背景。这对于生成纯净的天空盒背景很重要。渲染路径与光照烘焙出的Cubemap会忠实记录当前渲染设置下的所有信息。这意味着如果你使用了烘焙光照Baked GI那么Cubemap中就会包含间接光照和阴影信息。如果你使用实时光照那么它就是实时光照的快照。为了获得最佳效果和一致性建议在烘焙Cubemap时使用与游戏运行时相同的光照和后期处理设置。3. 实战构建一个编辑器烘焙工具理论说再多不如一行代码。我们将从最简单的编辑器工具开始逐步构建一个功能完整、用户友好的场景烘焙工具。3.1 基础脚本一键烘焙首先我们创建一个继承自ScriptableWizard的编辑器脚本。这是一个快速创建自定义向导窗口的类非常适合这种工具型功能。using UnityEngine; using UnityEditor; using System.IO; public class RenderCubemapWizard : ScriptableWizard { public Transform renderFromPosition; // 烘焙视点的Transform public int cubemapSize 1024; // Cubemap分辨率 public string assetName NewBakedCubemap; // 资源保存名称 public bool includePostProcessing true; // 是否包含后期处理效果 [MenuItem(Tools/场景工具/烘焙场景为Cubemap...)] static void CreateWizard() { // 显示向导窗口标题为“烘焙Cubemap”主按钮文字为“烘焙” ScriptableWizard.DisplayWizardRenderCubemapWizard(烘焙Cubemap, 烘焙); } void OnWizardUpdate() { // 实时验证输入有效性并更新帮助信息 helpString 请指定一个GameObject作为烘焙的视点中心。; isValid (renderFromPosition ! null) cubemapSize 0 !string.IsNullOrEmpty(assetName); } void OnWizardCreate() { if (renderFromPosition null) { Debug.LogError(烘焙视点未指定); return; } // 1. 创建临时摄像机 GameObject tempCameraGO new GameObject(TempCubemapCamera, typeof(Camera)); Camera tempCamera tempCameraGO.GetComponentCamera(); // 复制主摄像机或场景视图摄像机的部分设置可选更高级的做法 // 这里我们先进行基础设置 tempCamera.transform.position renderFromPosition.position; tempCamera.transform.rotation Quaternion.identity; // 朝向不重要RenderToCubemap会处理 tempCamera.clearFlags CameraClearFlags.Skybox; // 使用天空盒作为背景 tempCamera.fieldOfView 90f; // 立方体贴图每个面是90度视锥 tempCamera.nearClipPlane 0.01f; tempCamera.farClipPlane 1000f; // 根据场景调整 // 是否启用后期处理栈需要安装Post Processing包 #if UNITY_POST_PROCESSING_STACK_V2 if (includePostProcessing) { // 这里可以添加代码来复制当前主摄像机的后处理体积设置到临时摄像机 // 简化处理临时摄像机继承场景的后处理效果 } #endif // 2. 创建目标Cubemap资产在内存中 Cubemap bakedCubemap new Cubemap(cubemapSize, TextureFormat.RGBAHalf, false); // 使用Half精度保存HDR信息 bakedCubemap.name assetName; // 3. 执行烘焙 bool success tempCamera.RenderToCubemap(bakedCubemap); if (success) { // 4. 将内存中的Cubemap保存为项目中的资产文件 string path Assets/BakedCubemaps/; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } string fullPath path assetName .cubemap.asset; AssetDatabase.CreateAsset(bakedCubemap, fullPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($Cubemap烘焙成功已保存至: {fullPath}); EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPathCubemap(fullPath)); } else { Debug.LogError(Cubemap烘焙失败请检查摄像机设置或图形API支持。); } // 5. 销毁临时摄像机 DestroyImmediate(tempCameraGO); } }注意这个基础版本有几个明显的限制。它使用CameraClearFlags.Skybox这意味着如果你的场景没有设置天空盒或者你希望以纯色背景捕获室内场景它可能不会按预期工作。此外它没有处理自定义的渲染层Layers可能会把UI等不需要的元素也烘焙进去。3.2 进阶功能让工具更强大一个生产可用的工具需要考虑更多细节。我们来增强它。// ... 前面的using和类声明 ... public class AdvancedCubemapBaker : ScriptableWizard { public Transform renderFromPosition; public int cubemapSize 1024; public TextureFormat textureFormat TextureFormat.RGBAHalf; // 增加格式选项 public bool mipmaps true; // 是否生成mipmap public string assetName BakedCubemap; [Header(摄像机设置)] public CameraClearFlags clearFlags CameraClearFlags.Skybox; public Color backgroundColor Color.black; // 当ClearFlags为SolidColor时使用 public LayerMask cullingMask -1; // 默认渲染所有层可以排除UI层等 public float farClipPlane 1000f; [Header(后期与光照)] public bool copyMainCameraSettings true; // 是否从主摄像机复制设置 public bool useBakedLighting true; // 是否使用烘焙光照 [MenuItem(Tools/场景工具/高级Cubemap烘焙...)] static void Init() { DisplayWizardAdvancedCubemapBaker(高级Cubemap烘焙, 开始烘焙); } void OnWizardCreate() { // 路径检查与创建 string saveFolder Assets/BakedCubemaps/; if (!Directory.Exists(saveFolder)) Directory.CreateDirectory(saveFolder); string fullPath saveFolder assetName .asset; if (File.Exists(fullPath)) { if (!EditorUtility.DisplayDialog(文件已存在, $文件 {fullPath} 已存在是否覆盖, 覆盖, 取消)) { return; } } // 创建临时摄像机 GameObject go new GameObject(CubemapBaker_Camera, typeof(Camera)); Camera cam go.GetComponentCamera(); go.hideFlags HideFlags.HideAndDontSave; // 不在层级视图显示 // 设置摄像机属性 cam.transform.position renderFromPosition ! null ? renderFromPosition.position : Vector3.zero; cam.transform.rotation Quaternion.identity; cam.clearFlags clearFlags; cam.backgroundColor backgroundColor; cam.cullingMask cullingMask; cam.farClipPlane farClipPlane; cam.fieldOfView 90f; cam.orthographic false; // 高级设置从场景主摄像机复制属性如HDR、抗锯齿等 if (copyMainCameraSettings Camera.main ! null) { Camera mainCam Camera.main; cam.allowHDR mainCam.allowHDR; cam.allowMSAA mainCam.allowMSAA; cam.depthTextureMode mainCam.depthTextureMode; // 注意不要复制transform和clearFlags } // 强制使用烘焙光照如果场景使用了混合或烘焙光照 if (useBakedLighting) { // 临时切换光照模式以确保捕获的是烘焙光照结果 // 这是一个关键技巧Unity的渲染在编辑器和运行时可能不同。 // 更可靠的方法是在烘焙前手动启用“Baked Global Illumination”。 // 此处简化处理提示用户。 Debug.LogWarning(提示为了确保捕获烘焙光照请在烘焙前确认‘Window Rendering Lighting’设置中‘Baked Global Illumination’已启用并已生成光照贴图。); } // 创建Cubemap Cubemap cube new Cubemap(cubemapSize, textureFormat, mipmaps); // 关键步骤在渲染前强制刷新场景视图和光照数据 // 这能确保所有静态合批、光照贴图等都已就绪 UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); // 等待一帧让渲染状态稳定在编辑器协程中更好这里简化 System.Threading.Thread.Sleep(100); // 执行渲染 EditorUtility.DisplayProgressBar(烘焙Cubemap, 正在渲染六个面..., 0.5f); bool renderSuccess cam.RenderToCubemap(cube); EditorUtility.ClearProgressBar(); if (renderSuccess) { // 保存资产 AssetDatabase.CreateAsset(cube, fullPath); AssetDatabase.SaveAssets(); Debug.Log($colorgreen成功/color Cubemap已保存至: {fullPath}, AssetDatabase.LoadAssetAtPathCubemap(fullPath)); // 可选在Inspector中预览 Selection.activeObject cube; } else { Debug.LogError(渲染失败。可能原因图形设备不支持、摄像机参数错误或目标Cubemap格式不受支持。); DestroyImmediate(cube); // 清理内存中的临时资源 } // 清理 DestroyImmediate(go); } }这个进阶版本增加了大量可控参数如剔除遮罩、背景色、是否复制主摄像机设置等实用性大大增强。它还加入了文件覆盖确认和进度条提升了用户体验。4. 核心环节实现处理光照与HDR静态烘焙的灵魂在于光照信息的捕获。一个只有漫反射颜色的Cubemap是缺乏生气的。我们需要的是包含高光信息、环境遮蔽、甚至全局光照的HDR高动态范围Cubemap。4.1 确保捕获烘焙光照在Unity中光照贴图Lightmap和光照探针Light Probe的数据在编辑器模式下可能不会自动应用到由脚本创建的临时摄像机的渲染中。为了确保我们的Cubemap能“看到”和游戏运行时一样的光照我们需要采取一些措施手动设置光照模式在烘焙Cubemap之前确保你的场景光照模式设置为Baked或Mixed并且已经通过Window Rendering Lighting窗口生成了光照贴图点击Generate Lighting。脚本强制设置可以在烘焙脚本中临时修改Lightmapping.giWorkflowMode或强制进行一步光照计算。但更简单可靠的方法是在编辑器播放模式Play Mode下进行烘焙。在播放模式下Unity会完整地初始化所有光照数据。我们可以创建一个在运行时执行的脚本按下一个键如F12来触发Cubemap烘焙并将结果保存到Resources或StreamingAssets文件夹供后续使用。// 这是一个在运行时Play Mode执行的烘焙脚本示例 using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif public class RuntimeCubemapBaker : MonoBehaviour { public KeyCode bakeKey KeyCode.F12; public string savePathInResources BakedCubemaps/; // 保存在Resources文件夹下 public int size 1024; void Update() { if (Input.GetKeyDown(bakeKey)) { BakeFromCurrentPosition(); } } void BakeFromCurrentPosition() { Camera currentCam Camera.main; // 或者指定一个摄像机 if (currentCam null) { Debug.LogError(没有找到主摄像机); return; } Cubemap cube new Cubemap(size, TextureFormat.RGBAHalf, false); // 使用当前摄像机的位置和旋转 // 注意RenderToCubemap会忽略摄像机的旋转始终渲染360度环境 bool success currentCam.RenderToCubemap(cube); if (success) { #if UNITY_EDITOR // 在编辑器下我们可以将资源保存到Assets目录 string assetPath Assets/Resources/ savePathInResources RuntimeBaked_ System.DateTime.Now.ToString(yyyyMMdd_HHmmss) .asset; string folderPath System.IO.Path.GetDirectoryName(assetPath); if (!System.IO.Directory.Exists(folderPath)) System.IO.Directory.CreateDirectory(folderPath); AssetDatabase.CreateAsset(cube, assetPath); AssetDatabase.SaveAssets(); Debug.Log(运行时Cubemap已保存: assetPath); #else // 在构建版本中我们可以将Cubemap保存为二进制文件到持久化路径 // 这里需要自己实现Cubemap到字节数组的转换和保存较为复杂。 Debug.LogWarning(运行时烘焙在非编辑器模式下保存资源功能受限。); #endif } } }4.2 生成真正的HDR Cubemap默认的TextureFormat.RGBA32格式每通道8位只能存储LDR低动态范围数据会丢失亮部细节。为了保存HDR信息比如发光的灯泡、太阳我们必须使用支持HDR的格式。推荐格式TextureFormat.RGBAHalf半精度浮点16位每通道或TextureFormat.RGBAFloat全精度浮点32位每通道。RGBAHalf在质量和大小之间取得了很好的平衡是Unity中HDR纹理的常用格式。保存为.exr文件.cubemap资源文件在Unity内部是可行的但如果你想在外部软件如Nuke、Photoshop中查看或编辑或者想使用更通用的格式可以将其保存为.exr图像序列六张图或.hdr经纬图格式。Unity的Texture2D类有EncodeToEXR()方法但Cubemap没有直接的方法。一个变通方案是将Cubemap的六个面分别读取出来保存为六个独立的.exr文件。// 将Cubemap六个面保存为单独的EXR文件编辑器脚本 using UnityEngine; using UnityEditor; using System.IO; public static class CubemapToEXRExporter { [MenuItem(Assets/导出Cubemap为EXR序列, true)] static bool ValidateExport() { return Selection.activeObject is Cubemap; } [MenuItem(Assets/导出Cubemap为EXR序列)] static void Export() { Cubemap cube Selection.activeObject as Cubemap; if (cube null) return; string folderPath EditorUtility.SaveFolderPanel(选择保存EXR序列的文件夹, , cube.name); if (string.IsNullOrEmpty(folderPath)) return; // Cubemap的六个面顺序X, -X, Y, -Y, Z, -Z CubemapFace[] faces new CubemapFace[] { CubemapFace.PositiveX, CubemapFace.NegativeX, CubemapFace.PositiveY, CubemapFace.NegativeY, CubemapFace.PositiveZ, CubemapFace.NegativeZ }; string[] faceNames new string[] { px, nx, py, ny, pz, nz }; for (int i 0; i 6; i) { // 从Cubemap中获取每个面的颜色数据 Color[] pixels cube.GetPixels(faces[i]); // 创建一个临时的Texture2D来保存这个面 Texture2D tex new Texture2D(cube.width, cube.height, TextureFormat.RGBAHalf, false); tex.SetPixels(pixels); tex.Apply(); // 将Texture2D编码为EXR字节流 byte[] exrBytes tex.EncodeToEXR(Texture2D.EXRFlags.CompressZIP); // 使用ZIP压缩 string filePath Path.Combine(folderPath, ${cube.name}_{faceNames[i]}.exr); File.WriteAllBytes(filePath, exrBytes); DestroyImmediate(tex); } Debug.Log($Cubemap {cube.name} 已导出为EXR序列至: {folderPath}); EditorUtility.RevealInFinder(folderPath); } }5. 性能优化与批量处理技巧当你需要为一个大场景的多个位置比如多个房间的中心烘焙Cubemap时手动操作每个点是不现实的。我们需要批量处理。5.1 基于空物体的批量烘焙一种常见的做法是在场景中放置多个空GameObject比如命名为“CubemapSpawnPoint”然后编写脚本遍历所有这些点进行烘焙。using UnityEngine; using UnityEditor; using System.Collections.Generic; public class BatchCubemapBaker : EditorWindow { [SerializeField] private ListTransform bakePoints new ListTransform(); [SerializeField] private int cubemapSize 512; [SerializeField] private string baseName SceneSection; private Vector2 scrollPos; [MenuItem(Tools/场景工具/批量Cubemap烘焙)] static void ShowWindow() { GetWindowBatchCubemapBaker(批量烘焙工具); } void OnGUI() { scrollPos EditorGUILayout.BeginScrollView(scrollPos); EditorGUILayout.LabelField(批量烘焙设置, EditorStyles.boldLabel); cubemapSize EditorGUILayout.IntField(Cubemap尺寸, cubemapSize); baseName EditorGUILayout.TextField(资源基础名, baseName); EditorGUILayout.Space(); EditorGUILayout.LabelField(烘焙点列表拖入空物体); ScriptableObject target this; SerializedObject so new SerializedObject(target); SerializedProperty pointsProperty so.FindProperty(bakePoints); EditorGUILayout.PropertyField(pointsProperty, true); so.ApplyModifiedProperties(); EditorGUILayout.EndScrollView(); EditorGUILayout.Space(); if (GUILayout.Button(开始批量烘焙, GUILayout.Height(40))) { if (bakePoints.Count 0) { EditorUtility.DisplayDialog(错误, 请至少添加一个烘焙点。, 确定); return; } string folderPath EditorUtility.SaveFolderPanel(选择保存批量Cubemap的文件夹, Assets/, BatchBakedCubemaps); if (string.IsNullOrEmpty(folderPath)) return; // 确保路径在Assets目录下 if (folderPath.StartsWith(Application.dataPath)) { folderPath Assets folderPath.Substring(Application.dataPath.Length); } else { Debug.LogError(请选择项目Assets目录下的文件夹。); return; } for (int i 0; i bakePoints.Count; i) { if (bakePoints[i] null) continue; EditorUtility.DisplayProgressBar(批量烘焙中, $正在处理点 {i1}/{bakePoints.Count}: {bakePoints[i].name}, (float)i / bakePoints.Count); // 复用之前的烘焙逻辑例如AdvancedCubemapBaker中的核心部分 BakeSingleCubemap(bakePoints[i].position, folderPath, ${baseName}_{i:000}_{bakePoints[i].name}); } EditorUtility.ClearProgressBar(); AssetDatabase.Refresh(); EditorUtility.DisplayDialog(完成, $批量烘焙完成共处理 {bakePoints.Count} 个点。, 确定); } } void BakeSingleCubemap(Vector3 position, string folderPath, string assetName) { // 这里应嵌入之前编写的单点烘焙逻辑 // 创建临时摄像机、设置参数、调用RenderToCubemap、保存资产到folderPath/assetName.asset // 注意处理路径和资产创建的细节 Debug.Log($烘焙点位 {position} 到 {folderPath}/{assetName}); // ... (具体实现代码较长此处省略可参考前文AdvancedCubemapBaker的OnWizardCreate方法) ... } }5.2 烘焙性能注意事项分辨率与数量权衡1024x1024的Cubemap每个面1024x1024其纹理像素总量是1024x1024x6相当可观。对于背景天空盒2048甚至4096可能是必要的但对于小物体的反射贴图256或512就足够了。批量烘焙时合理设置尺寸至关重要。异步操作与进度反馈批量烘焙可能很耗时。一定要使用EditorUtility.DisplayProgressBar给用户反馈并考虑在特别大的批量任务中将烘焙操作分散到多个编辑器帧中进行防止界面卡死。内存管理在循环中创建Cubemap和临时Camera后务必在每轮循环结束时使用DestroyImmediate销毁它们避免内存泄漏。6. 常见问题与排查技巧实录在实际操作中你肯定会遇到各种奇怪的问题。下面是我踩过坑后总结出来的排查清单。6.1 烘焙结果一片黑或颜色异常检查摄像机farClipPlane这是最常见的原因。如果裁剪平面设置得太近场景的大部分都没被渲染自然就是黑色或背景色。将其设置为一个大于场景边界距离的值。检查cullingMask确保摄像机的剔除遮罩包含了你想渲染的物体所在的层。默认值-1Everything通常没问题。检查光照和材质如果场景完全依赖实时动态光照而烘焙时这些光源被禁用或不存在结果就会很暗。确保在烘焙时所有必要的光源都已启用。对于使用着色器球Shader Graph或复杂Shader的物体确保它们在编辑器预览模式下能正确渲染。检查HDR和颜色空间如果你用LDR格式如PNG保存了HDR内容颜色会严重失真。确保使用RGBAHalf等HDR格式并在需要时保存为.exr。6.2 烘焙出的Cubemap有接缝或面方向错误接缝问题这通常是由于立方体贴图六个面的边缘像素采样不连续造成的。在创建Cubemap时确保TextureWrapMode设置为Clamp默认通常是Repeat这可能导致边缘重复。你可以在创建后设置bakedCubemap.wrapMode TextureWrapMode.Clamp;。面方向错误Unity的Cubemap遵循特定的面序X, -X, Y, -Y, Z, -Z。如果你将六个面的图片导入Unity手动创建Cubemap或者用其他软件处理务必确认面的顺序和朝向与Unity约定一致。使用我们脚本烘焙则不存在此问题。6.3 编辑器脚本在播放模式下不工作DestroyImmediate在非编辑器模式在运行时Play Mode脚本中不能使用DestroyImmediate来销毁由脚本创建的、非持久化的资源如临时Texture2D。在运行时应该使用Destroy。但在编辑器脚本中DestroyImmediate是正确选择。资产创建路径在运行时AssetDatabase.CreateAsset是无效的因为它属于编辑器API。这就是为什么之前的运行时示例脚本使用了#if UNITY_EDITOR预处理指令。如果需要在构建后的游戏中动态保存资源你必须使用System.IO将纹理数据写入磁盘文件如PNGEXR这个过程要复杂得多。6.4 烘焙过程导致编辑器卡顿或崩溃分辨率过高尝试烘焙4096的Cubemap可能会耗尽GPU内存。尤其是批量处理时先从低分辨率如256开始测试。内存未及时释放在循环中创建资源后没有及时销毁。确保每一个new Cubemap和new GameObject都在其作用域结束前被妥善销毁。使用协程分帧烘焙对于超大批量任务可以将每一次单点烘焙封装成一个方法然后用编辑器协程EditorCoroutine或简单的计数器在OnGUI中分帧执行避免阻塞主线程。最后分享一个我个人的小技巧在烘焙用于物体反射的Cubemap时我通常会把这个物体本身从渲染层中临时剔除通过cullingMask这样得到的Cubemap里就不会包含这个物体自身的倒影避免产生错误的自身反射干扰。这个技巧在制作车辆漆面、金属球等反射材质时特别有用。