はじめに
どうも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 も 正方形 にする必要がある。
もしミニマップを長方形にしたい場合は:
- サブカメラ fov / aspect を変更
- RenderTarget も同じ比率に合わせる
- 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° | 0° | +359° → 遠すぎる | -1°(最短) |
| 0° | 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
初期値を間違えると:
targetとcurrentRotの差が巨大- 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 バージョン
今回の実装は、これらの拡張をすべて可能にする “基礎フレーム” になっている。
💬 コメント