[JavaScript] 3D ROOM制作 3日目 ポータル通路・Export・VRMフィギュア配置 (Three.js)

[JavaScript] 3D ROOM制作 3日目 ポータル通路・Export・VRMフィギュア配置 (Three.js)

概要

3Dウォークスルー空間「WIRED ROOM」の開発が大きく進んだ一日。 バグ修正から始まり、VRMフィギュア配置、公開用エクスポート、プロシージャル通路生成、そしてポータルによる部屋間移動まで。 ビルドツールなし、vanilla JS + Three.js + WebXR という構成で、個人サイトに置くだけで動く3D空間が形になった。

GitHubリポジトリも公開


Questブラウザのカクツキ問題 ― 根本原因の特定

朝一番のバグ報告。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作成