three.jsで3DのFPSを作る

# 1. はじめに
[three.js](https://threejs.org/)を使って**ブラウザベースのFPSゲーム**を作りました．銀河に散らばる宇宙ゴミを撃ち落とすという設定になってます．[デモ](https://te260ku.github.io/three_fps/)を公開してます（音が出ます）．
![](https://i.imgur.com/LCS6khv.png)
![](https://i.imgur.com/VbciISx.jpg)

## three.jsとは
three.jsとは，ブラウザ上でリアルタイムで3DCGを描画できるJavaScriptライブラリです．これを使うと，単純な立体の表示からアニメーション，他のツールで作成したモデルの読み込みまで，[あんなこと](https://threejs.org/examples/#webgl_animation_cloth)や[こんなこと](https://threejs.org/examples/?q=fr#webgl_materials_shaders_fresnel)がブラウザ上で実装できちゃいます．すごいですね．

<img width="1136" alt="スクリーンショット 2020-01-24 15.47.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263889/9a521a98-e9ba-4759-8621-a17e1b809e9c.png">


# 2. 実装

ソースコードは[こちら](https://github.com/te260ku/three_fps)にあります．以下ではFPSに必要な機能をいくつか抜粋して，その詳細などを書いていきます．

## キー入力で弾丸を発射する

キーボードの入力を検出できるようにしておきます．キーが押されると以下の関数を実行して弾丸を生成します．具体的には以下のような処理を行なっています．

- 衝突判定用のプロパティを付与した弾丸のメッシュを構築する
- 座標と傾きを初期化する
- 残存時間を初期化する
- 管理用の配列へ追加する

```main.js
function Shoot() {
   let bullet = new THREE.Mesh(new THREE.SphereGeometry(2, 8, 4), new THREE.MeshBasicMaterial({
      color: "aqua"
   }));
   if (bullet.geometry.BoundingSphere == null) {
      bullet.geometry.computeBoundingSphere();
   }
   bullet.geometry.boundingSphere.radius *= bulletRadius;

   bullet.position.copy(emitter.getWorldPosition());
   bullet.quaternion.copy(camera.quaternion);

   bullet.alive = true;
   setTimeout(function () {
      bullet.alive = false;
      scene.remove(bullet);
      bullets.shift();
   }, 5000);

   scene.add(bullet);
   bullets.push(bullet);
}
```

## マウス座標を用いた視点移動
今回フィールドとして使用した360度画像には明らかな特徴点が少なく，プレイヤーの座標移動によっては自己定位を失う可能性が高いと考えたので，自由度を3DoFに制限しています．ですので，キーボードで前後左右に移動はせず，マウスを動かして見る方向を変化させます．シーンにThree.jsに組み込まれているFirstPersonControllerを追加して，座標移動やロールおよびピッチの制限などの設定をします．

ちなみにこのコントローラーはデフォルトではキー入力による座標移動にも対応しているので，本格的なFPSを作りたい場合にもそのまま使えます便利．

```main.js
   // コントローラーの生成
   controls = new THREE.FirstPersonControls(camera, renderer.domElement);
   controls.lookSpeed = 0.1;
   controls.movementSpeed = 1;
   controls.noFly = true;
   controls.lookVertical = true;
   controls.constrainVertical = false;
```

## 弾丸と敵の衝突判定

intersectsSphereメソッドを用いて弾丸と敵の衝突を取得します．intersectsSphereメソッドは2つのオブジェクトの距離をベースにした衝突判定を行います．

フレームごとにシーンに存在する弾丸と敵オブジェクトを配列から取得して，それらの座標が交差しているかどうかを検証します．衝突時には効果音の再生とスコアの更新をして，対象となるオブジェクトは配列およびシーンから削除します．

```main.js
if (targetsphere.intersectsSphere(targetBullet)) {
            StruckAudio.play();
            score++;
            scene.remove(targetBullet);
            scene.remove(b);
            scene.remove(targetsphere);
            t = scene.getObjectByName(spheres[i].name);
            scene.remove(t);
            spheres.splice(i, 1);
            scoreLabel.innerHTML = score;
         }
```

## プレイヤー用オブジェクトの作成
1人称視点であることを認識させるために，銃の3Dモデルをカメラに追従させます．

```main.js
   var mtlLoader = new THREE.MTLLoader();
   mtlLoader.load("./model/uziGold.mtl", function(materials){
   
        materials.preload();
        var objLoader = new THREE.OBJLoader();
        objLoader.setMaterials(materials);
        
        objLoader.load("./model/uziGold.obj", function(gun){
            gun.traverse(function(node){
                if( node instanceof THREE.Mesh ){
                    node.castShadow = false;
                    node.receiveShadow = false;
                }
            });
            gun.rotation.y += degreesToRadians(180);
            gun.scale.set(10,10,10);
            gun.position.set(0.6, -0.5, -1.2);
            camera.add(gun);
        });
   });
```


## フィールドの生成
360度画像をマッピングした巨大な球体をフィールドとして使用しています．球体は中心を原点として，プレイヤーを取り囲むように広がっています．

```main.js
const geometry_f = new THREE.SphereGeometry(1000, 80, 80);
geometry_f.scale(-1, 1, 1);

const loader_f = new THREE.TextureLoader();
const texture_f = loader_f.load("./img/starry-deep-outer-space-galaxy.jpg");

const material_f = new THREE.MeshBasicMaterial({
      map: texture_f
   });

//球体を生成
const sphere_f = new THREE.Mesh(geometry_f, material_f);
scene.add(sphere_f);
```


# 3. できなかったこと

## ガバガバな衝突判定
今回はthree.jsにデフォルトで搭載れている衝突判定のメソッドを使いましたが，**精度が死ぬほど悪いです**．どれくらい悪いかと言うと，至近距離でガンガン撃ち込んでも敵が消えなかったり，かと思うと1発当てただけで周囲の敵も同時に10体くらい消滅したりします．[こちら](https://qiita.com/adrs2002/items/dd26fa318f7aa6aedc04)にも書かれているように，自動的に作られる衝突判定用のオブジェクトは大きめに設定されるようですが，それにしても挙動がおかしすぎるので，これは衝突判定以外の問題かもしれないです．

## 敵オブジェクトにシェーダーをかける
シャボン玉のようなシェーダーをかけたり，動的な環境マッピングを適応したりしたかったんですが，僕の知識が浅いために実装できませんでした．GLSLとか何もわからん．また機会があったらお勉強しようと思います．


# 4. おわりに
three.jsを使えば初心者でも簡単にブラウザベースのFPSが作れると思います．

「これUnity使えばすぐ作れるじゃん」は禁句ですのでよろしくお願いします．