🖼️

UnityでSpriteAtlasを使用した状態でUVを0〜1に正規化する方法

2023/09/28に公開

要約

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を使用せずに表示した時のUVは0-1

一方、SpriteAtlasを使用すると0-1ではなく、Atlas内でどの位置に頂点があるかを示すローカルなUVへ自動的に置き換えられます。

SpriteAtlasを使用して表示した時のUVはローカルのUV

今回はFullRectを指定しているので画像の四隅がUVになっていますが、それぞれの値が0-1ではなく、中途半端な値になっていますね。

シェーダーの多くはUVが0-1の範囲であることを前提に動作するため、この状態では正しく表示できません。そこで、UVを元の0-1に正規化してから処理したいのですが、Unityではおそらくその方法が提供されていません。(issuetrackerによると仕様らしい

回避策として「MaterialPropertyBlockでUVの範囲を設定する」という方法はあるのですが、Spriteごとに渡すパラメータが変わってしまうため、描画時にバッチングの対象外となりDrawCallが増えてしまいます。バッチングが効かなくなるとSpriteAtlasのメリットが薄れてしまうので別の方法が欲しいところです。

様々なページを見てもどうやら完全には回避不能な仕様らしく年単位で悩んでいたのですが、最近になってようやくバッチングを壊さない回避策を見つけました。それが、別のテクスチャに1ピクセルずつUVパラメータをColorとして書き込み、シェーダーでそのテクスチャを読み込んで元のUVを復元する方法です。

テクスチャに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)

重ね合わせるとこのような対応関係になっています。

画像とUVの対応関係

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を定義し、そこにテクスチャを設定する必要があります。(テクスチャは後述するコードで生成)

ShaderGraphの内容

NormalizeUVとDenormalizeUVはSub-Graphになっており、それぞれ次のようになっています。

NormalizeUVの内容

DenormalizeUVの内容

複雑に見えますが、やっていることは(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回で描画できていることも確認できました。

DrawCall1回で描画している様子

まとめ

SpriteAtlasを使用した状態で、バッチングを壊さずにUVを0〜1の範囲に正規化する方法を説明しました。

正規の方法ではないのであまりおすすめしませんが、回避策が何もないよりはある方が良いと思い記事にしました。私自身もこの方法が最善とは思っていないので、題意を満たす他の方法がぜひ記事にして教えてください。よろしくお願いします。

Discussion