Unity5中光照系统替换为Enlighten是非常大的革新。但是对手游来说,好处还未享受到,坑先踩上了。并且是我研究了两天都没有很好的解决办法的深坑。
我并没有系统的学过图形学,所以以下所说的内容都是自己的理解,可能存在错误的地方,敬请见谅。
所谓lightmap,就是用一组预先烘焙好的贴图来替代运行时光影计算。在Unity5之前使用的是beast系统,Unity5使用的是enlighten系统,新系统的好处是支持运行时光照计算,支持全局光。
作为使用者来说就是两个部分,一个是烘焙的部分,一个是加载的部分。
Unity5的lighting分页中有两个勾选,一个是precompute GI,一个是baked GI。一般来说手游还是无法承载precompute的计算量,从效率的角度考虑这两个GI系统在一个场景中只开启一个。我们使用baked GI,其实也就跟之前的光照系统差不多了。另外烘焙的过程真心慢(万一同时勾选了两个GI,那么就是慢上加慢),GI Cache文件夹如果不限制大小的话,动辄几十G,可见其计算量。
烘焙如果勾上auto则所有的烘焙结果存在GI Cache文件夹下,一般这种模式没有什么价值。点击Bake按钮进行烘焙,烘焙完成后会生成跟场景同名的文件夹,里面有光照贴图和一个lightmapsnapshot.asset文件。这个文件就是第一个坑。
光照贴图的加载原理其实很简单,每个renderer中记录了一个lightmapindex和lightmapscaleoffset,LightmapSetting中有一个全局的光照贴图的数组,包含当前场景中所有光照贴图的索引。每个renderer根据index和offset确定自己应该使用光照贴图的哪个部分,最终渲染出实际带光影的效果。
原本这些数据是存在每个renderer里面的,也即存在scene或者是prefab中。但是Unity5为了多场景合作编辑,把这些数据移到snapshot文件中了。场景中不再保存这些信息,snapshot中保存了场景中的renderer对应的光照数据是什么。但是Unity并没有提供访问snapshot的方法。所以原本很简单的问题在这里变得非常恶心。
由于snapshot中保存的是当前场景中的renderer的信息,所以拷贝新的renderer或者在代码中实例化一个物体都是没有光照信息的。如果场景中的物体保存为prefab,则光照信息也会丢失,因为此时场景中关联的是一个prefab文件,而场景物件是保存在prefab中的。
补充说明,snapshot中根据GameObject的udid来保存对应的光照信息。所以只要udid不变,则光照信息正常,只要改变,则光照信息丢失。所以新实例化的物体是没有光照信息的,这个要自己手动设置(其实也很简单,把原物体所有Renderer中的光照信息赋值给新物体中对应的Renderer就好)。 如果烘焙的时候物体不是Prefab,后来保存为Prefab,或者原来烘焙的时候是Prefab,后来取消Prefab的关联,这两个操作都会使光照信息丢失。
一个简单的解决办法是将光照信息保存在一个组件上面,然后加载场景或者物体的时候再恢复。代码如下:
PrefabLightmapDataEditor.cs
using UnityEngine; using UnityEditor; using System.Collections; using System.Collections.Generic; public class PrefabLightmapDataEditor : Editor { // 把renderer上面的lightmap信息保存起来,以便存储到prefab上面 [MenuItem("GameObject/Lightmap/Save", false, 0)] static void SaveLightmapInfo () { GameObject go = Selection.activeGameObject; if (go == null) return; PrefabLightmapData data = go.GetComponent<PrefabLightmapData>(); if (data == null) { data = go.AddComponent<PrefabLightmapData>(); } data.SaveLightmap(); EditorUtility.SetDirty(go); } // 把保存的lightmap信息恢复到renderer上面 [MenuItem("GameObject/Lightmap/Load", false, 0)] static void LoadLightmapInfo() { GameObject go = Selection.activeGameObject; if (go == null) return; PrefabLightmapData data = go.GetComponent<PrefabLightmapData>(); if (data == null) return; data.LoadLightmap(); EditorUtility.SetDirty(go); new GameObject(); } [MenuItem("GameObject/Lightmap/Clear", false, 0)] static void ClearLightmapInfo() { GameObject go = Selection.activeGameObject; if (go == null) return; PrefabLightmapData data = go.GetComponent<PrefabLightmapData>(); if (data == null) return; data.m_RendererInfo.Clear(); EditorUtility.SetDirty(go); } }
using UnityEngine; using System.Collections; using System.Collections.Generic; using UnityEngine.Rendering; public class PrefabLightmapData : MonoBehaviour { [System.Serializable] public struct RendererInfo { public Renderer renderer; public int lightmapIndex; public Vector4 lightmapOffsetScale; } public List<RendererInfo> m_RendererInfo; void Awake () { LoadLightmap(); Debug.Log("--------------------------------------------"); var renderers = GetComponentsInChildren<MeshRenderer>(); foreach (var item in renderers) { Debug.Log(item.lightmapIndex); } // if (m_RendererInfo == null || m_RendererInfo.Count == 0) // return; // // var lightmaps = LightmapSettings.lightmaps; // var combinedLightmaps = new LightmapData[lightmaps.Length + m_Lightmaps.Count]; // // lightmaps.CopyTo(combinedLightmaps, 0); // for (int i = 0; i < m_Lightmaps.Count; i++) // { // combinedLightmaps[i+lightmaps.Length] = new LightmapData(); // combinedLightmaps[i+lightmaps.Length].lightmapFar = m_Lightmaps[i]; // } // // ApplyRendererInfo(m_RendererInfo, lightmaps.Length); // LightmapSettings.lightmaps = combinedLightmaps; } public void SaveLightmap() { m_RendererInfo.Clear(); var renderers = GetComponentsInChildren<MeshRenderer>(); foreach (MeshRenderer r in renderers) { if (r.lightmapIndex != -1) { RendererInfo info = new RendererInfo(); info.renderer = r; info.lightmapOffsetScale = r.lightmapScaleOffset; info.lightmapIndex = r.lightmapIndex; // Texture2D lightmap = LightmapSettings.lightmaps[r.lightmapIndex].lightmapFar; // // info.lightmapIndex = m_Lightmaps.IndexOf(lightmap); // if (info.lightmapIndex == -1) { // info.lightmapIndex = m_Lightmaps.Count; // m_Lightmaps.Add(lightmap); // } m_RendererInfo.Add(info); } } } public void LoadLightmap() { if (m_RendererInfo.Count <= 0) return; foreach (var item in m_RendererInfo) { item.renderer.lightmapIndex = item.lightmapIndex; item.renderer.lightmapScaleOffset = item.lightmapOffsetScale; } } }
如果我们有做地图动态生成、动态加载等需求,那么就必须要自己处理lightmap的加载。大体思路是将场景中物体的光照信息(lightmapindex等)和当前场景的光照贴图(lightmapsetting中获取)保存成一个配置,然后运行时自己加载这些信息。不过这样做有一个前提是场景中的物体一定不能做成prefab,原因如前文所述。
如果不考虑场景的动态更新,那么就简化很多。一个场景一个Scene,每个场景自己烘焙,然后使用LoadLevelAdditive加载场景就可以了。Unity可以正确处理好光照贴图的合并和索引的更新。