Unity5中lightmap的坑

        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);
    }
}


PrefabLightmapData.cs

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;
        }
    }
}

            场景中的光照贴图的设置跟之前版本一样,直接设置LightmapSetting中的lightmap数组就可以了,光照贴图同样可以打包成assetbundle,只要能够正确的加载并设置就可以。

            如果我们有做地图动态生成、动态加载等需求,那么就必须要自己处理lightmap的加载。大体思路是将场景中物体的光照信息(lightmapindex等)和当前场景的光照贴图(lightmapsetting中获取)保存成一个配置,然后运行时自己加载这些信息。不过这样做有一个前提是场景中的物体一定不能做成prefab,原因如前文所述。

            如果不考虑场景的动态更新,那么就简化很多。一个场景一个Scene,每个场景自己烘焙,然后使用LoadLevelAdditive加载场景就可以了。Unity可以正确处理好光照贴图的合并和索引的更新。

你可能感兴趣的:(Unity5中lightmap的坑)