[JavaScript] 3D ROOM制作 2日目(午後) VR対応・エディタ・ROOM追加機能 (Three.js)
はじめに
午前中の続き。
VR対応と、オリジナルルームロート及び、エディタ、サムネ撮影など。
VR対応
WebXRのVRモードを実装。XRButtonでVRセッション開始、Quest 3のコントローラで部屋を歩き回れる。
移動はADVゲーム(PROTOCOL.LAIN)と同じ方式。ヘッドセットが向いている方向を基準に、左スティック上でまっすぐ前進する。renderer.xr.getCamera().getWorldDirection()で向きを取得して、そこからforward/rightベクトルを生成。
コントローラはcameraGroupの子として配置。グループを動かせばカメラもコントローラも一緒に追従する。衝突判定はPC版のcheckCollisionをそのまま共有。
VRはあくまで「閲覧・体験」専用。カスタムやエディタ操作はPC側に限定した。VR用のUI実装(3D空間内メニュー、レイキャスト判定)は一人開発では割に合わないので、役割を分けた。
// ヘッドセットの向きから移動方向を計算
const camera = S.renderer.xr.getCamera();
camera.getWorldDirection(vrCameraDirection);
const facingAngle = Math.atan2(vrCameraDirection.x, vrCameraDirection.z);
const camForward = new THREE.Vector3(0, 0, -1)
.applyEuler(new THREE.Euler(0, facingAngle, 0));
レンダーループもrequestAnimationFrameからsetAnimationLoopに切り替え。WebXRではこれが必須。
VRカメラの距離問題で試行錯誤した話
最初はアバターの後ろにカメラを置こうとしたが、vrCamDirベースのオフセットだとヘッドセットを回すたびに旋回する問題が発生。avatarYawベースに変えてもスティック入力時にワープする。ADVControllerではcamera.parent.rotation.yで解決していたが、vanilla WebXRだとヘッドトラッキングと衝突する。結局cameraGroup.position = playerPosの直置きに落ち着いた。VRは物理的に後ろに下がれるので問題ない。
コントローラの軸が毎回逆になる問題。WebXRのaxes配列はaxes[2]/axes[3]だが、符号がQuestのスティック方向と逆。-gp.axes[2]で反転する必要があった。修正するたびにコードが上書きされて再発したので、最終的にコメントで「DO NOT change these signs」と記録した。
エディタセクションに追加:
S.roomModelの参照を保持する設計判断。room-loader.jsでGLBをscene.addした後にS.roomModel = modelで保持。これがないとスライダーからどのオブジェクトを操作するか特定できない。
Copy JSONの用途。スライダーで調整した値をmodels.jsonに手動で貼る想定。models.jsonはstaticファイルだからJSから書き換えられない。「コピペで済む」ワークフローにした。
データ設計セクションに追加:
localStorageリセット時にRoom Infoが消える問題に1時間ハマった。原因はウィザードでカスタムROOMを選んだ時にcustomGLBKeyがconfigに入らなかったこと。selectedRoom.isCustomフラグを見てconfig.isCustomGLB = trueを設定する修正で解決。IndexedDBのメタキーがmeta_custom_room_xxxとmeta_room_xxxの2系統あるのもこれが理由。
右クリックメニュー
Tab キーまたは右クリックで表示するコンテキストメニュー。PointerLock中はTabで開き、閉じるとPointerLockを再取得。addMenuItem()で外部から項目を追加できる設計。
現在のメニュー項目:Room Info、Reset Settings、Fullscreen、Take Thumbnail、Add Custom Room、Adjust Room。
カスタムROOMアップロード
メニューから「Add Custom Room」でGLBファイルを選択すると、info入力フォームが表示される。
Name(ファイル名から自動入力)、Author、Description、License(セレクト)、Source URL、Credit List。stats(triangles/vertices/fileSize)はGLBを一時ロードして自動取得。
// GLBから自動でポリゴン数を取得
model.traverse((child) => {
if (child.isMesh && child.geometry) {
const geo = child.geometry;
if (geo.index) {
triangles += Math.floor(geo.index.count / 3);
} else {
triangles += Math.floor(geo.attributes.position.count / 3);
}
vertices += geo.attributes.position.count;
}
});
GLBデータはIndexedDB(Dexie)にArrayBufferとして保存、メタデータは別キーで保存。ファイル名ベースのキーで重複防止。
ROOM調整スライダー
メニューから「Adjust Room」で右サイドにスライダーパネルを表示。Scale、Position XYZ、Rotation Y、Spawn XZをリアルタイムで変更できる。S.roomModelへの直接操作で即座に反映。
「Save」でlocalStorageに保存、「Copy JSON」でmodels.jsonに貼れる形式でクリップボードにコピー。新しいGLBを追加する時のワークフローが「アップロード → スライダーで調整 → Copy JSON → models.jsonに貼る」で完結する。
Room Info編集
メニューの「Room Info」でいつでもクレジット情報を編集・保存できる。データはlocalStorageとIndexedDBの両方に保存。localStorageはリセットで消えるが、IndexedDBは永続。リセット後にROOMを選び直しても、Room Infoの入力内容はIndexedDBから復元される。
Credit Listフィールドを用意して、部屋と家具のクレジット情報を改行区切りで管理。ライセンス違反防止のための基盤。
サムネ撮影
メニューの「Take Thumbnail」で現在のカメラアングルをCanvas撮影してIndexedDBに保存。ウィザードのROOM選択画面でカスタムROOMのサムネとして表示される。
S.renderer.render(S.scene, S.camera);
const dataUrl = S.renderer.domElement.toDataURL('image/webp', 0.8);
データ設計
永続(IndexedDB) リセット可能(localStorage)
──────────────── ──────────────────────
GLBデータ 選択中のROOM/アバター/スカイ
Room Infoメタデータ スライダー調整値
カスタムVRM ランタイム設定
サムネイル
今後
家具配置(GLB追加・移動・回転)とメディア配置(画像・動画を壁に貼る)。「展示空間」としてのコア機能。BOOTHリンクなどの外部URLを作品に紐付けて、近づいたら表示する仕組み。