← ブログ一覧に戻る
MIRAIS3DReact Three FiberWebGL

MIRAIS 設計・実装記録 #4|3D バーチャル会場 — 見取り図から歩ける空間へ

第 4 回 3D バーチャル会場

通信制学部の新設で生まれた「遠隔地の学生の展示の場をどう確保するか」という課題への解が、3D バーチャル会場でした。実際の会場の見取り図から起こした空間に、その時間帯の発表者のポスターが自動で貼られていく仕組みを扱います。

第 IX 部 3D バーチャル会場


27. 全体像

3D 会場は Vite + React Three Fiber で構築した独立したアプリケーションで、ビルド成果物をポータル側に配置し、iframe で埋め込んでいます。

なぜ独立アプリにしたのか。

  1. React のバージョンが異なる(ポータル本体と 3D 側)。3D エコシステムの最新 React 対応が当時未成熟でした。
  2. 3D シーンは重い。iframe で分離することで、シーンの再マウントがポータル本体に波及しません。
  3. 開発サイクルを分離でき、3D 担当が独立して作業できます。

3 番目が実務的には一番効きました。ポータル本体のビルドを待たずに 3D だけを動かせるので、担当者が自分のペースで試行錯誤できます。

ファイル構成(約 3,100 行)

ファイル行数役割
建物ジオメトリ1,175床・壁・ガラス・階段・ステージ・ポスターボード・什器・屋外景観
アプリ統括510シーン統括、状態管理、UI オーバーレイ、PDF 自動割り当て
人型モデル331他プレイヤーの描画
見取り図データ313実測図から抽出した座標データ
テクスチャ生成213Canvas による手続き的テクスチャ
マルチプレイ148プレイヤー位置の同期
アバター114自分のアバターと移動制御
ポスター割り当て104提出物 → ボード面のマッピング
その他約 200API クライアント、PDF 変換、セッション復元

28. 見取り図から 3D 空間へ

28-1. 座標系の変換

実際の会場の見取り図から座標を抽出し、3D 空間へマッピングしています。

図面空間: 建物の長辺方向を 1,080 単位として正規化
図面空間: 上下方向を 3D 空間の奥行き軸に対応させる
1F と 2F は図面上で縦に並んでいるため、2F は原点をずらして読み込む

注目すべき判断:建物の平面を実寸の 2 倍にスケールし、高さは等倍のままにしました。実寸だとアバターで歩き回るには狭すぎて窮屈だったためです。3D 空間は物理的な正確さより、回遊体験の質を優先しています。

これは意図的な「嘘」です。ただし、ブースの相対的な位置関係は正確に保たれているので、「どのエリアにどの研究があるか」という本来の目的は損なわれません。何を正確に保ち、何を犠牲にするかを決めるのが設計だと考えています。

28-2. 高さの設定

1F 有効天井高      6.0 m
1F 天井 ↔ 2F 床    1.0 m(構造ゾーン)
2F 床レベル         7.0 m
ポスターボード      高さ 4.5 m / 幅 4.5÷√2 ≈ 3.18 m(A 判の縦横比)

ポスターボードの縦横比を A 判に合わせているのは、実際に貼られるポスターが歪まないようにするためです。

28-3. カテゴリ分類

18 種類のカテゴリで要素を分類し、それぞれ異なるマテリアル・コリジョン・描画を適用しています。

床 / 立入禁止 / 入口 / ステージ / キッチン / 教室 /
階段 / エレベータ / エレベータホール / 吹抜 / 廊下 /
スタッフルーム / 実験室 / 壁 / ガラス壁 / 自動ドア /
ガラス手すり / 椅子 / メイン階段

カテゴリごとにレンダラを実装しました。床タイル、壁(ガラスの透過を含む)、ステージ、ポスターボード、モニター、エレベータ筐体、折り返し階段、宙吊りのメイン階段、傾斜コライダー、階間の構造帯、街路樹や屋外テラスといった景観まで含みます。


29. ポスターの自動割り当て

会場に並ぶ 36 のポスター面に、その時間帯の発表者のポスターを自動で貼り付ける仕組みです。

① 全提出物を取得(認証必須)
② 「現在の部」を取得(3 秒ごとにポーリング)
③ 現在の部に該当する提出物だけを抽出
④ ブース番号を 1〜36 の位置番号へ正規化
⑤ 位置番号からボード面を逆引き
⑥ ポスター PDF を認証付きで取得
⑦ 1 ページ目をレンダリングしてテクスチャ化
⑧ 該当するボード面のマテリアルに適用

29-1. 現実のデータの揺れを吸収する

実データを扱ううえで最も手間がかかったのがここでした。

発表の部の表記ゆれ:  "1" / "2" / "3" / "part_1" / "part_2" / "part_3" / 数値
ブース番号の表記ゆれ: "1-27"(部-位置の複合)→ 27
                     "27"(位置のみ)        → 27
                     "01-05"(ゼロ埋め)     → 5
                     " 27 "(空白付き)      → 27
                     範囲外の値              → 無効として除外

CSV から取り込んだ実データの表記ゆれを 3D 側で吸収する設計にしました。現実のデータは綺麗ではないという前提に立っています。バックエンド側で正規化する案もありましたが、過去データを書き換えると当時の記録が失われるため、読み取り側で吸収する方を選びました。

29-2. 日本語 PDF の描画

日本語 PDF の描画には、文字集合マップと標準フォントデータが必要です。これらを CDN から参照する設定を入れました。日本語のポスターが文字化けせずに 3D 空間の壁面に表示されるのは、この設定によるものです。

レンダリング時は白背景で塗りつぶしてから描画し(透過ピクセルをなくす)、解像度と異方性フィルタリングを引き上げて品質を確保しています。ポスターは近づいて読むものなので、文字が潰れると意味がありません。


30. マルチプレイと会場体験

30-1. ルームの分離

未ログイン → 匿名ルーム(他の参加者は見えない)
ログイン済 → イベントごとの認証済ルーム

未ログインを匿名ルームに隔離することで、「未ログインを 3D 会場から締め出す」という設計要件を実装上満たしています。ページ自体は開けますが、他の参加者もポスターも見えません。

30-2. 複数タブ問題

マルチプレイ実装で最も時間を溶かした問題を記録しておきます。

同期ライブラリは内部でプレイヤー ID をブラウザのセッションストレージに永続化します。ところが、

  • タブを複製するとセッションストレージがコピーされる
  • 一部のブラウザや拡張機能ではセッションストレージが共有される

結果、複数のタブが同じプレイヤー ID でルームに入り、サーバー側で「同一プレイヤーの再接続」と判定されて前のセッションが切断されます。デモ中に 2 つのタブを開くと片方が落ちる、という現象で発覚しました。

ライブラリの読み込み後・初期化前に、内部ストレージをメモリ上のものに切り替えるフラグを立てることで、タブごとに独立した ID が発行されるようにして解決しています。

30-3. iframe 埋め込みの落とし穴

ポータルから 3D 会場へは、URL クエリで認証情報を受け渡しています。ここでも一つ問題がありました。

iframe の src はブラウザ側でしか確定できません。サーバーレンダリングとクライアントレンダリングで src が変わると、React が iframe を再マウントします。すると同期用の接続が張り直され、複数タブの同期が破綻します

マウント完了まで iframe を描画せず、要素のキーを固定することで回避しました。3D とマルチプレイが絡むと、フレームワークの当たり前の挙動が問題になる——という良い教訓でした。


31. 会場の機能一覧

機能説明
歩行モード / 飛行モード物理コリジョンの有無を切り替え
ズーム制御視野角を段階的に調整
リスポーン初期位置へ復帰
ポインタロックマウスによる視点操作
ポスター自動割り当て現在の部に応じて 36 面へ自動マッピング。進捗を表示
手動 PDF 割り当て任意の PDF を任意の面に貼る
画面共有共有した画面を会場内のモニターに表示
スクリーンショットシーンのキャプチャ
部の切替運営ロールのみ操作可能
部のポーリング3 秒ごとに確認し、変化があれば全ポスターを張り替え

当日の運用イメージ:運営が「今は 2 部です」と設定すると、3 秒以内に会場内の全ポスターが 2 部の発表者のものへ自動で切り替わります。現地の入れ替えと 3D 空間の入れ替えが同期する——これが、3D 会場を「作ってみた」で終わらせないために必要な最後のピースでした。

スライド資料

MIRAIS 発表スライド / 「コア機能デモ①」がこの回に対応しますタップしてPDFを開く