unity中camera攝像頭控制詳解


目錄

1 緣起

我們的產品是使用unity開發水利BIM(水利建築信息模型),項目中需要控制攝像 頭對模型進行360度查看,請注意所有操作都是移動攝像頭,不是移動模型。攝 像頭能進行移動、旋轉、改變焦距操作,類似於SketchUp的控制操作:

  1. 攝像頭移動時,根據當前旋轉方向(Rotation)進行移動
  2. 攝像頭距離模型越遠,攝像頭移動速度越快,距離越近,移動速度越慢
  3. 攝像頭最初放置在距離模型中心點正前方distance距離(即z軸),攝像頭旋轉方向(Rotaion) 改變后,再根據旋轉方向(Rotation)在z軸移動distance距離;用戶看到的就是模型圍繞自己得中心點進行 360度旋轉
  4. 攝像頭移動后,比如向左平移了left距離(即x軸),那么攝像頭旋轉時,攝像 頭旋轉方向(Rotaion),再根據旋轉方向(Rotation)在x軸移動left距離,z軸distance 移動距離;用戶看到的就是模型圍繞旋轉的中心點是:模型中心點向左平移left距離的那個點
  5. 如果攝像頭移動后,攝像頭在旋轉過程中,移動距離會逐步減小,最終攝像 頭會回到最初位置;用戶看到的就是隨着旋轉,模型回到屏幕中心

這些需求其實挺簡單,本想在網上找到一個現成的例子,但是發現網上關於攝像 頭的資料要么太簡單,要么有錯誤,關鍵地方含糊其詞,代碼也寫得不規范,因 此自己研究了下,把攝像頭這種控制搞清楚了,在這里分享一下。

這里使用的unity版本是2018.2.6f1,使用mac系統,編譯環境使用Rider2018.2,但是攝像頭沒有特殊功能,估計unity5以上,windows或mac下都可以適用。

2 開發

 

2.1 建立項目

首先建立一個名為FreeCamera的空項目,在右上角的Layout下拉框中把布局改為 "2 by 3",在中間名為Project的tab右上角Column Layout下拉框把Project布局 改為"Two Column Layout",這是我常用的布局方式。

two column layout

two column layout

在Hierarchy的tab內點擊右鍵,按右鍵在3D Object中建立一個Cube作為模型; 在Project的tab內Assets上點擊右鍵,建立一個文件夾Scripts,在文件夾上點 擊右鍵建立一個FreeCameraController.cs的腳本。

FreeCameraController

打開腳本,現在腳本只有Start()和Update()兩個空函數,首先明確腳本是掛載 在攝像頭上的,模型(Cube)位置未知,那么攝像頭的初始位置如何定在距離模型 中心點正前方呢,腳本必須知道模型的位置,因此腳本定義Transform屬性,就 是模型,屬性名為model,public類型,我們實現一下。

using UnityEngine;

/**
 * 自由攝像頭
 * 2018-10-03 by flysic, 119238122@qq.com
 */
public class FreeCameraController : MonoBehaviour
{
        // 模型
        public Transform model;
        // 默認距離
        private const float default_distance = 5f;

        // Use this for initialization
        void Start ()
        {
                // 旋轉歸零
                transform.rotation = Quaternion.identity;
                // 初始位置是模型
                Vector3 position = model.position;
                position.z -= default_distance;
                transform.position = position;
        }

        // Update is called once per frame
        void Update () {

        }
}

代碼里default_distance是默認攝像頭和模型的距離,首先旋轉Rotation歸零, 位置Position是模型的位置基礎上在z軸減去默認距離,即在模型正前方 default_distance距離。

我們在unity中設置一下,在Hierarchy的tab中選中Main Camera,在Inspector 的tab中最后位置點擊Add Component按鈕,選擇"Free Camera Controller"腳本; 在Hierarchy的tab中選中Cube,拖拽到腳本的Model屬性上;在Hierarchy的tab 中選中Cube,把Cube的Positon改為(128,64,64)。

Position

現在,我們運行一下看看效果,攝像頭果然移動到了模型的正前方,仔細觀察現 在攝像頭Positon,x,y軸位置和模型一樣,z軸位置果然減去了 default_distance。

default_distance

2.2 旋轉

我們現在需要讓攝像頭圍着模型進行360度旋轉,先嘗試一下,使用鼠標右鍵移 動時,讓攝像頭的隨着鼠標方向旋轉。

using UnityEngine;

/**
 * 自由攝像頭
 * 2018-10-03 by flysic, 119238122@qq.com
 */
public class FreeCameraController : MonoBehaviour
{
        // 模型
        public Transform model;
        // 默認距離
        private const float default_distance = 5f;

        // Use this for initialization
        void Start ()
        {
                // 旋轉歸零
                transform.rotation = Quaternion.identity;
                // 初始位置是模型
                Vector3 position = model.position;
                position.z -= default_distance;
                transform.position = 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;
                                // 設置攝像頭旋轉
                                Quaternion rotation = Quaternion.identity;
                                rotation.eulerAngles = new Vector3(angles.x, angles.y, 0);
                                transform.rotation = rotation;
                        }
                }
        }
}
我們這里使用了四元數和歐拉角之間的轉換,具體詳情見后面的四元數這節。

 

為什么攝像頭沒有針對模型進行360度旋轉?因為我們只是改變了攝像頭的本身 的旋轉方向,但是攝像頭和模型的朝向發生了改變,原來是正對着模型,旋轉后 改變了朝向;和模型距離也發生了改變,選來和模型距離是defaule_distance, 旋轉后,距離發生了改變。要達到360度旋轉,要保證攝像頭在旋轉時,和模型 朝向不變,距離不變。

我們把Update函數改變一下:

// 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;
                        // 設置攝像頭旋轉
                        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.position)就是在模型位置(model.position)上朝着攝像 頭旋轉方向(transform.rotation)后退默認距離(default_distance),注意不 是在z軸方向回退!怎么達到這個目的呢,就是使用了旋轉方向(rotation)和向 量(Vector3(0,0,default_distance))相乘,相當於在z軸后退了 default_disatnce距離后,又進行了旋轉。

transform.position = position - rotation * new Vector3(0, 0, default_distance);

position減去這個旋轉后的向量,相當於攝像頭位置在模型位置(model.position)上朝着攝像 頭旋轉方向(transform.rotation)后退默認距離(default_distance)。要說清楚詳情,要介紹一下兩個概念:

2.2.1 四元數

在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目 前只是記錄模型位置,后面移動時就會改變。

2.3 移動

前面我們定義了變量position,記錄了模型的初始位置,可以假設position是一 個虛擬物體的位置,初始位置恰好和模型(model)位置重合,隨着鼠標左鍵的操作,虛擬物體位置發生變化,攝像頭的位 置根據虛擬物體位置計算得來見代碼。

注意,模型(model)本身不移動,只是虛擬物體位置(position)發生變化,positon既不是模型位置,也不是攝像頭位置
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;    
        // 移動速度
        public float moveSpeed = 1f;
        public float moveLerp = 10f;

        // 計算移動
        private Vector3 position, targetPosition;
        // 計算旋轉
        private Quaternion rotation, targetRotation;
        // 默認距離
        private const float default_distance = 5f;

        // Use this for initialization
        void Start ()
        {
                // 旋轉歸零
                targetRotation = Quaternion.identity; 
        // 初始位置是模型
                targetPosition = 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(0))
                {
                        dx *= moveSpeed;
                        dy *= moveSpeed;
                        targetPosition -= transform.up * dy + transform.right * dx;
                }

                // 鼠標右鍵旋轉
                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);
                position = Vector3.Lerp(position, targetPosition, Time.deltaTime * moveLerp);   
                // 設置攝像頭旋轉
                transform.rotation = rotation;
                // 設置攝像頭位置
                transform.position = position - rotation * new Vector3(0, 0, default_distance);
        }
}

位置也定義了新的變量targetPosition,和position配合實現阻尼效果,鼠標左 鍵點擊移動會產生移動效果,注意看這句:targetPosition -= transform.up * dy + transform.right * dx,使用一連串的Vecor3向量操作實現了完美的移動 操作,首先,這里使用的是transform.up和dy相乘,而不是Vector3.up, transform.up是世界坐標系的,Vector3.up是本地坐標系的,對transform.up進 行移動時附加了攝像頭的旋轉信息,所以攝像頭旋轉后,移動也是正確的; Vector3.up移動方向是固定的,旋轉后移動方向就錯了。

2.3.1 向量操作

Unity中得Vector3實際上是向量,在數學中向量的定義是:既有大小又有方向的 量叫作向量。在空間中,向量可以用一段有方向的線段來表示。向量在Unity中 的應用十分廣泛,可用於描述具有大小和方向兩個屬性的物理量。

  • 向量相關概念
    • 模(magnitude):向量的長度,簡單的說就是這條線有多長,就是你用尺子量出來的數據
    • 標准化(normalizing):保持方向不變,將向量的模變為1
  • 向量的運算
    • 加減:向量的加法(減法)為各分量分別相加(相減),表示位置變化疊加,這 里transform.up * dy + transform.right * dx就是將y軸和x軸的移動向量 相加,最后在targetPositon減去這個結果
    • 數乘:向量與一個標量相乘稱為數乘。數乘可以對向量的長度進行縮放,如 果標量大於0,那么向量的方向不變;若標量小於0,則向量的方向會變為反 方向,程序中例子是transform.up*dy

2.4 鏡頭拉伸

下面就是改變鏡頭拉伸了,也就是改變攝像頭和模型的距離,這個比較簡單,通過中間的滾輪改變。

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;    
        // 移動速度
        public float moveSpeed = 1f;
        public float moveLerp = 10f;
        // 鏡頭拉伸速度
        public float zoomSpeed = 10f;
        public float zoomLerp = 4f;     

        // 計算移動
        private Vector3 position, targetPosition;
        // 計算旋轉
        private Quaternion rotation, targetRotation;
        // 計算距離
        private float distance, targetDistance;
        // 默認距離
        private const float default_distance = 5f;

        // Use this for initialization
        void Start ()
        {
                // 旋轉歸零
                targetRotation = Quaternion.identity; 
        // 初始位置是模型
                targetPosition = model.position;
                // 初始鏡頭拉伸
                targetDistance = default_distance;
        }

        // Update is called once per frame
        void Update()
        {
                float dx = Input.GetAxis("Mouse X");
                float dy = Input.GetAxis("Mouse Y");

                // 鼠標左鍵移動
                if (Input.GetMouseButton(0))
                {
                        dx *= moveSpeed;
                        dy *= moveSpeed;
                        targetPosition -= transform.up * dy + transform.right * dx;
                }

                // 鼠標右鍵旋轉
                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);
                        }
                }

                // 鼠標滾輪拉伸
                targetDistance -= Input.GetAxis("Mouse ScrollWheel") * zoomSpeed;               
        }

        private void FixedUpdate()
        {
                rotation = Quaternion.Slerp(rotation, targetRotation, Time.deltaTime * rotateLerp);
                position = Vector3.Lerp(position, targetPosition, Time.deltaTime * moveLerp);
                distance = Mathf.Lerp(distance, targetDistance, Time.deltaTime * zoomLerp);
                // 設置攝像頭旋轉
                transform.rotation = rotation;
                // 設置攝像頭位置
                transform.position = position - rotation * new Vector3(0, 0, distance);
        }
}

也是定義兩個變量distance、targetDistance,還有拉伸速度,拉伸阻尼。 

2.5 復位

觀察SketchUp操作,發現當模型被移動的比較遠時,旋轉時模型位置會很快復位 移到中間,這個功能會有用,幫助找到移出屏幕的模型,我們嘗試制作一下。在鼠標右鍵代碼邏輯里,增加兩句。

// 鼠標右鍵旋轉
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);
                // 隨着旋轉,攝像頭位置自動恢復
                Vector3 temp_position =
                        Vector3.Lerp(targetPosition, model.position, Time.deltaTime * moveLerp);
                targetPosition = Vector3.Lerp(targetPosition, temp_position, Time.deltaTime * moveLerp);
        }
}

隨着旋轉,虛擬的物體位置會逐步變成模型初始位置。 

2.6 優化

攝像頭基本功能就實現完了,但是還有幾點細節需要優化:

  1. y軸旋轉需要控制一下,旋轉范圍應在-89度至89度,這樣模型會在Y軸被翻轉超過360度,會產生異常情況
  2. Input.GetAxis("Mouse X"),Input.GetAxis("Mouse Y")的異常波動需要處 理,當使用alt+tab切換程序時這個問題非常明顯
  3. 模型移動不僅需要鼠標左鍵控制,還需要鍵盤控制
  4. 移動速度應該和模型距離有關系,距離越遠,移動速度越快,距離越近,移動速度越慢

最終程序如下,我覺得這是攝像頭控制比較詳盡的文章了,大家有什么問題意見歡迎溝通!

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;    
        // 移動速度
        public float moveSpeed = 1f;
        public float moveLerp = 10f;
        // 鏡頭拉伸速度
        public float zoomSpeed = 10f;
        public float zoomLerp = 4f;     

        // 計算移動
        private Vector3 position, targetPosition;
        // 計算旋轉
        private Quaternion rotation, targetRotation;
        // 計算距離
        private float distance, targetDistance;
        // 默認距離
        private const float default_distance = 5f;
        // y軸旋轉范圍
        private const float min_angle_y = -89f;
        private const float max_angle_y = 89f;  


        // Use this for initialization
        void Start ()
        {

                // 旋轉歸零
                targetRotation = Quaternion.identity; 
                // 初始位置是模型
                targetPosition = model.position;
                // 初始鏡頭拉伸
                targetDistance = default_distance;
        }

        // Update is called once per frame
        void Update()
        {
                float dx = Input.GetAxis("Mouse X");
                float dy = Input.GetAxis("Mouse Y");

                // 異常波動
                if (Mathf.Abs(dx) > 5f || Mathf.Abs(dy) > 5f)
                {
                        return;
                }

                float d_target_distance = targetDistance;
                if (d_target_distance < 2f)
                {
                        d_target_distance = 2f;
                }

                // 鼠標左鍵移動
                if (Input.GetMouseButton(0))
                {
                        dx *= moveSpeed * d_target_distance / default_distance;
                        dy *= moveSpeed * d_target_distance / default_distance;
                        targetPosition -= transform.up * dy + transform.right * dx;
                }

                // 鼠標右鍵旋轉
                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;
                                angles.x = ClampAngle(angles.x, min_angle_y, max_angle_y);
                                // 計算攝像頭旋轉
                                targetRotation.eulerAngles = new Vector3(angles.x, angles.y, 0);
                                // 隨着旋轉,攝像頭位置自動恢復
                                Vector3 temp_position =
                                        Vector3.Lerp(targetPosition, model.position, Time.deltaTime * moveLerp);
                                targetPosition = Vector3.Lerp(targetPosition, temp_position, Time.deltaTime * moveLerp);
                        }
                }

                // 上移
                if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W))
                {
                        targetPosition -= transform.up * d_target_distance / (2f * default_distance);
                }

                // 下移
                if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S))
                {
                        targetPosition += transform.up * d_target_distance / (2f * default_distance);
                }

                // 左移
                if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
                {
                        targetPosition += transform.right * d_target_distance / (2f * default_distance);
                }

                // 右移
                if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
                {
                        targetPosition -= transform.right * d_target_distance / (2f * default_distance);
                }               

                // 鼠標滾輪拉伸
                targetDistance -= Input.GetAxis("Mouse ScrollWheel") * zoomSpeed;               
        }

        // 控制旋轉角度范圍:min max
        float ClampAngle(float angle, float min, float max)
        {
                // 控制旋轉角度不超過360
                if (angle < -360f) angle += 360f;
                if (angle > 360f) angle -= 360f;
                return Mathf.Clamp(angle, min, max);
        }       

        private void FixedUpdate()
        {
                rotation = Quaternion.Slerp(rotation, targetRotation, Time.deltaTime * rotateLerp);
                position = Vector3.Lerp(position, targetPosition, Time.deltaTime * moveLerp);
                distance = Mathf.Lerp(distance, targetDistance, Time.deltaTime * zoomLerp);
                // 設置攝像頭旋轉
                transform.rotation = rotation;
                // 設置攝像頭位置
                transform.position = position - rotation * new Vector3(0, 0, distance);
        }
}

Author: flysic

Created: 2018-10-05 Fri 17:00

Validate


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM