Unity 異步使用Additive加載場景導致的光照貼圖出錯的坑及解決辦法

Lightmap(光照貼圖)是Unity裏很常用的一個功能,畢竟移動設備上實時光照不靠譜,要出效果還是得烘焙。但是在使用中發現了幾坑的地方,通過自己寫了個兩個腳本就輕鬆搞定,這裏分享下。

坑1: 異步加載場景

這個其實還是挺常見的坑:在編輯器裏烘焙+播放場景都沒問題,但是當從別的場景利用 異步加載 方式切換之後,就發現場景裏的效果一團糟。下圖是我使用 Application.LoadLevelAdditiveAsync 切換後的效果:

如果是同步的加載場景倒是沒有這個問題,但是實際遊戲一般也不會讓玩家卡住等待一段時間。這個問題在官方論壇和issue tracker上都有人提及,產生的原因是切換場景之後無法自動切換該場景使用的lightmap數據,具體可以參考 UnityEngine.LightmapSettings 相關API。

解決辦法也很簡單,我實現了一個小腳本,掛在場景中;當場景載入完成,這個物體會在 Start() 中自動刷新當前的光照貼圖設置。爲了美術方便,腳本能夠在每次場景烘焙完之後自動保存新的光照貼圖設置,這樣就避免了手動操作。

using UnityEngine;
[ExecuteInEditMode]
public class SerializedLightmapSetting : MonoBehaviour
{
  [HideInInspector]
  public Texture2D []lightmapFar, lightmapNear;
  [HideInInspector]
  public LightmapsMode mode;
#if UNITY_EDITOR
  public void OnEnable()
  {
    //Debug.Log("[SerializedLightmapSetting] hook");
    UnityEditor.Lightmapping.completed += LoadLightmaps;
  }
  public void OnDisable()
  {
    //Debug.Log("[SerializedLightmapSetting] unhook");
    UnityEditor.Lightmapping.completed -= LoadLightmaps;
  }
#endif
  public void Start ()
  {
    if(Application.isPlaying)
    {
      LightmapSettings.lightmapsMode = mode;
      int l1 = (lightmapFar == null) ? 0 : lightmapFar.Length;
      int l2 = (lightmapNear == null) ? 0 : lightmapNear.Length;
      int l = (l1 < l2) ? l2 : l1;
      LightmapData[] lightmaps = null;
      if (l > 0)
      {
        lightmaps = new LightmapData[l];
        for (int i = 0; i < l; i++)
        {
          lightmaps[i] = new LightmapData();
          if (i < l1)
            lightmaps[i].lightmapFar = lightmapFar[i];
          if (i < l2)
            lightmaps[i].lightmapNear = lightmapNear[i];
        }
      }
      LightmapSettings.lightmaps = lightmaps;
      Destroy(this);
    }
  }
#if UNITY_EDITOR
  public void LoadLightmaps()
  {
    mode = LightmapSettings.lightmapsMode;
    lightmapFar = null;
    lightmapNear = null;
    if (LightmapSettings.lightmaps != null && LightmapSettings.lightmaps.Length > 0)
    {
      int l = LightmapSettings.lightmaps.Length;
      lightmapFar = new Texture2D[l];
      lightmapNear = new Texture2D[l];
      for (int i = 0; i < l; i++)
      {
        lightmapFar[i] = LightmapSettings.lightmaps[i].lightmapFar;
        lightmapNear[i] = LightmapSettings.lightmaps[i].lightmapNear;
      }
    }
    MeshLightmapSetting[] savers = GameObject.FindObjectsOfType<MeshLightmapSetting>();
    foreach(MeshLightmapSetting s in savers)
    {
      s.SaveSettings();
    }
  }
#endif
}

ps. 這裏額外提一個坑的地方:一開始我想避免使用 MonoBehavior 這種比較笨重的東西來保存數據,然後參考了Unity官方博客裏的 ScriptableObject 。結果嘗試了一下午發現博客中的代碼壓根不起作用,感覺就是從頭就寫錯了...

ps2. 編輯器提供的API還是挺方便的~最早版本的腳本每次烘焙完之後需要美術手動保存光照貼圖設置,總是會忘,現在改成自動的就好多了。

坑2: Prefab化物體

在前面代碼中有幾行涉及到了 MeshLightmapSetting ,這個其實就是設計了第二個問題:利用 Instantiate 實例化的prefab的光照貼圖也是一團糟,只有始終在場景裏的物體纔是正常的。

這個問題的本質原因是,prefab物體的光照貼圖信息其實是保存在場景文件裏,而不是對應的prefab中;這樣以後當場景中有一個prefab的多個實例時,引擎能找到對應的光照貼圖位置。但是實際項目中,爲了提高場景的加載速度,我們的物體基本全是在代碼里加載的,避免了加載場景時就同時加載了一堆prefab,所以就導致光照貼圖全跪。

找到原因之後,就比較好解決了——既然引擎不保存,我可以手動保存一份就行了:

using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(Renderer))]
public class MeshLightmapSetting : MonoBehaviour {
   [HideInInspector]
  public int lightmapIndex;
   [HideInInspector]
  public Vector4 lightmapScaleOffset;
  public void SaveSettings()
  {
    Renderer renderer = GetComponent<Renderer>();
    lightmapIndex = renderer.lightmapIndex;
    lightmapScaleOffset = renderer.lightmapScaleOffset;
  }
  public void LoadSettings()
  {
    Renderer renderer = GetComponent<Renderer>();
    renderer.lightmapIndex = lightmapIndex;
    renderer.lightmapScaleOffset = lightmapScaleOffset;
  }
  void Start () {
    LoadSettings();
    if(Application.isPlaying)
      Destroy(this);
  }
}

其實從這兩點出發,可以更加靈活的使用光照貼圖:

  • 美術在不同光照條件下烘焙的lightmap保存起來,然後在運行時利用第一個腳本的思路切換,就實現了天氣系統;
  • 通過設置模型renderer的屬性,可以使得不同物體複用(當然你得真正理解清楚光照貼圖);

之後有機會的話,我會再整理下利用prefab加速場景加載的話題,目前單個場景從未優化的14s左右降到4s左右~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章