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();
if (data == null) {
data = go.AddComponent();
}
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();
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();
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 m_RendererInfo;
void Awake ()
{
LoadLightmap();
Debug.Log("--------------------------------------------");
var renderers = GetComponentsInChildren();
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();
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可以正确处理好光照贴图的合并和索引的更新。