テクスチャ、あなたの VRAM を食い尽くす大食い
前回は頂点を半分にし、モデルは少し小さくなりましたが、稲妻のように細くはなりませんでした──本当の容量の主役がまだそこにいたからです:テクスチャ。PBR モデルではテクスチャが通常 80% 以上の容量を占め、しかも VRAM で最も膨らむ部分です。
この記事はテクスチャの「VRAM 大食い」問題の特効薬です。三つのことを説明します:なぜ PNG/JPG が GPU の目に罪深いのか、GPU 自身のテクスチャ形式がどんな姿で、なぜそのままでは使えないのか、そして Basis Universal + KTX2 が三つをどうつなぐのか。
復習:なぜ JPG は VRAM を爆発させるのか
前回の記事で挙げた公式:
VRAM 使用量 = 幅 * 高さ * 4バイト(RGBA) * 1.333(mipmap含む)
4096×4096 のテクスチャは、ディスク上で 1.5MB の JPG だろうが 8MB の PNG だろうが、VRAM では 約 87MB になります。理由は一つ:GPU は JPG/PNG を理解しない。
GPU のテクスチャサンプラーが理解するのは一つだけ──UV 座標が与えられたら、固定サイズのピクセルブロックから色を読み出す。テクスチャは VRAM 上で「敷き詰められた生ピクセル」でなければなりません。だからブラウザは JPG を GPU にアップロードする前に、CPU で完全に RGBA ピクセルへ展開し、ブロックごと VRAM に押し込まなければなりません。
この処理には三重の問題があります:
- 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 ピクセルの小さなブロック(block)単位で圧縮保存 され、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 はその背後にある端末のハードウェア能力に依存する
一枚のテクスチャを「すべてのデバイスで GPU ネイティブ形式として存在させたい」なら、プラットフォームごとに別々に用意 しなければなりません。デスクトップ+Android+iOS に出す製品なら、同じテクスチャに BC+ETC2/ASTC の三版が必要です。パッケージは 3 倍、作業量も 3 倍。
さらに悪いことに、Web ではユーザーがどんなデバイスでページを開くか分かりません。全形式を事前生成するのは非現実的で、実行時検出では間に合いません。
Basis Universal:一度エンコード、どこでもトランスコード
Basis Universal(略称 Basis)はこのフラグメンテーションを解決するために生まれました。考え方は一文です:
まずテクスチャを「中間形式」にエンコードし、実行時に現在のデバイスの GPU 能力に合わせて対応するネイティブ形式へトランスコード(transcode)する。
トランスコードの流れ(イメージ図):
ソーステクスチャ(PNG/JPG)
│ 一回限りのオフラインエンコード(遅い、一度だけ)
▼
Basis 中間形式(ETC1S または UASTC)
│ KTX2 コンテナにパッキング
▼
Web に公開 ──┬── デスクトップ GPU ──→ 実行時トランスコード → BC1/3/7
├── Android ────→ 実行時トランスコード → ETC2
└── iOS/VR ─────→ 実行時トランスコード → ASTC
ポイント:
- オフラインエンコードは一度だけ、コンパクトな中間表現を得る
- 実行時トランスコードは極めて速い(純粋な計算、数ミリ秒)、しかもブロック形式を変換するのでピクセル単位の展開は不要
- トランスコード後に VRAM に入るのは本物の GPU ネイティブ形式で、VRAM 使用量はブロック圧縮レートで計算され、GPU ネイティブ形式と一致
Basis は二つの中間エンコードモードを提供します。次の記事で展開しますが、名前だけ覚えておきましょう:
- ETC1S:圧縮率極めて高く、ディフューズ/albedo などのカラーマップ向け
- UASTC:より高品質、法線など精度に敏感なマップ向け
KTX2:GPU テクスチャを入れる標準コンテナ
ここでもう一つ工学的な問題が残ります:エンコードされた Basis データをどこに置き、どうマークし、glTF とどう関連付けるか。答えは KTX2。
KTX2(Khronos Texture 2)はまた別の画像形式ではなく、コンテナ形式 です──.zip が中身が文書でも画像でも気にしないように、KTX2 は GPU テクスチャデータ(Basis エンコードを含む)を標準構造でパッキングし、メタ情報(形式、mipmap レベル、色空間など)を添えるだけです。
glTF では、KTX2 は拡張 KHR_texture_basisu で接続されます:テクスチャはもはや PNG ファイルではなく、Basis エンコードを含む KTX2 ファイルになります。読み込み時にエンジンがデバイス能力を検出し、対応する BC/ETC/ASTC へトランスコードします。
三つの役割を整理しましょう、混同しないように:
| 名前 | 役割 | 例え |
|---|---|---|
| Basis Universal | エンコード方式(テクスチャを中間形式にどう圧縮するか) | 「圧縮アルゴリズム」の一種 |
| KTX2 | コンテナ形式(エンコード後のデータをどうパッキングするか) | 「箱」一つ |
| KHR_texture_basisu | glTF 拡張(エンジンに Basis テクスチャだと伝える) | 「ラベル」一つ |
KTX2 ファイルの中身は Basis エンコード(クロスプラットフォーム)にも、何らかのネイティブ形式(例えば生 BC7)にもなれます。Web で 99% は Basis を入れます──私たちが求めるのは「一度エンコード、どこでもトランスコード」だからです。
VRAM 実例:4096 テクスチャの比較
先ほどの公式と GPU 形式を重ねて、4096×4096 RGBA テクスチャの各方式での実際の占有を見てみましょう:
| 方式 | ディスクサイズ | VRAM 使用量(mipmap 含む) | アップロード速度 | クロスプラットフォーム |
|---|---|---|---|---|
| PNG | ~8MB | ~87MB | 遅い(展開が必要) | ✅ |
| JPG | ~1.5MB | ~87MB | 遅い(展開が必要) | ✅ |
| WebP | ~2MB | ~87MB | 遅い(展開が必要) | ✅ |
| KTX2 (ETC1S) | ~2-3MB | ~11-14MB | 速い | ✅(トランスコード) |
| KTX2 (UASTC) | ~6-8MB | ~22MB | 速い | ✅(トランスコード) |
VRAM の数値の出どころ:GPU のブロック圧縮は通常 4bpp(ピクセルあたり 4 ビット)または 8bpp で計算します。4096×4096 を 4bpp で約 8MB、mipmap で ×1.333 ≈ 11MB。UASTC は大半が 8bpp へトランスコードされるので約 22MB。
重要なのはある一行の正確な数値ではなく、次の二点です:
- 従来形式(PNG/JPG/WebP)は VRAM 使用量がほぼ同じ──いずれも展開後の生ピクセルで 87MB。ディスクがいくら小さくても VRAM は節約されない。
- KTX2 は VRAM を 1/4 から 1/8 に直撃、しかもディスクサイズも引けを取りません。
だから VR やモバイル Web ではほぼ必ず KTX2 を使います──2GB の VRAM を持つスマホに 87MB のテクスチャは何枚置けるでしょうか?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 のピクセル単位展開」を省き、アップロードするデータ量も一桁小さくなります。初回フレームが速く、VRAM も省ける──これがこの方式のコアバリューです。
次のステップ
理論は終わりました、次は実践です。toktx と gltf-transform で実際にテクスチャを KTX2 に圧縮し、Three.js / Babylon.js で読み込み、ETC1S と UASTC の選び方や圧縮パラメータの調整方を扱います。