[Astro #104] Three.js GLTFExporterによるブラウザ完結型GLB/GLTFエクスポートの実装
はじめに
前回の記事 [[Astro #103] ブラウザ完結型GLBコンプレッサーの実装 — WebPテクスチャ変換とDraco圧縮の統合] では、WebPテクスチャ変換とDraco圧縮を組み合わせたブラウザ完結型の最適化パイプラインを実装しました。
[Astro #103] ブラウザ完結型GLBコンプレッサーの実装 — WebPテクスチャ変換とDraco圧縮の統合 // PROTOCOL.LAIN
ブラウザ上で動作するGLB圧縮ツールの実装解説。gltf-transformを活用したDracoジオメトリ圧縮と、Canvas APIを用いたテクスチャのWebP化を組み合わせたファイルサイズ削減手法、およびAstroへの統合手順について詳解します。
lain-lab.com今回は、 Three.js (r183) の GLTFExporter を用いたブラウザ完結型GLB/GLTFエクスポート機能の組み込みについてまとめます。
VRChatやVRM規格で使用されるような複雑なボーン構造(J_Bip_C_Hips, J_Bip_C_Chest など)や表情用モーフターゲット(Fcl_ALL_Fun, Fcl_EYE_Close など)を維持したまま、サーバーレスで軽量かつ安全に書き出す実装を実現しています。
開発の目的と課題
目的
- 完全クライアントサイド動作: サーバーに3Dモデルやアバターデータをアップロードさせることなく、ブラウザのメモリ内だけでエクスポートを完了する。
- 構造の維持: アバターモデル特有のヒップ・胸・頭部などのボーン階層構造、およびフェイス(表情)の BlendShape / Morph Targets を欠落させずに保持する。
- 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_HipsやJ_Bip_C_Chestなどのボーン構造が正確な親子関係でエクスポートされる。 - 正規化データ(VRM Rig):
VRMHumanoidRigやNormalized_J_Bip_...等のノード構造も正しく維持。 - モーフターゲット(Morph Targets):
Fcl_ALL_FunやFcl_EYE_Closeなどのフェイスアニメーション用キーが正しく出力データに含まれる。
3. ブラウザでのダウンロード処理の実装
生成された ArrayBuffer を Blob に変換し、アンカータグ(<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パイプライン処理をサーバーに任せることなくクライアント側で処理できる範囲がどんどん広がっています。