はじめに
UnityEditorのSceneViewを触っていて、今見ている画面と同じになるカメラ位置をゲームで使いたい!と思うことがあると思います。InspectorでCameraのパラメータをポチポチ手入力するのは直感的でないし、カメラ位置設定専用のツールを作るのも面倒ですからね。
さらに、カメラ位置を複数登録してゲーム中にパッと変更できるようにしたいし、それを自分以外の人が設定できるようにして楽したい…ということで、そのために必要な、SceneViewとGameViewのカメラ情報を相互変換する方法を調べてみました! (不安にさせる導入)
Editorから手動で行う方法
まず、Editorから手動で行う方法です。HierarchyウィンドウでCamera(例えば"Main Camera")を選択して右クリックメニューから"Align With View"を実行すると、現在のSceneViewのカメラ位置や方向が選択中のCameraに設定されます。
逆に、Cameraに設定されている位置と方向をSceneViewに設定したいときは、同じく対象のCameraを選択して"Align View to Selected"を実行します。以上です。
ただ、ここで反映されるのは位置と方向だけなので、例えばFOV(Field of View)などカメラの他の情報はあらかじめ合わせておく必要があります。SceneViewのFOVはSceneView右上のカメラアイコン、GameViewのFOVはCameraのInspectorから変更できます。
そして、この方法の場合はCameraがシーンにあらかじめ置いてある必要があります。まぁ、普通は置いてあるんですが、ゲーム開始時じゃなくて何かのタイミングで視点を切り替えたいとか、複数持ちたいとかになると、ひと工夫必要です。
スクリプトから行う方法
ここからが本題です。それをスクリプトから行えるようにして、さらに便利にしたいですよね。
SceneView から GameView
SceneViewからGameViewへは簡単です。同じCameraクラスを持つのでコピーするだけです。ここでは位置と方向とFOVだけ反映させていますが、他にもカメラにはいろいろなパラメータがあるので、必要なものがあれば各自実装してください。(とはいえ、用途的に他はいらない気もする)
var gameCamera = Camera.main;
var sceneCamera = SceneView.lastActiveSceneView.camera;
gameCamera.transform.position = sceneCamera.transform.position;
gameCamera.transform.rotation = sceneCamera.transform.rotation;
gameCamera.fieldOfView = sceneCamera.fieldOfView;
GameView から SceneView
問題は、逆にGameViewからSceneViewのカメラにパラメータを設定する場合です。単純に逆にすれば良さそうなものですが、SceneViewが内部でCameraのパラメータを別の値で上書きしてしまうようで、実質的にSceneView.cameraはパラメータの参照しかできません。
そこで使うAPIがSceneView.LookAt(LookAtDirect)なんですが、このLookAt、一般的なビュー行列のLookAtとは違うもので、「ターゲット位置」と「方向」は指定できても「カメラ位置」が指定できません。さらに「サイズ(size)」という謎のパラメータもあります。が、なんとかしてみます。
// 注・2回実行しないと想定通りの動きにならない!
var gameCamera = Camera.main;
var sceneView = SceneView.lastActiveSceneView;
sceneView.LookAtDirect(gameCamera.transform.position + gameCamera.transform.rotation * Vector3.forward * sceneView.cameraDistance, gameCamera.transform.rotation, 10.0f);
sceneView.cameraSettings.fieldOfView = gameCamera.fieldOfView;
ターゲット位置をカメラ位置(Camera.transform.position)と方向(Camera.transform.rotation)とカメラ距離(SceneView.cameraDistance)から計算してLookAtしています。sizeがどういう単位のパラメータなのかさっぱり分からないので、SceneViewのデフォルト値である10.0fに設定してあります。(いいのか?)
うまくいってそうではあるんですが、2回実行しないと想定通りにならないんですよね…。そこで、いろいろこねくり回したところ、ちゃんと一発で動く実装にたどり着きました。
var gameCamera = Camera.main;
var sceneView = SceneView.lastActiveSceneView;
sceneView.cameraSettings.fieldOfView = gameCamera.fieldOfView;
sceneView.size = 10.0f;
sceneView.pivot = gameCamera.transform.position + gameCamera.transform.rotation * Vector3.forward * sceneView.cameraDistance;
sceneView.rotation = gameCamera.transform.rotation;
LookAtではなくsize,pivot,rotationを別々に、しかもこの順番で設定するのがキモです。おそらくfieldOfViewとsizeを変更することでcameraDistanceが再計算されるので、それを使ってpivotとrotationを設定すればOKということのようです。LookAtだとsizeも同時に指定するのでcameraDistanceが以前のまま計算してしまうのかもしれません(cameraDistanceはget専用)。知らんけど。
これでスクリプトから出来るということが分かったんですが、ここまでだと手動でEditorを操作するのと同じなので、例えば位置と方向などのVector3やQuaternionを持つクラスを作ってそれを設定するようにしたり、複数持って選択出来たりするようにすると、夢が広がりますね。(…というサンプルを作ったのでのちほど)
補足
Editorから手動で行う方法については、実はそのものずばりのAPI、SceneView.AlignWithViewとSceneView.AlignViewToObject("Align View to Selected"のこと)があります。ただ、困ったことに引数がTransformクラスで、何が困るのかと言うとTransformはnewできないのと、ダミーのGameObjectを作ってTransformを流用するのもちょっとなぁ…ということで今回は上記の方法を取りました。
おわりに
あまりSceneViewのカメラに触れた記事にはたどり着かなかったのでいろいろ試してみたんですが(SceneView→GameViewはあるけど、GameView→SceneViewがない)、私が何か見落としている可能性は残りつつも、ここに記録する次第です。
サンプル(CameraBookmark)
ということで、視点をブックマークみたいに複数登録してゲーム内で簡単に選択できたりしたら便利だろうなーと思い、SceneViewからカメラ情報を取得して記録、それをGameViewのCameraに反映させたり、逆にSceneViewのカメラに戻す機能を、ScriptableObjectとCustomPropertyDrawerで実装してみました。
導入
CameraBookmark.csはお好きなフォルダ、CameraBookmarkDrawer.csはお好きなEditorフォルダに入れてください。("#if UNITY_EDITOR"使いたくない派)
使い方
AssetsメニューかProjectウィンドウの右クリックメニューで"Create→Camera Bookmarks"を選択して、New Camera Bookmarksというアセットを作成します。これを選択してInspectorで"List is empty"となっているところを"+"ボタンを押してデータを作り、以下で調整します。
- Name : 任意の識別用の名前
- Position, Rotation, Field Of View : 位置と方向とFOV (直接入力してもOK)
- Captureボタン : SceneViewからパラメータをコピー
- Applyボタン : SceneViewへパラメータをコピー
- Resetボタン : SceneViewをデフォルト位置に戻す (このアセットの値は維持)
実際にゲーム内で使うには、このアセット(CameraBookmarksクラス)の参照を持って、スクリプトからApply()で呼び出してください。また、CameraBookmarkクラスをMonoBehaviourとかのメンバにして使うのもありです。PrpertyDrawerなので、MonoBehaviourのInspectorでもちゃんとボタンが表示されます。(なのでカスタムインスペクターにしなかった)
ここではCamera.mainを直接操作していますが、そこはサンプルということで、そこから先はご自由にどうぞ!
using UnityEngine;
[System.Serializable]
public class CameraBookmark
{
public string Name;
public Vector3 Position;
public Vector3 Rotation;
public float FieldOfView;
public void Apply(Camera camera = null)
{
if (camera == null) camera = Camera.main;
camera.transform.position = Position;
camera.transform.eulerAngles = Rotation;
camera.fieldOfView = FieldOfView;
}
}
[CreateAssetMenu(menuName = "Camera Bookmarks")]
public class CameraBookmarks : ScriptableObject
{
[SerializeField]
private CameraBookmark[] _cameraBookmarks;
public bool Apply(string name, Camera camera = null)
{
var bookmark = System.Array.Find(_cameraBookmarks, b => b.Name == name);
if (bookmark != null)
{
bookmark.Apply(camera);
return true;
}
return false;
}
}
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(CameraBookmark))]
public class CameraBookmarkDrawer : PropertyDrawer
{
private enum ButtonType
{
None,
Capture,
Apply,
Reset
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property, true) + (property.isExpanded ? EditorGUIUtility.singleLineHeight : 0.0f);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.PropertyField(position, property, true);
if (property.isExpanded)
{
var propertyHeight = EditorGUI.GetPropertyHeight(property, true);
position.y += propertyHeight;
position.height -= propertyHeight;
position.width /= 3;
var buttonType = ButtonType.None;
if (GUI.Button(position, "Capture")) buttonType = ButtonType.Capture;
position.x += position.width;
if (GUI.Button(position, "Apply")) buttonType = ButtonType.Apply;
position.x += position.width;
if (GUI.Button(position, "Reset")) buttonType = ButtonType.Reset;
if (buttonType != ButtonType.None)
{
var sceneView = SceneView.lastActiveSceneView;
var pos = property.FindPropertyRelative("Position");
var rot = property.FindPropertyRelative("Rotation");
var fov = property.FindPropertyRelative("FieldOfView");
switch (buttonType)
{
case ButtonType.Capture:
var name = property.FindPropertyRelative("Name");
if (string.IsNullOrEmpty(name.stringValue)) name.stringValue = $"{label.text}";
pos.vector3Value = sceneView.camera.transform.position;
rot.vector3Value = sceneView.camera.transform.rotation.eulerAngles;
fov.floatValue = sceneView.camera.fieldOfView;
break;
case ButtonType.Apply:
sceneView.cameraSettings.fieldOfView = fov.floatValue;
sceneView.size = 10;
sceneView.pivot = pos.vector3Value + Quaternion.Euler(rot.vector3Value) * Vector3.forward * sceneView.cameraDistance;
sceneView.rotation = Quaternion.Euler(rot.vector3Value);
break;
case ButtonType.Reset:
sceneView.ResetCameraSettings();
sceneView.LookAtDirect(Vector3.zero, Quaternion.Euler(26.33425f, 225.0f, 0.0f), 10.0f);
break;
}
}
}
}
}
補足
上記の解説と違い、RotationはQuaternionではなくVector3(オイラー角)で実装してあります。クォータニオンを直接読める人はいないと思うので…。
Resetボタンですが、SceneViewのカメラをデフォルト位置に戻すAPIはないようなので、まっさらな状態の実測値を設定することでリセットとしています(なので値の意図は分からない…)。SceneViewのカメラ位置はEditorを終了させても勝手に保存されて復元されますが、SceneViewを一旦閉じて開き直すとデフォルト位置に戻るようです。
あと、SceneViewが存在しない場合の動作は未確認です。SceneView.lastActiveSceneViewがnullになりそう。SceneViewとGameViewのアスペクト比が違う場合も未確認…。