[Astro #103] ブラウザ完結型GLBコンプレッサーの実装 — WebPテクスチャ変換とDraco圧縮の統合

[Astro #103] ブラウザ完結型GLBコンプレッサーの実装 — WebPテクスチャ変換とDraco圧縮の統合

はじめに

前回の「[Astro #102] VRM Thumbnail Generator v2」に引き続き、3Dアセットの最適化に関する実装を取り上げます。

本稿では、Web 3D ROOM用に実装したGLB圧縮処理のコードを分離し、スタンドアロンのGLBコンプレッサーとしてAstroプロジェクト内に統合した実装の解説を行います。本ツールは外部サーバーにデータを送信せず、ブラウザ上で完全にローカル処理されるアーキテクチャを採用しています。

GLBファイルの軽量化における2大コア機能である「テクスチャのWebP化」および「メッシュのDraco圧縮」の実装手法と、Astro環境特有の静的ファイル配信の解決策について詳解します。

[Astro #103] ブラウザ完結型GLBコンプレッサーの実装 — WebPテクスチャ変換とDraco圧縮の統合

1. テクスチャのWebP変換処理

GLBファイルの容量の大部分は、内部に埋め込まれたテクスチャ画像(PNGまたはJPEG)によって占められています。これらをWebPフォーマットに変換し、必要に応じて解像度を制限することでファイルサイズを劇的に削減します。

GLBのバイナリパースと再構築

GLBフォーマットは、構造を定義するJSONチャンクと、頂点データや画像データを格納するバイナリチャンクで構成されています。WebP化の処理手順は以下の通りです。

  1. DataViewを用いてGLBバイナリを読み込み、JSONチャンクとバイナリチャンクを分離してパースする。
  2. JSON内の images 配列を参照し、対応する bufferViews から画像データのバイナリ配列(Uint8Array)を抽出する。
  3. Blob および createImageBitmap を用いて画像をメモリ上に展開し、OffscreenCanvas に描画する。
  4. OffscreenCanvas.convertToBlob({ type: 'image/webp', quality }) を実行し、WebPバイナリを取得する。

元のテクスチャよりファイルサイズが縮小した場合のみWebPデータを採用し、画像サイズが変更されたことに伴い、JSON側の bufferViewsbyteOffsetbyteLength をすべて再計算してバイナリを結合し直します。

2. Dracoジオメトリ圧縮の適用

テクスチャの圧縮に加え、頂点データの圧縮にはGoogleのDracoライブラリを使用する。

当初は @gltf-transform/functionsdraco() を esm.sh 経由で利用する方針だったが、 WASMモジュールの読み込みで fs.readFileSync がブラウザ非対応のエラーが発生し、 CDN経由のバージョン互換性問題も解消できなかったため、 Google Draco エンコーダを直接利用する方式に切り替えた。

Draco WASM の読み込み

Google が公開している Draco の JavaScript/WASM モジュールをローカルに配置し、 <script> タグで動的にロードする。

// WASM バイナリを fetch
const encoderWasm = await fetch('./draco/draco_encoder.wasm').then(r => r.arrayBuffer());
// JS glue をスクリプトタグで読み込み(グローバルに DracoEncoderModule が定義される)
await loadScript('./draco/draco_encoder.js');
// WASM バイナリを渡してインスタンス化
const encoder = await DracoEncoderModule({ wasmBinary: encoderWasm });

メッシュプリミティブのエンコード

GLB の JSON から各メッシュプリミティブのインデックスと頂点属性を抽出し、 Draco の MeshBuilder API で直接エンコードする。

const meshBuilder = new encoder.MeshBuilder();
const dracoMesh = new encoder.Mesh();
// 三角形インデックスを追加
meshBuilder.AddFacesToMesh(dracoMesh, numFaces, faces);
// 頂点属性(POSITION, NORMAL, TEXCOORD等)を追加
meshBuilder.AddFloatAttributeToMesh(dracoMesh, encoder.POSITION, count, 3, positions);
// エンコード実行
const enc = new encoder.Encoder();
enc.SetEncodingMethod(encoder.MESH_EDGEBREAKER_ENCODING);
enc.EncodeMeshToDracoBuffer(dracoMesh, encodedData);

エンコード後のバイナリを新しい bufferView として GLB のバイナリチャンクに追加し、 各プリミティブに KHR_draco_mesh_compression 拡張を付与して GLB を再構築する。

3. Astro環境におけるWASM/JSモジュールの配信

Dracoエンコーダおよびデコーダ(.wasm.js)をクライアントサイドで動的に読み込む場合、Astroのビルドプロセスにおけるファイルパスの解決に注意が必要です。

スクリプトファイル内で相対パス(例: ./draco_encoder.wasm)を指定すると、ルーティングの階層によっては404エラーが発生します。これを回避するため、静的アセットはプロジェクトルートの public/ ディレクトリ内に配置します。

/public
  └─ /draco
       ├─ draco_encoder.wasm
       ├─ draco_decoder.wasm
       ├─ draco_encoder.js
       └─ draco_decoder.js

コード側からは、ルート相対パス(/draco/〜 またはツールを配置するディレクトリベースパス)を利用して fetch および script タグの挿入を行うことで、開発サーバー・ビルド後の環境を問わず確実にアセットをロードさせることができます。

4. 圧縮結果と検証

本ツールの性能検証として、フォトグラメトリによって生成された「観音崎砲台跡」のGLBモデルに対して圧縮処理を行いました。

  • オリジナルサイズ: 53.4 MB
  • 圧縮後サイズ: 7.4 MB
  • 圧縮率: -86.2%

WebP変換(品質 75%、最大テクスチャサイズ 2048px)およびDraco圧縮を併用することで、視覚的な劣化を最小限に抑えたまま約86%の容量削減を達成しました。 また、出力されたGLBファイルはBabylon.js Sandbox等の厳格な外部ビューアー上でもエラーなく描画されることが確認できており、glTF/GLBの標準仕様に完全準拠したフォーマットとして出力されています。

まとめ

Astroの単一ページ内で、OffscreenCanvas を用いたWebP変換と、Google Draco WASM エンコーダを用いたジオメトリ圧縮を統合しました。

これにより、サーバーリソースを消費することなく、クライアントサイドの処理のみでセキュアかつ高速に3Dアセットの最適化が可能となりました。

本ツールは今後の3Dコンテンツ制作において、Webアップロード前のアセット最適化パイプラインとして活用していきます。