前情提要
在上一篇文章中,读者应该能看到scene窗口的神奇情况
这样的
这样的
诶,怎么摄像机歪了?是unity自己抽风了,累计了太多的浮点误差导致摄像机歪了吗?
非也非也,为了方便开发太空主题的游戏,自然需要一个6自由度的Scene窗口的相机,否则在scene窗口内在星球表面飞行摄像机总是会相对于星球的局部平面“歪掉”,还是挺麻烦的。下面我写的脚本晕3d的人慎用。
效果演示 && 操作指南
默认情况下:wasd移动,空格上升,c下降
6自由度模式:wasd移动,qe滚转
按住alt+鼠标左键拖动,围绕摄像机的Pivot旋转,和unity默认scene窗口功能一样,但是可以在6自由度和默认之间切换
按住alt+鼠标中键拖动,围绕当前摄像机的z轴旋转
ctrl+1,重置摄像机的上方向为默认,回正摄像机的roll
ctrl+2,设置摄像机的y轴方向为当前上方向
ctrl+3,解锁/锁定摄像机roll
实现原理:
(1)拦截unity scene窗口的输入 SceneView.duringSceneGui+=一个接受镜头控制输入的函数,通过 Event e = Event.current拦截unity在scene窗口内的输入,处理完输入信息后使用e.Use()吃掉该事件,这样unity就不会自己处理这个输入事件了,把输入的信息保存在静态类内部,这个事件的更新时间是不固定的,如果在这个函数内实现摄像机旋转和移动,就会一卡一卡的,笔者已经踩过坑了。 SceneView.update+=一个每帧更新的函数,处理上面接收到的输入信息,
(2)scene窗口的摄像机有一个Pivot,可以把pivot想象成拿自拍杆的手,对 sceneView.rotation的修改可以很方便地让摄像机的围绕Pivot旋转,但是要如何实现右键围绕摄像机自身旋转呢?很粗暴的想法就是,既然scene窗口每一次旋转之后,摄像机的位置都会偏离到另一个地方,那我通过改变pivot强行把摄像机拉回到原来的位置,不就行了?我不确定unity内部默认的右键旋转功能是不是这么实现的,但是scene窗口里的能用就行。
Vector2 delta = e.mousePosition - lastMousePos;
lastMousePos = e.mousePosition;
//---------绕着场景摄像机自身的x、y轴旋转--------------
Quaternion rotToPivot = sceneView.rotation;
Vector3 camPos = sceneView.camera.transform.position;
Vector3 pivotPos = sceneView.pivot;
// float distance = sceneView.cameraDistance;
// 改变相机看向pivot的方向
Quaternion deltaRot;
if (lockZAxis)
{
deltaRot = Quaternion.AngleAxis(delta.y * 0.2f, sceneView.camera.transform.right)
* Quaternion.AngleAxis(delta.x * 0.2f, currUpDir);
}
else
{
deltaRot = Quaternion.AngleAxis(delta.y * 0.2f, sceneView.camera.transform.right)
* Quaternion.AngleAxis(delta.x * 0.2f, sceneView.camera.transform.up);
}
Quaternion newRotToPivot = deltaRot * rotToPivot;
// 计算原本相机在旋转后会到达的位置
Vector3 newCamPos = pivotPos + deltaRot * (camPos - pivotPos);
//相应地改变pivot的位置,使得相机本身的位置保持不变
Vector3 newPivot = pivotPos - (newCamPos - camPos);
sceneView.rotation = newRotToPivot;
sceneView.pivot = newPivot;
完整代码
#define USE_CUSTOM_SCENE_MOVE_CONTROL
#define USE_EXP_SPEED
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
[InitializeOnLoad]
public static class SceneViewRoller
{
#if USE_CUSTOM_SCENE_MOVE_CONTROL
static bool lockZAxis = false; //锁定摄像机的Z轴的时候用于平面编辑,如果解锁,则适合用于星球表面的编辑
//当前摄像机的上方向向量,可以在运行时更改
static Vector3 currUpDir = Vector3.up;
static readonly float shift_acceleration = 4f; //按下LeftShift键时的加速度倍数
static readonly Dictionary<KeyCode, bool> keyPressMap = new Dictionary<KeyCode, bool>()
{
{ KeyCode.W, false },//是否按下了W键
{ KeyCode.S, false },//是否按下了S键
{ KeyCode.A, false },//是否按下了A键
{ KeyCode.D, false },//是否按下了D键
{ KeyCode.Q, false },//是否按下了Q键,用于向左滚转
{ KeyCode.E, false },//是否按下了E键,用于向右滚转
{ KeyCode.Space, false },//是否按下了Space键,用于上升
{ KeyCode.C, false },//是否按下了C键,用于下降
{ KeyCode.LeftShift, false },//是否按下了LeftShift键,用于加速
};
static readonly float rollSpeed = 40f;
static bool isMoving = false;
static Vector2 lastMousePos;
static readonly float moveAcceleration = 10f;
static readonly float MOVE_SPEED_LIMIT = 200f;
static readonly float speedAtten = 0.5f;
static Vector3 moveVelocity = Vector3.zero;
static SceneViewRoller()
{
last_second = Time.realtimeSinceStartup;
// 每帧刷新
SceneView.duringSceneGui += OnSceneGUI;
EditorApplication.update += OnUpdate;
}
static double last_second;
static float holdTime = 0f;
private static void OnUpdate()
{
double this_second = EditorApplication.timeSinceStartup;
double realDeltaTimeD = (this_second - last_second);
float realDeltaTime = (float)(realDeltaTimeD);
last_second = this_second;
//Debug.Log("Editor is updating,");
var sceneView = SceneView.lastActiveSceneView;
//根据WASD键计算加速的方向
Vector3 moveDir = Vector3.zero;
bool is_wasd = false;
foreach(var key in keyPressMap.Keys)
{
if(keyPressMap[key])
{
is_wasd = true;
switch(key)
{
case KeyCode.W:
moveDir += sceneView.camera.transform.forward;
break;
case KeyCode.S:
moveDir -= sceneView.camera.transform.forward;
break;
case KeyCode.A:
moveDir -= sceneView.camera.transform.right;
break;
case KeyCode.D:
moveDir += sceneView.camera.transform.right;
break;
case KeyCode.Space:
moveDir += sceneView.camera.transform.up;
break;
case KeyCode.C:
moveDir -= sceneView.camera.transform.up;
break;
case KeyCode.Q:
if(!lockZAxis)
{//如果没有锁定Z轴,才允许滚转,否则会导致万向节死锁
sceneView.rotation = Quaternion.AngleAxis(rollSpeed * realDeltaTime, sceneView.camera.transform.forward) * sceneView.rotation;
}
break;
case KeyCode.E:
if(!lockZAxis)
{
sceneView.rotation = Quaternion.AngleAxis(-rollSpeed * realDeltaTime, sceneView.camera.transform.forward) * sceneView.rotation;
}
break;
}
}
}
moveDir = moveDir.normalized;
if(!is_wasd)
{
holdTime = 0f;
//计算速度衰减
moveVelocity *= speedAtten;
if(moveVelocity.magnitude < 0.01f)
{
return;
}
sceneView.pivot += moveVelocity * realDeltaTime;
}
else
{
holdTime += realDeltaTime;
//根据moveDir和加速度计算新的速度
var delta_v = moveAcceleration * realDeltaTime * moveDir;
// Debug.Log("delta_v: " + delta_v);
// moveVelocity += delta_v;
moveVelocity = moveVelocity.magnitude * moveDir + delta_v * ((moveVelocity.magnitude < 1E-5f) ? 1f : Vector3.Dot(moveDir, moveVelocity.normalized));
moveVelocity = Vector3.ClampMagnitude(moveVelocity, MOVE_SPEED_LIMIT);
#if USE_EXP_SPEED
// 指数增长速度
float k = 0.1f;
float speed = MOVE_SPEED_LIMIT * (1f - Mathf.Exp(-k * holdTime));
sceneView.pivot += (keyPressMap[KeyCode.LeftShift] ? shift_acceleration : 1f) * speed * realDeltaTime * moveDir;
#else
//根据moveVelocity计算pivot移动位置
// Debug.Log("moveVelocity: " + moveVelocity);
sceneView.pivot += (keyPressMap[KeyCode.LeftShift] ? shift_acceleration : 1f) * realDeltaTime * moveVelocity;
#endif
}
}
private static void OnSceneGUI(SceneView sceneView)
{
Event e = Event.current;
// Rect sceneRect = sceneView.position; // SceneView 的矩形区域
//=========================锁定Z轴相关=========================
if (e.control && e.type == EventType.KeyDown && e.keyCode == KeyCode.Alpha1)
{//ctrl + 1,重置当前摄像机上方向为Vector3.up,并同时把摄像机摆正。 如果不摆正的话,会导致摄像机的Z轴和场景中的Z轴不一致,当摄像机的欧拉角的Z轴为90度时,甚至会导致万向节死锁
Quaternion rot = sceneView.rotation;
rot.eulerAngles = new Vector3(rot.eulerAngles.x, rot.eulerAngles.y, 0f);
sceneView.rotation = rot;
currUpDir = Vector3.up;
e.Use(); // 吃掉事件
SceneViewMsg("重置当前摄像机上方向为Vector3.up,回正摄像机Z轴");
}
if (e.control && e.type == EventType.KeyDown && e.keyCode == KeyCode.Alpha2)
{//ctrl + 2,设置currUpDir为sceneView.camera.transform.up
currUpDir = sceneView.camera.transform.up;
lockZAxis = true;
e.Use(); // 吃掉事件
SceneViewMsg("设置currUpDir为摄像机当前的上方向,并把Z轴锁定");
}
if (e.control && e.type == EventType.KeyDown && e.keyCode == KeyCode.Alpha3)
{//ctrl + 3,开关lockZAxis
lockZAxis = !lockZAxis;
e.Use(); // 吃掉事件
SceneViewMsg(lockZAxis ? "Z轴现在被锁定" : "Z轴现在被解锁");
}
//=========================场景中自由移动相关=========================
// 开始右键自由移动
if (e.type == EventType.MouseDown && e.button == 1)
{
isMoving = true;
lastMousePos = e.mousePosition;
e.Use(); // 阻止 Unity 默认右键旋转
// Cursor.lockState = CursorLockMode.Locked;
// Cursor.visible = false;
}
// 停止右键自由移动
if (e.type == EventType.MouseUp && e.button == 1)
{
isMoving = false;
e.Use();
UnableAllButton();
// Cursor.lockState = CursorLockMode.Locked;
// Cursor.visible = true;
}
// 自定义右键转动摄像机的逻辑
if (isMoving && e.type == EventType.MouseDrag && e.button == 1)
{
// EditorGUIUtility.AddCursorRect(sceneRect, MouseCursor.MoveArrow);
Vector2 delta = e.mousePosition - lastMousePos;
lastMousePos = e.mousePosition;
//---------绕着场景摄像机自身的x、y轴旋转--------------
Quaternion rotToPivot = sceneView.rotation;
Vector3 camPos = sceneView.camera.transform.position;
Vector3 pivotPos = sceneView.pivot;
// float distance = sceneView.cameraDistance;
// 改变相机看向pivot的方向
Quaternion deltaRot;
if (lockZAxis)
{
deltaRot = Quaternion.AngleAxis(delta.y * 0.2f, sceneView.camera.transform.right)
* Quaternion.AngleAxis(delta.x * 0.2f, currUpDir);
}
else
{
deltaRot = Quaternion.AngleAxis(delta.y * 0.2f, sceneView.camera.transform.right)
* Quaternion.AngleAxis(delta.x * 0.2f, sceneView.camera.transform.up);
}
Quaternion newRotToPivot = deltaRot * rotToPivot;
// 计算原本相机在旋转后会到达的位置
Vector3 newCamPos = pivotPos + deltaRot * (camPos - pivotPos);
//相应的改变pivot的位置,使得相机本身的位置保持不变
Vector3 newPivot = pivotPos - (newCamPos - camPos);
// Debug.LogFormat("newCamPos: {0}", newCamPos);
sceneView.rotation = newRotToPivot;
sceneView.pivot = newPivot;
e.Use();
//sceneView.Repaint();
}
//记录键盘输入,将在Update中用于自由移动
if (isMoving && keyPressMap.ContainsKey(e.keyCode))
{
bool isKeyDown = e.type == EventType.KeyDown;
// Debug.LogFormat("按下{0}键",e.keyCode.ToString());
keyPressMap[e.keyCode] = isKeyDown;
e.Use();
}
//=========================自定义Alt旋转相关=========================
//开始alt旋转
if (e.alt && e.type == EventType.MouseDown && e.button == 0)
{
lastMousePos = e.mousePosition;
e.Use();
}
//alt旋转中
if (e.alt && e.type == EventType.MouseDrag && e.button == 0)
{
Vector2 delta_mouse = e.mousePosition - lastMousePos;
lastMousePos = e.mousePosition;
if (lockZAxis)
{//在Z轴锁定的状态下:alt旋转围绕pivot旋转时,以currUpDir为北极方向
Quaternion rotToPivot = sceneView.rotation;
Quaternion deltaRot = Quaternion.AngleAxis(delta_mouse.x * 0.2f, currUpDir)
* Quaternion.AngleAxis(delta_mouse.y * 0.2f, sceneView.camera.transform.right);
Quaternion newRotToPivot = deltaRot * rotToPivot;
sceneView.rotation = newRotToPivot;
}
else
{//在Z轴解锁的状态下:自由旋转,类似戴森球计划里的星球旋转方式,可以通过Q和E旋转摄像机的Z轴
Quaternion rotToPivot = sceneView.rotation;
Quaternion deltaRot = Quaternion.AngleAxis(delta_mouse.x * 0.2f, sceneView.camera.transform.up)
* Quaternion.AngleAxis(delta_mouse.y * 0.2f, sceneView.camera.transform.right);
Quaternion newRotToPivot = deltaRot * rotToPivot;
sceneView.rotation = newRotToPivot;
}
e.Use();
}
//alt+鼠标中键按下拖拽调整摄像机Z轴
if (e.alt && e.type == EventType.MouseDown && e.button == 2)
{
lastMousePos = e.mousePosition;
e.Use();
}
if (e.alt && e.type == EventType.MouseDrag && e.button == 2)
{
float delta_mouse = (e.mousePosition - lastMousePos).x;
lastMousePos = e.mousePosition;
//调整摄像机的Z轴方向
Quaternion rotToPivot = sceneView.rotation;
Quaternion deltaRot = Quaternion.AngleAxis(delta_mouse * 0.2f, sceneView.camera.transform.forward);
Quaternion newRotToPivot = deltaRot * rotToPivot;
sceneView.rotation = newRotToPivot;
e.Use();
}
}
static void UnableAllButton()
{
// 创建键的副本以避免在枚举时修改集合
List<KeyCode> keys = new List<KeyCode>(keyPressMap.Keys);
foreach(var key in keys)
{
keyPressMap[key] = false;
}
}
#endif
private static void SceneViewMsg(string msg)
{
Debug.Log("SCENE_VIEW_LOG:"+msg);
}
}
完整代码
IcoSphereTerrainLODSystem: 实现了基于20面体球面网格的三角形四叉树细分星球LOD地形系统。
参考
GPU驱动的四叉树地形以及参考了这个文章的代码
地形的噪声生成、海洋和大气层渲染参考了这个仓库的代码