Kris

モデルをスリムにする第一歩:頂点圧縮の三種の神器

3D 圧縮頂点圧縮MeshOptDracoglTF

前回、GLBファイルを分解して、テクスチャが容量の80%を占め、頂点はわずか10〜20%であることを確認した。だから頂点圧縮はどうでもいいように思えるかもしれない。

しかし、それは全く逆だ。モデルのテクスチャがすでにKTX2に圧縮され、頂点がびっしりと詰まっている場合、残りの20%が頂点であり、この20%はさらに半分、あるいは90%も削減できる可能性がある。さらに重要なのは、頂点圧縮はほぼゼロコストで即座に効果が現れる数少ない最適化の一つだということだ。数行のコマンドを追加し、デコーダーを切り替えるだけで、ファイルは軽くなる。

この記事では3つのことを明確にする:頂点データが実際にどのような構造か、量子化・MeshOpt・Dracoのそれぞれの特性、そして失敗を避けるための結論——「最良の」ソリューションはなく、「最適な」ソリューションがあるだけだ

1つの頂点はどれくらいの大きさか

まず、1つの頂点に何が含まれているかを見てみよう。glTFでは、各頂点は複数の属性(attribute)で構成されている:

属性用途デフォルト精度頂点あたりのバイト数
position(位置)頂点の空間座標3 × float3212
normal(法線)ライティング方向の決定3 × float3212
tangent(接線)法線マップ計算4 × float3216
texcoord_0(UV)テクスチャサンプリング座標2 × float328
color(頂点カラー)頂点レベルの着色4 × float3216

PBRの完全な属性を持つ頂点は、ジオメトリデータだけで 48〜64バイト になる。10万頂点のモデルなら、頂点だけで5〜6MBになる。

ここでほぼ全てが float32(32ビット浮動小数点数)を使用していることに注目してほしい。これはデフォルト設定であり、頂点圧縮の突破口でもある——なぜなら、ほとんどの属性は32ビットの精度を必要としないからだ。

第一の神器:量子化(Quantization)

量子化はすべての頂点圧縮の基本原理であり、DracoやMeshOptも内部でこれを使用している。

量子化(高精度の浮動小数点数を低精度の整数にマッピングすること) の本質は、浮動小数点数 3.141592653.14 と覚えておけば十分だということだ。空間範囲内の座標群を、32ビットで各小数を正確に記録する代わりに、より小さな範囲の整数で表現する。

元:   position.x = 1.234567   (float32, 4バイト)
量子化後: position.x = 1234       (int16,   2バイト)  + scale/offset で復元

量子化前後の比較:

属性float32 バイト数量子化後(16ビット)削減率
position12650%
normal126(または4、int8 + octahedral)50-67%
tangent164-850-75%
texcoord8450%

先ほどの48〜64バイトの頂点は、量子化後には基本的に 16〜24バイト に圧縮でき、サイズは半分以上になる。

量子化を使うべき時

  • サイズを減らしたいだけで、極限の圧縮率は必要ない場合
  • デコーダー依存ゼロ を望む場合——量子化されたglTFは標準の KHR_mesh_quantization 拡張を使用し、主要エンジンがネイティブ対応しているため、追加のデコードライブラリを導入する必要がない
  • ターゲットプラットフォームがパッケージサイズに敏感な場合(例えばLINEミニアプリ、Dracoデコーダーを追加するだけで数十KBになる)

使うべきでない時

  • モデル自体が非常に小さく、ディテールが売りの場合(例えばミリメートル単位の工業部品)。量子化は小さなモデルで特に問題が露呈しやすい——テクスチャは問題ないが、頂点位置の0.1mmのずれがクローズアップで肉眼に見える。

量子化の精度損失の実際の落とし穴:ジュエリー展示シーンで、指輪モデルを16ビットに量子化した後、クローズアップで金属のエッジにジャギーが発生した。原因は頂点数ではなく、ワールド座標系が小さすぎて、16ビット整数で表現できる範囲が十分に細かくなかったためだ。解決策は量子化範囲を狭める(position のバウンディングボックスを縮小する)か、小さなモデルにはより高いビット深度を使用することだ。

第二の神器:MeshOpt

MeshOptはglTF公式拡張 EXT_meshopt_compression で、「圧縮率は良好、デコードは超高速」という位置づけだ。

その手法は、まず属性を量子化し(上記と同じ)、次に LISS(lossless entropy coding、ロスレスエントロピー符号化) と呼ばれる手法で量子化された整数をさらにロスレスで圧縮する。つまり、量子化(非可逆)+ エントロピー符号化(可逆)= より小さなサイズ、画質は量子化と同じ ということだ。

  • 圧縮率:単純な量子化よりさらに 30〜50% 小さい
  • デコード速度:非常に高速、純C/JS実装、シングルスレッドで毎秒数千万頂点を処理
  • デコーダーサイズ:非常に小さい(gzip後 約20〜30KB)
  • 互換性:Three.js、Babylon.jsがネイティブ対応、Web標準の事実上の標準の一つ

MeshOptを使うべき時

  • より高い圧縮率が必要だが、Dracoのような遅いデコードを受け入れられない場合
  • Web中心、モバイル、WebXR——デコード速度はファーストビューの読み込み体験に直結する
  • モデルを頻繁に解凍する必要がある場合(例えば動的ロードのステージ)

使うべきでない時

  • ターゲットプラットフォームが EXT_meshopt_compression を認識しない場合(ごく一部の古いエンジン)
  • 「動けばいい」だけで、30%の差を気にしない場合——純粋な量子化の方がシンプルで、依存関係が一つ減る

第三の神器:Draco

DracoはGoogleが開発した圧縮ソリューションで、「極限の圧縮率」を目指している。

前の2つとの根本的な違い:Dracoは頂点の接続関係(トポロジー構造)を変更する。量子化は各頂点の数値表現のみを変更し、MeshOptはその上でロスレス符号化を行うが、Dracoは三角形メッシュを再編成し、「どの頂点が三角形を形成するか」をよりコンパクトな方法で表現する。

  • 圧縮率:3つの中で最高、頂点密度の高いモデルでは 90%以上の削減 がよく達成される
  • デコード速度:3つの中で最も遅いが、それでも高速(相対的に遅いだけ)
  • デコーダーサイズ:大きめ(約100〜200KB、通常はwasmを別途ロードする必要がある)
  • 画質:調整可能だが、極端な圧縮率では目に見える変形が発生する

Dracoを使うべき時

  • モデルが非常に大きく、頂点が超密度(数百万頂点のスキャンモデル、地形)
  • 一度ロードして、解凍後に長時間再利用する場合(デコードが少し遅くても許容できる)
  • パッケージサイズがボトルネックではなく、ダウンロード速度が問題な場合

使うべきでない時

  • モバイル + 高速なファーストビューが必要な場合——デコーダーとモデル自体の両方をダウンロードする必要があり、むしろ遅くなる
  • LINEミニアプリなど、パッケージサイズに厳しい環境
  • スキンアニメーションやモーフターゲット(変形ターゲット)が必要なモデル——Dracoはこれらのサポートが弱く、設定を誤ると問題が発生する

3つを並べて:選択のための一覧表

以下の圧縮比はコミュニティのベンチマークテスト(Qiitaの記事 + Reddit r/threejs の議論)を参考にしている。モデルによって差はあるが、相対的な関係はほぼ安定している:

方式圧縮比(float32比)デコード速度デコーダーサイズ非可逆かglTF拡張
純量子化~50%ネイティブ、デコード不要0はい(精度)KHR_mesh_quantization
MeshOpt~25-35%非常に高速~25KBはい(精度)EXT_meshopt_compression
Draco~10-20%高速(3つ中最遅)~100-200KBはい(精度+トポロジー)KHR_draco_mesh_compression

デコーダーとプラットフォームの互換性:

プラットフォーム純量子化MeshOptDraco
デスクトップWeb✅ ネイティブ✅ ネイティブ✅ デコーダー要
モバイルWeb✅ ネイティブ✅ ネイティブ⚠️ デコーダーが重い
WebXR/VR✅ ネイティブ✅ 推奨⚠️ 注意が必要
LINEミニアプリ✅ 推奨✅ 推奨❌ 避けるべき

一言でまとめると:手間いらず・依存ゼロ → 純量子化;バランス重視 → MeshOpt;極限の圧縮率で待てる → Draco。

実践:gltfpackで量子化とMeshOptを行う

gltfpack はglTF公式ツールで、1行のコマンドで量子化とMeshOptを実行できる。

まずインストール(バイナリは gltfpack release からダウンロード):

# model.glb を16ビットに量子化し、MeshOpt圧縮を追加
gltfpack -i model.glb -o model-packed.glb -cc

# -cc = compress(デフォルトの量子化の上に EXT_meshopt_compression を重ねる)

よく使うパラメータ:

# 量子化のみ、MeshOptなし(最軽量、デコーダー依存ゼロ)
# gltfpack はデフォルトで頂点を16ビット量子化(KHR_mesh_quantization)するため、追加パラメータ不要
gltfpack -i model.glb -o model-quant.glb

# 量子化 + MeshOpt有効
gltfpack -i model.glb -o model-meshopt.glb -cc

# 頂点が非常に多い場合、簡略化も可能(頂点数を減らす、モデルが変わる)
gltfpack -i model.glb -o model-simplify.glb -cc -si 0.5
# -si 0.5 は頂点を約50%に簡略化

-cc について:「compress」スイッチで、追加で EXT_meshopt_compression を重ねる。-cc を書かない場合、gltfpackはデフォルトで量子化を行う——つまり gltfpack -i in.glb -o out.glb というコマンド自体がすでに「純量子化、デコーダー依存ゼロ」だ。(-v はverbose詳細ログのスイッチで、混同しないように。)

典型的な効果(5MB、12万頂点のPBRモデル、参考値):

処理ファイルサイズ説明
元(float32)5.0MB基準
純量子化(デフォルト)2.6MB半分に、視覚的に明確な差なし
MeshOpt(-cc1.7MBさらに35%削減、ロードが少し速い

注意:-si 簡略化はモデルのジオメトリを変更する非可逆操作で、圧縮とは別物だ。圧縮は視覚的な一貫性を保つことを目指し、簡略化は積極的にディテールを減らす。両者は組み合わせられるが、シーンが許すかどうかを確認する必要がある。

よくある落とし穴

  • 量子化後に法線の向きが変わる:おそらく精度が低すぎる。normalは少なくとも16ビット、または八面体符号化(octahedral)の8ビットを使用する。
  • Dracoデコード後にマテリアルが失われる:Dracoはメッシュのみを圧縮し、マテリアルとテクスチャは別途処理する必要がある。ロード時にはDracoデコーダーとKHR拡張を同時に設定する必要がある。
  • ミニアプリでDracoがロードできない:デコーダーのwasmが一部のランタイム環境でロード制限を受ける。MeshOptに切り替えると通常は解決する。
  • 量子化後にモデルが「ドリフト」する:モデルが座標原点から遠すぎる場合、16ビット精度では大きな座標+小さなディテールを表現できない。解決策はモデルを原点付近に移動してから量子化するか、ビット深度を上げることだ。

次のステップ

頂点圧縮は完了したが、祝うのはまだ早い——前述の通り、テクスチャはモデルの80%のサイズを占めている。次回は戦場を変えて、なぜ従来のPNG/JPGがGPUにとって「大食漢」なのか、そしてGPUネイティブのテクスチャ形式がこの問題をどう解決するのかを見ていこう。

応援する