Any3DAny3D
·Any3D Team

テクスチャ、あなたの VRAM を食い尽くす大食い

3d-compressiontexture-compressionwebglwebgpu

前回は頂点を半分にし、モデルは少し小さくなりましたが、稲妻のように細くはなりませんでした──本当の容量の主役がまだそこにいたからです:テクスチャ。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 に押し込まなければなりません。

この処理には三重の問題があります:

  1. VRAM の爆発:展開された生ピクセルは巨大。87MB は誇張ではなく、公式で計算された値です。
  2. アップロードの阻塞:大きなピクセルブロックを CPU メモリから GPU の VRAM へ移すのは遅い操作で、最初のフレームの描画を止めてしまいます。
  3. CPU の展開コスト:大きな画像の展開自体が時間がかかり、モバイルでは特に顕著です。

前回の「圧縮スポンジ」の比喩を延ばすと:PNG/JPG は運びやすいように平たく圧縮したスポンジ。GPU に載った途端、スポンジは水を吸って元の大きさに膨らみます。ダウンロードは速くなったが、VRAM は全く節約できていない。

GPU 自身のテクスチャ形式:生まれながらに VRAM で圧縮されている

GPU が圧縮済みの PNG を受け付けないなら、テクスチャを VRAM 内でも圧縮状態のまま 保てないでしょうか?GPU はサンプリング時に個々のピクセルブロックをその場で復号し、ほぼコストなしに行えます。

それが GPU ネイティブテクスチャ形式のする事です。代表的なファミリー:

形式ファミリー正式名主なプラットフォーム特徴
BC1-7Block Compressionデスクトップ(PC、Mac)古参、世代ごとに 4×4 ピクセルブロック圧縮
ETC1/2Ericsson Texture Compressionモバイル(古い Android/iOS)モバイルの古い標準
ASTCAdaptive Scalable Texture Compressionモバイル/VR(新端末)柔軟、品質最高、ブロックごとに調整可
PVRTCPowerVR古い iOSASTC に順次取って代わられつつある

これらの形式に共通するのは:テクスチャが 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_basisuglTF 拡張(エンジンに 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。

重要なのはある一行の正確な数値ではなく、次の二点です:

  1. 従来形式(PNG/JPG/WebP)は VRAM 使用量がほぼ同じ──いずれも展開後の生ピクセルで 87MB。ディスクがいくら小さくても VRAM は節約されない。
  2. KTX2 は VRAM を 1/4 から 1/8 に直撃、しかもディスクサイズも引けを取りません。

だから VR やモバイル Web ではほぼ必ず KTX2 を使います──2GB の VRAM を持つスマホに 87MB のテクスチャは何枚置けるでしょうか?11MB なら 7 枚置けます。

プラットフォーム対応マトリクス:どの GPU がどの形式を認識するか

Basis が詳細を隠してくれますが、基礎の対応関係を知っておくとトラブル解消に役立ちます。主要デバイスのネイティブ形式への対応状況:

プラットフォーム / デバイスBC1-7ETC2ASTCPVRTC
デスクトップ 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 も省ける──これがこの方式のコアバリューです。

次のステップ

理論は終わりました、次は実践です。toktxgltf-transform で実際にテクスチャを KTX2 に圧縮し、Three.js / Babylon.js で読み込み、ETC1S と UASTC の選び方や圧縮パラメータの調整方を扱います。

応援する