Any3DAny3D
·Any3D Team

モデルを減量する第一歩:頂点圧縮の三つの武器

3d-compressionvertex-compressionmeshoptdracogltf

前回の記事で GLB ファイルを開き、テクスチャが容量の 80% を占有し、頂点はわずか 10-20% に過ぎないことを確認しました。では頂点圧縮は無関係なのでしょうか?

まさに逆です。モデルのテクスチャが既に KTX2 に圧縮され、頂点も密集している場合、残りの 20% こそが頂点です──そしてこの 20% は、さらに半分、あるいは 90% まで削れます。さらに重要なのは、頂点圧縮はほぼ無料で即効性のある、数少ない最適化のひとつであることです。コマンドを数行追加し、デコーダを替えるだけで、ファイルが細くなります。

この記事では三つのことを整理します:頂点データが実際にどんな姿をしているか、三つの手法(量子化、MeshOpt、Draco)それぞれの性格、そして落とし穴を避けるための結論──「最も良い」解はなく、「最も適した」解しかない、ということです。

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

まず頂点の中身を見てみましょう。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 も内部で使っています。

量子化(quantization:高精度の浮動小数点を低精度の整数に対応させる) の本質はこうです:浮動小数点 3.14159265 なら、3.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 拡張を使い、主要エンジンがネイティブ対応、追加のデコーダライブラリが不要
  • 対象プラットフォームがパッケージサイズに敏感(例: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

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

プラットフォーム純量子化MeshOptDraco
デスクトップ 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(-cc1.7MBさらに 35% 削減、読み込みやや高速

注意:-si 簡略化はモデルのジオメトリを変える損失あり操作で、圧縮とは別物です。圧縮は視覚的一致を保とうとし、簡略化は能動的に詳細を削ります。両者は重ねられますが、シーンが許すか次第です。

よくある落とし穴

  • 量子化で法線方向が変わった:大抵は精度が低すぎるのが原因。法線は最低 16 ビット、または 8 ビットの octahedral 符号化を使う。
  • Draco 解凍後にマテリアルが消えた:Draco はメッシュしか圧縮しない。マテリアルとテクスチャは別途処理が必要。読み込み時に Draco デコーダと KHR 拡張を両方設定すること。
  • ミニプログラムで Draco が読み込めない:一部のランタイムではデコーダ wasm の読み込みが制限される。MeshOpt に切り替えると大抵解決する。
  • 量子化でモデルが「ずれる」:モデルが原点から離れすぎていると、16 ビット精度では大きな座標と小さな詳細を両立できない。解決策は原点付近に移動してから量子化するか、ビット深度を上げること。

次のステップ

頂点の圧縮は終わりました──でも早すぎるお祝いは禁物。前述の通り、テクスチャがモデル容量の 80% を占めます。次は戦場を変え、従来の PNG/JPG が GPU の目にどう映る「大食い」なのか、そして GPU ネイティブテクスチャ形式がどう解決するのかを見ていきます。

応援する