UnityCameraにAddComponentするだけで3DViwerを作る

Unityに使い慣れる為に、3DViwerを作った時に、一つのファイルにまとまって、CameraにAddComponentするだけで出来た為、そのメモ。
3Dゲームをやっていると、図鑑とかキャラの詳細で、キャラをぐりぐり360°見れるようにするのは、良くある話だと思います。
方法も既に、確立していて割と誰が作成してもほとんど同じ方法で実現されると思っていますが、どうしても簡単な数学が必要で、そこを理解していないとなかなか実現が難しいという話です。


## 方法
以下の式がカメラの移動を表現しており、半径(<font color="Red">r</font>)が拡大・縮小の役割を行い、<font color="Green">sin/cos</font>の計算が回転の役割を行い、<font color="Blue">a</font>でカメラと視点を移動させる事で、平行移動を行います。

* x = <font color="Red">r</font> * <font color="Green">sin θ</font> * <font color="Green">cos φ</font> + <font color="Blue">a</font>
* y = <font color="Red">r</font> * <font color="Green">sin θ</font> * <font color="Green">sin φ</font> + <font color="Blue">a</font>
* z = <font color="Red">r</font> * <font color="Green">cos θ</font> + <font color="Blue">a</font>



### 拡大
マウスホイールの変化量を、**r**に増減させます。
スマホアプリだと、ピンチイン/アウトになる為、その変化量を増減させる事になります。
また、サンプルコードだと、最小値を入れていない為、視点に減り込みますので、対応する必要があるかと思います。

### 回転
極座標系の[球面座標](https://ja.wikipedia.org/wiki/%E6%A5%B5%E5%BA%A7%E6%A8%99%E7%B3%BB#%E7%90%83%E5%BA%A7%E6%A8%99)の考え方を用いる。式をそのまま使えます。
縦の移動量を**θ**の角度の変化量に適用し、横の移動量を**φ**の変化量に適用します。
スマホアプリだと、指一本のフリックやスワイプ操作で行うのが一般的でしょう。
![9dTFqAzn.png](https://qiita-image-store.s3.amazonaws.com/0/153021/b7791708-8302-80c0-8b2a-552e5d5cfb3d.png)


### 平行移動
特定の操作(サンプルコードの場合だと、右ボタン操作)で、縦横の変化量を**a**へ増減させます。コメントも入れていますが、**transform.right**と**transform.up**は単位ベクトルが入っている為、移動方向に使用しています。
スマホアプリだと、指二本のスワイプ操作が一般的なのでしょうか。


## コード
実際に書いてみたコードです。検証用に書いた為、品質は保証ありません。
Inputで入力を取っている為、マウスだけで簡単に試す事ができます。

```
using UnityEngine;
using System.Collections;

namespace App
{
    public class PolarCoordinatesCameraController : MonoBehaviour
    {
        enum MouseDirection
        {
            None,
            Front,
            Back,
        }
 
         enum MouseClickType
        {
            None,
            Left,
            Right,
        }
 
        [SerializeField] float wheelLength = 2f;
        [SerializeField] float wheelSpeed = 0.1f;
        [SerializeField] float mouseLeftSpeed = 0.1f;
        [SerializeField] float mouseRightSpeed = 0.001f;

        Vector3 cameraLookAt = new Vector3(0f, 0f, 0f);
        Vector3 cameraPostion = new Vector3(0f, 0f, 0f);
        Vector2 mouseTouchStartPos;
        MouseClickType mouseClickType = MouseClickType.None;
        bool isMouseClick = false;
        // Fixed:初期Ⅰ。調整してこの角度
        float pie = -118f;
        float fhi = 310f;
        float radius = 3f;


        void Update ()
        {
            var wheel =  Input.GetAxis("Mouse ScrollWheel");

            if (wheel == 0f)
            {
                var moveX = 0f;
                var moveY = 0f;
                var moveZ = 0f;
                if (Input.GetMouseButtonDown(0))
                {
                    mouseTouchStartPos = Input.mousePosition;
                    mouseClickType = MouseClickType.Left;
                    isMouseClick = true;
                }
                else if (Input.GetMouseButtonDown(1))
                {
                    mouseTouchStartPos = Input.mousePosition;
                    mouseClickType = MouseClickType.Right;
                    isMouseClick = true;
                }
                else if (Input.GetMouseButton(0))
                {
                    moveX = (Input.mousePosition.x - mouseTouchStartPos.x) * mouseLeftSpeed;
                    moveY = (Input.mousePosition.y - mouseTouchStartPos.y) * mouseLeftSpeed;
                    mouseTouchStartPos = Input.mousePosition;
                }
                else if (Input.GetMouseButton(1))
                {
                    var moveVectorX = Input.mousePosition.x - mouseTouchStartPos.x;
                    var moveVectorY = Input.mousePosition.y - mouseTouchStartPos.y;
                    // memo : transform.right / transform.up は正規化済
                    var moveRight = transform.right * moveVectorX * mouseRightSpeed;
                    var moveUp = transform.up * moveVectorY * mouseRightSpeed;

                    moveX = moveRight.x + moveUp.x;
                    moveY = moveRight.y + moveUp.y;
                    moveZ = moveRight.z + moveUp.z;
                    mouseTouchStartPos = Input.mousePosition;
                }
                else if (Input.GetMouseButtonUp(0) || Input.GetMouseButtonUp(1))
                {
                    mouseTouchStartPos = Vector2.zero;
                }

                if (isMouseClick && mouseClickType == MouseClickType.Left)
                {
                    pie -= moveY;
                    fhi -= moveX;
                }
                else if (isMouseClick && mouseClickType == MouseClickType.Right)
                {
                    cameraLookAt.x -= moveX;
                    cameraLookAt.y -= moveY;
                    cameraLookAt.z -= moveZ;
                    cameraPostion.x -= moveX;
                    cameraPostion.y -= moveY;
                    cameraPostion.z -= moveZ;
                }
            }
            else
            {
                var direction = MouseDirection.None;
                if (Input.GetAxis("Mouse ScrollWheel") < 0)
                {
                    if (wheelLength < radius)
                    {
                        direction = MouseDirection.Front;
                    }
                }
                else
                {
                    direction = MouseDirection.Back;
                }

                switch(direction)
                {
                    case MouseDirection.Front:
                        radius -= wheelSpeed;
                        break;
                    case MouseDirection.Back:
                        radius += wheelSpeed;
                        break;
                }
            }

            var angelPie = pie * 0.01f;
            var angelFhi = fhi * 0.01f;

            var pos = new Vector3(
                radius * Mathf.Sin(angelPie) * Mathf.Sin(angelFhi) + cameraPostion.x,
                radius * Mathf.Cos(angelPie) + cameraPostion.y,
                radius * Mathf.Sin(angelPie) * Mathf.Cos(angelFhi) + cameraPostion.z
            );   

            transform.position = pos;
            transform.LookAt(cameraLookAt);
        }
    }
}
```

Inspectorの画像から、CameraにこのComponentの記事を追加しているだけという事が分かると思います。
![Qita記事1.png.jpg](https://qiita-image-store.s3.amazonaws.com/0/153021/f0836a4b-babe-6594-4d87-3ff1059407c6.jpeg)



## 最後に
3DViewerも2DViewerも作ったのですが、基本用いる式は同じですが、インターフェース的に、全く同じやり方は出来ない為、少し実現方法を変える必要があります。例えば、回転をカメラに適用してしまうと背景もクルクル回る為、操作している人の気分を害してしまいそうだとか。
時間が取れたら、2DViewer記事も上げていきたいと思います。