Unity大世界LightMap處理

在Unity中,烘焙LightMap採用的是一個場景烘焙一組LightMap。而對於大世界場景來說,沒辦法把世界上所有的物體在同一場景下烘焙。Unity提供的解決辦法是通過SubScene來解決,就是分場景烘焙,然後再通過加載卸載Scene的方式來實現。但有時候有這樣的需求,同一組室內場景可能在多個地方存在,美術希望烘焙好一組物體,能複製到各個地方,並且能很好地預覽,這樣使用SubScene來說就比較麻煩了。

先說一下 unity的LightMap機制,烘焙分爲動態物體和靜態物體,動態物體走的是GI,通過環境光,LightProb等這些算出三維光照數據,然後計算動態物體的球諧光照。對於靜態物體來說就會烘焙成 lightmap,一般有幾組三張貼圖(color,dir以及shadow)。靜態物體的MeshRender上會有個lightmapIndex存放採用第幾組lightmap,還有個lightmapScaleOffset存放uv偏移,通過這兩個數據就能顯示正確。

知道LightMap的原理後就比較簡單了,我們只需要存好我們需要使用的數據,然後設置對應的位置就能正確顯示了。

首先,我們定義好我們的數據結構,我們期望在一個prefab上掛一個我們的腳本,然後加載這個prefab上所有的MeshRender。我們就需要一個這樣的ScriptObject。

public class CustomLightMapDataMap : ScriptableObject
{
    public MeshLightmapData[] LightMapDatas = null;
}
[Serializable]
public struct CustomLightmapData
{
    /// <summary>
    /// The color for lightmap.
    /// </summary>
    public Texture2D LightmapColor;

    /// <summary>
    /// The dir for lightmap.
    /// </summary>
    public Texture2D LightmapDir;

    /// <summary>
    /// The shadowmask for lightmap.
    /// </summary>
    public Texture2D ShadowMask;

    /// <summary>
    /// Initializes a new instance of the <see cref="CustomLightmapData"/> struct.
    /// </summary>
    /// <param name="data">lightmapdata.</param>
    public CustomLightmapData(LightmapData data)
    {
        this.LightmapColor = data.lightmapColor;
        this.LightmapDir = data.lightmapDir;
        this.ShadowMask = data.shadowMask;
    }

    public bool IsA(LightmapData data)
    {
        return this.LightmapColor == data.lightmapColor &&
        this.LightmapDir == data.lightmapDir &&
        this.ShadowMask == data.shadowMask;
    }

    public LightmapData GetLightmapData()
    {
        LightmapData data = new LightmapData();
        data.lightmapColor = this.LightmapColor;
        data.lightmapDir = this.LightmapDir;
        data.shadowMask = this.ShadowMask;
        return data;
    }
}

[Serializable]
public struct MeshLightmapData
{
    public Vector4 LightmapScaleOffset;

    public CustomLightmapData LightmapData;
}

然後再在編輯器上弄一個菜單,選中物體就能自動幹這件事情。

[MenuItem("Window/LightMapGenerate")]
    private static void Generated()
    {
        string outputPath = "Assets/LightMapPrefab";
        var lightmapPath = GetLightMapPath();
        if (!string.IsNullOrEmpty(lightmapPath))
        {
            outputPath = Path.GetDirectoryName(lightmapPath);
        }

        GameObject obj = Selection.activeGameObject;
        if (obj == null)
        {
            return;
        }

        var dataMap = (CustomLightMapDataMap)ScriptableObject.CreateInstance(typeof(CustomLightMapDataMap));
        var renders = obj.GetComponentsInChildren<MeshRenderer>();
        List<MeshLightmapData> datas = new List<MeshLightmapData>();
        var lightmaps = LightmapSettings.lightmaps;
        foreach (var render in renders)
        {
            if (render.lightmapIndex < 0 || render.lightmapIndex >= lightmaps.Length)
            {
                Debug.LogError("lightmap error:" + render.gameObject.name);
                return;
            }
            var data = new MeshLightmapData()
            {
                LightmapScaleOffset = render.lightmapScaleOffset,
                LightmapData = new CustomLightmapData(lightmaps[render.lightmapIndex]),
            };
            datas.Add(data);
        }

        dataMap.LightMapDatas = datas.ToArray();
        var loader = obj.GetComponent<LightMapDataLoader>();
        if (loader == null)
        {
            loader = obj.AddComponent<LightMapDataLoader>();
        }

        outputPath = Path.Combine(outputPath, obj.name + ".asset");
        AssetDatabase.CreateAsset(dataMap, outputPath);
        AssetDatabase.SaveAssets();
        loader.Asset = AssetDatabase.LoadAssetAtPath<CustomLightMapDataMap>(outputPath);
    }

    private static string GetLightMapPath()
    {
        var lightmaps = LightmapSettings.lightmaps;
        if (lightmaps.Length == 0)
        {
            return string.Empty;
        }

        return AssetDatabase.GetAssetPath(lightmaps[0].lightmapColor);
    }

數據保存好了,我們只需要加載就好了。加載除了要加載MeshRender上的數據,還要設置好場景的LightMap。這裏還有個特別重要的問題就是卸載,在物體銷燬時,我們要處理場景的lightmap,這裏需要通過一個計數器去幹這件事情,當引用計數爲0了,我們就去清理lightmap貼圖數據。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

[ExecuteInEditMode]
public class LightMapDataLoader : MonoBehaviour
{
    private static Dictionary<CustomLightmapData, int> lightmapDataRefenceCount = new Dictionary<CustomLightmapData, int>();

    [SerializeField]
    private CustomLightMapDataMap asset;

    private HashSet<CustomLightmapData> lightmapDatas = new HashSet<CustomLightmapData>();

    public CustomLightMapDataMap Asset
    {
        get { return this.asset; }
        set { this.asset = value; }
    }

    public static void Clear()
    {
        lightmapDataRefenceCount.Clear();
    }

    // Start is called before the first frame update
    private void Awake()
    {
        if (this.asset != null)
        {
            var lightmaps = LightmapSettings.lightmaps;
            var renders = this.GetComponentsInChildren<MeshRenderer>();
            var datas = this.asset.LightMapDatas;
            if (datas.Length != renders.Length)
            {
                return;
            }

            List<LightmapData> lightmapList = new List<LightmapData>(lightmaps);
            for (int i = 0; i < datas.Length; i++)
            {
                var lightMapIndex = -1;
                var nullIndex = -1;
                LightmapData currentData = null;
                for (int j = lightmapList.Count - 1; j >= 0; j--)
                {
                    var lightmap = lightmapList[j];
                    if (datas[i].LightmapData.IsA(lightmap))
                    {
                        lightMapIndex = j;
                        currentData = lightmap;
                    }

                    if (lightmap.lightmapColor == null &&
                        lightmap.lightmapDir == null &&
                        lightmap.shadowMask == null)
                    {
                        nullIndex = j;
                    }
                }

                if (lightMapIndex == -1)
                {
                    currentData = datas[i].LightmapData.GetLightmapData();
                    if (nullIndex == -1)
                    {
                        lightmapList.Add(currentData);
                        lightMapIndex = lightmapList.Count - 1;
                    }
                    else
                    {
                        lightmapList[nullIndex] = currentData;
                        lightMapIndex = nullIndex;
                    }
                }

                this.lightmapDatas.Add(datas[i].LightmapData);
                renders[i].lightmapIndex = lightMapIndex;
                renders[i].lightmapScaleOffset = datas[i].LightmapScaleOffset;
            }

            foreach (var data in this.lightmapDatas)
            {
                if (!lightmapDataRefenceCount.TryGetValue(data, out var count))
                {
                    count = 0;
                }
                else
                {
                    lightmapDataRefenceCount.Remove(data);
                }

                count++;
                lightmapDataRefenceCount.Add(data, count);
            }

            LightmapSettings.lightmaps = lightmapList.ToArray();
        }
    }

    private void OnDestroy()
    {
        foreach (var data in this.lightmapDatas)
        {
            if (lightmapDataRefenceCount.TryGetValue(data, out var count))
            {
                count--;
                lightmapDataRefenceCount.Remove(data);
                if (count == 0)
                {
                    var lightmaps = LightmapSettings.lightmaps;
                    for (int i = 0; i < lightmaps.Length; i++)
                    {
                        if (data.IsA(lightmaps[i]))
                        {
                            lightmaps[i].lightmapColor = null;
                            lightmaps[i].lightmapDir = null;
                            lightmaps[i].shadowMask = null;
                        }
                    }

                    LightmapSettings.lightmaps = lightmaps;
                }
                else
                {
                    lightmapDataRefenceCount.Add(data, count);
                }
            }
        }
    }
}

做好這些事情之後,我們就可以在場景中烘焙一組物體,然後選中Root,點擊Window/LightMapGenerate,會幫你組織好數據,掛好腳本。你可以把這個物體複製到任何地方都是顯示正確,也可以保存成prefab通過程序加載和銷燬。

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