3Dモデルを使った作品制作におけるモデルを映し出すカメラの視点というのは、そのコンテンツの顔を決めるものとなるので非常に重要です。

画像とは違いコンピュータグラフィックは360°すべての視点から見せることのできる点が魅力ですが、3Dモデルをどの角度からも隙なく作り込むというのは、時間的、あるいはデータサイズ的に難しかったり、時には無意味なものかもしれません。

カメラの制御を適切に行うことで、注目させたい点をより際立たせることもできますし、意図的に隠すことで魅力が高まることもあるかもしれません。

コンテンツの制作においてモデルを作ることに重きを置きがちですが、それを映し出すカメラの視点について少し深堀したので、備忘録的にまとめておきます。


ネコ後輩
モデルの下の面は作り込んでないから見せたくないな、、
タコ先輩
OrbitControlsでは、カメラ制御に制限を付けたりもできるぞ!

OrbitControlsを用いたカメラ操作実装例


Three.jsのライブラリであるOrbientControlsを用いることで、下のようにカメラをマウスのドラッグやホイールで操作することが簡単に実装できます。
マウス
  • マウスクリック&ドラッグ
  • マウスクリック&ドラッグ
  • マウスホイール
  • マウスホイールクリック&ドラッグ
スマホ
  • スワイプ
  • ピンチイン/アウト
この例では、上記のような操作に対して操作が割り当てられているはずです。言葉で説明するより実際に体感してみた方が理解が早いと思うので、実際に動かしてみてそれぞれどのような動きとなるか確かめてみてください。
コードに数行追加するだけで、ここまで複雑なカメラ操作を実現できるので、基本的にはこれで問題ないと思います。

基準となるコード全体

OrbitControlsを用いた基本的なコードを以下に挙げます。
このコードを使うことで、上のモデルを表示して、視点を360°回転させるカメラを実装することができています。

typescript:
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

let scene: THREE.Scene;               // シーン定義
let camera: THREE.PerspectiveCamera;  // カメラ定義
let renderer: THREE.WebGLRenderer;    // レンダラー定義
let controls: OrbitControls;          // OrbitControlsを追加

function init() {
  /* シーン作成 */
  scene = new THREE.Scene();
  /* カメラ作成 */
  camera = new THREE.PerspectiveCamera(
    45,                                       // 視野
    window.innerWidth / window.innerHeight,   // アスペクト
    0.1,                                      // どの程度のカメラ距離から描画を始めるか
    1000                                      // どのくらい遠くまで見えるか
  );
  scene.add(camera);
  // カメラのポジションと向きを決定
  camera.position.x = -30;
  camera.position.y = 40;
  camera.position.z = 30;
  camera.lookAt(scene.position); // シーンの中心にカメラを向ける
  /* レンダラー作成 */
  renderer = new THREE.WebGLRenderer();
  renderer.setClearColor(new THREE.Color(0xeeeeee));
  renderer.setPixelRatio(window.devicePixelRatio);    // 解像度変更
  renderer.setSize(window.innerWidth, window.innerHeight);
  /* ライト作成 */
  // 均等光源(影ができない)
  var ambientLight = new THREE.AmbientLight(0x0c0c0c);
  scene.add(ambientLight);
  // 点座標への光源(影ができる)
  var spotLight = new THREE.SpotLight(0xffffff);
  spotLight.position.set(-20, 30, -5);
  spotLight.castShadow = true; // 影を落とす
  scene.add(spotLight);
  // 無限遠からの平行光源
  var directionalLight = new THREE.DirectionalLight(0xffffff, 0.3);
  directionalLight.position.set(20, -30, 5).normalize(); // 光源の方向を設定
  scene.add(directionalLight);
  // glbファイルの読み込み
  const loader = new GLTFLoader();
  loader.load ('assets/models/vrm/AliciaSolid.vrm', function(gltf){
    gltf.scene.scale.set(33, 33, 33);
    gltf.scene.position.set(0, -33, 0);
    gltf.scene.rotation.set(0, (90 * Math.PI /180), 0);
    scene.add(gltf.scene);
  }, undefined, function (error) {
    console.error(error);
  }); 
  
  // レンダラーの出力をhtmlに追加
  const container = document.getElementById("WebGL-output");
  if (container) {
    container.appendChild(renderer.domElement);
  }

  // OrbitControlsを初期化、カメラとレンダラーを渡す
  /* ここを編集 */
  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableRotate = true; // カメラの回転を有効化

  // シーンを描画
  render();
}

// シーンを描画
function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}

// 表示領域をウィンドウサイズに合わせる
function onResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

// 表示が終わってからThree.js関連処理(関数init)を実行
window.addEventListener("load", init);
// リサイズイベント
window.addEventListener("resize", onResize, false);

本記事では、主に61行目からのOrbitControlsに関連する記述に注目して、より応用的なカメラを制御を実現していきます。

開発環境を含む実践的なコード


npmやwebpackなどの設定ファイルを含むコード全体については以下のGitHubにあります。


また、よりnpmやwebpackを用いない純粋なhtmlとjabvascript環境で実装したい方は、以下の記事で取り上げているコードでも動かすことができます。




OrbitControlsを用いたカメラ制御パターン


OrbitControlsときに機能が豊富過ぎるので、次のようにして機能を制限して使用することが可能です。以下によく使うパターンの実装例を随時追加していきます。

OrbitControlsの制御可能項目


下の記述でズーム操作や回転、パン(カメラの上下左右の移動)について、有効/無効を切り替えることができます。
controls.enableZoom = false; // ズームを無効化
controls.enableRotate = false; // 回転を無効化
controls.enablePan = false; // パンを無効化

また、次の記述でカメラの動きを制限することができます。ここで代入している値は一例です。
controls.minDistance = 100; // 最小距離を設定
controls.maxDistance = 200; // 最大距離を設定
controls.minPolarAngle = Math.PI / 4; // 最小仰角を設定
controls.maxPolarAngle = Math.PI / 2; // 最大仰角を設定

上半分の領域でカメラが移動するパターン


良く使用する制御の一例として、モデルの上面だけカメラが映すようにしたい場合があります。この場合、以下のように「maxPolarAngle」にカメラが移動する最大角度を指定することで、見上げる方向のカメラ移動を制限できます。

平面を用意してその上に実装を行うゲームなどでは重宝しそうですね、

「controls.enablePan = false;」などを用いてパンの設定も無効にしておかないと、カメラを下に移動することができるので、場合によっては、その点も併せて設定する必要があるかもしれません。
// OrbitControlsを初期化、カメラとレンダラーを渡す
controls = new OrbitControls(camera, renderer.domElement);
controls.enableRotate = true; // カメラの回転を有効にする
controls.maxPolarAngle = Math.PI * 0.5; // カメラの仰角の最大値を90度に設定する(下から見上げる角度を制限)
▼動作例


下半分の領域でカメラが移動するパターン


上とは逆でカメラの移動範囲を球の下半分に制限します。「minPolarAngle」(カメラの角度の最小値)を設定することで、設定可能です。
// OrbitControlsを初期化、カメラとレンダラーを渡す
controls = new OrbitControls(camera, renderer.domElement);
controls.enableRotate = true; // カメラの回転を有効にする
controls.minPolarAngle = Math.PI * 0.5; // カメラの仰角の最小値を90度に設定する(上から見上げる角度を制限)
▼動作例


Y軸(高さ)を初期位置に固定してY軸周りに回す


カメラを任意軸の一定値の位置に固定して、その軸周りにカメラを回転させるよう制御します。

以下は、仰角を初期位置に固定して、カメラをY軸周りに回転させる実装例です。カメラのズームを無効化を行うと、完全にY軸を初期位置から固定可能になります。

上記2つと比べると、少々複雑ですが、sqrt関数(平方根)を用いて、いわゆる三平方の定理を用いていたり、atan2関数(アークタンジェント)を用いて、Y軸の値と、円の半径の値から仰角を求めるなど、読み解ける程度の数学が使われていることが分かるかと思います。
// OrbitControlsを初期化、カメラとレンダラーを渡す
controls = new OrbitControls(camera, renderer.domElement);
controls.enableRotate = true; // カメラの回転を有効にする
// カメラの仰角を固定してY軸を固定する
controls.minPolarAngle = Math.atan2(camera.position.y, Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z)); // 仰角の最小値を初期位置のY軸位置に設定する
controls.maxPolarAngle = Math.atan2(camera.position.y, Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z)); // 仰角の最大値を初期位置のY軸位置に設定する
▼動作例


カメラを動作するオブジェクトに追従させる

カメラを動きのあるオブジェクトに追従して動くように設定します。

この時、OrbientControlsの持つ回転や、拡大縮小機能を失わないようにすることで、カメラと物体までの距離や、視点の方向は自在に変えられるようにしています。

下の例では、動き続ける球体をカメラが画面中心に常にとらえ続けるように実装しています。カメラの注視点を常にrender関数内で更新し続けることで実現します。

カメラを常にオブジェクトに追尾させる(一定の距離を保たせる)場合は、renderの中でターゲットのオブジェクトとカメラとの距離を更新する処理を追加することで実現可能です。
// オブジェクトをシーンに追加
scene.add(sphere);
targetObject = sphere; // 追従対象のオブジェクトとして設定
function render() {
  // 球体を動かす
  step += 0.017;
  sphere.position.x = 0 + 10 * Math.cos(step);
  sphere.position.y = -10 + 15 * Math.abs(Math.sin(step * Math.PI));
  // カメラの向きをターゲットへ向ける
  controls.target.copy(targetObject.position);
  camera.lookAt(targetObject.position);
  // レンダリング(再帰)
  requestAnimationFrame(render);
  // 表示
  renderer.render(scene, camera);
}
▼動作例


OrbitControlsを使用しないカメラ制御方法


汎用的に用いられるライブラリでは満足できない、より複雑な処理を実装したいアクティブな方は、カメラ制御を自力で実装する方法もあります。

下のようにマウス操作イベントに関連した関数を作成して、その中のコードを改変することで機能を作り込むことが可能です。

実装の一例を以下に置いていくので、これを基に改造したりしてみてください。

▼Y軸周りに回転するカメラ操作実装例
let isDragging = false;
let previousMousePosition = {
  x: 0,
  y: 0
};
/* マウスホイールイベント */
function onMouseWheel(event: WheelEvent) {
  const direction = event.deltaY < 0 ? 1 : -1;
  const factor = 0.1 * direction;
  // カメラの位置をシーンの中心に向かって拡大縮小
  const newPosition = camera.position.clone().sub(scene.position).multiplyScalar(1 + factor);
  camera.position.copy(newPosition.add(scene.position));
}

function onMouseDown(event: MouseEvent) {
  isDragging = true;
  previousMousePosition = { x: event.clientX, y: event.clientY };
}

function onMouseMove(event: MouseEvent) {
  if (isDragging) {
    const deltaMove = {
      x: event.clientX - previousMousePosition.x,
      y: event.clientY - previousMousePosition.y
    };

    // マウスの移動に応じてカメラの回転角度を計算
    const theta = toRadians(deltaMove.x * 0.5);
    // カメラの位置をシーンの中心に移動
    camera.position.sub(scene.position);
    // カメラをY軸周りに回転
    camera.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), theta);
    // カメラの位置を元に戻す
    camera.position.add(scene.position);
    // カメラが常にシーンの中心を見るように設定
    camera.lookAt(scene.position);
    // マウスの位置を更新
    previousMousePosition = { x: event.clientX, y: event.clientY };
  }
}

function onMouseUp() {
  isDragging = false;
}

function toRadians(angle: number) {
  return angle * (Math.PI / 180);
}

// 表示領域をウィンドウサイズに合わせる
function onResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

// カメラ操作
document.addEventListener('wheel', onMouseWheel);
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);



参考:

このエントリーをはてなブックマークに追加
コメントを閉じる

コメント

コメントフォーム
記事の評価
  • リセット
  • リセット