テクスチャ、VRAMを食い荒らす大食い王
前回の記事で頂点数を半分に減らしてモデルを軽くしましたが、まだ「スリムになった」とは言えません。なぜなら、本当のボリュームの巨漢は別にいるからです。それがテクスチャです。PBRモデルでは、テクスチャが通常80%以上の容量を占め、しかもVRAM上で最も激しく膨張します。
この記事では、テクスチャの「VRAM大食い」問題に焦点を当てます。3つのポイントを説明します:なぜPNG/JPGはGPUにとって「原罪」を負っているのか?GPU独自のテクスチャフォーマットはどんなもので、なぜそのまま使えないのか?そしてBasis Universal + KTX2の組み合わせがどのようにしてこれら3つを結びつけるのか。
まず復習:なぜJPGがVRAMを爆発させるのか
前回の記事で次の式を示しました:
VRAM使用量 = 幅 × 高さ × 4バイト(RGBA) × 1.333(ミップマップ含む)
4096×4096のテクスチャ1枚の場合、ディスク上では1.5MBのJPGでも8MBのPNGでも、VRAMに読み込まれると 約87MB になります。理由はただ1つ:GPUはJPG/PNGを認識しないからです。
GPUのテクスチャサンプリングユニットは、与えられたUV座標に対して、固定サイズのピクセルブロックから色を読み出すことしかできません。そのため、テクスチャはVRAM上で「展開された生のピクセル」として存在する必要があります。ブラウザはJPGをGPUにアップロードする前に、CPUで完全にデコードしてRGBAピクセルに戻し、そのブロック全体をVRAMに送り込む必要があります。
このプロセスには3つの問題があります:
- VRAM爆発:デコード後の生ピクセルは巨大で、87MBは誇張ではなく、計算上の結果です。
- アップロードのブロッキング:大きなピクセルブロックをCPUメモリからGPU VRAMに転送するのは低速な処理で、初回フレームのレンダリングを遅らせます。
- CPUデコードのオーバーヘッド:大きな画像のデコード自体が時間がかかり、特にモバイル端末で顕著です。
前回の「圧縮スポンジ」の比喩を続けると、PNG/JPGは圧縮されたスポンジで、輸送には便利です。しかしGPUに渡されると、スポンジは水を吸って元のサイズに戻ります。ダウンロードは速くなっても、VRAMはまったく節約できません。
GPU独自のテクスチャフォーマット:最初からVRAM上で圧縮されている
GPUが圧縮されたPNGを受け入れないなら、テクスチャをVRAM上でも圧縮状態のまま保持できるようにすればいいのではないでしょうか?GPUがサンプリング時にリアルタイムで個々のピクセルブロックをデコードすれば、オーバーヘッドはほぼゼロです。
これがGPUネイティブテクスチャフォーマットの役割です。代表的なファミリーは次の通りです:
| フォーマットファミリー | 正式名称 | 主なプラットフォーム | 特徴 |
|---|---|---|---|
| BC1-7 | Block Compression | デスクトップ(PC、Mac) | 老舗、各世代4×4ピクセルブロック圧縮 |
| ETC1/2 | Ericsson Texture Compression | モバイル(Android/iOS旧機種) | モバイルの旧標準 |
| ASTC | Adaptive Scalable Texture Compression | モバイル/VR(新機種) | 柔軟で品質最高、ブロックごとに調整可能 |
| PVRTC | PowerVR | 旧iOS | 徐々にASTCに置き換えられつつある |
これらのフォーマットの共通点は、テクスチャを4×4ピクセルのブロック単位で圧縮して保存し、GPUがサンプリング時にそのブロックを必要に応じてデコードする点です。デコードされるのは単一ピクセルではなく、ブロック全体です。利点は、VRAM使用量がコンテンツに関係なく固定比率で削減されることです。
比較してみましょう:
| PNG/JPG(従来) | GPUネイティブフォーマット | |
|---|---|---|
| ディスクサイズ | 小さい(特にJPG) | 中程度(ブロック圧縮、固定ビットレート) |
| VRAM使用量 | 大きい(デコード後の生ピクセル) | 小さい(ブロック圧縮、常駐) |
| GPUへのアップロード | 遅い(CPUデコード+大容量転送) | 速い(そのまま転送、デコード不要) |
| サンプリング速度 | 速い(すでに生ピクセル) | 速い(ハードウェアによるリアルタイムデコード) |
一見するとGPUフォーマットは完璧な解決策に見えます。では、なぜそのまま使えないのでしょうか?
問題発生:デバイスごとに認識するフォーマットが異なる
これこそがGPUテクスチャフォーマットの最大の落とし穴です——断片化です。
- デスクトップPCはBC1-7を認識するが、ASTCは認識しない
- AndroidスマートフォンはETC2/ASTCを認識するが、BCは認識しないことが多い
- iOS(A7以降)はASTCを認識し、旧機種はPVRTCを認識する
- WebGPU/WebGLは結局、デバイスが持つ同じハードウェア能力に依存する
1枚のテクスチャを「すべてのデバイスでGPUネイティブフォーマットとして存在させる」ためには、プラットフォームごとに別々のバージョンを用意する必要があります。製品をデスクトップ+Android+iOSでリリースする場合、同じテクスチャに対してBC、ETC2、ASTCの3セットを作成する必要があります。パッケージサイズは3倍、作業量は3倍になります。
さらに悪いことに、Webではユーザーがどのデバイスでページを開くか事前にわかりません。すべてのフォーマットを事前生成するのは非現実的で、ランタイム検出では間に合いません。
Basis Universal:1回のエンコードで、どこでもトランスコード
Basis Universal(略称Basis)は、この断片化問題を解決するために生まれました。その考え方は一言で言えば:
テクスチャをまず「中間フォーマット」にエンコードし、ランタイムで現在のデバイスのGPU能力に応じて、対応するネイティブフォーマットにトランスコード(変換)する。
トランスコードの流れ(図):
元のテクスチャ(PNG/JPG)
│ オフラインで1回エンコード(低速、1回だけ)
▼
Basis中間フォーマット(ETC1S または UASTC)
│ KTX2コンテナにパッケージング
▼
Webに公開 ──┬── デスクトップGPU ──→ ランタイムトランスコード → BC1/3/7
├── Android ──→ ランタイムトランスコード → ETC2
└── iOS/VR ──→ ランタイムトランスコード → ASTC
重要なポイント:
- オフラインエンコードは1回だけ行い、コンパクトな中間表現を得る
- ランタイムトランスコードは非常に高速(純粋な計算、数ミリ秒レベル)で、ブロックフォーマットへの変換なのでピクセル単位のデコードは不要
- トランスコード後、VRAMに送られるのは真のGPUネイティブフォーマットであり、VRAM使用量はブロック圧縮に基づき、GPUネイティブフォーマットと同じ
Basisは2つの中間エンコードモードを提供します。次の記事で詳しく説明しますが、ここでは名前だけ覚えておいてください:
- ETC1S:圧縮率が非常に高く、拡散色(アルベド)などのカラーマップに適している
- UASTC:品質が高く、法線マップなど精度が重要なマップに適している
KTX2:GPUテクスチャ用の標準コンテナ
ここでさらにエンジニアリング上の問題があります:エンコード後のBasisデータをどこに格納し、どのようにラベル付けし、glTFと関連付けるか?答えは KTX2 です。
KTX2(Khronos Texture 2)は、別の画像フォーマットではありません。コンテナフォーマットです——.zipファイルが中身が文書か画像かを気にしないように、KTX2はGPUテクスチャデータ(Basisエンコードを含む)を標準的な構造でパッケージ化し、メタ情報(フォーマット、ミップマップレベル、カラースペースなど)を添付します。
glTFでは、KTX2は拡張機能 KHR_texture_basisu を介して統合されます:テクスチャはPNGファイルではなく、BasisエンコードされたKTX2ファイルになります。ロード時にエンジンがデバイスの能力を検出し、対応するBC/ETC/ASTCにトランスコードします。
3つの関係を整理しておきましょう。混同しないように:
| 名前 | 役割 | 類推 |
|---|---|---|
| Basis Universal | エンコード方式(テクスチャを中間フォーマットに圧縮する方法) | 「圧縮アルゴリズム」 |
| KTX2 | コンテナフォーマット(エンコードデータをパッケージ化する方法) | 「包装箱」 |
| KHR_texture_basisu | glTF拡張(エンジンにこれがBasisテクスチャであることを伝える) | 「ラベル」 |
KTX2ファイルは、内部にBasisエンコード(クロスプラットフォーム)を持つことも、ネイティブフォーマット(例えばBC7を直接格納)を持つこともできます。Web上では99%のケースでBasisを格納します。なぜなら、私たちが求めているのは「1回のエンコードで、どこでもトランスコード」だからです。
VRAMの実例:4096テクスチャの比較
前述の式とGPUフォーマットを組み合わせて、4096×4096 RGBAテクスチャの各方式での実際の使用量を見てみましょう:
| 方式 | ディスクサイズ | VRAM使用量(ミップマップ含む) | アップロード速度 | クロスプラットフォーム |
|---|---|---|---|---|
| PNG | ~8MB | ~87MB | 遅い(デコード必要) | ✅ |
| JPG | ~1.5MB | ~87MB | 遅い(デコード必要) | ✅ |
| WebP | ~2MB | ~87MB | 遅い(デコード必要) | ✅ |
| KTX2 (ETC1S) | ~2-3MB | ~11-14MB | 速い | ✅(トランスコード) |
| KTX2 (UASTC) | ~6-8MB | ~22MB | 速い | ✅(トランスコード) |
VRAMの数値の内訳:GPUブロック圧縮は通常4bpp(1ピクセルあたり4ビット)または8bppで計算されます。4096×4096で4bppの場合、約8MB、ミップマップを含めて1.333倍で約11MB。UASTCはほとんどの場合8bppにトランスコードされるので、約22MBです。
重要なのは各行の正確な数字ではなく、次の2点です:
- 従来フォーマット(PNG/JPG/WebP)のVRAM使用量はほぼ同じ——いずれもデコード後の生ピクセルで87MB。ディスク上で小さくてもVRAMは節約できません。
- KTX2のVRAM使用量は直接1/4から1/8に削減され、ディスクサイズも決して不利ではありません。
これが、VRやモバイルWebでKTX2がほぼ必須である理由です——87MBのテクスチャ1枚を2GB VRAMのスマートフォンに何枚載せられるでしょうか?11MBなら7枚載せられます。
プラットフォームサポートマトリックス:どのGPUがどのフォーマットを認識するか
Basisが細かい部分を隠蔽してくれますが、基盤となるマッピングを理解しておくとトラブルシューティングに役立ちます。以下は、現在の主流デバイスにおけるネイティブフォーマットのサポート状況です:
| プラットフォーム / デバイス | BC1-7 | ETC2 | ASTC | PVRTC |
|---|---|---|---|---|
| デスクトップPC(D3D11/12、Vulkan、WebGPU) | ✅ | ❌ | 一部(新GPU) | ❌ |
| macOS(Metal) | ✅(新機種) | ❌ | ✅ | ❌ |
| Android(主流) | ❌ | ✅ | ✅ | ❌ |
| iOS(A8+) | ❌ | ✅ | ✅ | ✅(旧機種) |
| WebGL 2 | 拡張による | ✅ | 一部 | ❌ |
| WebGPU | ✅(デスクトップ) | ✅ | ✅(デバイスによる) | ❌ |
Basisはランタイムでこれらの能力を検出し、同じ中間エンコードを最も適切なものにトランスコードします。これが、Web上でBasisのレイヤーがほぼ代替不可能である理由です——公開前にユーザーのデバイスを知ることはできません。
アップロードフローの比較:従来方式 vs GPUフォーマット
最後に、フローチャートで違いを明確にします。
従来のPNG/JPG:
PNGファイル ──ダウンロード──> CPUメモリ ──CPUデコード(低速)──> RGBAピクセルブロック ──アップロード(大、低速)──> VRAM(87MB)
KTX2 + Basis:
KTX2ファイル ──ダウンロード──> CPUメモリ ──ランタイムトランスコード(高速)──> GPUブロックフォーマット ──アップロード(小、高速)──> VRAM(11MB)
後者では「CPUによるピクセル単位のデコード」という大きな部分がなくなり、アップロードされるデータ量も1桁少なくなります。初回フレームのレンダリングが速く、VRAMも節約できる——これがこの方式の核となる価値です。
次のステップ
理論は説明しました。次