unity中camera摄像头控制详解
2021-05-18 00:30
在Unity的Transform中,rotation属性对应的就是欧拉角,一共分为3个轴,x、 y和z,而每一个数值对应的是绕对应的轴旋转的度数。
如上图所示,表示按照坐标顺序旋转,X轴旋转30°,Y轴旋转90°,Z轴旋转 10°。欧拉角的优点:只需使用3个值,即三个坐标轴的旋转角度;缺点:必须 严格按照顺序进行旋转(顺序不同结果就不同;容易造成“万向节锁”现象,造 成这个现象的原因是因为欧拉旋转是按顺序先后旋转坐标轴的,并非同时旋转, 所以当旋转中某些坐标重合就会发生万向节锁,这时就会丢失一个方向上的选择 能力,除非打破原来的旋转顺序或者三个坐标轴同时旋转;由于万向节锁的存在, 欧拉旋转无法实现球面平滑插值。
四元数是用于表示旋转的一种方式,而且transform中的rotation属性的数据类 型就是四元数,那么四元数该如何表示呢?从本质上来讲,四元数就是一个高阶 复数,也就是一个四维空间。话说当时十九世纪的时候,爱尔兰的数学家 Hamilton一直在研究如何将复数从2D扩展至3D,他一直以为扩展至3D应该有两个 虚部(可是他错了,哈哈)。有一天他在路上突发奇想,我们搞搞三个虚部的试 试!结果他就成功了,于是乎他就把答案刻在了Broome桥上。说到这里,也就明 白了,四元数其实就是定义了一个有三个虚部的复数w xi yj zk。记法 [w,(x,y,z)]。四元数优点:可以避免万向节锁现象;只需要一个4维的四元数就 可以执行绕任意过原点的向量的旋转,方便快捷,在某些实现下比旋转矩阵效率 更高;可以提供平滑插值;缺点:比欧拉旋转稍微复杂了一点点,因为多了一个 维度;理解更困难,不直观。四元数与欧拉角转换:
// 获取摄像机欧拉角 Vector3 angles = transform.eulerAngles; // 设置摄像头欧拉角 targetRotation.eulerAngles = new Vector3(euler.y, euler.x, 0);
现在让我们再看Update里的旋转代码:
if (Mathf.Abs(dx) > 0 || Mathf.Abs(dy) > 0) { // 获取摄像机欧拉角 Vector3 angles = transform.rotation.eulerAngles; // 欧拉角表示按照坐标顺序旋转,比如angles.x=30,表示按x轴旋转30°,dy改变引起x轴的变化 angles.x = Mathf.Repeat(angles.x + 180f, 360f) - 180f; angles.y += dx; angles.x -= dy; // 设置摄像头旋转 Quaternion rotation = Quaternion.identity; rotation.eulerAngles = new Vector3(angles.x, angles.y, 0); transform.rotation = rotation; // 重新设置摄像头位置 Vector3 position = model.position; Vector3 distance = rotation * new Vector3(0, 0, default_distance); transform.position = position - distance; }
首先我们从四元数(transform.rotation)取得欧拉角angles,由于欧拉角表示按 照坐标顺序旋转,比如angles.x=30,表示按x轴旋转30°,dy改变引起欧拉角的 x轴的变化,所以angles.y+=dx,然后再设置摄像头旋转,即设置摄像头四元数 rotation,现在明白了设置旋转的写法了吧。
下面是重点,重新设置摄像头位置,我们看到rotation*new Vector3(0,0,default_distance)这句,一个Quaternion实例和一个Vector3相乘 的运算,作用是对参数坐标点进行rotation变换,也就是说对 Vector3(0,0,default_distance)进行rotation旋转,最后一句 transform.position = position - distance,进行一个Vector3的向量计算, 最终结果就是摄像头沿着选中后的方向移动-distance的距离,就是我们要的结果。
如果对向量计算不清楚,请看下面的向量计算这节
在进行下面开发之前我们把程序西安优化一下,我们不在Update函数里直接修改 摄像头旋转和位置,而是记录旋转变化,在FixUpdate函数里设置摄像头最终的 旋转和位置,Update和FixedUpdate的区别:Update跟当前平台的帧数有关,而 FixedUpdate是真实时间,所以处理物理逻辑的时候要把代码放在FixedUpdate而 不是Update。
using UnityEngine; /** * 自由摄像头 * 2018-10-03 by flysic, 119238122@qq.com */ public class FreeCameraController : MonoBehaviour { // 模型 public Transform model; // 默认距离 private const float default_distance = 5f; // 计算移动 private Vector3 position; // 计算旋转 private Quaternion rotation; // Use this for initialization void Start () { // 旋转归零 transform.rotation = Quaternion.identity; // 初始位置是模型 position = model.position; } // Update is called once per frame void Update() { float dx = Input.GetAxis("Mouse X"); float dy = Input.GetAxis("Mouse Y"); // 鼠标右键旋转 if (Input.GetMouseButton(1)) { if (Mathf.Abs(dx) > 0 || Mathf.Abs(dy) > 0) { // 获取摄像机欧拉角 Vector3 angles = transform.rotation.eulerAngles; // 欧拉角表示按照坐标顺序旋转,比如angles.x=30,表示按x轴旋转30°,dy改变引起x轴的变化 angles.x = Mathf.Repeat(angles.x + 180f, 360f) - 180f; angles.y += dx; angles.x -= dy; // 计算摄像头旋转 rotation.eulerAngles = new Vector3(angles.x, angles.y, 0); } } } private void FixedUpdate() { // 设置摄像头旋转 transform.rotation = rotation; // 设置摄像头位置 transform.position = position - rotation * new Vector3(0, 0, default_distance); } }
最上面定义了两个私有属性,private Vector positon,private Quaternion rotation,position在Start函数里记录模型的位置(目前不变化,后面移动时要 变化),rotation用于记录Update里计算的旋转,FixedUpdate函数里根据 rotation、position、default_distance计算摄像头最终的位置。
我们操作一下发现,虽然旋转达到要求,但是操作感觉很生硬,现在给旋转加上 速度和阻尼,效果就会好很多。
using UnityEngine; /** * 自由摄像头 * 2018-10-03 by flysic, 119238122@qq.com */ public class FreeCameraController : MonoBehaviour { // 模型 public Transform model; // 旋转速度 public float rotateSpeed = 32f; public float rotateLerp = 8; // 计算移动 private Vector3 position; // 计算旋转 private Quaternion rotation, targetRotation; // 默认距离 private const float default_distance = 5f; // Use this for initialization void Start () { // 旋转归零 transform.rotation = Quaternion.identity; // 初始位置是模型 position = model.position; } // Update is called once per frame void Update() { float dx = Input.GetAxis("Mouse X"); float dy = Input.GetAxis("Mouse Y"); // 鼠标右键旋转 if (Input.GetMouseButton(1)) { dx *= rotateSpeed; dy *= rotateSpeed; if (Mathf.Abs(dx) > 0 || Mathf.Abs(dy) > 0) { // 获取摄像机欧拉角 Vector3 angles = transform.rotation.eulerAngles; // 欧拉角表示按照坐标顺序旋转,比如angles.x=30,表示按x轴旋转30°,dy改变引起x轴的变化 angles.x = Mathf.Repeat(angles.x + 180f, 360f) - 180f; angles.y += dx; angles.x -= dy; // 计算摄像头旋转 targetRotation.eulerAngles = new Vector3(angles.x, angles.y, 0); } } } private void FixedUpdate() { rotation = Quaternion.Slerp(rotation, targetRotation, Time.deltaTime * rotateLerp); // 设置摄像头旋转 transform.rotation = rotation; // 设置摄像头位置 transform.position = position - rotation * new Vector3(0, 0, default_distance); } }
最上面增加了旋转速度(rotateSpeed)和苏尼(rotateLerp),rotateSpeed越高旋 转越快,rotateLerp越高阻尼越小,阻尼使用了四元数的球面差值(前面说过, 只有四元数能做到球面差值),使旋转有个渐变过程,大家可以在Inspector的 tabFree Camera Controller脚本处修改参数尝试最佳的设置;定义了新的变量 targetRotation,用于计算最终旋转,配合rotation实现阻尼效果;positon目 前只是记录模型位置,后面移动时就会改变。