はじめに
React、Three.js、React Three Fiberを使い、インストールやアカウント登録なしで動くブラウザ完結のボクセルエディタを個人開発しました。
この記事では、ブラウザ上で多数の立方体を編集し、複数の3D形式やGIFとして出力するために採った構成を紹介します。
技術構成
- React 18
- TypeScript
- Three.js
- React Three Fiber / Drei
- Vite + Vike
- gif.js
- three-stdlib
画面はReactコンポーネント、3DシーンはReact Three Fiber、モデル出力はブラウザ側のBlob生成で完結させています。
1ボクセル1メッシュにしない
ボクセルを素直に1個ずつMeshとして描画すると、ボクセル数に比例してドローコールやReact側の管理対象が増えます。そこで同じGeometryとMaterialを共有できる部分は THREE.InstancedMesh で描画しています。
各インスタンスには位置・回転・スケールを行列として設定します。編集時の当たり判定では、交差したInstancedMeshとインスタンスIDから対象ボクセルを特定します。
この方法ではボクセルデータを配列として扱いつつ、描画側はまとめられます。一方、色ごとにMaterialを分ける場合は色数に応じたグルーピングが必要です。
編集データと描画データを分ける
編集状態は次のような最小単位で保持します。
```ts
type VoxelData = {
position: [number, number, number]
color: string
layerId?: string
}
```
Reactの状態をそのままThree.jsオブジェクトとして保存せず、シリアライズ可能なデータを正としました。これにより以下が楽になります。
- JSONへの保存
- undo/redo用スナップショット
- ローカルストレージへのプロジェクト保存
- 別形式へのエクスポート
- ギャラリーや共有機能への受け渡し
エクスポーターは純粋関数に寄せる
VOX、Minecraft Schematic、Roblox RBXLなどは、ボクセル配列を受け取りBlobを返す関数として分離しています。
```ts
const blob = exportToVOX(voxels)
const url = URL.createObjectURL(blob)
```
GLB出力では一時的なThree.jsシーンとInstancedMeshを組み立て、three-stdlib の GLTFExporter に渡します。UIイベントとバイナリ生成を分けることで、出力処理だけを検証しやすくなりました。
GIF生成をワーカーへ逃がす
アニメーションや制作過程のGIF化にはgif.jsを使っています。エンコードをメインスレッドで実行すると編集UIが止まりやすいため、専用Workerを読み込ませます。
ViteではWorkerのURL解決をビルドに任せるため、次のような小さなモジュールを挟んでいます。
```ts
import workerUrl from 'gif.js/dist/gif.worker.js?url'
export default workerUrl
```
各フレームでCanvasをキャプチャしてWorkerへ渡し、進捗をUIへ反映します。フレーム数、解像度、FPSを上げるほどメモリ使用量も増えるため、出力設定には上限が必要です。
ローカル保存の割り切り
アカウントなしですぐ使えることを優先し、プロジェクトはlocalStorageへ保存しています。この構成はサーバー実装なしで始められる一方、ブラウザデータの削除や容量制限に弱いです。
そのため、重要な作品にはJSONのダウンロードを案内しています。将来データ量が増えた場合はIndexedDBへの移行も候補です。
SSGと多言語SEO
編集画面だけでなく、英語・日本語・中国語・スペイン語の説明ページをVikeでプリレンダリングしています。各ページにはcanonical、hreflang、OGP、JSON-LDを設定し、編集UIに入る前でも検索エンジンが内容を理解できる構成にしています。
まとめ
ブラウザ3Dエディタでは、編集データをシリアライズ可能な形に保ち、描画をInstancedMeshへまとめ、重いエンコードをWorkerへ分離すると機能を追加しやすくなります。
同じ構成を検討している方の参考になれば幸いです。実装例は以下で操作できます。