[JavaScript] Three.jsでミニマップ実装:サブカメラ+RenderTarget+カーナビ式スムーズ回転

はじめに

どうもUnityをやる気が起きないので、気晴らしにThree.jsのコードをリファクタしてたら、左上のミニマップをカーナビのように進行方向に回転する処理を実装してみたので、リハビリメモです。

やっぱり、JavaScriptはコードの全体像が見えるので、こっちの方が実装が楽ですね…。

動かない、挙動がおかしいはすべて自分の責任なので、管理がしやすいです。

Unityは、システム自体がおかしくなると、自分の責任ではない問題で振り回されるのでそれが疲れますね…。

1. 目的:Three.js で “まともな” ミニマップを作る

Three.js でミニマップらしい UI を作ろうとすると、 「単に小さいカメラを右上にレンダリングする」だけでは全然足りない。

今回の目的は、ゲームでよくある “まともに使えるミニマップ” を作ること。

具体的には 次の 3 点を同時に実現する。


① 別カメラ(俯瞰)でシーンを撮影する

ミニマップ専用のカメラ(= サブカメラ)を上空に置いて、 地形・建物・プレイヤーの位置を俯瞰視点で撮影する。

Unity だと「Minimap Camera」が標準で作れるが、 Three.js では手動で構築する必要がある。


② WebGLRenderTarget へ描画してミニマップ用テクスチャを作る

サブカメラで撮影した内容を WebGLRenderTarget(texture) にレンダリングし、 そのテクスチャを Plane に貼り付けて “画面左上のミニマップ” として表示する。

Three.js には UI レイヤーの概念がないため、 レンダリング結果を自作 UI として貼り付ける必要がある。


③ キャラの進行方向に合わせてミニマップを “ゆっくり回転” させる

普通のミニマップ(GTA / ゼルダ / Apex / Fortnite)は、

  • キャラが向いている方向が “マップの上”
  • 地形がぐるっと回転する
  • 回転は 急に切り替わらずスムーズ

こうした “カーナビ風の回転” が必須。

Three.js は角度管理がシビアなので、以下が必要になる:

  • 角度差を -π〜+π に正規化(atan2)
  • 補間(lerp)でゆっくり追従
  • サブカメラを Z軸回転させて自然なミニマップ回転を実現

これらを組み合わせて 「完全に実用的なミニマップ」 を Three.js で作るのが今回のゴール。

2. ミニマップ用サブカメラの準備

ミニマップの核となるのが 専用の俯瞰カメラ(サブカメラ)。

これはメインカメラとは独立して動作し、 ミニマップに必要な情報(地形・建物・プレイヤー位置)だけを撮影する役割 を持つ。

まず最初に、このサブカメラを作る。


基本コード

config.cameraSub = new THREE.PerspectiveCamera(
  60,   // 視野角(広すぎず狭すぎず)
  1,    // アスペクト比(正方形のミニマップ)
  0.1,  // near
  2000  // far
);

// ミニマップは layer0 だけを見る(不要なUIやエフェクトを除外)
config.cameraSub.layers.set(0);

// 上空50の地点からワールド中心を見下ろす
config.cameraSub.position.set(0, 50, 0);
config.cameraSub.lookAt(0, 0, 0);

補足1:アスペクト比を「1」にする理由

ミニマップは 円形・正方形 UI が基本なので、 RenderTarget と Plane に貼った時に歪まないよう、 アスペクト比 1(正方形) にしておく。

new THREE.PerspectiveCamera(fov, 1, near, far);

これで常に正しい縦横比で描画される。


補足2:layers を分けるメリット

config.cameraSub.layers.set(0);

こうすることで、

  • UI
  • パーティクル
  • VR用Transform
  • デバッグオブジェクト

など、“ミニマップに不要なもの” を表示しないようにできる。

メインカメラは layer0 + layer1、 サブカメラは layer0 のみ という運用が非常に安定する。


補足3:位置は固定でOK(後で追従に変更可能)

config.cameraSub.position.set(0, 50, 0);

これはひとまず 「真上から俯瞰」 の形を作るための初期値。

実際には次のように プレイヤーに追従 させると より “ゲームらしいミニマップ” になる。

config.cameraSub.position.x = config.player.box.position.x;
config.cameraSub.position.z = config.player.box.position.z;

この “追従” は後の章で扱う。


補足4:lookAt は下方向(センター)を見る

config.cameraSub.lookAt(0, 0, 0);

ここが重要で、 ミニマップカメラは Y軸真上から、XZ平面を見ている という前提が崩れると、後の “回転” 処理が狂う。


ここまででやったこと

  • ミニマップ用のカメラを作った
  • レイヤー分離
  • 正方形アスペクト
  • 上空から見下ろす視点を確保
  • のちの回転用に正しい姿勢を準備した

次のステップは RenderTarget を作る。

3. RenderTarget の作成

ミニマップは 画面の一部に小さく貼り付ける UI なので、 メインカメラのレンダリングとは別に、

👉 専用カメラで撮った映像 → テクスチャとして保存 👉 そのテクスチャを Plane に貼って UI として表示する

という流れになる。

Three.js ではこの “別のテクスチャへ描画する仕組み” を WebGLRenderTarget を使って実現する。


RenderTarget の生成

config.miniMapRT = new THREE.WebGLRenderTarget(512, 512);

RenderTarget の役割

1. サブカメラが描いた映像を保存する“キャンバス”

メインカメラ → 画面 サブカメラ → RenderTarget に描画される。

2. ミニマップPlaneのテクスチャになる

この RenderTarget.texture が そのままミニマップの画像データとして使える。

map: config.miniMapRT.texture

サイズ(512×512)について

new THREE.WebGLRenderTarget(512, 512)

これは一般的な “正方形ミニマップ” の解像度。

ゲームでは以下がよく使われる:

  • 256×256(軽量)
  • 512×512(標準)
  • 1024×1024(高精細/負荷大)

あなたのケースでは 512 がちょうど良い。

理由:

  • 上空からの俯瞰(解像度不足でも違和感が出ない)
  • Plane に小さく映すため解像度が暴れない
  • WebXRでも負荷が少ない

注意点:RenderTarget の aspect は「サブカメラのアスペクトに依存」

サブカメラを アスペクト1 にしているので RenderTarget も 正方形 にする必要がある。

もしミニマップを長方形にしたい場合は:

  1. サブカメラ fov / aspect を変更
  2. RenderTarget も同じ比率に合わせる
  3. PlaneGeometry の比率も変える

本記事では「正方形」が最も簡単で扱いやすい。


結果:サブカメラの映像を自由に UI 化できる

この RenderTarget のおかげで、

  • HUDとしてUIカメラに貼る
  • 画面の好きな位置に配置
  • 大きさや透明度を自由に変更
  • 円形ミニマップに加工
  • ゆっくり回転
  • マーカー表示(敵・NPC・アイテム)

など、多くのゲームで必要とされる “本格的 UI ミニマップ” を Three.js でも再現できるようになる。

4. Plane に貼ってミニマップとして表示

サブカメラで RenderTarget に描いた内容は、 まだ画面には表示されていない。

次はこのテクスチャを Plane(板ポリゴン)に貼り付けて UI として画面に表示 する。

Three.js には Unity の「Overlay UI」が無いので、 今回は以下の仕組みで実現する:


仕組み

メインカメラに Plane を add() し、 常にカメラからの相対位置に表示される UI にする。

こうすることで:

  • プレイヤーが動いても UI は画面固定
  • カメラを回転してもミニマップは左上に表示されたまま
  • WebVR / WebXR のカメラにも追従

など、ゲームで求められる UI 表示が可能になる。


実装コード

config.miniMapPlane = new THREE.Mesh(
  new THREE.PlaneGeometry(0.3, 0.3), // ミニマップの表示サイズ
  new THREE.MeshBasicMaterial({
    map: config.miniMapRT.texture,    // RenderTarget のテクスチャ
    transparent: true,                // 透過を有効に
    opacity: 0.8,                     // 少し半透明
    depthTest: false,                 // 背景オブジェクトと干渉しない
  })
);

// メインカメラにアタッチ → UI として画面に固定される
config.camera.add(config.miniMapPlane);

パラメータの意味

PlaneGeometry(0.3, 0.3)

これは “画面上のサイズ” ではなく、 カメラ空間における 3D での大きさ。

後で画面左上に合わせて x, y, z の位置を調整する。


MeshBasicMaterial を使う理由

ミニマップはライトの影響を受けてはいけないので、

  • MeshLambert
  • MeshStandard

などは使わず MeshBasicMaterial が正しい。

「光の影響を受けない UI にする」

ということ。


depthTest: false が重要

depthTest: false

これが無いと:

  • プレイヤーの近くの物体
  • カメラの前の透明エフェクト
  • 草・木などのポリゴン

がミニマップ描画の “前に” 出てしまい、表示が乱れる。

depthTest を切ることで 必ず画面の最前面に表示される UI になる。


メインカメラに add() するメリット

  • UI がカメラに固定される
  • シーンに置いたオブジェクトの影響を一切受けない
  • WebXR の HMD カメラでも問題なく固定表示
  • 画面サイズ変更(リサイズ)にも対応しやすい
  • ゲームでよく見る “画面左上ミニマップ” が簡単に再現できる

Three.js の UI 表示では “カメラに UI を貼る” のがもっとも安定した手法。


このあとやること

Plane をカメラに貼っただけでは 位置がカメラ正面の中央 にあるので、

  • FOV
  • アスペクト比

を使って “画面左上” に移動させる必要がある。

これは次章で扱う。

5. 画面左上に常に固定する

ミニマップPlaneはメインカメラに add() したので、 常にカメラの前方に存在する 3D オブジェクト になっている。

しかしこのままでは:

  • 画面中央に出る
  • 画面サイズ変更で位置がズレる
  • FOV(視野角)を変えると破綻する

という問題がある。

ゲームのように 「画面の左上に固定」 するためには、 カメラの FOV とアスペクト比から “カメラ前方の可視範囲” を逆算 する必要がある。


カメラ空間における「画面の見える大きさ」を求める

Three.js の PerspectiveCamera は本来:

距離Zの位置には、FOVから見える縦幅が決まる

縦幅は以下で求まる:

const height = 2 * Math.tan( vFov / 2 ) * distance;

横幅は「縦幅 × アスペクト比」

const width = height * cam.aspect;

つまり、 distance 先の位置にある UI は width × height の矩形内に表示される。

これを使って左上を求める。


左上の座標を決める

画面左上はこうなる:

  • 左端 = -width / 2
  • 上端 = height / 2

そこから少し内側に寄せたいなら:

const offsetX = -width / 2 + 0.17; // 左へ
const offsetY =  height / 2 - 0.17; // 上へ

0.17 は UI を少し内側に寄せるための “手動オフセット”。


実際のコード

const distance = 1.2;

// 縦幅
const vFov = (cam.fov * Math.PI) / 180;
const height = 2 * Math.tan(vFov / 2) * distance;

// 横幅
const width = height * cam.aspect;

// 左上
const offsetX = -width / 2 + 0.17;
const offsetY =  height / 2 - 0.17;

// ミニマップPlaneの位置を更新
config.miniMapPlane.position.set(offsetX, offsetY, -distance);

これで実現すること

この処理のおかげで:

  • 画面リサイズしてもミニマップの位置がずれない
  • どんなアスペクト比でも左上にピタッと固定
  • カメラの FOV を変えても破綻しない
  • WebXR の視野角変化でも安定
  • UI が 3D空間にあることを感じさせない自然な表示

Three.js では “本物の UI” が存在しないため、 この FOV 逆算テクニックが最も安定する。


さらに応用

  • Plane のスケールを画面サイズに比例させられる
  • ミニマップを右上/左下/右下にも置ける
  • FPS ゲームの “武器アイコン” も同じテクニックで固定可能
  • VRHUD(VRのフローティングUI)にも応用できる
  • UIを複数並べるときも破綻しない

6. キャラの向きに合わせてミニマップを回転

ミニマップはただ表示するだけでは “ゲーム用” としては不十分。 GTA / ゼルダ / Apex / Fortnite のように、

✔ キャラが向いている方向をミニマップの「上」にする

✔ ミニマップ(地形)がゆっくり回転して追従する

✔ 急旋回してもガクッと飛ばない

というカーナビ式の回転が不可欠。

Three.js の角度管理は独特なので、 そのまま角度を代入すると以下の問題が起きる:

  • 0° → 359° の境界で急に大回転する
  • 角度差が正しく取れず “ガクッ” と飛ぶ
  • 回転速度が急に速くなる or 一瞬反対向きに回る

このため、角度の正規化(atan2)+スムーズ補間(lerp) が必要になる。


✔ 回転の基本アイデア

  • プレイヤーの向き(Y軸回転)を取得
  • ミニマップは “真上から見ている” ので Z軸を回転
  • 回転の差分だけをゆっくり反映する

これで「キャラが常に北を向く」ような動きになる。


実装コード

// ▼ターゲット角
const target = -config.player.box.rotation.y + Math.PI;

// ▼角度差を -π〜+π に正規化
let delta = target - config.cameraSubCurrentRotZ;
delta = Math.atan2(Math.sin(delta), Math.cos(delta));

// ▼スムーズ回転(lerp)
config.cameraSubCurrentRotZ += delta * config.cameraSubSmooth;

// ▼サブカメラを回転
config.cameraSub.rotation.z = config.cameraSubCurrentRotZ;

📖 何が起きているのか(分解)


① ターゲット角を作る

const target = -player.rotation.y + Math.PI;
  • Three.js の forward が Z−方向
  • プレイヤーの正面とミニマップの前後が逆
  • 180° 反転が必要なので +Math.PI

必要なら後で調整すればよい。


② 角度差の正規化(atan2)

let delta = target - currentRot;
delta = Math.atan2(Math.sin(delta), Math.cos(delta));

これは「角度のショートカット」を求める魔法。

例:

現在 ターゲット そのまま差を取ると atan2補正後
359° +359° → 遠すぎる -1°(最短)
359° -359° → 遠すぎる +1°(最短)

この1行があるだけで、 回転が常に最短距離で安定する。


③ スムーズ補間(lerp)

currentRot += delta * smooth;

smooth の値で追従速度を調整:

  • 0.05 → ぬるっとゆっくり
  • 0.1 → 標準(ほぼゲームぽい)
  • 0.2 → キビキビ反応

Three.js では slerp(クォータニオン補間)を使う必要はない。 2D 平面の回転なので 角度の線形補間だけで十分。


④ サブカメラを Z軸で回転させる

config.cameraSub.rotation.z = currentRot;

ミニマップは上方向から XZ平面を撮影するので、 回転は Z軸 が正解。

Unity でも “Top view camera の yaw 回転は roll(Z軸)” として扱う。


これで得られる効果

  • キャラが向く方向がミニマップの上へ
  • 地形がぐるっと回り、カーナビのように方向が分かりやすい
  • 旋回速度はなめらかに補間され、不自然な回転が消える
  • 角度が 0° ↔ 360° で飛ぶことが一切なくなる
  • 方向転換してもミニマップは自然に追従する

応用:北固定モード/追従モードの切替

ワンフラグで切り替え可能:

if (!config.miniMapFollow) {
  config.cameraSub.rotation.z = 0; // 北固定
  return;
}

本格ゲームと同じUIにできる。

7. 初期化時にズレを消す(重要)

ミニマップの回転処理は 「現在角度 → 目標角度 の 角度差 を lerp で補間する」 という方式で実装している。

そのため、 スタート時点での初期角度がズレていると、 ミニマップが急にクルッと回ってしまう。

これを避けるためには、 初期フレームで “現在角度” をキャラクターの向きに完全同期させておく 必要がある。


初期同期のコード

const initRot = -config.player.box.rotation.y + Math.PI;
config.cameraSub.rotation.z = initRot;
config.cameraSubCurrentRotZ = initRot;

📖 何をしているのか?

✔ プレイヤーの向き(rotation.y)からミニマップ上の向きを算出

ミニマップの方向は、

  • 上からの視点 → Z軸回転
  • Three.js の forward との差分 → + Math.PI

を反映して計算される。


✔ サブカメラの回転を “その角度に合わせる”

config.cameraSub.rotation.z = initRot;

この時点で ミニマップはキャラと完全に同じ向き になる。


✔ スムーズ回転用の「現在角度」も同じ値にそろえる

config.cameraSubCurrentRotZ = initRot;

これをやっておくことで:

  • 初期フレームで lerp が “0” になる
  • ミニマップが最初から完璧に方向一致
  • 起動直後に “カクッ” と回る現象が完全に消える

✔ なぜ必要なのか?

lerp(補間)は次のように動く:

currentRot += (target - currentRot) * smooth

初期値を間違えると:

  • targetcurrentRot の差が巨大
  • lerp が「1フレーム目に暴発」
  • ミニマップが一瞬だけクルっと回転する

という不自然な回転が必ず起きる。

ゲームとして違和感が出るため、 初期化時点で必ず合わせておくべき。


✔ これでミニマップ回転の動きが “完成形” になる

  • どの角度からスタートしてもズレない
  • プレーヤーの初期向きと完全一致
  • 旋回中だけ滑らかに追従
  • 三人称視点でも FPS でも破綻しない
  • WebXR(VR)に移行しても問題なし

初期同期は小さなコードだが、 ゲーム品質を一気に高める非常に重要なパート。

8. 最後に:Three.js でここまでやる人は少ない

ミニマップはゲームでは欠かせない UI 要素だけど、 Three.js の世界では 「ゲームとして本気で使えるミニマップ」 を ここまで作り込んだ実装例はほとんど存在しない。

本記事で扱った内容は、すべて “ゲームエンジン的なアプローチ” を Three.js に落とし込んだものだ。


✔ Plane を使った UI 表示

HTML/CSS ではなく、 3D空間に存在する本物の UI として Plane を扱う。

✔ 別カメラ(サブカメラ)構成

メインとは独立したカメラで ミニマップ専用の視界を持たせる。

✔ WebGLRenderTarget

サブカメラの映像をテクスチャ化し、 UI として扱う仕組み。

✔ FOV とアスペクト比から UI の位置を計算

Three.js には Unity の Canvas のような仕組みがないため、 カメラ空間の理論値 を使って自前で固定位置を計算する。

✔ atan2 による角度補正

0° ↔ 360° の境界問題を解決し、 常に“最短の回転方向” を計算する。

✔ slerp 風の lerp 回転

クォータニオンを使わずに、 ゲームらしい滑らかな回転を実現する。


つまりこれは

“Unity のミニマップシステム” を Three.js で完全再構築した実装。

Web ゲームや WebXR に本格的に対応した 実用レベルのミニマップ になっている。

Three.js を UI ベースではなく “フルゲームエンジンとして使う” 観点で見ると、 この実装はそのまま テンプレート級 に価値がある。


さらに発展させるなら?

  • ミニマップの円形マスク
  • プレイヤーアイコン表示
  • 敵/NPC/スポットマーカー追加
  • 北固定/キャラ追従モード切替
  • フェードイン・フェードアウト
  • ズーム(スクロール or キー入力)
  • ミニマップ専用ラベル(テキストやアイコン)
  • WebXR 用 HUD バージョン

今回の実装は、これらの拡張をすべて可能にする “基礎フレーム” になっている。