[JavaScript] Three.jsでPC向けミニマップを実装する方法(viewport / scissor を使った複数カメラ描画)

はじめに

大晦日、元旦も、休まず実装を続けてますが、流石に、疲れてきたので今日は半日休んでました。

毎朝、AM4時起きで炊事・洗濯・掃除して、一日分の家族の食事をまとめて作った後、24時間スーパーで買い物。
帰宅途中に、近所の神社で初詣。
帰宅後は、クルマの洗車して部屋の掃除をした後、コーヒーを飲みつつ昨日のコードを読み直してリファクタと、実装相談をAIとするという日課です。

以前は、毎年、出雲大社へ参拝して、時間があれば元旦に出雲で初詣してましたが、コロナ以降は出歩かなくなくなり、出不精になってます。

出かけても車が多くて疲れますし、特にほしい物もなく、コーディングしている方が楽しく、スキルアップになり出来る事が増えるので、結局休まずやってます。

去年の夏ごろからなんとなくブログをはじめて、半年以上経過しましたが、ほぼ毎日休まず継続していて、初期の頃に比べたら出来る事は格段に増えたと思います。

JavaScriptの基本から、カードゲームアプリ開発でHTML・CSS・JavaScriptでDOM操作、indexedDB、P2P。
ブロック崩しゲーム開発で、CANVASを駆使したアニメーションや、物理演算処理。
そして、Three.jsでWebGLを駆使した3Dゲーム開発。

この半年間の間に、かなりスキルアップした気がします。

このブログは、去年の夏、何となく立ち上げたもので、今見るとデザインが良くないので、折を見て、ゼロから作り直して、今まで学んだスキルを全部出す感じでWEBを構築しようと考えてます。

Three.jsの話に戻して、大分やりたい事をやった感があり、今日、何をやるか考えてたのですが、昨晩寝る前に、AIと対話・アイデア出しする中で、 3Dゲームでよくある、画面端にミニマップを表示するというのを実装してみたので、その備忘録メモです。

viewport/scissorで複数カメラ描画

Three.jsで「メイン視点+ミニマップ視点」を同じcanvasに同時表示する方法。

いわゆるゲームの右上ミニマップ方式は、viewport/scissor を使うのが一番シンプルで壊れにくい。

  • カメラを2台用意する

    • camera:通常のメイン視点
    • cameraSub:俯瞰(ミニマップ)視点
  • 1フレーム内で renderer.render() を2回呼ぶ

    • その前に setViewport() / setScissor() で「描画枠」を切り替える

「どちらが描かれるか」は renderer ではなく camera の役割。renderer は「枠(どこに描くか)」だけ担当する。


カメラ準備(例)

// メインカメラ
config.camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
config.camera.position.set(0, 10.6, 3);

// サブカメラ(俯瞰)
config.cameraSub = new THREE.PerspectiveCamera(60, 1, 0.1, 2000);
config.cameraSub.position.set(0, 100, 0);
config.cameraSub.lookAt(0, 0, 0);

cameraSub の aspect は「ミニマップ枠は正方形」にするので 1 にしておくと扱いやすい。


描画ループ:viewport/scissorで2回描く

ポイントはこれだけ。

  • renderer.setScissorTest(true) を有効にする
  • メインを描く → ミニマップ枠に切り替える → サブカメラで描く
  • 1フレームの中で render() を2回呼んでもOK
function updateRender() {
  const renderer = config.renderer;
  const scene = config.scene;

  const cameraMain = config.camera;
  const cameraSub = config.cameraSub;

  const w = window.innerWidth;
  const h = window.innerHeight;

  renderer.setScissorTest(true);
  renderer.clear();

  // ===== メインビュー(全画面)=====
  renderer.setViewport(0, 0, w, h);
  renderer.setScissor(0, 0, w, h);
  renderer.render(scene, cameraMain);

  // ===== ミニマップ(右上)=====
  const size = Math.min(w, h) / 3;
  const x = w - size - 10;
  const y = h - size - 10;

  renderer.setViewport(x, y, size, size);
  renderer.setScissor(x, y, size, size);
  renderer.render(scene, cameraSub);
}

setViewport / setScissor を2回呼んでいるけど「区別がつかない問題」はない。 区別は“枠”でつき、描画内容は“カメラ”で決まる。


window resizeの注意(必須)

ミニマップは正方形枠なので、メインカメラとサブカメラで更新が別。

window.addEventListener('resize', () => {
  const w = window.innerWidth;
  const h = window.innerHeight;

  config.renderer.setSize(w, h);

  // メインカメラ
  config.camera.aspect = w / h;
  config.camera.updateProjectionMatrix();

  // サブカメラ(正方形前提)
  config.cameraSub.aspect = 1;
  config.cameraSub.updateProjectionMatrix();
});

よくある疑問:rendererの中でカメラが配列になる?

ならない。 renderer は「今から描く枠」を切り替えてるだけ。

  • renderer:描画する場所(枠)を操作する
  • camera:何をどう見るか(視点)を決める
  • scene:描かれる対象

同じ renderer に対して render(scene, camera) を2回呼んでいるだけ。


VRでは複数カメラが難しい?理由(具体)

PC(非XR)では上の方法で問題なく動く。 ただ、WebXR(VR)に入ると状況が変わる。

1) WebXRは描画フレームバッファ管理が特殊

VR中はブラウザ側(XR compositor)が表示用のフレームバッファを管理していて、通常の「1フレーム内でviewportを切り替えて2回描画」という発想がそのまま通りにくい。

2) VR中は「描画に使われるカメラ」がThree.js内部で差し替わる

renderer.render(scene, camera) を呼んでいても、VR中は内部的に XR用のカメラ(左右目) が使われる。 その結果、PCと同じ感覚で「サブカメラを描く」「HUDを貼る」などをやると、予想外に破綻しやすい。

3) RenderTargetやHUD表示は、フリーズ/黒画面になりやすい

VR中に setRenderTarget() を多用して「サブカメラをRTに描いて、それを右手の板に貼る」みたいな構成をすると、環境によっては極端に重くなったり、真っ黒になったり、最悪フリーズすることがある(エラーが出ずに起きるのが厄介)。

そのためVRでは、「PCのミニマップ」をそのまま持ち込むより、

  • 方角だけを出す(コンパス)
  • 静的マップ+現在地マーカー
  • ワールド内の端末として表示

みたいに、VR向けに設計を変えた方が安定する。