[Astro #104] Three.js GLTFExporterによるブラウザ完結型GLB/GLTFエクスポートの実装

[Astro #104] Three.js GLTFExporterによるブラウザ完結型GLB/GLTFエクスポートの実装

はじめに

前回の記事 [[Astro #103] ブラウザ完結型GLBコンプレッサーの実装 — WebPテクスチャ変換とDraco圧縮の統合] では、WebPテクスチャ変換とDraco圧縮を組み合わせたブラウザ完結型の最適化パイプラインを実装しました。

今回は、 Three.js (r183) の GLTFExporter を用いたブラウザ完結型GLB/GLTFエクスポート機能の組み込みについてまとめます。

VRChatやVRM規格で使用されるような複雑なボーン構造(J_Bip_C_Hips, J_Bip_C_Chest など)や表情用モーフターゲット(Fcl_ALL_Fun, Fcl_EYE_Close など)を維持したまま、サーバーレスで軽量かつ安全に書き出す実装を実現しています。

[Astro #104] Three.js GLTFExporterによるブラウザ完結型GLB/GLTFエクスポートの実装

開発の目的と課題

目的

  1. 完全クライアントサイド動作: サーバーに3Dモデルやアバターデータをアップロードさせることなく、ブラウザのメモリ内だけでエクスポートを完了する。
  2. 構造の維持: アバターモデル特有のヒップ・胸・頭部などのボーン階層構造、およびフェイス(表情)の BlendShape / Morph Targets を欠落させずに保持する。
  3. Astro へのスムーズな組み込み: クライアントサイド・アイランド(client:only<script> タグ)を活用し、Astroのパフォーマンスを損なわずに組み込む。

主な実装手順

1. Three.js (r183) と GLTFExporter のセットアップ

Three.js から GLTFExporter をインポートし、Astro のクライアントコンポーネントまたはフロントエンドスクリプト内で初期化します。

import * as THREE from 'three';
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';

// エクスポート関数の定義
export async function exportSceneToGLB(inputObject: THREE.Object3D): Promise<ArrayBuffer> {
  const exporter = new GLTFExporter();

  return new Promise((resolve, reject) => {
    const options = {
      binary: true, // .glb(バイナリ形式)で出力する場合は true
      embedImages: true,
      animations: inputObject.animations || [],
      includeCustomExtensions: true,
    };

    exporter.parse(
      inputObject,
      (gltf) => {
        if (gltf instanceof ArrayBuffer) {
          resolve(gltf);
        } else {
          reject(new Error('GLTFのパース結果がArrayBufferではありません。'));
        }
      },
      (error) => {
        reject(error);
      },
      options
    );
  });
}

2. ボーン階層とモーフターゲットの保持確認

エクスポートされた JSON / バイナリ構造を検証すると、人型アバターモデルに必要な情報がしっかり保持されています。

  • ボーン階層(Node Hierarchy): J_Bip_C_HipsJ_Bip_C_Chest などのボーン構造が正確な親子関係でエクスポートされる。
  • 正規化データ(VRM Rig): VRMHumanoidRigNormalized_J_Bip_... 等のノード構造も正しく維持。
  • モーフターゲット(Morph Targets): Fcl_ALL_FunFcl_EYE_Close などのフェイスアニメーション用キーが正しく出力データに含まれる。

3. ブラウザでのダウンロード処理の実装

生成された ArrayBufferBlob に変換し、アンカータグ(<a>)を動的に作成してローカルに書き出します。

export function downloadBuffer(buffer: ArrayBuffer, fileName: string = 'model.glb') {
  const blob = new Blob([buffer], { type: 'application/octet-stream' });
  const url = URL.createObjectURL(blob);

  const link = document.createElement('a');
  link.href = url;
  link.download = fileName;
  document.body.appendChild(link);
  link.click();

  document.body.removeChild(link);
  URL.revokeObjectURL(url);
}

4. Astro コンポーネントへの組み込み例

Astro コンポーネント内では、クライアントサイドで動的にモジュールを呼び出すコンポーネント(例: React, Svelte, または vanilla JS)として配置します。

---
// src/components/GlbExporter.astro
---

<div class="exporter-wrapper">
  <button id="export-btn" class="px-4 py-2 bg-blue-600 text-white rounded">
    GLBモデルを出力する
  </button>
</div>

<script>
  import { exportSceneToGLB, downloadBuffer } from '../utils/gltfExporter';

  const btn = document.getElementById('export-btn');
  btn?.addEventListener('click', async () => {
    try {
      // 読み込み済みの 3D オブジェクト(例: window.currentScene や Three.js 側で管理しているモデル)
      const scene = (window as any).myThreeScene;
      if (!scene) return;

      const glbBuffer = await exportSceneToGLB(scene);
      downloadBuffer(glbBuffer, 'exported_avatar.glb');
      console.log('エクスポート成功!');
    } catch (err) {
      console.error('エクスポートエラー:', err);
    }
  });
</script>

動作確認と結果

  • 動作環境: ブラウザ(Chrome / Firefox / Edge)
  • Three.js バージョン: r183
  • 結果:
  • メモリ上で直接生成された .glb ファイルの復元度が非常に高く、Blender や Three.js Viewer で再読み込みした際にもボーンアニメーションや表情モーフがそのまま利用可能であることを確認しました。

まとめと今後の展望

前回実装した WebPテクスチャ変換&Draco圧縮(#103)と組み合わせることで、「モデルの読み込み ➔ ブラウザ上での編集・変換 ➔ 最適化&再エクスポート」 という一連の3D処理パイプラインを完全にブラウザ完結で提供できるようになります。

Web3Dの進歩により、重い3Dパイプライン処理をサーバーに任せることなくクライアント側で処理できる範囲がどんどん広がっています。