見出し画像

UnityのSceneViewカメラを扱う時に知っておきたい事をまとめてみた

 Unity EditorにあるSceneView。ゲームの世界を構築するUnityの中核となる画面です。このSceneViewはゲームとは別の特別なカメラ(SceneViewカメラ)でワールドを撮影し投影しています。

 普段皆さんは実はこのSceneViewカメラをマウスで操作しているのですが、Editorツールを作る時にこのSceneViewカメラをスクリプトから直接操作したくなる事があります。所がこのカメラを扱うにはゲーム用のカメラと全然違う概念が必要なんです。知らずに触ると動かないし思った方向を見ないしで「???」となります。

 そこで今回はSceneViewカメラの操作についてピックアップしようと思います。

SceneViewクラス

 SceneViewの画面はSceneViewクラスが管理しています。現在アクティブなSceneViewはstaticなSceneView.lastActiveSceneViewプロパティで取得できます。

SceneViewカメラの位置を可視化してみる

 SceneView画面を映し出しているカメラはSceneView.cameraプロパティで取得できます。普通のCameraクラスなので、transformからその位置や向きを取得可能です。

 なので例えばこんなEditowWindowを作るとSceneViewのカメラの位置を視覚化できます:

using UnityEngine;
using UnityEditor;

public class SceneViewCameraWindow : EditorWindow
{
    Vector3 cameraPos_ = Vector3.zero;

    [ MenuItem("Tool/SceneView Camera Window")]
    static void create() {
        var window = GetWindow<SceneViewCameraWindow>();
        window.Show();
    }

    private void OnEnable() {
        // SceneViewが更新された時にonSceneViewGUIメソッドが呼ばれるようにする
        SceneView.duringSceneGui += onSceneViewGUI;
    }

    private void OnDisable() {
        SceneView.duringSceneGui -= onSceneViewGUI;
    }

    private void OnGUI() {
        EditorGUILayout.Vector3Field( "Camera position", cameraPos_ );
    }

    void onSceneViewGUI(SceneView sceneView) {
        // SceneViewカメラの位置を格納
        cameraPos_ = sceneView.camera.transform.position;

        // EditorWindow再描画
        Repaint();
    }
}

 SceneViewで移動するとCamera positionの座標が変化します。

カメラ位置は直接指定できない!「pivot」が肝

 さて、Cameraが取れるのであればtransform.positionに位置を指定すれば座標が変わるはず…、っと思うかもしれません。しかし、これが全く動かないんです!設定自体はできるのですが、すぐさまSceneViewが自身のパラメータで上書きしてしまうんです。

 試しに先のSceneViewCameraWindowのOnGUIメソッドにボタンを追加して、カメラの座標を(0,0,0)にしてみます:

    private void OnGUI() {
        EditorGUILayout.Vector3Field( "Camera position", cameraPos_ );

        if (GUILayout.Button("位置セット")) {
            var camera = SceneView.lastActiveSceneView.camera;
            camera.transform.position = Vector3.zero;
        }
    }

 [位置セット]ボタンを押してもCamera pisitionの座標は反応してくれませんし、SceneViewの見た目も変化しません。実はSceneViewカメラの位置を変更する別の方法があるんです。

SceneViewカメラの構成

 SceneViewカメラはSceneViewクラスにがっつり紐づいているため、SceneViewが提供する仕組みを通さないとまともに操作できません。

 まず先にSceneViewが想定しているカメラの構成をご覧ください:

この模式図、SceneViewカメラを扱う上で極めて大切です。

 ヒエラルキーにあるゲーム用のCameraと違い、SceneViewのカメラは常に「pivot(注視点)」を見ています。このpivotはVector3型でSceneView.pivotプロパティで取得そして設定できます。

カメラの位置はpivotで間接的に設定

 SceneViewカメラの位置はこのpivotを設定して間接的に指定します。pivotを移動するとSceneViewクラスはカメラの向き(forward)と距離(distance)を元に、上の模式図に合うように紐づいているSceneViewカメラの位置と向きを設定します。

 先のOnGUIメソッドにpivot位置を表示する項目とpivot位置を指定する項目を追加してみましょう:

    private void OnGUI() {
        EditorGUILayout.Vector3Field( "Camera position", cameraPos_ );
        EditorGUILayout.Vector3Field( "Pivot", pivot_ );

        GUILayout.Space( 25 );

        // pivot位置指定
        pivotPos_ = EditorGUILayout.Vector3Field( "Pivot位置指定", pivotPos_ );
        if (GUILayout.Button("セット")) {
            var sceneView = SceneView.lastActiveSceneView;
            sceneView.pivot = pivotPos_;
        }
    } 
 
    void onSceneViewGUI(SceneView sceneView) {
        // SceneViewカメラの情報を格納
        cameraPos_ = sceneView.camera.transform.position;
        pivot_ = sceneView.pivot;

        // EditorWindow再描画
        Repaint();
    }

 SceneView上で移動したり視点をぐるぐる回すとpivotの座標が目まぐるしく動くのが確認できます。視点の回転がpivotを動かすことで実現されている事がわかりますね。

 数値指定でpivotを設定してみましょう。ターゲットが欲しいので、ヒエラルキーに適当なCubeを配置し、座標を例えば(5,0,0)に設定します。[Pivot位置指定]にも同じ(5,0,0)を入力して[セット]を押してみて下さい。すると、

カメラがどんな方向を向いていても、どこに移動していても、ぐい~っとカメラが移動して画面の真ん中にCubeが表示されます。

SceneViewカメラの視線を変更する

 pivot位置を指定すればその位置をカメラが見てくれるのはわかりました。ここで注意なのはカメラが回転してその方向を向くのではなくて、あくまでも平行移動してpivotを捉えるという事です。

 ではカメラの視線を変更するにはどうするか?これはSceneView.LookAtメソッドを使います:

var sceneView = SceneView.lastActiveSceneView;
sceneView.LookAt( pivot_, Quaternion.LookRotation( forwardVec_) );

 第1引数にはpivotの位置を指定します。これ気を付けて下さい、カメラの位置ではありません。第2引数には視線を表すQuaternionを渡します。上のようにLookRotationメソッドを通す事で視線方向を表すQuaternionを得られます。

 これで第2引数の姿勢でpivotを向くように(そして今のdistance距離で)カメラの位置が調整されます。もうカメラの位置はおまけみたいな感じですよね(^-^;

pivotまでの距離を変更する…のが面倒くさい!

カメラを引く2つの方法

 pivotを見ているSceneViewカメラを、そのpivotを見たまま後ろに引きたいとします。

 方法の一つはpivotをカメラの視線方向の逆向きに移動させます。カメラは常にpivotを見るので、結果としてpivotと一緒に視線の真後ろに移動する事になります。これはUnityのSceneView上で右クリックしながら[S]キーを押した時の操作と同じです。

 ここまでの説明でSceneViewカメラの向き(forward)も取れますし、pivot位置も変更する事もできますから、この方法はもう簡単に実装できますね。

 もう一つの方法は下図にある「distance」を伸ばす事です:

 こちらの方法はpivot位置を変えずにカメラの位置だけ引く(もしくはズームする)事に相当します。これはSceneView画面上でマウスホイールを手前にコロコロした時の挙動と一緒です。

distanceは直接変更できない!

 SceneViewにはcameraDistanceというpivotまでの距離を表すプロパティが用意されています。しかしこのcameraDistanceプロパティはgetしかありません。setが公開されていないので距離を設定できないんです。これはUnityが明確に「distanceを直接指定するな」と言っています。

 ではdistanceを変更するにはどうするか?答えは上図の「size」を変えるんです。SceneViewのカメラはpivotを中心とし半径sizeの球を常に真ん中に捉えようとします。なのでsizeを今の設定値より小さくすれば、相対的にdistanceも短くなってズームに、sizeを大きくすればdisntaceが長くなってカメラが引くんです。

 こういう設計なのはSceneViewのカメラの意味や使われ方を考えると一理あるのは理解できるのですが…正直直観的ではありません。実際ツールを作っていた時「めんどいのぉ…」って思いましたww

 指定のdistanceになるようsizeを変更するには三角関数が必要です。模式図のfov(視野角:Feald of View)はSceneView.cameraSettings.fieldOfViewにあります。distanceは直角三角形の斜辺に当たるので、以下の関係式が成り立つのがわかります:

という事で、上式のdistanceに距離を指定すると必要なsizeが割り出せます。OnGUIに機能を追加しましょうか:

private void OnGUI() {

    ...

	distanceLen_ = EditorGUILayout.FloatField( "距離指定", distanceLen_ );
	if ( GUILayout.Button( "セット" ) ) {
	    var sceneView = SceneView.lastActiveSceneView;
	    float fov = sceneView.cameraSettings.fieldOfView;
	    sceneView.size = distanceLen_ * Mathf.Sin( fov * 0.5f * Mathf.Deg2Rad );
	}
}

カメラの位置を変えずに視点を変えたい

 カメラの位置を固定して、視点となるpivotを変更して、カメラをそちらに向かせたい。こういう用途は恐らく多いはずです。いわゆるLookAtってやつですね。でもSceneView.LookAtメソッドはpivot位置を設定する仕様になっているので、通常のLookAtとは概念が違います。そのため回りくどい事を考えないと通常のLookAtを実現できません。…めんどいのぉ…

 カメラの位置をCとし、pivotの位置をPとします。ここからカメラの向きとなるforwardは、

 となります(正規化が必要)。これをSceneView.LookAtメソッドに突っ込むと、そちらの方向を向いてはくれるのですが、distanceが未設定なのでカメラがそちらの方向に移動してしまいます:

 最初右を向いていたSceneViewカメラをLookAtメソッドで右下のpivot位置に向くようにすると、disntace(size)がかわらないため、pivotからdistanceの長さだけ引いた位置に移動しちゃうんです。そのためLookAtした後distanceを調整して元のカメラ位置に合わせる必要があります。

 元のカメラの位置Cはわかっていますから、必要なdistanceは新しいpivotからCまでの長さになります。そのdistanceに該当するsizeを先の式で計算してSceneView.sizeに与えればいいんです。

 異常を踏まえて、カメラの位置をそのままに、pivotの方向に向き直すlookAtPivotメソッドを作るとこんな感じになります:

   void lookAtPivot(SceneView sceneView, Vector3 pivot) {
        var cameraPos = sceneView.camera.transform.position;
        var forward = pivot - cameraPos;
        float distance = forward.magnitude;
        float fovRad = sceneView.cameraSettings.fieldOfView * 0.5f * Mathf.Deg2Rad;
        float size = distance * Mathf.Sin( fovRad );

        sceneView.LookAt( pivot, Quaternion.LookRotation( forward.normalized ) );
        sceneView.size = size;
    }

ごにょごにょやっていますが、ここまでの前知識が身についていればもう大丈夫ですよね(^-^)

SceneViewカメラ拡張メソッド

 今回はSceneViewカメラを操作する時の概念や操作方法をざっとまとめてみました。

 最後にSceneViewカメラの設定をしやすくするSceneViewクラスの拡張メソッドを公開します。あまり厳密には作っていませんが、よろしければ参考にしてみて下さい(^-^)

using UnityEngine;
using UnityEditor;

public static class SceneViewEx
{
    // pivotを見据えたままカメラだけ移動
    // カメラの視線が変わる
    // positionがpivotと一致している場合は今のカメラの向きで1m引いた位置に強制
    public static void MoveCamera( this SceneView sceneView, Vector3 position ) {
        var pivot = sceneView.pivot;
        if ( pivot == position ) {
            position = pivot - sceneView.camera.transform.forward;
        }
        var v = pivot - position;
        var distance = v.magnitude;
        var forward = v.normalized;
        sceneView.LookAt( pivot, Quaternion.LookRotation( forward ), CalcSizeFromDistance( sceneView, distance ) );
    }

    // カメラ位置からpivot位置を見る
    public static void LookAtPivot( this SceneView sceneView, Vector3 cameraPosition, Vector3 pivot ) {
        sceneView.pivot = pivot;
        MoveCamera( sceneView, cameraPosition );
    }

    // カメラ位置を変えずにpivotの方向を向く
    public static void TurnToPivot( this SceneView sceneView, Vector3 pivot ) {
        LookAtPivot( sceneView, sceneView.camera.transform.position , pivot );
    }

    // ゲームカメラの位置と方向をSceneViewカメラの値に合わせる
    public static void SetTransformToGameCamera( this SceneView sceneView, Camera camera ) {
        camera.transform.position = sceneView.camera.transform.position;
        camera.transform.rotation = sceneView.camera.transform.rotation;
    }

    // 距離を設定
    public static void SetCameraDistance( this SceneView sceneView, float distance ) {
        sceneView.size = CalcSizeFromDistance( sceneView, distance );
    }

    // 距離に対応するsizeを計算
    public static float CalcSizeFromDistance( this SceneView sceneView, float distance ) {
        return Mathf.Abs( distance ) * Mathf.Sin( sceneView.cameraSettings.fieldOfView * 0.5f * Mathf.Deg2Rad );
    }
}

ではまた(^-^)/

いいなと思ったら応援しよう!