Unity5中lightmap的坑

Kaitiren發表於2015-12-09

 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

[csharp] view plaincopy
  1. using UnityEngine;  
  2. using UnityEditor;  
  3. using System.Collections;  
  4. using System.Collections.Generic;  
  5.   
  6. public class PrefabLightmapDataEditor : Editor  
  7. {  
  8.     // 把renderer上面的lightmap資訊儲存起來,以便儲存到prefab上面  
  9.     [MenuItem("GameObject/Lightmap/Save"false, 0)]  
  10.     static void SaveLightmapInfo ()  
  11.     {  
  12.         GameObject go = Selection.activeGameObject;  
  13.         if (go == nullreturn;  
  14.         PrefabLightmapData data = go.GetComponent<PrefabLightmapData>();  
  15.         if (data == null) {  
  16.             data = go.AddComponent<PrefabLightmapData>();  
  17.         }  
  18.   
  19.         data.SaveLightmap();  
  20.         EditorUtility.SetDirty(go);  
  21.     }  
  22.   
  23.     // 把儲存的lightmap資訊恢復到renderer上面  
  24.     [MenuItem("GameObject/Lightmap/Load"false, 0)]  
  25.     static void LoadLightmapInfo()  
  26.     {  
  27.         GameObject go = Selection.activeGameObject;  
  28.         if (go == nullreturn;  
  29.   
  30.         PrefabLightmapData data = go.GetComponent<PrefabLightmapData>();  
  31.         if (data == nullreturn;  
  32.   
  33.         data.LoadLightmap();  
  34.         EditorUtility.SetDirty(go);  
  35.   
  36.         new GameObject();  
  37.     }  
  38.   
  39.     [MenuItem("GameObject/Lightmap/Clear"false, 0)]  
  40.     static void ClearLightmapInfo()  
  41.     {  
  42.         GameObject go = Selection.activeGameObject;  
  43.         if (go == nullreturn;  
  44.   
  45.         PrefabLightmapData data = go.GetComponent<PrefabLightmapData>();  
  46.         if (data == nullreturn;  
  47.   
  48.         data.m_RendererInfo.Clear();  
  49.         EditorUtility.SetDirty(go);  
  50.     }  
  51. }  


PrefabLightmapData.cs

[csharp] view plaincopy
  1. using UnityEngine;  
  2. using System.Collections;  
  3. using System.Collections.Generic;  
  4. using UnityEngine.Rendering;  
  5.   
  6. public class PrefabLightmapData : MonoBehaviour  
  7. {  
  8.     [System.Serializable]  
  9.     public struct RendererInfo  
  10.     {  
  11.         public Renderer     renderer;  
  12.         public int          lightmapIndex;  
  13.         public Vector4      lightmapOffsetScale;  
  14.     }  
  15.       
  16.     public List<RendererInfo> m_RendererInfo;  
  17.   
  18.     void Awake ()  
  19.     {  
  20.         LoadLightmap();  
  21.         Debug.Log("--------------------------------------------");  
  22.         var renderers = GetComponentsInChildren<MeshRenderer>();  
  23.         foreach (var item in renderers) {  
  24.             Debug.Log(item.lightmapIndex);  
  25.         }  
  26.   
  27.         //      if (m_RendererInfo == null || m_RendererInfo.Count == 0)  
  28.         //          return;  
  29.         //        
  30.         //      var lightmaps = LightmapSettings.lightmaps;  
  31.         //      var combinedLightmaps = new LightmapData[lightmaps.Length + m_Lightmaps.Count];  
  32.         //        
  33.         //      lightmaps.CopyTo(combinedLightmaps, 0);  
  34.         //      for (int i = 0; i < m_Lightmaps.Count; i++)  
  35.         //      {  
  36.         //          combinedLightmaps[i+lightmaps.Length] = new LightmapData();  
  37.         //          combinedLightmaps[i+lightmaps.Length].lightmapFar = m_Lightmaps[i];  
  38.         //      }  
  39.         //        
  40.         //      ApplyRendererInfo(m_RendererInfo, lightmaps.Length);  
  41.         //      LightmapSettings.lightmaps = combinedLightmaps;  
  42.     }  
  43.   
  44.     public void SaveLightmap()  
  45.     {  
  46.         m_RendererInfo.Clear();  
  47.   
  48.         var renderers = GetComponentsInChildren<MeshRenderer>();  
  49.         foreach (MeshRenderer r in renderers) {  
  50.             if (r.lightmapIndex != -1) {  
  51.                 RendererInfo info = new RendererInfo();  
  52.                 info.renderer = r;  
  53.                 info.lightmapOffsetScale = r.lightmapScaleOffset;  
  54.                 info.lightmapIndex = r.lightmapIndex;  
  55.   
  56. //                Texture2D lightmap = LightmapSettings.lightmaps[r.lightmapIndex].lightmapFar;  
  57. //  
  58. //                info.lightmapIndex = m_Lightmaps.IndexOf(lightmap);  
  59. //                if (info.lightmapIndex == -1) {  
  60. //                    info.lightmapIndex = m_Lightmaps.Count;  
  61. //                    m_Lightmaps.Add(lightmap);  
  62. //                }  
  63.   
  64.                 m_RendererInfo.Add(info);  
  65.             }  
  66.         }  
  67.     }  
  68.   
  69.     public void LoadLightmap()  
  70.     {  
  71.         if (m_RendererInfo.Count <= 0) return;  
  72.   
  73.         foreach (var item in m_RendererInfo) {  
  74.             item.renderer.lightmapIndex = item.lightmapIndex;  
  75.             item.renderer.lightmapScaleOffset = item.lightmapOffsetScale;  
  76.         }  
  77.     }  
  78. }  

            場景中的光照貼圖的設定跟之前版本一樣,直接設定LightmapSetting中的lightmap陣列就可以了,光照貼圖同樣可以打包成assetbundle,只要能夠正確的載入並設定就可以。

            如果我們有做地圖動態生成、動態載入等需求,那麼就必須要自己處理lightmap的載入。大體思路是將場景中物體的光照資訊(lightmapindex等)和當前場景的光照貼圖(lightmapsetting中獲取)儲存成一個配置,然後執行時自己載入這些資訊。不過這樣做有一個前提是場景中的物體一定不能做成prefab,原因如前文所述。

            如果不考慮場景的動態更新,那麼就簡化很多。一個場景一個Scene,每個場景自己烘焙,然後使用LoadLevelAdditive載入場景就可以了。Unity可以正確處理好光照貼圖的合併和索引的更新。

相關文章