スプライトアトラスのパッキング:余白、2の累乗、色のにじみ

スプライトアトラスは、多数の小さなファイルではなく、すべてのスプライトを1枚の大きなテクスチャに置くという単純な考え方に見えます。しかし、どう詰め込むかという細部が、ゲームの描画をきれいに保つか、すべてのスプライトの端に薄い色の縁を生じさせるかを左右します。アトラスが実際に行うこと、境界で色がにじむ理由、それを止める具体的な設定を説明します。

アトラスが必要な理由:描画呼び出し

GPUがあるテクスチャでの描画から別のテクスチャでの描画へ切り替えるたびに、コストがかかります。概念的には新しい「ドローコール」です。個別のテクスチャを持つ50個のスプライトのシーンを単純に描画すると、すべてが小さくても50回の別々のドローコールになる場合があります。特にモバイルのハードウェアでは、GPUが実際のピクセル処理を大量に行うよりずっと前に、ドローコールの負荷がフレームレートに影響するほど積み上がります。

スプライトアトラスは、多数のスプライトを1枚の共有テクスチャに置いて、この問題を回避します。同じバッチ内で描くすべてが同じテクスチャを参照する限り、レンダラーは描画をまとめ、呼び出し回数を大幅に減らせます。最良の場合、アトラスとマテリアルを共有するすべてのスプライトを1回のドローコールで描画できます。このためアトラスは、ドローコール数がもともとボトルネックではない少数の大きな背景画像より、パーティクルエフェクト、UIアイコン、タイルセットのような、頻繁に再描画する小さなスプライトを多数使うゲームで重要です。

ビンパッキング:スプライトをテクスチャへ収める

異なるサイズのスプライト一式を、できるだけ無駄な空間を少なく1枚のテクスチャへ配置するのは、古典的な次の問題の一種です ビンパッキング問題。アトラスツールの多くは、次の2つの方法のいずれかを応用しています。

  • 棚詰め方式: スプライトを1行の「棚」に左から右へ配置し、収まらなくなると下に新しい棚を作ります。簡単で高速ですが、棚内の高さが大きく異なると空間を無駄にします。
  • 最大矩形 / ギロチン配置: 配置後に残る空き矩形を管理し、最適な空間に新スプライトを入れ、矩形を分割していきます。より密に詰められ、各スプライトの計算量は増えます。少数の単純な場合を超える用途では、TexturePacker、Unity Sprite Atlas、Godotの出力など、現在の多くのアトラスツールの標準方式です。

一部のパッカーは、さらに最適化として回転を使います。90度回すと、回さずには入らない残りの細い領域に収められる場合があります。密に配置できますが、UV座標で回転を考慮するなど描画が複雑になるため、通常デフォルトではなく明示的な設定です。

これらを自分で実装する必要はありません。エンジンのツールや専用パッカーが処理します。理解すべきなのは、配置密度と次に説明する余白が相反することです。密に詰めれば無駄なテクスチャ領域は減りますが、スプライト間の余白が下記のにじみを防ぎます。詰めすぎる設定では、領域を数%減らすために視覚的な不具合を再発させる場合があります。

テクスチャのにじみ:現象と原因

テクスチャのにじみ は、描いたスプライトの縁にアトラスの隣のスプライトの色の細片が出る現象です。1辺以上に、その絵柄とは無関係な細い色線が出ます。よくあるアトラス固有の不具合で、混同される2つの別の原因があります。

フィルタリングによる色のにじみ。 1:1以外で描画したり、カメラがピクセルに合わない位置へ動かしたりすると、GPUのテクスチャフィルターは最も近い1テクセルだけでなく、各点の周囲の小さな範囲をサンプリングします。テクスチャでpoint/nearestを設定していても、サンプリング段階のフィルターやミップマップがUV矩形の少し外へ届く場合があります。隣のアトラス領域が別のスプライトなら、その色が漏れます。

ミップマップによる色のにじみ。 ミップマップは、画面で小さく描画する際に使う、テクスチャ全体を事前に縮小した版です。生成時にフル解像度テクスチャのブロックを混ぜるため、個別のスプライトを描く前でも、あるスプライトの色が隣のスプライトのミップレベルへ混ざる場合があります。これが、拡大時は問題なくても、遠くや小さい倍率でだけにじみが現れる理由です。

どちらの原因も、同じ根本条件にたどり着きます。無関係な2つのスプライトがアトラス内で直接隣り合い、サンプリングの誤差を吸収するものが間にないことです。

左:アトラス内でスプライトを端同士で詰め、拡大した注釈で各境界の誤った色の継ぎ目を示す。右:同じスプライトを透明な余白で分け、注釈内にきれいな端を示す
拡大した箇所が問題のすべてを示しています。端を接して詰めると、境界から半テクセルはみ出したサンプラーが隣のスプライトに入り、その色を継ぎ目へ引き込みます。

色のにじみを修正:余白と押し出し

標準的な対策は 余白 — アトラス内の全スプライト間に数ピクセルの透明、または縁を複製した隙間を残し、はみ出すサンプリングが隣の実際の内容ではなく隙間へ届くようにします。原寸では通常1〜2ピクセルで十分ですが、描画で大きく拡大するかミップマップを有効にする場合は、広い余白で誤差の余裕を増やします。

単純な透明余白は隣のスプライトの漏れを防ぎますが、より小さな問題を作ります。スプライトの端で、フィルタリングが自身の端の色と透明余白を混ぜ、完全にきれいな端でなく、境界に薄い暗さやにじみが出る場合があります。 押し出し (エッジ拡張とも呼ばれます)では、余白を透明のままにせず、スプライト自体の端のピクセルを外側へ引き伸ばしたコピーで埋めることで、この問題を解決します。隣接するスプライトの問題も同じように解決されます。異なるスプライトの間には引き続き隙間がありますが、スプライト自体の境界は空の透明部分ではなく、自分自身の色と混ざるようになります。

TexturePacker、UnityのSprite Atlasパッカー、Godotのアトラス書き出しなど、大半のアトラスツールは余白サイズと押し出しの切り替えを設定に持ちます。余白を0のままにする理由はほとんどありません。端ごとの1〜2ピクセルの領域コストは、特定のズームでだけ出る断続的な継ぎ目の劣化を調べる手間より小さいからです。

エディターではなく、出荷したビルドだけで現れる色のにじみ。 通常、ビルドでミップマップやテクスチャ圧縮が有効なのに、エディタープレビューでは無効だったことを意味します。にじみやすいアトラスは、エディターの標準設定だけでなく、最終ビルドと同じテクスチャ設定で確認してください。

2の累乗は今も重要ですか?

以前のGPUは、ミップマップや特定のラップモードを効率よく使うために、テクスチャの寸法を2の累乗(256、512、1024、2048…)にする必要がありました。それ以外のテクスチャは完全に失敗するか、遅い描画経路へ切り替わりました。最新のGPUとAPI(OpenGL ES 3+、Metal、Vulkan、DirectX 11+)は、2の累乗でないテクスチャをネイティブに扱い、基本的な2D描画では意味のある性能低下もないため、現世代の対象環境では厳格な要件はほぼなくなっています。

特定の状況では、今も重要です。

  • ミップマップ。 一部のプラットフォームや古いAPIでは、完全なミップチェーンを正常に生成するために、寸法が2の累乗であることが今も必要です。アトラスでミップマップを使う場合は、思い込みではなく対象プラットフォームの実際の制約を確認してください。
  • 古い、または制限されたハードウェア。 低性能のモバイル端末、WebGL 1の環境、一部の組み込み環境では、今も2の累乗のサイズが有利、または必要です。
  • メモリのアライメントと圧縮形式。 モバイルでよく使うブロック圧縮形式は固定サイズのブロックで圧縮し、合わない寸法を次の有効なサイズまで余白で埋める場合があります。最初から2の累乗のサイズを選べば、この見えない余白で無駄なメモリを使うのを避けられます。

最新のデスクトップ、ゲーム機、モバイルで標準2D処理を使うなら、2の累乗でないアトラスで問題ありません。WebGL 1、古いモバイル、組み込みでは、2の累乗が今も安全で低コストです。大半のパッカーは、設定を有効にすると次の2の累乗へ自動で切り上げます。

トリミングとピボット

トリミング アトラスに配置する前に、各スプライトを実際の不透明な内容まで切り詰め、周囲の透明余白を捨てることです。ほぼ常にパッキング密度には有利です。小さいキャラクターの周囲に大量の透明余白がある場合、トリミングすると占有面積が大幅に減ります。

注意点は、トリミングでスプライトの実効寸法とオフセットが変わることです。ゲームロジックやアニメーションシステムが、すべてのフレームで同じ寸法と元キャンバスに対する同じピボットを前提にする場合、重要になります。左に傾く歩行フレームと右に傾くフレームでは不透明な範囲が異なります。個別にトリミングすると寸法が変わり、単純に切り替えたときキャラクターの見かけの位置がフレームごとにずれます。

トリミングに対応するすべてのアトラスツールは、まさにこの問題を解決するため、トリミングしたデータとともに、元の未トリミングのサイズとオフセットも記録します。エンジンが、元フレームの範囲内の正しい位置にトリミングしたスプライトを描画するため、透明ピクセルの保存に使うテクスチャ領域を減らしても、ピボットの見た目は一貫します。スプライトをパッカーへ渡す前に手で切るのではなく、ツールでトリミングを有効にすれば、UnityのSprite AtlasやGodotのインポーターが自動で処理します。配置前に自分で手動トリミングすると、エンジンが正しく位置を戻すために必要なオフセットのメタデータを失います。

アトラスは1つか、複数か?

すべてを1つの巨大アトラスへ詰めれば、自動的に良くなるわけではありません。代わりに複数のアトラスへ分ける実用的な理由がいくつかあります。

  • メモリへの常駐。 レベルがスプライトの一部しか使わない場合、レベルやシーンごとに1つのアトラスを読み込めば、プレイヤーの現在地から遠い内容の未使用データを、GPUメモリに常駐させずに済みます。
  • テクスチャサイズの上限。 ハードウェアにはテクスチャ寸法の上限があります。最新の対象環境では一般に4096や8192で、古いモバイルではさらに小さくなります。十分に大きいスプライト一式は、配置の効率にかかわらず、1つのアトラスに入りません。
  • 更新頻度。 頻繁に変わるスプライト、たとえば短い周期で修正するUIは、安定していてほとんど変更しない絵柄とは別の専用アトラスにすると有利です。小さな変更のためにアトラス全体を再構築・再アップロードする必要がなくなります。
  • マテリアルの違い。 異なるシェーダーやブレンドモードが必要なスプライトは、アトラスに関係なくまとめてバッチ処理できないため、一緒に詰める利点はありません。

妥当なデフォルトは、使用する場面ごとにまとめることです。レベルの環境タイル用、キャラクター用、UI用に、それぞれ1つのアトラスを作ります。対象のハードウェアでドローコールが実際にボトルネックだと計測で分かった場合にだけ、さらに統合してください。

よくある質問

実際にどれほどの余白が必要ですか?

ミップマップをオフにした元の表示倍率では、大半は1〜2ピクセルで対応できます。実行時に大きく拡大する、またはミップマップを使うなら増やしてください。普遍的な正解はなく、ゲームで実際に使う倍率やズームで、特定のアトラスをテストする価値があります。

特定のズームでだけにじむのはなぜですか?

このパターンは、基本解像度のフィルターによるにじみではなく、ミップマップのにじみを示します。その距離で使うミップレベルが、スプライト境界をまたぐ混合から生成されたのです。押し出しと十分な余白は両方の原因に有効ですが、ミップ固有の問題なら、基本テクスチャだけでなく複数のミップレベルへ縮小しても残る幅の余白があるか確認してください。

ミップマップもスプライトの拡大縮小も使わない場合、これらを気にする必要はありますか?

少なくなりますが、ゼロではありません。ポイントフィルターによる1:1のピクセル完全一致でも、サブピクセルのカメラ位置や整数でないスプライト配置により、GPUが境界にまたがる端数のピクセルをサンプリングする場合があります。完全なピクセル一致の2D設定でも、少量の余白は低コストの保険になります。

トリミングはヒットボックスや衝突形状に影響しますか?

一般的な設定である、衝突形状を独立に定義する場合は影響しません。寸法から自動で衝突範囲を作る場合は、トリミング後と元の未トリミングのどちらのサイズを読むか確認してください。前者を直接使うと、透明余白の大きいフレームでヒットボックスが予期せず縮む場合があります。

アトラスにまとめるスプライトを作っていますか?

Sprite Genは個別画像を1枚のシートに結合し、指定した余白を使って行単位で配置します。最大矩形法の変形ではなく、上記の棚詰め方式で、ほとんどのアイコンやフレームのセットには十分です。元のスプライトがまだ1枚の結合シートで、先に分割が必要なら、スプライトシートスライサーで個別フレームに切り出せます。どちらもブラウザー内だけで動作し、アップロードはありません。

スプライト生成を開く