目錄
1 緣起
我們的產品是使用unity開發水利BIM(水利建築信息模型),項目中需要控制攝像 頭對模型進行360度查看,請注意所有操作都是移動攝像頭,不是移動模型。攝 像頭能進行移動、旋轉、改變焦距操作,類似於SketchUp的控制操作:
- 攝像頭移動時,根據當前旋轉方向(Rotation)進行移動
- 攝像頭距離模型越遠,攝像頭移動速度越快,距離越近,移動速度越慢
- 攝像頭最初放置在距離模型中心點正前方distance距離(即z軸),攝像頭旋轉方向(Rotaion) 改變后,再根據旋轉方向(Rotation)在z軸移動distance距離;用戶看到的就是模型圍繞自己得中心點進行 360度旋轉
- 攝像頭移動后,比如向左平移了left距離(即x軸),那么攝像頭旋轉時,攝像 頭旋轉方向(Rotaion),再根據旋轉方向(Rotation)在x軸移動left距離,z軸distance 移動距離;用戶看到的就是模型圍繞旋轉的中心點是:模型中心點向左平移left距離的那個點
- 如果攝像頭移動后,攝像頭在旋轉過程中,移動距離會逐步減小,最終攝像 頭會回到最初位置;用戶看到的就是隨着旋轉,模型回到屏幕中心
這些需求其實挺簡單,本想在網上找到一個現成的例子,但是發現網上關於攝像 頭的資料要么太簡單,要么有錯誤,關鍵地方含糊其詞,代碼也寫得不規范,因 此自己研究了下,把攝像頭這種控制搞清楚了,在這里分享一下。
這里使用的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",這是我常用的布局方式。
在Hierarchy的tab內點擊右鍵,按右鍵在3D Object中建立一個Cube作為模型; 在Project的tab內Assets上點擊右鍵,建立一個文件夾Scripts,在文件夾上點 擊右鍵建立一個FreeCameraController.cs的腳本。
打開腳本,現在腳本只有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)。
現在,我們運行一下看看效果,攝像頭果然移動到了模型的正前方,仔細觀察現 在攝像頭Positon,x,y軸位置和模型一樣,z軸位置果然減去了 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 優化
攝像頭基本功能就實現完了,但是還有幾點細節需要優化:
- y軸旋轉需要控制一下,旋轉范圍應在-89度至89度,這樣模型會在Y軸被翻轉超過360度,會產生異常情況
- Input.GetAxis("Mouse X"),Input.GetAxis("Mouse Y")的異常波動需要處 理,當使用alt+tab切換程序時這個問題非常明顯
- 模型移動不僅需要鼠標左鍵控制,還需要鍵盤控制
- 移動速度應該和模型距離有關系,距離越遠,移動速度越快,距離越近,移動速度越慢
最終程序如下,我覺得這是攝像頭控制比較詳盡的文章了,大家有什么問題意見歡迎溝通!
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); } }
Created: 2018-10-05 Fri 17:00