こんにちは、エンジニアリングマネージャーの渡辺(@mochi_neko_7)です。
今回はUnityでの画像データのロード方法に関する記事になります。
たかが画像データですが、意外とまだ王道の取り回しが確立されていない印象です。
最近また画像のロード周りを触る機会があり、いろいろな方法を調べてみたのでその内容をまとめてみました。
背景
画像データのロード方法の悩み
画像データをただUnity内に取り込んで利用したいだけの場合は、JPGやPNGのファイルをUnityEditorで取り込むとTextureやSpriteに変換してくれますので、それほど困ることはないと思います。
しかしUnityのプロジェクトの外部から画像データをロードして表示したいという場合には、意外とひと手間二手間かかります。
今回私が調査した画像のロード方法で重要視した観点は以下になります。
- なるべくメインスレッドを止めないで、Unityの外にある画像データをUnity内で表示したい
- JPG、PNGを含む標準的な画像コーデックは対応したい
- Windows/macOS/Android/iOSなどのマルチプラットフォームで動作させたい
画像データはデータサイズが4Kや8Kまでいかなくてもデータの処理自体にそれなりに時間がかかるため、UnityのUpdateの数~数十フレーム分の負荷がかかってしまい、画面の更新が少し止まってしまうことも珍しくありません。
使用する画像が事前に分かっている場合には解像度を落とすなど最適化をする余地もありますが、外部から様々な画像データを読み込むようなユースケースでは制御が難しいです。
例えばVRのアプリケーションでは画面のフリーズはVR酔いにつながるため、致命的な体験の質の低下を起こしてしまうこともあります。
また、画像データのコーデックはいくつもあるため、それらの対応可能な範囲も気になるところです。
マルチプラットフォーム開発ができるというのがUnityの強みの一つであるので、どのプラットフォームで動作するのかも確認していきたいと思います。
Unityでの画像のロード方法の基本的な流れ
画像をロードしてUnityで表示させる処理の基本的な流れは以下のようになるかと思います。
JPGやPNGの画像データのbyte[]
(or Stream
)を取得する
→ デコードしてピクセルデータのbyte[]
(+WidthやHeightなどのメタデータ)に変換する
→ UnityのTexture(2D)にピクセルデータを読み込ませる
最初の画像データの取得方法はローカルデータならSystem.IO
でできますし、URLから取得するならHTTPClinet
でも構いません。
また、UnityのTextureなどのUnityが管理しているオブジェクトにアクセスして読み書きするAPIは基本的にはメインスレッドでしか行うことができません。
ですので要件1のメインスレッドをなるべく止めないことを実現するためには、画像データのデコード処理をメインスレッド以外の別スレッドで実行できることはポイントの一つになります。
紹介
UnityEngine.ImageConversion.LoadImage
Unityの公式のAPIに昔からあるLoadImageです。
対応しているコーデックはJPG、PNGだけというのはありますが、このロード方法では画像のバイナリーデータのデコードも含めてすべてメインスレッド上で行われるため、メインスレッドを占有してしまう時間が長くなってしまいます。
UnityWebRequest(と昔のWWWクラス)からTextureを取得する方法も(おそらく)内部では同じ挙動をするため、同じ問題を持っています。
メリット
- Unityに組み込まれているため特別な準備をすることなく使える
デメリット
- メインスレッドを止めてしまう
- JPG、PNGしか対応していない
AssetBundle/Addressables
Unity公式の機能のAssetBundleやAddressablesを使ってもTextureして外部からのロードをすることもできます。
ただし事前にAssetBundleデータのビルドをする必要があったり、プラットフォーム別にそれぞれビルドを用意する必要があります。
使用する画像データがある程度決まっていてコントロールできる場合には十分有用ですが、アプリのユーザーが画像をアップロードして使用する場合などデータが事前に用意できないケースには向きません。
メリット
- 圧縮されている画像を扱える
- 他でAssetBundle/Addressablesを使用している場合には乗っかれる
デメリット
- AssetBundleビルドがプラットフォーム毎に必要
- 事前に画像データの用意が必要
Unity.IO.LowLevel.Unsafe.AsyncReadManager.Read
比較的最近追加されたUnity公式のAPIに、AsyncReadManagerというものがあります。
ざっくり説明するとファイルのロードを非同期に、かつUnsafe(つまりUnmanaged Memory上で)でロードできるものです。
既にいくつか紹介記事もありますので詳しくはこちらなどをご覧ください。
これも十分有効に使える場面もあるとは思いますが、後者の記事にあるようにUnity上での画像の圧縮形式を考慮する必要があるため、前者の記事のように一度ローカルにTextureデータをロードしておいたり、プラットフォームによって異なる圧縮形式の対応を考えたりする必要があります。
一度ローカルに保存するためにUnityWebRequestなどでTextureに変換してしまうとそのプロセスでメインスレッドを占有するため、リアルタイムにロードするのにはあまり向かないかもしれません。
メリット
- Unsafeで扱えるのでメモリの負担が少ない
- 非同期APIが用意されている
デメリット
- 圧縮形式を考慮する必要がある
System.Drawing
C#の標準の機能を調べて見ると、System.Drawingというクラスで画像をBitMapにデコードできることが分かります。
ただUnityにはこのSystem.DrawingのDLLが含まれていないため、さっと使用するのは難しそうです。
メリット
- C#の標準機能
デメリット
- Unityで使用するのは大変
FreeImage
Unity公式のAPIでも、C#の標準APIでも適切なものが見つからない場合には、オープンソースのライブラリを探してみます。
比較的有名な画像処理のライブラリに、FreeImageというものがあります。
弊社のNEUTRANSというプロダクトでも採用しているライブラリです。
このライブラリの注意点は、動作する環境がStandalone(Windows/macOS/Linux)のみで、Android/iOSでは動かないという点です。
C/C++で書かれているため原理的には適切にビルドをすれば動きそうな気もしますが、弊社の別のメンバーが試したところうまくいかなかったとのことです。
メリット
- Unsafeで扱えるのでメモリの負荷が少ない
- 別スレッドで実行可能
- OSS
デメリット
- Standalone(Windows/macOS/Linux)のプラットフォームでしか動作しない
- 自分で導入する必要がある
UnityAsynImageLoader
「Unity Image Loading」などで検索していたところ、こんなライブラリを見つけました。
READMEに書かれていることはまさに同じ課題意識のため「これは!」と思ったのですが、内部ではFreeImageを使用しているようなので、スマホで動かすのは難しそうです。
APIは綺麗に作られているので、FreeImageのWrapperとしては使いやすいのではないでしょうか。
メリット
- FreeImageを触りやすくしてくれている
- OSS
デメリット
- FreeImageのデメリットを引き継いでいる
OpenCV for Unity
Twitterでいいライブラリはないものかとつぶやいていたところ、フォロワーさんからOpenCV for Unityというアセットを教えていただきました。
OpenCVだとmatというクラスを使って出来ますね。
— なつ@VR (@VRNatsuVR) 2022年5月17日
自分はこのアセットを使ってますが、これだとマルチプラットフォーム対応できるはずです(各プラットフォーム用のDLLが付いてくる)。https://t.co/sByRfnTf2X
これは画像処理のOSSで有名なOpenCVをUnity向けに組み込み、拡張したアセットになります。
なんとWindows/macOS/Android/iOSに加えて、WebGLやUWPなどのほとんどのメジャーなプラットフォームにも対応してます。
早速会社で購入してもらい、実際に触ってみました。
結論から言うと当初の要件を満たすことはできますが、少し問題点もありました。
- OpenCVのピクセルデータクラスのMatへの変換処理は別スレッドで実行できるので、メインスレッドの負荷を減らせる
- 各プラットフォームの動作確認もできたが、iOS向けのビルドのPostProcess処理(Xcodeでのライブラリ参照など)に癖があり少しカスタマイズが必要だった
- Native Pluginのファイルが単体で100MBを超えるものが複数あり、GitHubだとGit LFSを使用しないといけない、かつかなりの容量を使用する。
- OpenCV本家のThirdPartyLicenseが多くてライセンスのチェックが大変そう(全部確認したわけではありません)
- ただFFMPEGは手動で入れない限り入らないのでそこの不安はなさそう
- いろいろな画像処理をできる反面、画像をデコードしたいだけだとややオーバースペック
すごく便利なアセットであることには間違いないので一度導入はしてみたのですが、課題も見えてきたため最終的には別のライブラリに置き換えることになりました。
メリット
- NativePluginで主要なプラットフォームにはほとんど対応している
- 画像のデコード以外にもOpenCVの様々な機能が使える
- Unity向けの拡張やデモが用意されていて比較的触りやすい
デメリット
- NativePluginのファイルサイズが100MBを超えるものが複数ある
- iOS向けのビルドは少しケアが必要
- 画像をロードするだけのために入れるには機能が豊富過ぎる
- 有料
UnityのAssetStoreで検索
OpenCV for Unity以外ではあまりいいアセットが見つかりませんでした。
Native Pluginを自作する
適切なものがない場合や、パフォーマンスを重視するようなケースでは、Native Pluginを自作するのも一つの手です。
実際に凹さんが記事にしているものがあります。
Native Pluginは対応したいプラットフォーム向けにそれぞれ作る必要があること、そのメンテナンスも必要なことをクリアできる知識とリソースがあれば自由度が高くパフォーマンスも高い手段になるでしょう。
今回は対応プラットフォームが多く、かつ画像のロード機能はそこまでコアな機能でもないためそこまでリソースは割けませんでした。
メリット
- パフォーマンスが良い
- 自由にカスタマイズが可能
デメリット
- 複数のプラットフォーム別に用意する・運用するのが大変
結局どの方法を採用したのか?
OpenCV for Unityを導入して悩んでいたのですが、まったく別のところで3Dモデルをロードする機能を作った際に利用したTriLibというアセット
のライセンスのチェックをしていたところ、TriLibの内部で画像のデコードに利用しているStbImageSharpというライブラリがあることを知りました。
StbImageSharp
このライブラリの特徴的なところは以下になります。
- JPG、PNG含むメジャーなコーデックをサポートしている(PSDとGIFに対応しているのは謎にすごい)
- NativePluginを使用せずPure C#で書かれている、つまりUnityのどのプラットフォームでも動作する(パフォーマンスは少し落ちる)
- ただしUnity向けには作られていないので、少し拡張が必要
最後のUnity向けの拡張さえあれば当初の要件が満たせそうなため、自分で作ることにしました。
StbImageSharpForUnity
画像のデコード処理を別スレッドで処理するデモも用意しました。
まだメモリの取り扱いを最適化しきれていないですが、パフォーマンスをそこまで気にしなくてもいいのであればプラットフォームを気にせずに取りまわせる使いやすいライブラリなのではないかなと思います。
というわけで結局StbImageSharpを利用した方法を採用することになりました。
メリット
- デコード処理を別スレッドで実行できる
- 対応コーデックが多い
- マルチプラットフォームの対応が容易
デメリット
- パフォーマンスはそこまで良くない
まとめ
画像データのロード方法を様々紹介しましたが、どれが完璧というわけでもないですし、プロジェクトの要件によって最適な方法は変わるかなと思います。
今回はあまり紹介できませんでしたが、メモリの最適化、非同期処理、残るTextureのAPIアクセスのオーバーヘッド、大きい画像の分割ロード、GIF画像の取り扱いなどまだまだ突き詰められる余地があります。
たかが画像データと思いきや意外と奥が深い世界です。
また、今回紹介したのは私が知っている or 検索できた範囲になっていますので、「他にもこんな方法、アセット、OSSもあるよ!」というのもありましたらTwitterなどで教えていただけると嬉しいです!
不正確な記述もありましたらご指摘いただけますと幸いです。
最後に紹介したこちらも使ってみてのフィードバックも大歓迎です。
以上、Unityで画像データをいい感じにロードしたいけど何か困っているという方の参考になれば幸いです。