テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】AssetBundleでSpriteAtlasを使用する際に知らないと起こすかもしれないトラブルと、その回避方法

f:id:tsubaki_t1:20181224015603j:plain
Sprite Atlasの設定

幾つかのページ*1*2にて、Sprite AtlasとAssetBundleの組み合わせで問題が起きるという話を聞いたので、確認してみました。

SpriteAtlasをAssetBundleに格納しただけだと問題が起こる

「よぅし、SpriteAtlasでUIのスプライトをまとめちゃうゾ!」

この問題を起こすシナリオは、こんな感じ。

  1. Spriteを参照するUIを作って、AssetBundleに登録
  2. バッチングの為にSpriteをSprite Atlasに登録(&パック)
  3. Sprite Atlasは複数のAssetBundleから参照できるように独立したAssetBundleに格納

f:id:tsubaki_t1:20181224175125j:plain
Prefabが参照するSpriteをSpriteAtlasに登録しただけだと問題が起こる

これをしても、UIを格納したAssetBundleとSpriteAtlasを格納したAssetBundleで依存関係は生成されず、全てのAssetBundleがパックしたTextureを持つ事になります。またAssetBundle毎に違うTextureを使っているので、全体としてみればバッチングは殆ど効いていません。

「良かれと思ってやったが、まさかこんな結果になるとは…」

手っ取り早い解決策:SpriteAtlasに登録したSpriteもAssetBundleに格納

Sprite AtlasとSpriteを生成するTextureを同じAssetBundleに格納することで問題を回避出来ます。
SpriteAtlasが参照するSpriteを持つTextureのBundleがAtlasになっていればOKです。

f:id:tsubaki_t1:20181224175224j:plain
SpriteAtlasとSpriteを同じAssetBundleに登録、それを他AssetBundleから参照する

Spriteを参照している場合はAtlas Textureと元々のTextureの双方の画像データが含まれる事は無いです。元々のTextureの画像データは含まれません(条件有)

f:id:tsubaki_t1:20181224015626j:plain
Sprite Atlasが参照するSpriteが同じAssetBundleに含まれていればOK

何故そうなるのか?

多くの人が勘違いしている項目ですが、AssetBundleの依存関係は指定のアセットを参照しているAssetBundleから、指定のアセットを直接保持しているAssetBundle へ行われます。

例えば下の図では、AssetBundle Aが持つMaterialをAssetBundle Bが参照することが可能です。これはAssetBundle AがMaterialを直接保持しているためです。一方、AssetBundle Aが持つMaterialが参照しているTextureを、AssetBundle Cが参照することは出来ません。これはTextureはmaterialの参照に寄って暗黙的に含まれるアセットの為です。

もしAssetBundle CがAssetBundle のTextureを使用したい場合は、AssetBundle AはTextureにAssetBundle Nameを設定する必要があります。

f:id:tsubaki_t1:20181224030957j:plain

これを今回のケースに当てはめて考えてみます。

Prefabが参照しているのはあくまで「Sprite」であり、Spriteの先の何某かを見ていません。またSpriteAtlasもSpriteを見ていますしTextureをパックしていますが、Spriteを直接保持していません。

つまり上の「AssetBundle AとAssetBundle Cの関係」と同様で、PrefabとSpriteAtlasどちらも暗黙的に参照しているだけです。これではPrefabからSpriteAtlasへの依存関係は構築出来ません。

f:id:tsubaki_t1:20181224045335j:plain
Bundleがautoのテクスチャが含まれていると問題が発生する

なのでSpriteAtlasと同じAssetBundleにはSpriteを配置し、Prefabを格納したAssetBundleはSpriteを参照する形に切り替えます。これなら他のAssetBundleからSpriteAtlasを格納したAssetBundleのSprite(パックしたテクスチャ使用)を使えます。

Sprite生成元の画像データでAssetBundleのサイズが膨らむ事は無い(条件有)

ここで気になるのが、データの重複です。つまりSpriteAtlasでパックしたTextureとAssetBundleに含めた(Spriteを作った)Textureでデータサイズが増えないのか? という話です。

結論を言えば、Textureの持つ画像データは、普通に使っていれば増えませんでした

普通に使っている…つまりSpriteを経由してテクスチャにアクセスしてるだけの場合はデータサイズは増えません。AssetBundleに格納したTextureのデータは破棄され、Textureを取得してもNullを返されます。またSpriteは常にパックしたTextureを使用します。

f:id:tsubaki_t1:20181224043514j:plain
Textureを取得しようとしてもNull

一方普通ではない使い方…つまりSpriteを使いつつもTextureとしても使っている場合はデータサイズは増えます。例えばRawImageのPrefabで参照していたり、MaterialのMainTextureとかで参照している場合もそうでしょう。その場合はSpriteAtlasでパックした画像と、パック前の両方が含まれます
まぁMaterialで使いたいのはパック前の画像でしょうから、これは期待通りの動作と言えます。

f:id:tsubaki_t1:20181224181403j:plain
MaterialからTextureを参照する(パック後ではなくパック前のTextureが必要と判断される)

f:id:tsubaki_t1:20181224182030j:plain
左上:Spriteのみ参照 右下:Textureも参照(Materialで)

Spriteの情報が重複するんじゃないの?

たぶんその通り。
今回のアプローチは、SpriteAtlasが保持するSpriteとTextureが持つSpriteは多分重複します*3。まぁInclude in Buildの方はSprite情報が全ての参照元に残るのでコレの比ではないのですが。

で、この回避方法は簡単、SpriteAtlasをAssetBundleに含めない事です。

f:id:tsubaki_t1:20181225125419j:plain
SpriteAtlasをAssetBundleに含めなくても良い

そもそもSpriteAtlasはSpriteが参照するTextureやUVを差し替える機能です。そしてSpriteAtlasによるSprite情報の差し替えはビルドのタイミングで行われる為、別にAssetBundleにSpriteAtlasアセットを含めなくてもパックしたTextureを使用します
(なお、これは逆を言えば、Spriteを複数のAssetBundleにバラバラに配置すると、全てのAssetBundleにパックしたテクスチャが含まれるという事を意味します 。つまりSpriteAtlasで使用したSpriteは一つのAssetBundleに統合して格納するべきです)

f:id:tsubaki_t1:20181224200847j:plain
SpriteAtlasがAssetBundleに無くとも、Spriteははパックされたテクスチャを使用する

逆に、Sprite参照して使ってない場合…例えば殆どのUIはスプライトをソースコードでセットするようになっている場合は、Spriteを含める必要はありません。その場合はSpriteAtlasを入れてGetSpriteでスプライトを取得するのが手っ取り早いです。AssetBundleに明示的に含めるアセットは減らすべきなので、本当にほとんど動的に取得してるなら、コチラのほうが理にかなっています。

f:id:tsubaki_t1:20181225125743j:plain
GetSpriteでスプライトを取得する

どのアプローチで取得した場合も、テクスチャはパックしたTextureを使用します。

結局、どのように使えばよいのか

もしSpriteを他のPrefabから参照している…そういった設計を行いたい場合、SpriteAtlasでパックした後、パック対象のSpriteを単一のAssetBundleに格納します。これでSpriteAtlasがパックしたテクスチャを使用したSpriteを、他のAssetBundleが使用できます*4

逆に、特に殆どSpriteを動的にセットするといった場合は、Sprite AtlasのみをAssetBundleに格納し、GetSpriteでSpriteを取り出すというアプローチが良さそうです。AssetBundleに明示的に含めるアセットは出来る限り減らすべきです。

単一のAssetBundleでしかSpriteAtlasでまとめたスプライトを使用しない場合は、上2つのような面倒なことはしなくても良いです。SpriteはAssetBundleに格納しなくても良いですし、Prefabで参照する形でしかアクセスしないならSpriteAtlasも不要です。

まぁ、所詮スプライト…無圧縮で数KB程度の細かいデータなので、SpriteAtlasを入れるか入れないかで迷うよりは、普通にSpriteAtlasとSpriteAtlasが参照するスプライトを全部突っ込むという方が楽で良いよねという印象です。とにかくパックしたテクスチャが重複しなければ問題無いので、変に色々と考えるよりもSpriteとSpriteAtlasを一つのAssetBundleにまとめるという上で書いた「手っ取り早い解決策」を使った方が良いかなという
(本当に必要なら状況で使い分けは必要だろうけど)

補足

SpriteRendererは「Sprite Default」マテリアルを使ってるとアセットが重複する(=バッチが途切れる)ので、注意が必要です。
UIだと(MaterialがNullで、実行時に参照するので)コレは起こりません。

感想

ネタバレすれば非常に単純なルールなのですが、コレを知らないと凄いメモリやロード時間、コードサイズや工数のロスに繋がりそうな感じがします。

関連

本質的に似てる問題

tsubakit1.hateblo.jp

依存関係のルールが気に入らない? カスタムしようぜ!お前実装な!

tsubakit1.hateblo.jp

今回と同じ、知らないとエグい事になる系

tsubakit1.hateblo.jp

*1:unityでのatlas texture2018最新状況 - Qiita

*2:Spriteをパックする新しい仕組み、SpriteAtlasを使ってみた【Unity】【SpriteAtlas】 - (:3[kanのメモ帳]

*3:Instance IDでしか確認してないので、実際にそうなのかは把握出来てない

*4:1Asset1AssetBundle?今回それをすると死ぬぞ