モデルを減量する第一歩:頂点圧縮の三つの武器
前回の記事で GLB ファイルを開き、テクスチャが容量の 80% を占有し、頂点はわずか 10-20% に過ぎないことを確認しました。では頂点圧縮は無関係なのでしょうか?
まさに逆です。モデルのテクスチャが既に KTX2 に圧縮され、頂点も密集している場合、残りの 20% こそが頂点です──そしてこの 20% は、さらに半分、あるいは 90% まで削れます。さらに重要なのは、頂点圧縮はほぼ無料で即効性のある、数少ない最適化のひとつであることです。コマンドを数行追加し、デコーダを替えるだけで、ファイルが細くなります。
この記事では三つのことを整理します:頂点データが実際にどんな姿をしているか、三つの手法(量子化、MeshOpt、Draco)それぞれの性格、そして落とし穴を避けるための結論──「最も良い」解はなく、「最も適した」解しかない、ということです。
頂点一つはどれくらい大きいのか
まず頂点の中身を見てみましょう。glTF では、各頂点は複数の attribute(属性)で構成されます:
| 属性 | 用途 | デフォルト精度 | 頂点あたりバイト数 |
|---|---|---|---|
| position(位置) | 空間内の頂点座標 | 3 × float32 | 12 |
| normal(法線) | 光照方向を決める | 3 × float32 | 12 |
| tangent(接線) | 法線マップの計算 | 4 × float32 | 16 |
| texcoord_0(UV) | テクスチャのサンプリング座標 | 2 × float32 | 8 |
| color(頂点カラー) | 頂点レベルのシェーディング | 4 × float32 | 16 |
PBR の全属性を持つ頂点は、ジオメトリデータだけで 48-64 バイト になります。10 万頂点のモデルなら、頂点だけで 5-6MB です。
ここでほぼすべてが float32(32 ビット浮動小数点)を使っていることに注目してください。これがデフォルト設定であり、頂点圧縮の突破口でもあります──圧倒的多数の属性は 32 ビット精度を必要としないからです。
第一の武器:量子化(Quantization)
量子化はあらゆる頂点圧縮の根底にある原理で、Draco も MeshOpt も内部で使っています。
量子化(quantization:高精度の浮動小数点を低精度の整数に対応させる) の本質はこうです:浮動小数点 3.14159265 なら、3.14 と覚えておけば十分です。ある空間内の一連の座標について、32 ビットで各桁を正確に記録する代わりに、より範囲の小さい整数で表します。
元の値: position.x = 1.234567 (float32, 4バイト)
量子化後: position.x = 1234 (int16, 2バイト) + 復元用の scale/offset
量子化の前後比較:
| 属性 | float32 バイト数 | 量子化後(16 ビット) | 削減 |
|---|---|---|---|
| position | 12 | 6 | 50% |
| normal | 12 | 6(または 4、int8 + octahedral 使用) | 50-67% |
| tangent | 16 | 4-8 | 50-75% |
| texcoord | 8 | 4 | 50% |
先ほどの 48-64 バイトの頂点は、量子化で基本的に 16-24 バイト まで圧縮でき、サイズが半分以下になります。
量子化を使うとき
- 単にサイズを減らしたい、極限の圧縮率は求めない
- デコーダ依存ゼロ にしたい──量子化後の glTF は標準の
KHR_mesh_quantization拡張を使い、主要エンジンがネイティブ対応、追加のデコーダライブラリが不要 - 対象プラットフォームがパッケージサイズに敏感(例:WeChat ミニプログラム、Draco デコーダを同梱すると数十 KB 増える)
使わないほうがいいとき
- モデル自体が極小で、詳細が売り(例:ミリ単位の工業パーツ)。量子化は小さなモデルで最も破綻しやすい──テクスチャは大丈夫でも、頂点位置が 0.1mm ずれると接写で目立ちます。
精度劣化の実被害:あるジュエリー展示シーンで、指輪モデルを 16 ビットに量子化したところ、接写で金属の縁にジャギーが出ました。原因は頂点数不足ではなく、ワールド座標系が小さすぎて 16 ビット整数では表現範囲が足りなかったことです。解決策は量子化範囲を縮める(
positionのバウンディングボックスを小さくする)か、小モデルではより高いビット深度を使うことです。
第二の武器:MeshOpt
MeshOpt は glTF 公式拡張 EXT_meshopt_compression で、「圧縮率もそこそこ、解凍も爆速」という位置づけです。
やっていることは、まず属性を量子化し(上と同じ)、その後 エントロピー符号化(lossless、無損失) という手法で量子化後の整数をもう一度無損失で圧縮します。つまり:損失ありの量子化 + 無損失のエントロピー符号化 = より小さく、画質は量子化と同等。
- 圧縮率:量子化単体よりさらに 30-50% 小さい
- 解凍速度:極めて速い、純 C/JS 実装、シングルスレッドで毎秒数千万頂点
- デコーダサイズ:小さい(gzip 後約 20-30KB)
- 互換性:Three.js、Babylon.js がネイティブ対応、Web の事実上標準のひとつ
MeshOpt を使うとき
- より高い圧縮率が欲しいが、Draco のような遅い解凍は受け入れられない
- Web / モバイル / WebXR が中心──解凍速度が初回描画体験に直結
- モデルを頻繁に解凍する(例:動的に読み込むレベル)
使わないとき
- 対象プラットフォームが
EXT_meshopt_compressionすら認識しない(稀、古いエンジン) - 「動けばいい」で 30% の差は気にしない──なら純量子化のほうがシンプルで依存も減る
第三の武器:Draco
Draco は Google 製の圧縮方式で、「極限の圧縮率」が位置づけです。
前二者との根本的な違い:Draco は頂点の接続関係(トポロジー)を変える ことです。量子化は各頂点の数値表現だけを変え、MeshOpt はその上に無損失符号化を重ねますが、Draco は三角メッシュを再構成し、「どの頂点が三角形を作るか」をよりコンパクトに表現します。
- 圧縮率:三手法で最高、頂点密集モデルではしばしば 90% 以上の削減
- 解凍速度:三手法で最も遅いが、それでも絶対値では速い
- デコーダサイズ:やや大きい(約 100-200KB、通常 wasm として別途読み込み)
- 画質:調整可能だが、極限の圧縮率では目に見える歪みが出る
Draco を使うとき
- モデルが極大、頂点が超密集(100 万頂点スキャンモデル、地形)
- 一度読み込めば解凍後ずっと使い回す(遅い解凍を許容できる)
- パッケージサイズはボトルネックでなく、ダウンロード速度こそがボトルネック
使わないとき
- モバイル + 高速な初回描画が必要──デコーダもモデルもダウンロードする必要があり逆に遅くなる
- ミニプログラムなどパッケージサイズに厳しい環境
- スキンアニメーション、モーフターゲットが必要なモデル──Draco はこれらへの対応が弱く、設定を誤ると問題が出る
三つ並べて:選定表
下記の圧縮比はコミュニティのベンチマーク(DeepKolos のテスト + Reddit r/threejs の議論)を参考にしています。モデルによってばらつきはありますが、相対的な関係はほぼ安定しています:
| 手法 | 圧縮比(float32 比) | 解凍速度 | デコーダサイズ | ロスありか | glTF 拡張 |
|---|---|---|---|---|---|
| 純量子化 | ~50% | ネイティブ、解凍不要 | 0 | あり(精度) | KHR_mesh_quantization |
| MeshOpt | ~25-35% | 極めて速い | ~25KB | あり(精度) | EXT_meshopt_compression |
| Draco | ~10-20% | 速い(三手法で最も遅い) | ~100-200KB | あり(精度+トポロジー) | KHR_draco_mesh_compression |
デコーダとプラットフォーム互換性:
| プラットフォーム | 純量子化 | MeshOpt | Draco |
|---|---|---|---|
| デスクトップ Web | ✅ ネイティブ | ✅ ネイティブ | ✅ デコーダ設定が必要 |
| モバイル Web | ✅ ネイティブ | ✅ ネイティブ | ⚠️ デコーダが重め |
| WebXR/VR | ✅ ネイティブ | ✅ 推奨 | ⚠️ 注意して使用 |
| WeChat ミニプログラム | ✅ 推奨 | ✅ 推奨 | ❌ できるだけ避ける |
一言まとめ:手軽・依存ゼロ → 純量子化;バランス → MeshOpt;最高圧縮率で待てる → Draco。
実践:gltfpack で量子化と MeshOpt
gltfpack は glTF 公式ツールで、コマンド一本で量子化と MeshOpt をこなします。
まずインストール(バイナリは gltfpack releases からダウンロード):
# 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(-cc) | 1.7MB | さらに 35% 削減、読み込みやや高速 |
注意:
-si簡略化はモデルのジオメトリを変える損失あり操作で、圧縮とは別物です。圧縮は視覚的一致を保とうとし、簡略化は能動的に詳細を削ります。両者は重ねられますが、シーンが許すか次第です。
よくある落とし穴
- 量子化で法線方向が変わった:大抵は精度が低すぎるのが原因。法線は最低 16 ビット、または 8 ビットの octahedral 符号化を使う。
- Draco 解凍後にマテリアルが消えた:Draco はメッシュしか圧縮しない。マテリアルとテクスチャは別途処理が必要。読み込み時に Draco デコーダと KHR 拡張を両方設定すること。
- ミニプログラムで Draco が読み込めない:一部のランタイムではデコーダ wasm の読み込みが制限される。MeshOpt に切り替えると大抵解決する。
- 量子化でモデルが「ずれる」:モデルが原点から離れすぎていると、16 ビット精度では大きな座標と小さな詳細を両立できない。解決策は原点付近に移動してから量子化するか、ビット深度を上げること。
次のステップ
頂点の圧縮は終わりました──でも早すぎるお祝いは禁物。前述の通り、テクスチャがモデル容量の 80% を占めます。次は戦場を変え、従来の PNG/JPG が GPU の目にどう映る「大食い」なのか、そして GPU ネイティブテクスチャ形式がどう解決するのかを見ていきます。