はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】GIFをサポートするようにできる「Unity-GifDecoder」の使い方(Streamを扱えるので便利)

はじめに

今回はUnity-GifDecoderというUnityでGIFサポートできるライブラリの紹介をしたいと思います。

Custom gif decoder written from scratch, designed for Unity engine

There is no gif decoding library for .net, since GifBitmapDecoder is already included in PresentationCore.dll, but you cant use it in Unity (Since mono doesn't support WPF).

With this library you can decode .gif file from any Stream (file, network, memory, you name it) from any thread.

// DeepL翻訳
Unityエンジン用にゼロから書かれたカスタムgifデコーダです。

GifBitmapDecoderはすでにPresentationCore.dllに含まれていますが、Unityでは使用できません(monoはWPFをサポートしていないため)ので、.net用のGIFデコードライブラリは存在しません。

このライブラリを使うと、あらゆるストリーム(ファイル、ネットワーク、メモリ、何でもOK)から、あらゆるスレッドで.gifファイルをデコードすることができます。

github.com

利用サンプル

インストール方法

GitHubAssets/Scripts/Runtime以下のスクリプトをUnityにインポートします。

github.com

適当にGifDecoderといったフォルダを作成して、.asmdefを定義すると使いやすくなるかもしれません。


またGitHubpackage.jsonが上がっていたので、PackageManagerから取得できないか試したのですがうまくいきませんでした。

もしかしたら対応してくれているのかもしれませんが、私自身PackageManager対応についての知識が足らず、勉強します...。
Unity で .unitypackage で配布していたアセットを Package Manager 対応してみた - 凹みTips

使い方

まずは公式ドキュメントのコードを見てみます。
(あまりコーディングが得意でない方は、この後にサンプルとして載せるスクリプトを使いまわせばコードを書かずに使えます)

// Gifに書き込まれている画像・間隔(s)
var frames = new List<Texture>();
var frameDelays = new List<float>();

// Gifが置かれているパス (Assets/StreamingAssets/sample.gif)
var path = Path.Combine(Application.streamingAssetsPath, "sample.gif");
        
// Stream作成
using (var gifStream = new GifStream(path))
{
    // Streamの読み込み箇所が最後までいっていない場合は読み込み続ける
    while (gifStream.HasMoreData)
    {
        // Tokenの種類によって処理を変更する
        // NOTE : Graphic Control Extensionに間隔,Image Blockに画像データと間隔のデータが記載されている
        // 参考 : https://www.tohoho-web.com/wwwgif.htm
        switch (gifStream.CurrentToken)
        {
            // Imageだった場合は画像を読み取る(間隔はGraphic Control Extensionから読み取っていたもの)
            case GifStream.Token.Image:
                var image = gifStream.ReadImage();
                var frame = new Texture2D(
                    gifStream.Header.width, 
                    gifStream.Header.height, 
                    TextureFormat.ARGB32, false); 

                frame.SetPixels32(image.colors);
                frame.Apply();

                frames.Add(frame);
                frameDelays.Add(image.SafeDelaySeconds);
                break;
            
            // Comment Extensionだった場合はログ出力
            case GifStream.Token.Comment:
                var commentText = gifStream.ReadComment();
                Debug.Log(commentText);
                break;

            // それ以外(Headerを読み込んだり等)
            default:
                gifStream.SkipToken();
                break;
        }
    }
}

Gifの構造を知らないと理解するのは難しいかもしれませんが、GifStreamを生成して適宜Gifに記載されているデータを読み取っていきます。
www.tohoho-web.com

中のコードも少し読ませていただきましたが、かなりしっかりとしていると思います。

前紹介したmgGifと一番異なるところは、Streamを扱える箇所ですかね。
mgGifではメモリに格納されたbyte配列からしか読み取ることはできませんでした。

www.hanachiru-blog.com

// Unity-GifDecoderではStreamを扱える

// Gifが置かれているパス (Assets/StreamingAssets/sample.gif)
var path = Path.Combine(Application.streamingAssetsPath, "sample.gif");
var file = File.ReadAllBytes(path);

// ファイルから直接読み取り
using var gifStream1 = new GifStream(path);
        
// メモリから読み取り
using var gifStream2 = new GifStream(file);
using var gifStream3 = new GifStream(new MemoryStream(file));

mgGifの方でUnity-GifDecoderよりも少しパフォーマンスが優れているというベンチマークが存在していますが、このStreamの箇所が少し影響しているような気もします。
github.com

ファイルにアクセスするよりも、メモリにアクセスする方が高速です。ただし一度ファイルからデータをメモリに入れてから読み取る場合、メモリ使用量が増えるので一長一短ではあります。
(さすがに同じような条件で計測をしているとは思うので、コードの書き方の細かいチューニングの結果だとは思います...)

AnimatedTextureコンポーネント

流石にサンプルコードだけだと使いずらいと思うので、使いやすいようなコンポーネントを作成してみました。

public class AnimatedTexture : MonoBehaviour
{
    [SerializeField] private Renderer target;
    [SerializeField, Header("Relative path from StreamingAssets folder")] private string fileName;
    
    private List<Texture2D> _frames;
    private List<float> _frameDelay;

    private int _currentFrame = 0;
    private float _time = 0.0f;
    
    private void Start()
    {
        var path = Path.Combine(Application.streamingAssetsPath, fileName);
        ReadGif(path, out _frames, out _frameDelay);
        if(_frames.Count > 0) target.material.mainTexture = _frames[0];
    }

    private void Update()
    {
        if (_frames == null) return;
        if (_frameDelay == null) return;

        _time += Time.deltaTime;

        if(_time >= _frameDelay[_currentFrame])
        {
            _currentFrame = (_currentFrame + 1) % _frames.Count;
            _time = 0.0f;
            target.material.mainTexture = _frames[_currentFrame];
        }
    }

    private static void ReadGif(string path, out List<Texture2D> frames, out List<float> frameDelays)
    {
        frames = new List<Texture2D>();
        frameDelays = new List<float>();

        using var gifStream = new GifStream(path);
        while (gifStream.HasMoreData)
        {
            switch (gifStream.CurrentToken)
            {
                case GifStream.Token.Image:
                    var image = gifStream.ReadImage();
                    var frame = new Texture2D(
                        gifStream.Header.width, 
                        gifStream.Header.height, 
                        TextureFormat.ARGB32, false); 

                    frame.SetPixels32(image.colors);
                    frame.Apply();

                    frames.Add(frame);
                    frameDelays.Add(image.SafeDelaySeconds);
                    break;
                default:
                    gifStream.SkipToken();
                    break;
            }
        }
    }
}

Rendererへの参照と、GifAssets/StreamingAssets以下に配置し、名前を記載してあげれば動作します。

AnimatedTextureコンポーネント

AnimatedImageコンポーネント

またuGUIImageに適応したい場合の方が多そう?な気がするので、コードを書いてみました。

public class AnimatedImage : MonoBehaviour
{
    [SerializeField] private Image image;
    [SerializeField, Header("Relative path from StreamingAssets folder")] private string fileName;
    
    private List<Sprite> _frames;
    private List<float> _frameDelay;

    private int _currentFrame = 0;
    private float _time = 0.0f;
    
    private void Start()
    {
        var path = Path.Combine(Application.streamingAssetsPath, fileName);
        ReadGif(path, out _frames, out _frameDelay);
        if(_frames.Count > 0) image.sprite = _frames[0];
    }

    private void Update()
    {
        if (_frames == null) return;
        if (_frameDelay == null) return;

        _time += Time.deltaTime;

        if(_time >= _frameDelay[_currentFrame])
        {
            _currentFrame = (_currentFrame + 1) % _frames.Count;
            _time = 0.0f;
            image.sprite = _frames[_currentFrame];
        }
    }

    private static void ReadGif(string path, out List<Sprite> frames, out List<float> frameDelays)
    {
        frames = new List<Sprite>();
        frameDelays = new List<float>();

        using var gifStream = new GifStream(path);
        while (gifStream.HasMoreData)
        {
            switch (gifStream.CurrentToken)
            {
                case GifStream.Token.Image:
                    var image = gifStream.ReadImage();
                    var frame = new Texture2D(
                        gifStream.Header.width, 
                        gifStream.Header.height, 
                        TextureFormat.ARGB32, false); 

                    frame.SetPixels32(image.colors);
                    frame.Apply();

                    frames.Add(Texture2DtoSprite(frame));
                    frameDelays.Add(image.SafeDelaySeconds);
                    break;
                default:
                    gifStream.SkipToken();
                    break;
            }
        }
    }
    
    private static Sprite Texture2DtoSprite(Texture2D tex)
        => Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.zero);
}

Imageへの参照と、GifAssets/StreamingAssets以下に配置し、名前を記載してあげれば動作します。

AnimatedImageコンポーネント

自分で書いておいてあれですが、StreamingAssetsからファイルを読み取る実装はあんまり良いとは言えない気もします。
文字列で読み込むのもアレですし。

ResoucesからTextAssetsとして読み取る(あとAssetBundleとかで)というのでも良いのかもしれませんね。
StreamingAssetsと違って圧縮できるのが魅力です。

実現できるかはやっていないので分かりませんが、ScriptedImporterでインポート時にScriptableObjectに変換をしておき、コンポーネントと簡単に紐づけられるようにしたりとかができたら面白いかなと思ったりします。
ただその場合に前処理でSpriteに変換してScriptableObjectのサブアセットにしておくと良いかなとかファイルサイズ増加しないか等考え事ばかりして、重い腰が上がっていないと言った感じです。

一応OSSとして作るのも面白しろそうな気もするので、後で余裕があれば作ってみようかなと思ったり。

パフォーマンスについて

GIFサポートの有名なライブラリとしてUniGIFがありますが、それと比較してパフォーマンスが良くなっているようです。

Unity-GifDecoderReadmeに記載されている実験結果。

名前 処理時間 GC.Alloc
Unity-GifDecoder ~0.96ms 385.1kb
UniGif ~19.27ms 0.81gb