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左右~