はじめに
今回は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
のAssets/Scripts/Runtime
以下のスクリプトをUnityにインポートします。
適当にGifDecoder
といったフォルダを作成して、.asmdef
を定義すると使いやすくなるかもしれません。
またGitHub
にpackage.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
配列からしか読み取ることはできませんでした。
// 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
への参照と、Gif
をAssets/StreamingAssets
以下に配置し、名前を記載してあげれば動作します。
AnimatedImageコンポーネント
またuGUI
のImage
に適応したい場合の方が多そう?な気がするので、コードを書いてみました。
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
への参照と、Gif
をAssets/StreamingAssets
以下に配置し、名前を記載してあげれば動作します。
自分で書いておいてあれですが、StreamingAssets
からファイルを読み取る実装はあんまり良いとは言えない気もします。
文字列で読み込むのもアレですし。
Resouces
からTextAssets
として読み取る(あとAssetBundle
とかで)というのでも良いのかもしれませんね。
StreamingAssets
と違って圧縮できるのが魅力です。
実現できるかはやっていないので分かりませんが、ScriptedImporter
でインポート時にScriptableObject
に変換をしておき、コンポーネントと簡単に紐づけられるようにしたりとかができたら面白いかなと思ったりします。
ただその場合に前処理でSprite
に変換してScriptableObject
のサブアセットにしておくと良いかなとかファイルサイズ増加しないか等考え事ばかりして、重い腰が上がっていないと言った感じです。
一応OSS
として作るのも面白しろそうな気もするので、後で余裕があれば作ってみようかなと思ったり。
パフォーマンスについて
GIF
サポートの有名なライブラリとしてUniGIF
がありますが、それと比較してパフォーマンスが良くなっているようです。
Unity-GifDecoder
のReadme
に記載されている実験結果。
名前 | 処理時間 | GC.Alloc |
---|---|---|
Unity-GifDecoder | ~0.96ms | 385.1kb |
UniGif | ~19.27ms | 0.81gb |