【unity】動態圖集 dynamic atlas (runtime atlas)

不管NGUI還是UGUI,圖集都是在製作期間就生成了的,運行時是一張大圖,這樣做的好處在於我們可以在一定程度上去合併批次,但是圖集通常在製作過程中,會分成commonatlas和系統atlas兩類,一個界面prefab至少會用到兩張圖集,就會出現ABA的圖集穿插打斷合批的情況。還有一種遊戲內容多了以後,各種圖片也相應的變多,類似圖標、commonatlas這種圖集,一張2048x2048可能就放不下了,這時候如果用到兩張2048x2048,就又出現了之前說的ABA的情況,而且內存上也上去了。這時候就出現了新的解決方案:動態圖集。

動態圖集其實就是我們在打包的時候,圖片是零散的,但是最後運行時,自動生成一張空白大圖片,然後將界面上用到的零散的圖片繪製在這個大圖上,只將這個大圖傳入到gpu裏頭,達到合批的效果。由於手機界面製作過程中,標準分辨率往往是低於2048的,所以一張2048的動態圖集就能完全解決一個界面的繪製工作了,但是動態圖集也是有缺點的,動態圖集因爲將圖集的生成過程延遲到了遊戲運行時,所以必然會比靜態圖集多了圖集生成的成本,當然這也是可以優化的。並且在目前的動態圖集生成方案中,還沒有出現公開的支持壓縮的動態圖集解決方案,所以動態圖集目前看來只能是RGBA32的格式。還有一點,靜態圖集由於圖片在生成過程中是確定的,可以將分配算法做得很好,圖集的利用率也能做到很高。動態圖集由於圖片是動態生成的,在遊戲運行過程中也會動態的增減圖片,類似操作系統的內存分配算法,圖集必然會出現碎片,圖集的利用率也不可能做得很高。

說了那麼多 就做個demo來看看動態圖集的威力吧。

 

這個demo只是簡單的演示一下動態圖集的主要思路,圖片分配算法也只是將大圖片分成128x128的一個一個分區,每個分區採用引用計數開控制是否在使用圖片,用於維護整個UI系統的話,這種算法並不適用,但是如果只是用於icon圖標的話,由於icon圖標是固定尺寸的,所以這套算法就很合適了。下面上源碼:

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

public class NxSpriteInfo
{
    private int _x;
    private int _y;
    private Sprite _sprite;
    private int _referenceCount;

    private int _width;
    private int _height;

    public int x { get { return _x; } }
    public int y { get { return _y; } }

    public Sprite sprite
    {
        get { return _sprite; }
    }

    public NxSpriteInfo(int x, int y, Texture2D mainTexture, int startX, int startY, int width, int height)
    {
        _x = x;
        _y = y;
        _referenceCount = 0;

        _width = width;
        _height = height;

        _sprite = Sprite.Create(mainTexture, new Rect(startX, startY, width, height), Vector2.one / 2f);
    }

    public bool IsEmpty()
    {
        return _referenceCount == 0;
    }

    public void AddReference()
    {
        ++_referenceCount;
        Debug.Log(string.Format("[AddReference]Sprite:[{0},{1}] ref:{2}", x, y, _referenceCount));
    }

    public void RemoveReference()
    {
        if (_referenceCount == 0) return;
        --_referenceCount;

        Debug.Log(string.Format("[RemoveReference]Sprite:[{0},{1}] ref:{2}", x, y, _referenceCount));
    }
}

public class DynamicAtlas : MonoBehaviour
{
    private const int MAX_DYNAMIC_ATLAS_SIZE = 1024;
    private const int DYNAMIC_ATLAS_CELL_SIZE = 128;
    private const int DYNAMIC_ATLAS_CELL_COUNT = MAX_DYNAMIC_ATLAS_SIZE / DYNAMIC_ATLAS_CELL_SIZE;

    [SerializeField]
    private Texture2D _dynamicAtlasTex;

    // 策略 分成格子
    private List<NxSpriteInfo> _spriteCacheList;
    private Dictionary<int, int> _spriteRedirectMap = new Dictionary<int, int>();

    private void Awake()
    {
        _dynamicAtlasTex = new Texture2D(MAX_DYNAMIC_ATLAS_SIZE, MAX_DYNAMIC_ATLAS_SIZE, TextureFormat.RGBA32, false);
        _initCacheSprite();
    }

    private void _initCacheSprite()
    {
        int cellCount = DYNAMIC_ATLAS_CELL_COUNT;

        _spriteCacheList = new List<NxSpriteInfo>();
        for (int i = 0; i < cellCount; ++i)
        {
            for (int j = 0; j < cellCount; ++j)
            {
                _spriteCacheList.Add(new NxSpriteInfo(i, j, 
                    _dynamicAtlasTex,
                    i * DYNAMIC_ATLAS_CELL_SIZE, j * DYNAMIC_ATLAS_CELL_SIZE,
                    DYNAMIC_ATLAS_CELL_SIZE, DYNAMIC_ATLAS_CELL_SIZE));
            }
        }
    }

    public Sprite GetOrLoadSprite(Sprite sprite)
    {
        // 拿緩存
        var spriteInstanceID = sprite.GetInstanceID();
        //Debug.Log(string.Format(" name: {0} instanceid: {1}", sprite.name, spriteInstanceID));
        int index = -1;
        if (_spriteRedirectMap.TryGetValue(spriteInstanceID, out index))
        {
            var newSprite = _spriteCacheList[index];
            newSprite.AddReference();
            return newSprite.sprite;
        }

        // 檢查是不是本身就是動態生成的 如果是的話 什麼都不用做
        for (int i = 0; i < _spriteCacheList.Count; ++i)
        {
            var sp = _spriteCacheList[i];
            if (sp.sprite == sprite)
            {
                return sprite;
            }
        }

        // 拿不到緩存就找個空格子新增
        var emptySprite = GetEmptySprite();
        if (emptySprite != null)
        {
            // GPU上直接操作 速度快 兼容性差
            Graphics.CopyTexture(sprite.texture, 0, 0, (int)sprite.rect.x, (int)sprite.rect.y, (int)sprite.rect.width, (int)sprite.rect.height,
                                _dynamicAtlasTex, 0, 0, (int)emptySprite.sprite.rect.x, (int)emptySprite.sprite.rect.y);

            // 這裏要先刪除上一個的
            index = GetIndex(emptySprite);
            foreach (var redirect in _spriteRedirectMap)
            {
                if (redirect.Value == index)
                {
                    _spriteRedirectMap.Remove(redirect.Key);
                    break;
                }
            }
            _spriteRedirectMap.Add(spriteInstanceID, GetIndex(emptySprite));
            emptySprite.AddReference();
            emptySprite.sprite.name = sprite.name + "(Dynamic)";
            return emptySprite.sprite;
        }

        // 找不到空格子就直接返回sprite
        return sprite;
    }

    public void ReleaseSprite(Sprite sprite)
    {
        for (int i = 0; i < _spriteCacheList.Count; ++i)
        {
            var sp = _spriteCacheList[i];
            if (sp.sprite == sprite)
            {
                sp.RemoveReference();
                break;
            }
        }
    }

    private NxSpriteInfo GetEmptySprite()
    {
        for (int i = 0; i < _spriteCacheList.Count; ++i)
        {
            var sp = _spriteCacheList[i];
            if (sp.IsEmpty())
                return sp;
        }
        return null;
    }

    private int GetIndex(NxSpriteInfo sprite)
    {
        return sprite.x * DYNAMIC_ATLAS_CELL_COUNT + sprite.y;
    }

}

關鍵代碼都在GetOrLoadSprite這個函數裏面了,其中最重要的一句就是Graphics.CopyTexture,這個是直接在GPU上操作圖片,速度非常快,但是缺點是兼容性不是很好,也用備用方案,直接上內存copy再傳到gpu上,會慢一些,demo這裏就不做演示了,需要的自行查相關資料,我記得雨鬆似乎做過類似分享,當時應該是用於角色的貼圖合併。

另外配合這個DynamicAtlas,我也做了一個NxImage來配合它,簡單繼承了一下ugui的image,在awake和ondestory做了引用計數的加減,只是用於功能演示,真正用到項目中,應該會更加註重細節。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class NxImage : Image
{
    protected override void Start()
    {
        base.Start();

        if (Application.isPlaying && this.sprite != null)
        {
            var dynamicAtlasGo = GameObject.Find("DynamicAtlas");
            if (dynamicAtlasGo == null)
            {
                GameObject go = new GameObject();
                go.name = "DynamicAtlas";
                go.AddComponent<DynamicAtlas>();
                dynamicAtlasGo = go;
            }

            if (dynamicAtlasGo != null)
            {
                var dynamicAtlas = dynamicAtlasGo.GetComponent<DynamicAtlas>();
                if (dynamicAtlas != null)
                {
                    this.sprite = dynamicAtlas.GetOrLoadSprite(this.sprite);
                }
            }
        }
    }

    public void SetNewSprite(Sprite sp)
    {
        var dynamicAtlasGo = GameObject.Find("DynamicAtlas");
        if (dynamicAtlasGo != null)
        {
            var dynamicAtlas = dynamicAtlasGo.GetComponent<DynamicAtlas>();
            if (dynamicAtlas != null)
            {
                if (this.sprite != null)
                    dynamicAtlas.ReleaseSprite(this.sprite);
                this.sprite = dynamicAtlas.GetOrLoadSprite(sp);
            }
        }
    }

    protected override void OnDestroy()
    {
        base.OnDestroy();

        if (this.sprite != null)
        {
            var dynamicAtlasGo = GameObject.Find("DynamicAtlas");
            if (dynamicAtlasGo != null)
            {
                var dynamicAtlas = dynamicAtlasGo.GetComponent<DynamicAtlas>();
                if (dynamicAtlas != null)
                {
                    dynamicAtlas.ReleaseSprite(this.sprite);
                }
            }
        }

    }
}

簡單掛了幾個圖片上去測試,效果如下:

運行後:

可以看到drawcall明顯降低了,我們再看看合併後真正用到的圖片:

以上就是動態生成圖集的簡單思路 僅供參考

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