[JavaScript] フォルダ置くだけで動く3D ROOM機能を作る(Three.js)

[JavaScript] フォルダ置くだけで動く3D ROOM機能を作る(Three.js)

コンセプト

個人サイトに3Dの「部屋」を持つ。

ZIPを解凍して/public/room/に置くだけで、誰のサーバーでも動く。ビルドツールなし、npmなし、フレームワークなし。index.html、app.js、style.css、room.jsonの4ファイルで完結する3Dウォークスルー空間。

部屋のモデルはSketchfabからCC-BYのGLBをダウンロードして差し替えるだけ。VRMアバターを置いて歩かせる。将来的にはポータル(ドア)を通じて他のサイトのROOMと繋げる。空間的なハイパーリンク。

スクリーンショット:

[JavaScript] フォルダ置くだけで動く3D ROOM機能を作る(Three.js) [JavaScript] フォルダ置くだけで動く3D ROOM機能を作る(Three.js)

動画:

[JavaScript] フォルダ置くだけで動く3D ROOM機能を作る(Three.js)

構成

/public/room/
  index.html          ← importmapでCDNからThree.js読み込み
  app.js              ← ルームエンジン全部入り
  style.css           ← UI
  room.json            ← 部屋の定義(データ駆動)
  assets/
    room/
      models.json      ← 部屋テンプレートカタログ
      room_blank.glb
    avatar/
      default.vrm
      idle.vrma
      walk.vrma

importmapでCDNから全ライブラリを引っ張る。ローカルにインストールするものはゼロ。

<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/[email protected]/build/three.module.js",
    "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/",
    "three-mesh-bvh": "https://unpkg.com/[email protected]/build/index.module.js",
    "@pixiv/three-vrm": "https://unpkg.com/@pixiv/[email protected]/lib/three-vrm.module.min.js",
    "@pixiv/three-vrm-animation": "https://unpkg.com/@pixiv/[email protected]/lib/three-vrm-animation.module.min.js"
  }
}
</script>

room.json:データ駆動の部屋定義

部屋の全情報をJSONに外出し。GLBパス、スポーン位置、アバター設定、ポータル先URL、スカイボックスまで全部ここに書く。

{
  "name": "Lain's Room",
  "spawn": [0, 0, 3],
  "room": {
    "glb": "./assets/room/room_blank.glb",
    "scale": 1,
    "pos": [0, 0, 2],
    "collision": true
  },
  "avatar": {
    "vrm": "./assets/avatar/default.vrm",
    "scale": 1,
    "animations": {
      "idle": "./assets/avatar/idle.vrma",
      "walk": "./assets/avatar/walk.vrma"
    }
  },
  "portals": [
    { "pos": [4.9, 1, 0], "url": "https://someone.com/room/", "label": "Someone's Room" }
  ],
  "sky": {
    "type": "gradient",
    "topColor": "#0a0a2e",
    "midColor": "#1a1a4e",
    "stars": true,
    "moon": true
  }
}

room.jsonがなければプリミティブ(box/sphere)で部屋を自動生成するフォールバック付き。

GLBの部屋モデル読み込み

SketchfabのCC-BYモデルをダウンロードしてassetsに置くだけ。GLTFLoaderにDRACOLoaderを登録して読み込む。

ポイントは、GLBを読んだ後にBVHを構築すること。

import { MeshBVH, acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh';

// Three.jsにBVHパッチを当てる
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
THREE.Mesh.prototype.raycast = acceleratedRaycast;

// GLBロード時
model.traverse((child) => {
  if (child.isMesh && child.geometry) {
    child.geometry.computeBoundsTree();  // BVH構築
    colliderMeshes.push(child);
  }
});

これでレイキャストが高速化される。家具が多いGLBでも移動が重くならない。

衝突判定:レイキャスター方式

壁の衝突判定は8方向レイキャスト。腰と膝の2段で計16本のレイを飛ばして、PLAYER_RADIUS以内にメッシュがあれば移動をブロック。

加えて、床なし落下防止。移動先の真下にレイを飛ばして、床メッシュに当たらなければ進めない。窓から落ちたりしない。

// 床チェック
_floorRaycaster.set(
  new THREE.Vector3(newPos.x, newPos.y, newPos.z),
  new THREE.Vector3(0, -1, 0)  // 真下
);
const floorHits = _floorRaycaster.intersectObjects(colliderMeshes, false);
if (floorHits.length === 0) {
  return true; // 床がない = 進めない
}

VRMアバターの表示と歩行

@pixiv/three-vrmでVRMを読み込み、@pixiv/three-vrm-animationで.vrmaファイルからアニメーションクリップを生成。WASD入力の有無でidlewalkをクロスフェード切り替え。

ADV(PROTOCOL.LAIN)のR3F実装と同じロジックをvanilla Three.jsに移植した。

function playAvatarAnim(animId, fadeDuration = 0.3) {
  const clip = vrmAnims[animId];
  const newAction = playerMixer.clipAction(clip);

  if (currentAction && currentAction !== newAction) {
    currentAction.fadeOut(fadeDuration);
    newAction.fadeIn(fadeDuration).play();
  } else {
    playerMixer.stopAllAction();
    newAction.fadeIn(fadeDuration).play();
  }
  currentAction = newAction;
}

3人称カメラ

カメラはアバターの後方上空を周回する。マウスでカメラを回してもアバターは動かない。WASDで移動した時だけ、移動方向にアバターが向きを変える。

// アバターの向きはカメラから独立
const targetYaw = Math.atan2(moveDir.x, moveDir.z) + Math.PI;
avatarYaw += shortestAngleDiff * 0.15;  // 滑らかに回転

// カメラはyaw角でアバターの周囲を回る
const camX = playerPos.x + Math.sin(yaw) * CAM_DISTANCE;
const camZ = playerPos.z + Math.cos(yaw) * CAM_DISTANCE;
camera.position.lerp(desiredPos, CAM_LERP);
camera.lookAt(avatarTarget);

PBRマテリアルの黒潰れ対策

Sketchfabで綺麗に見えるモデルが、ローカルだと鏡やフレームが真っ黒になる。原因はHDRI環境マップがないこと。metalness が高い面は環境マップがないと何も反射しない。

対策はシンプルで、ロード時にmetalnessに上限をかける。

if (child.material.metalness > 0.5) {
  child.material.metalness = 0.3;
  child.material.roughness = Math.max(child.material.roughness, 0.4);
}

ポータル:空間的なハイパーリンク

部屋の中にポータル(緑のゲート)を置いて、他のURLに遷移できる。room.jsonにURLを書くだけ。近づくと[E] Someone's Roomと表示されて、Eキーで遷移。

HTMLのaタグと同じ。サーバー不要。各自が自分のサーバーに部屋を置いて、ポータルURLを手書きで相互リンク。90年代のウェブリングと同じ仕組み。

サーバーは作らない

荒らし対策の最適解は「荒らす場所を作らないこと」。

各ユーザーが自分のサーバーに自分の部屋を置く。中央サーバーは存在しない。カスタム結果はroom.json + GLBとしてエクスポートして再アップロード。localStorageは編集中の一時保存だけ。訪問者が見る部屋は全部静的ファイル。

「ZIPを解凍してアップするだけ」の敷居は、荒らしにとっては面倒すぎて、本当に部屋を作りたい人にとってはちょうどいい。

今後

  • 部屋テンプレート選択UI(models.jsonからサムネイル一覧)
  • 初回セットアップ(アバター選択、名前入力)
  • ポータル遷移の実装テスト
  • WebXR対応(Quest 3でMR表示)