【Three.js】カメラ前方の衝突判定を簡単に行う方法(ソースコード付き)

スポンサーリンク
Three.js

衝突判定を実装したいけどどうしたらいいの?

こういったお悩みを解決いたします!

開発するコンテンツにもよりますが、ゲームは特に、衝突判定は必須になりますよね。

Three.jsでちょっとしたゲームを制作している方も多いのではないでしょうか。

当記事では、誰でも簡単に衝突判定できる方法をご紹介しています。

後半にソースコードとデモを載せていますので是非参考にしてみてください。

カメラの制御にはPointerLockControlsに移動機能を追加したものを用いています。

カメラの制御についてはこちらの記事で詳しく解説しています。

当記事を読むと、

  • 衝突判定ができるようになる
スポンサーリンク

衝突判定の基本的なステップ

衝突判定は以下の流れで行います。

  1. カメラの位置にRaycasterを設置し、常に前方に光線を出しておく
  2. 光線を遮ったオブジェクトを取得する
  3. そのオブジェクトとの距離を測る
  4. 距離が設定値未満であれば任意の処理を実行(前進を制御とかオブジェクトの削除とか)

①が一番厄介かもしれません。

カメラの回転に合わせてRaycasterが光線を発射する方向ベクトルも回転させないといけないので。

スポンサーリンク

衝突判定のソースコード

ソースコードを見ていきましょう。少し長いかもしれません。

<!DOCTYPE html>
<html>
  <head>
    <title>three.js Sample</title>
    <meta charset="utf-8">
    <script src="./three.js-master/build/three.min.js"></script><!--three.jsライブラリの読み込み-->
    <script src="./three.js-master/examples/js/controls/PointerLockControls.js"></script><!--PointerLockControls.jsの読み込み-->
  </head>
  <body style="overflow: hidden;"><!--スクロールバーが出る方はoverflowをhiddenに設定すると消えます-->
    <script>
      const scene = new THREE.Scene();  //シーンを作成
      const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 5000);  //カメラを作成
      const renderer = new THREE.WebGLRenderer(); //レンダラーを作成
      const controls = new THREE.PointerLockControls(camera, renderer.domElement);  //カメラにOrbitControls機能を付与
      const boxGeometry = new THREE.BoxGeometry(100, 100, 100); //boxの形状データ
      const boxMaterial = new THREE.MeshNormalMaterial(); //boxの材質(マテリアル)情報
      const box1 = new THREE.Mesh(boxGeometry, boxMaterial); //形状と材質から実際の3Dオブジェクトを生成
      const box2 = new THREE.Mesh(boxGeometry, boxMaterial);
      const box3 = new THREE.Mesh(boxGeometry, boxMaterial);
      let moveForward = false;
			let moveBackward = false;
			let moveLeft = false;
      let moveRight = false;
      let prevTime = performance.now(); //1フレーム前の時刻を記憶
      let velocity = new THREE.Vector3();
      let direction = new THREE.Vector3();
      //衝突判定用変数
      const RAY_ROTATE_AXIS = new THREE.Vector3(0, 1, 0); //光線の回転軸
      const raycaster_for_collision = new THREE.Raycaster(); 
      init();
      animate();

      function init(){    //初期化用関数
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.BasicShadowMap;
        document.body.appendChild(renderer.domElement); //canvas要素をbodyタグに追加
        box1.position.set(0, 0, -300);
        box2.position.set(300, 0, 0);
        box3.position.set(-300, 0, 0);
        scene.add(box1); scene.add(box2); scene.add(box3);
        scene.add(controls.getObject());
        renderer.domElement.addEventListener('click', function() {
          controls.lock(); 
        });
      }

      function animate(){
        if(check_collision_forward()) set_moveForward(false); //衝突を検知したとき前進を不可とする
        move();
        renderer.render(scene, camera);
        requestAnimationFrame(animate); //毎フレーム呼び出すことで描画
      }

      function move(){  //カメラの移動を制御する関数.毎フレーム呼ばれる
        let time = performance.now();
        if ( controls.isLocked === true ) { //マウスのポインタがロックされているときのみ有効
          let delta = ( time - prevTime ) / 1000;

          //速度を減衰させる
          velocity.x -= velocity.x * 10.0 * delta;
          velocity.z -= velocity.z * 10.0 * delta;

          //進行方向のベクトルを設定
          direction.z = Number( moveForward ) - Number( moveBackward );
          direction.x = Number( moveRight ) - Number( moveLeft );
          direction.normalize();

          if ( moveForward || moveBackward ) velocity.z -= direction.z * 1000.0 * delta;
          if ( moveLeft || moveRight ) velocity.x -= direction.x * 1000.0 * delta;

          controls.moveRight( - velocity.x * delta );
          controls.moveForward( - velocity.z * delta );

        }
        prevTime = time;
      }

      function check_collision_forward(){ //true: 衝突  false: 無衝突
        const CAMERA_LOCAL_FORWARD = new THREE.Vector3(0, 0, -1);  //カメラの初期前方ベクトル. applyAxisAngleは対象ベクトルを直接変更してしまうため毎回初期化する必要あり
        let dir_forward = CAMERA_LOCAL_FORWARD.applyAxisAngle(RAY_ROTATE_AXIS, camera.rotation.y);  //カメラのy軸に関する回転を初期前方ベクトルに掛ける
        raycaster_for_collision.set(camera.position, dir_forward);  //カメラの前方に光線を発射
        let distance, intersects;
        intersects = raycaster_for_collision.intersectObjects(scene.children);  //光線にぶつかったすべてのオブジェクトを取得
        if(intersects.length <= 0) return false;  //ぶつかっていないので無衝突
        distance = intersects[0].distance;  //光線にぶつかったオブジェクトの中で先頭のものとの距離
        if(distance > 20) return false;   //この距離の判定は各々で値を決める
        return true;
      }

      function set_moveForward(can/*bool*/){
        moveForward = can;
      }

      let onKeyDown = function ( event ) {  //キーボード押下時の処理
        switch ( event.keyCode ) {
          case 87: // w
            moveForward = true;
            break;
          case 65: // a
            moveLeft = true;
            break;
          case 83: // s
            moveBackward = true;
            break;
          case 68: // d
            moveRight = true;
            break;
        }
      };

      let onKeyUp = function ( event ) {  //キーボードから離れたとき
        switch ( event.keyCode ) {
          case 87: // w
            moveForward = false;
            break;
          case 65: // a
            moveLeft = false;
            break;
          case 83: // s
            moveBackward = false;
            break;
          case 68: // d
            moveRight = false;
            break;
        }
      };

      document.body.addEventListener( 'keydown', onKeyDown, false );  //キーボードに関するイベントリスナ登録
      document.body.addEventListener( 'keyup', onKeyUp, false );


    </script>
  </body>
</html>

デモを見たい方はこちらをクリック(ページは遷移しません) ⇒

↑のデモでboxに突っ込んでみてください。貫通できないことが確認できると思います。

ソースコード内のcheck_collision_forwardという関数が前方の衝突判定を検知しています。

制御の流れは前節で説明したものに沿っています。ソースコード内のコメントと合わせて見ていただけたらと思います。

今回は衝突判定距離を20にしていますが、そこは開発コンテンツに合わせて適宜変更をお願いします。

また、このサンプルでは衝突判定を行う対象がboxであり、これはスクリプトから生成したものになります。

外部ファイル(.objファイルとか)から生成したものの場合、少し実装が異なるので注意してください。

(近々別記事で解説いたします)

まとめ

いかがでしたでしょうか。

当記事を参考にしていただけたら幸いです。

もしも、

もっと楽な方法があるよ

このソースコード動かないよ

という方がいらっしゃいましたらコメントでお知らせください。

以上です。

コメント

タイトルとURLをコピーしました