GifがきついがVideoPlayerもきつい
この記事なに?
UnityでGifを良い感じに使えるようにならないか奮闘した記録です。
環境
- Unity v2020.3.36
- Andoird11 MotoG8
- iOS14.0 iPhone7
目標
Android/iOS環境で、できるだけ大量のGifを一度に再生できるようにする
UnityでGifを扱う正攻法
UnityはGif形式を公式でサポートしていませんので自前で対応する必要があります。だいたいの処理の流れは以下のようになります。
- サードパーティ製のデコーダーでGifを読み込む。
- Texture2D化してマテリアルやRawImageに設定して表示する。
- 指定された時間が経過したらTexture2Dを差し替える。
Unity向けのGifのデコーダーは複数公開されています。適当にGitHubで "Unity Gif" とでも検索すれば結構ヒットします。
どれもMITライセンスなのでお好みのデコーダーを適当に選べば良いと思います。動作しませんが、イメージとしてはだいたい以下のような実装になると思います。
public class GifPlayer : MonoBehaviour
{
class GifData
{
public Texture2D Texture;
public float Seconds;
}
[SerializeField] Renderer renderer;
GifData[] gifDataArray;
bool playing = false;
int currentFrameIndex = -1;
async Task Start()
{
var gifFilePath = Path.Combine(Application.streamingAssetsPath,"hoge.gif");
this.gifDataArray = await GifDecoder.Decode(gifFilePath);
this.PlayGif();
}
async Task PlayGif()
{
if (this.playing) return;
this.playing = true;
while (this.playing)
{
this.currentFrameIndex++;
if (this.gifDataArray.Length<this.currentFrameIndex) this.currentFrameIndex = 0;
renderer.material.mainTexture = this.gifDataArray[this.currentFrameIndex].Texture;
await Task.Delay(TimeSpan.FromSeconds(this.gifDataArray[this.currentFrameIndex].Seconds));
}
}
void StopGif()
{
this.playing = false;
}
void OnDestory()
{
foreach(var gifData in this.gifDataArray)
{
Destory(gifData.Texture);
}
}
}
メモリがきつい
実際に実装して実機で検証してみると、メモリがきついことが分かります。
- デコードしたTexture2Dは非圧縮形式であること
- Gifファイル全体を一度にデコードするので全てのフレームのTexture2Dがメモリを専有すること
から、非常にメモリがきついです。
適当なGif画像とmgGifを使って検証してみます。
UnityEditor上でなら簡単に確認できます。まず画像ファイル自体は0.6MB程度
Profilerで見るとメモリ上では1.1MB程度のようです。
このGif画像は全部で21フレームなので、全体では1.1x21=24.2MB程度でしょうか。
ひとつのアプリで使用できるメモリのサイズは端末に依りますが、最大1.5GB程度のようです。
1枚のGifをただ表示するだけなら許容できるかもしれませんが、同時に大量に表示したい場合はきついです。また、再生したいGifファイルのフレーム数やファイルサイズが不明な場合や、大きいことが予測される場合もきついです。UnityでGifを読み込むまえにGifに対して最適化をかけられれば問題は軽減しますが、事前にGifを最適化する機会があるなら動画形式にしたほうが良い気がします。
どうするか?
Unity側でどうにかして最適化するように頑張ってみます。
リサイズできないか?
利用目的に沿ってリサイズするのは良いアプローチのはずです。特に2のべき乗にすれば多少無駄が省けるはずです。とりあえず256x256にしてみます。実装方法はyano様が書いてくださったこちらを参考にしました。
多少良くなりました。
もう1段階小さくしてみます。
171.1KBと、結構改善できました。Gif全体で171.1*21=3.5MB程度でしょうか。画質に直結するので極端に小さくはできませんが、手軽にできて良い方法だと思われます。ただResizeにRenderTextreを使っているので重い可能性があります。
Texture2Dを圧縮できないか?
Texture2Dを圧縮することを考えてみます。自分の理解では画像のエンコードは重い処理のはずですが、Gifがメモリを大量に使用することを考えれば多少許容して良いと思います。
Editor上で確認してみるとmgGifでTexutre2D化したTexutreはRGBA32という形式でした。これは非圧縮系の中でも最大のサイズになる形式らしいです。参考 どうにか圧縮できれば1/8程度にできるはずです。
Texture2D.Compress()
Texture2D自体に圧縮する機能があるようです。
UnityEditor上で試してみると以下のようになりました。
リサイズして128x128にしてみると
43KBx21枚=0.9MB程度と結構良い結果になりました。しかし、Androidの実機で試してみたところ動作せずRGBA32形式のままでした。調べてみるとUnity2020系だとAndroid/iOSでは動作しないようです。v2021以降はETCに対応したようなので、実機で試してみても良いかもしれません。
エンコーダーを使って圧縮する
基本的にC++で実装されていてWindows/macOS/Linuxで動作するcliツールというのが多いので、Unityから気軽に使えるものは少なく、選択肢はあまりないようです。
iOS
Unity.PVRTC
UnityEditor上で動作することを確認できましたが、Androidでは動作せずARGB32のままでした。iOSではPVRTC_RGB4にエンコードできているようです。
Unity.PVRTCは2のべき乗サイズのテクスチャしかエンコードできないようなので、とりあえず128x128にリサイズした上でエンコードしてみます。
16.5KBなので、相当圧縮できました。ちょっと画質がきつい気がするので、1段階サイズを上げて256x256でやってみます。
64.5KBになりました。非圧縮状態で0.7MBなので、だいたい1/10にできるようです。これは非常に良い選択肢に思えます。
試しに15枚同時に再生してみます。
問題なく再生できました。もっと増やして50枚同時に再生してみます。
全然問題ないですね。64KBx21フレームx50枚=67MB程度でメモリも全然良さそうです。
ちなみに非圧縮で50枚表示しようとすると、アプリが落ちました。0.9MBx21フレームx50枚=945MB。検証に使ったのはiPhone7でメモリが2GBなので、945MBはちょっときつそうです。Textureの圧縮がいかに効果的かを実感します。
Android
Realtime Texture Compression for Android(ETC1)
Google謹製のrg-etc1をC#でラップして扱いやすくしたライブラリのようです。残念ながら手元にあったAndroid機ではうまく動作せず。圧縮すると画像が変な感じになります。
Unity v2021以降のTexture2D.Compress()メソッドは内部ではetcpakというものを使っているらしいので、etcpackを自前でビルドしてC#から呼び出してみるのも良いかもしれせん。
また、ここを確認してみると、Editor上では、etcpak、ETCPACK、Etc2Compを使ってるらしいです。Etc2Compの内部のコードは、素人目に見るとOS側のライブラリに依存しているように思えるので、Androidへの移植はきつそうです。ETCPACKはそういうふうには見えないので、ETCPACKかetcpackをAndroid向けにビルドできないか頑張ってみるのが良いかもしれません。
Gifを動画にできないか?
まったく別の方向で、Gifの動画化を考えます。適当にオンラインコンバーターで検証用のGifをMP4に変換してみるとファイルサイズが1MB->367KBになりました。Gifよりも大分効率的なファイルフォーマットであることが分かります。Android/iOSで動作するffmpeg-kitというものがあるのでこれを使ってGifを動画化してみる方向で考えます。ffmpeg-kit自体は本家ffmpegと同様にGPLライセンスなので、製品に組み込む際は注意が必要です。ソースを直接組み込みのではなくバイナリにして呼び出すようにすれば良かったはず。
ざったな処理の流れは以下のようなイメージです。再生する仕組みも自前でTexture2Dを差し替えるみたいな変な実装じゃなくて、VideoPlayerに任せられるので、動画化さえできれば、もろもろ良い感じにしてくれるだろうという目論見です。
- ffmpeg-kitでGifをMP4化
- UnityのVideoPlayerでMP4を再生
iOS
未検証。ちょっと動かしてみた感じ、素直には動かなさそう。
Android
導入の仕方がwikiに書いてあるので、それ通りに実装したところ、素直に動作しました。以下のような感じでffmpegを動かせます。
public static int Execute(string command)
{
#if UNITY_ANDROID
Debug.Log("Excute: " + command);
using (var configClass = new AndroidJavaClass("com.arthenica.ffmpegkit.FFmpegKitConfig"))
{
using (var paramVal = new AndroidJavaClass("com.arthenica.ffmpegkit.Signal").GetStatic<AndroidJavaObject>("SIGXCPU"))
{
configClass.CallStatic("ignoreSignal", new object[] { paramVal });
}
}
using (var javaClass = new AndroidJavaClass("com.arthenica.ffmpegkit.FFmpegKit"))
{
using (var session = javaClass.CallStatic<AndroidJavaObject>("execute", new object[] { command }))
{
var returnCode = session.Call<AndroidJavaObject>("getReturnCode", new object[] {});
int rc = returnCode.Call<int>("getValue", new object[] {});
return rc;
}
}
return 0;
#endif
}
ffmpegは色々オプションをつけられますが、とりあえず特に指定せずただMP4に変換してみます。
VideoPlayerのメモリ使用量は、RenderTexture/TempBufferというのを見れば良いようです。
0.6MB程度でしょうか。非常に軽量で良い気がします。試しに15枚程度同時に再生してみます。
少しカクつくところもありますが、良い感じですね。
メモリも0.6MBx15=9MB程度と非常に小さくて良いです。
しかし、しばらく再生しつづけていると...
ほとんど動かなくなってしまいました。原因がどこにあるか不明ですが、安定して連続再生は難しいようです。
これは普通のMP4ファイルをUnityに取り込んでVideoClipで再生したときも起きるのでVideoPlayerの問題のようです。SkipOnDropが怪しい気がしますが、ちゃんと有効化されているんですよね...なぜでしょう?
減色する
未検証です。
エンコーダーを実装するのは、ハードルが高いですが、非圧縮形式から非圧縮形式に変換するだけなら、頑張れば実装できるような気がします。参考:KAYAC平山様の記事
- ARGB32->RGB24
- ARGB32->ARGB4444
- ARGB32->RGB565
あたりは実装できるかも。
結論
Unity v2021以上は、mgGifでデコードしてTexture2D.Compress()で圧縮。
Unity v2021未満の場合は、iOSはmgGifでデコードしてUnity.PVRTCで圧縮。Androidは...もっと検証が必要そうです。
GifがきついがVideoPlayerもきつい...
Discussion