[WebGL] OrbitController風の操作を実装してみる


Orbit（軌道上）にあるようなカメラの視点移動を提供するものです。
Three.jsでも提供されています。

ちょっとしたモデルを色々な方向から見せたい、ということはよくあると思います。
今回はそのOrbitController風の視点移動をさせるコードを載せています。

## サンプルコード

```js:orbit
(function () {
    'use strict';

    function OrbitViewer(camera, target) {
        this._camera = camera;
        this._target = target;
        this._init();
    }
    OrbitViewer.prototype = {
        constructo: OrbitViewer,

        _camera: null,
        _target: null,

        _prevX: 0,
        _prevY: 0,

        orbitX: 0,
        orbitY: 0,
        scale : 10,
        maxRotationY: 90 * Math.PI / 180,
        minRotationY: 0,

        _init: function () {
            document.body.addEventListener('mouseup',    this._mouseup.bind(this),    false);
            document.body.addEventListener('mousedown',  this._mousedown.bind(this),  false);
            document.body.addEventListener('mousewheel', this._mousewheel.bind(this), false);
        },

        _mousedown: function (e) {
            this._prevX = e.pageX;
            this._prevY = e.pageY;

            if (!this._bindMousemove) {
                this._bindMousemove = this._mousemove.bind(this);
            }
            document.body.addEventListener('mousemove', this._bindMousemove, false);
        },

        _mouseup: function (e) {
            document.body.removeEventListener('mousemove', this._bindMousemove, false);
        },

        _mousewheel: function (e) {
            this.scale -= e.wheelDelta / 5;
            e.preventDefault();
        },

        _mousemove: function (e) {
            var deltaX = e.pageX - this._prevX;
            var deltaY = e.pageY - this._prevY;
            
            this.orbitX -= deltaX / 1000;
            this.orbitY += deltaY / 1000;

            if (this.orbitY > this.maxRotationY) {
                this.orbitY = this.maxRotationY;
            }
            else if (this.orbitY < this.minRotationY) {
                this.orbitY = this.minRotationY;
            }

            this._prevX = e.pageX;
            this._prevY = e.pageY;
        },

        update: function () {
            // Yが0（sin(0)）のときは1となり、地面からの角度は0
            var x = Math.sin(this.orbitX) * Math.cos(this.orbitY) * this.scale;

            // Yが0でXも0のときに1となる計算。
            var z = Math.cos(this.orbitX) * Math.cos(this.orbitY) * this.scale;

            // Yは単純にYの位置のsinで決まる
            var y = Math.sin(this.orbitY) * this.scale;

            this._camera.position.set(x, y, z);
            this._camera.lookAt(this._target.position);
        }
    };

    window.OrbitViewer = OrbitViewer;
}());
```

まぁなんだかんだやっていますが、`update`メソッド以外は単純にマウスイベントを取ってドラッグ状態を取得しているだけです。
`update`メソッドの部分が実際に軌道上にあるようなカメラの視点移動をしている部分です。

*（ちなみに、Three.jsと一緒に使う想定でサンプルを作っているのでいくつかThree.jsで定義されているものを使っていますが、基本的に`x, y, z`を適切に設定するようにすればどんな環境でも動きます）*


## `update`メソッドの実装

`update`メソッド内でなにやら色々やってます。
`x, y, z`それぞれを`sin`、`cos`を使って計算しています。
<del>（実はここをメモしておきたかっただけなんて言えない）</del>

それぞれなにをしているか見て行きましょう。
*（ちなみに球体上に点をプロットするのも同じ理屈です）*

### Y座標

一番シンプルなY座標から。
（`this.scale`はたんに距離の調整用なので割愛します。大きい値になれば注視点とカメラの位置が離れます）

```js:y-axis
var y = Math.sin(this.orbitY);
```

ただの`sin`ですねｗ
Y軸に関しては上下に移動するだけなので`Math.sin`に`orbitY`（カメラ視点移動の計算用に保持している現在の位置）を入れているだけです。

`sin`は`-1〜1`の間を往復するので、それに距離である`scale`を掛けてあげればOKです。
（サンプルコードでは反転しないようにYの値の最小／最大値を設定しています）


### X座標

```js:x-axis
var x = Math.sin(this.orbitX) * Math.cos(this.orbitY);
```

X座標についての計算です。

簡単な図を書いてみました↓
![OrbitImage.png](https://qiita-image-store.s3.amazonaws.com/0/5616/c50de67c-3704-8c59-cfa2-17ead33b1a52.png "OrbitImage.png")


ここでの計算は「X軸の位置」なので、当然着目するのはX軸の値です。
上図では円上になっていますが、着目するのは実線で書いた線分の長さです。
-1〜1の範囲で上（下）にいくに連れて短くなっているのが分かります。
そしてこの長さの変化はYの値によります。

つまり、 **X軸の値はYの位置（高さ）のcosによる比率** ということができます。

なので、`Math.sin(this.orbitX) * Math.cos(this.orbitY)`となるわけです。


### Z座標

```js:z-axis
var z = Math.cos(this.orbitX) * Math.cos(this.orbitY);
```

最後はZ座標です。
といっても考え方はX軸のときとほぼ同じです。
視点を90度上に持ってきて見れば、X, Yの座標系がX, Zの座標系に変わるだけですね。

ただひとつ違う点があって、X軸の計算の場合、`orbitX`、`orbitY`ともに`0`の場合は`0`でした。
つまり出発点が`0`、ということです。

しかしZ軸の出発点は`0`だと困ります。出発点は`1`としたいですね。
答えは単純で、`Math.cos(this.orbitX)`とすれば初期値が`1`となります。（`cos(0) = 1`）
あとは高さに依存するのはX軸と同様なので、X軸同様、`Math.cos(this.orbitY)`を掛けてやればZ軸の値が求まります。


## まとめ

以上で`x, y, z`それぞれの値が計算できました。
あとは自由に半径などの補正をして、それをそのままカメラの位置として設定してあげれば軌道上の人工衛星から撮影するような感じで、対象物のまわりをぐるぐる回るカメラの視点移動が完成する、というわけです。

（本当は`lookAt`的なこともやらないとなりませんが、位置の計算、ということで割愛。サンプルコードではThree.jsの`lookAt`メソッドをそのまま使っています）