UnityでSpriteAtlasを使用した状態でUVを0〜1に正規化する方法
要約
UnityでSpriteAtlasを使用した状態で、バッチングを壊さずにUVを0〜1の範囲に正規化する方法を説明します。各SpriteのMinUVとMaxUVを事前に計算して1枚のテクスチャにすることで、シェーダーでAtlas上のUVを0〜1に正規化できます。
I will explain how to normalize UV to the range of 0-1 without breaking batching when using SpriteAtlas in Unity. By pre-computing the MinUV and MaxUV of each Sprite and making it into a texture, you can normalize the UV on the Atlas to 0-1 in the shader.
方法の説明
UnityのSpriteRendererでSpriteを表示するとき、そのUVは(0,00, 0.00)から(1.00, 1.00)の範囲になるようにシェーダーへ渡されます。次のように。
一方、SpriteAtlasを使用すると0-1ではなく、Atlas内でどの位置に頂点があるかを示すローカルなUVへ自動的に置き換えられます。
今回はFullRectを指定しているので画像の四隅がUVになっていますが、それぞれの値が0-1ではなく、中途半端な値になっていますね。
シェーダーの多くはUVが0-1の範囲であることを前提に動作するため、この状態では正しく表示できません。そこで、UVを元の0-1に正規化してから処理したいのですが、Unityではおそらくその方法が提供されていません。(issuetrackerによると仕様らしい)
回避策として「MaterialPropertyBlockでUVの範囲を設定する」という方法はあるのですが、Spriteごとに渡すパラメータが変わってしまうため、描画時にバッチングの対象外となりDrawCallが増えてしまいます。バッチングが効かなくなるとSpriteAtlasのメリットが薄れてしまうので別の方法が欲しいところです。
様々なページを見てもどうやら完全には回避不能な仕様らしく年単位で悩んでいたのですが、最近になってようやくバッチングを壊さない回避策を見つけました。それが、別のテクスチャに1ピクセルずつUVパラメータをColorとして書き込み、シェーダーでそのテクスチャを読み込んで元のUVを復元する方法です。
この1pxずつにColorとしてそのSpriteのMinUVとMaxUVを埋め込んでいます。
MinUV(0.00, 0.00)
MaxUV(0.39, 0.78)
RGBA(0.000, 0.000, 0.391, 0.781)
重ね合わせるとこのような対応関係になっています。
Bが0.391でAが0.781なので、少し透明な暗い青に見えているわけですね。
SpriteAtlasが1024x512でテクスチャが2048x2048なので少し分かりづらいですが、UVはピクセルは関係なく相対的な位置で指定するため、サイズが違っていても動作への影響はありません。
このテクスチャをシェーダーで読み込むことでUVのMinとMaxが分かるので、max - min
でSizeを求めたり、(min + max) / 2
でCenterを求めることもできます。最終的に(uv - min) / (max - min)
とすることで0-1に正規化もでき、戻すときは逆の操作であるuv * (max - min) + min
を行うことで元のローカルなUVに戻せます。
ということで、ShaderGraphで実際にUVを正規化します。変数としてUVTextureを定義し、そこにテクスチャを設定する必要があります。(テクスチャは後述するコードで生成)
NormalizeUVとDenormalizeUVはSub-Graphになっており、それぞれ次のようになっています。
複雑に見えますが、やっていることは(uv - min) / (max - min)
でUVを正規化し、任意のUV操作をしたあとにuv * (max - min) + min
で元に戻し、最後にUV操作ではみ出たUVをminとmaxの間にClampしているだけです。(ClampしないとAtlasの別の部分が映ってしまいます)
実際に操作するときは、最初の図のEdit UVのグループの部分で0-1に正規化されたUVが扱えるので、その部分で操作してください。
コード
次はUVのテクスチャを生成するコードです。SpriteAtlasのUVは実行時に置き換えられるため、実行中にAwakeなど任意のタイミングで実行する必要があります。
SpriteAtlasの更新があったときに1度呼び出せば良いので、更新ごとに生成できる仕組みさえあれば毎回呼び出す必要はないかもしれませんが、環境によって変わるかもしれないし、SpriteAtlasの動作に全く自信がないので塩梅はお任せします。
注意事項
- 書き出すテクスチャのTextureTypeはDefault、Read/Writeをオンにする必要があります
- Sprite Atlasの"Allow Rotation"には対応していません
- Sprite Atlasの"Tight Packing"にも対応していません(別のSpriteのUVを上書きしてしまうので)
- いわゆるハックに近い方法なので、様々な環境で正しく動くことを確認してください
実行結果
ではこれで必要なものは揃ったので、試しにサカバンバスピスをねじってみます。
SpriteAtlasが設定されている状態でUV操作できましたね。
Frame Debuggerから複数のSpriteをDrawCall1回で描画できていることも確認できました。
まとめ
SpriteAtlasを使用した状態で、バッチングを壊さずにUVを0〜1の範囲に正規化する方法を説明しました。
正規の方法ではないのであまりおすすめしませんが、回避策が何もないよりはある方が良いと思い記事にしました。私自身もこの方法が最善とは思っていないので、題意を満たす他の方法がぜひ記事にして教えてください。よろしくお願いします。
Discussion