[JavaScript] 3D ROOM制作 3日目 ポータル通路・Export・VRMフィギュア配置 (Three.js)
概要
3Dウォークスルー空間「WIRED ROOM」の開発が大きく進んだ一日。 バグ修正から始まり、VRMフィギュア配置、公開用エクスポート、プロシージャル通路生成、そしてポータルによる部屋間移動まで。 ビルドツールなし、vanilla JS + Three.js + WebXR という構成で、個人サイトに置くだけで動く3D空間が形になった。
GitHubリポジトリも公開
GitHub - fixtan/WIRED_ROOM: A 3D walkable room you can deploy on any web server. No build tools. Vanilla JS + Three.js + WebXR.
A 3D walkable room you can deploy on any web server. No build tools. Vanilla JS + Three.js + WebXR. - fixtan/WIRED_ROOM
github.comQuestブラウザのカクツキ問題 ― 根本原因の特定
朝一番のバグ報告。ROOMモードに入ると3〜4秒おきに数フレームのフリーズが発生する。
最初はVR描画ループ内の毎フレーム new THREE.Vector3() によるGCプレッシャーを疑った。
controls.js、vr.jsの全アロケーションをモジュールレベルのプリアロケートに変更。
しかし直らない。
調査を進めると、症状に法則があった。
- PCでは発生しない
- Questブラウザでのみ発生
- VR没入モードに入ると消える
/room/index.htmlに直接アクセスすると消える- トップページからiframe経由で開いた時だけ発生
原因は 2つのWebGLコンテキストの競合 だった。
トップページにはReact Three Fiber(R3F)のCanvasが常駐していて、ROOMボタンを押すと avatar.style.display = 'none' でDOMを非表示にするだけ。Canvasのレンダーループは止まらない。iframe内のROOMが持つThree.jsレンダラーと合わせて、Questの限られたGPUリソースで2つのWebGLコンテキストが同時に回る。
修正は一行。ROOMモード突入時に scene-changed イベントを発火して、R3FのCanvasをアンマウントさせた。
window.dispatchEvent(new CustomEvent('scene-changed', { detail: { index: -1 } }));
コード単体では見えないバグ。ページ全体のアーキテクチャを理解していないと辿り着けない。
VRMフィギュア配置 ― 自分の分身をフィギュア化
右クリックメニューに「Add My Avatar」を追加。 現在選択中のVRMアバターを、ポーズ付きのフィギュアとして部屋に配置できる。
実装パターンは画像・動画の配置と同じ。media.jsに placeFigurine() を追加し、MEDIA ADJUSTパネルにポーズ切り替えUI(◀ ▶ + ボタン)を組み込んだ。
ポーズデータは .vrma ファイル。25個のプリセットに加えて、カスタムポーズのアップロードにも対応。IndexedDBに pose_custom_* キーで保存し、起動時に自動復元する。
VRM展示スペースとしても機能する。ただし静的ファイル配信である以上、VRMデータのダウンロードは防げない。公開時にVRMを含めるかどうかはexportの設計で制御する。
フィギュアは1体制限にした。Quest 2で4体表示するとクラッシュするため。
Export ZIP ― 公開用データの出力
右クリックメニューから「Export ZIP」を選ぶと、部屋の公開データがZIPでダウンロードされる。
public/
├── manifest.json ← 部屋の全定義
├── room.glb ← 部屋モデル
├── idle.vrma ← アバターアニメーション
├── walk.vrma
├── media_123456.webp ← 配置画像
├── media_123457.mp4 ← 配置動画
├── default.vrm ← フィギュアVRM(配置時のみ)
└── pose.vrma ← 使用ポーズ(配置時のみ)
ランタイム(JS/HTML/CSS)は含めない。開発プロジェクトの public/ フォルダにZIPを解凍するだけで公開モードになる。
manifest.jsonには部屋設定、アバター参照、メディア配置(座標・回転・スケール)、ポータル定義が全て入る。JSZipをCDNからオンデマンドロードするため、importmapの変更も不要。
画像の拡張子判定バグも修正した。元ファイルがPNGの場合、export時に正しく .png として出力し、manifest.jsonにも正しいファイル名を記録する。
公開/編集モード ― manifest.jsonの有無で自動判定
app.js の起動時に ./public/manifest.json をfetchする。存在すれば公開モード、なければ編集モード。
起動 → fetch('./public/manifest.json')
→ あり → 公開モード(ウィザード・エディタ・右クリックメニュー無効)
→ なし → 編集モード(従来通り)
/ キーで公開/編集モードを切り替え可能。ヘッダーの部屋名に [PUBLIC] / [EDIT] が表示される。
公開モードではメディアをmanifestから直接読み込む。IndexedDBを一切使わないため、異なるドメインへのデプロイでも問題ない。
プロシージャル十字路 ― コリドー生成
ポータルを踏むと、部屋が非表示になり、プロシージャル生成された十字路の通路空間に遷移する。
WIRED(Serial Experiments Lain)の美学に合わせた空間設計。
- ダークな壁面にグリッドテクスチャ(Canvas生成)
- 通路の四隅にグローストリップ(AdditiveBlending)
- 中央ハブにワイヤーフレームの正二十面体 + 軌道リング
- 各通路の色分け(北=シアン、東=ブルー、西=パープル、南=レッド)
- PointLightによる雰囲気照明
壁の衝突判定はBox3コライダー。床はy=0のフラットなので既存の重力処理がそのまま使える。
ポータル移動 ― portal_list.jsonから行き先をランダム抽出
コリドーに入ると、GitHubに置いた portal_list.json をfetchする。
[
{ "name": "lain's lab", "url": "https://lain-lab.com/room/", "description": "Three.js game & art gallery" },
{ "name": "alice's room", "url": "https://alice.example.com/room/", "description": "pixel art gallery" }
]
自分自身のURLをフィルタし、残りからランダムに3件を抽出。十字路の北・東・西の行き止まりにWavyRingエフェクト付きのワープポイントとして配置する。南は「RETURN」で自分の部屋に戻る。
raw.githubusercontent.com はCORSヘッダー付きで返すため、どのドメインからでもfetch可能。サーバー不要。
これは90年代〜00年代の個人サイトリンク集の3D版だ。
GitHubリポジトリ公開
https://github.com/fixtan/WIRED_ROOM
MIT License。アセット(GLB、VRM、VRMA)は .gitignore で除外し、JS/HTML/CSSのみをバージョン管理する。
メディア保存バグとの戦い
今日最も苦しんだのはメディアデータの保存・復元バグだった。
画像、動画、フィギュア、クレジットボード、ポータルを配置してSave → リロード → 消える。原因は createCreditBoard() 内の saveMediaList() が、画像の非同期ロード完了前に不完全なリストで上書き保存していたこと。
修正として isLoadingMedia フラグを導入。loadSavedMedia() の実行中は saveMediaList() を完全にブロックする。
let isLoadingMedia = false;
function saveMediaList() {
if (isLoadingMedia) return;
// ...
}
個別の関数にロード中パラメータを渡す方法では対処しきれなかった。根本的なガードが必要だった。
ファイル構成
room/
├── index.html
├── app.js
├── style.css
├── portal_list.json
├── roadmap.md
├── js/
│ ├── avatar.js
│ ├── controls.js
│ ├── corridor.js ← NEW
│ ├── editor.js
│ ├── media.js
│ ├── menu.js
│ ├── room-loader.js
│ ├── setup.js
│ └── vr.js
└── assets/
├── room/
├── avatar/
└── pose/ ← NEW(25プリセット)
設計原則
- サーバーは作らない。静的ファイルで完結。
- 荒らす場所を作らない。各自が自分のサーバーに自分の部屋を持つ。
- ビルドツール不要。importmap + CDN。
- manifest.jsonがすべての定義。データ駆動。
- 段階的に機能追加。動くものを先に出す。
次にやること
- portal_list.json のURL修正とデプロイテスト
- コリドーのエフェクト修正(公開モード対応)
- 通路の視覚的改善(照明強化)
- コリドー内でのアバター表示
- VRモードでのポータル操作
- README.md作成