Unity中Cinemachine的基礎功能介紹可詳見之前寫的博客:
https://www.cnblogs.com/koshio0219/p/11820654.html
本篇的重點是討論,在給定規則地圖的長寬和中心點坐標的情況下,如何動態生成一個透視攝像機的碰撞盒子以限定攝像機的視野永遠不會超出地圖的邊界。
例如,下面這種規則地圖:(或者其他用程序生成的單位塊地圖)
在輸入一些參數后:
可以自動創建形如:
這樣的攝像機運動范圍,且輸出的范圍能夠適配到屏幕的分辨率,考慮到相機繞某一軸向的旋轉等問題。
其實基本都是純粹的數學運算,開始之前,必須先弄清楚透視攝像機的一些基本原理,它的視窗大小和屏幕分辨率之間到底是什么關系:
1.FOV:這是透視攝像機區別於正交攝像機最重要的一個特性——視口大小,它表示的是當前攝像機視野范圍的開口角度,也因該角度大小的不同,使得透視攝像機的近裁剪平面和遠裁剪平面大小不一,從而產生三維空間中近大遠小的特點。
2.Aspect:當前攝像機的寬高比。為什么要設置這樣一個東西呢?理由就是屏幕有不同的分辨率,而相機映照出來的畫面最終是要在屏幕當中顯示的,當我們的屏幕分辨率發生變化時,相機的視口面積也會對應的發生變化,這時,僅僅只有一個FOV沒辦法滿足不同類型的屏幕分辨率,於是就需要額外設置相機的寬高比來對最終呈現的攝像機視口大小進行輔助調整。
在Unity中,是以視口的高為基准進行計算的,也就是說,Unity中的透視攝像機的Fov角度其實是按照屏幕分辯率的高度進行對應的,而寬度對應的Fov則隨着Aspect的變化而變化,不是面板設置的Fov大小。
試比較下面兩張圖,分別是攝像機的寬和高的Fov:
設置的Fov為40度,當前的屏幕分辨率為2960*1440:
很顯然,只有高度對應的Fov為面板中顯示的值,而寬度對應的Fov明顯大於40度。實際寬的的Fov應該是82度左右(40*2960/1440)。
知道了上面這些后我們才能更愉快的進行接下來的計算,不然只會計算出許多錯誤也搞不清是什么原因。
在Cinemachine中,一般會設置一個跟隨目標,且跟蹤該目標的距離是一個常量,可以從面板中取得:
我們先分析攝像機的左右運動范圍是如何計算的:(本例中的攝像機只在X軸向上存在旋轉值,一般斜向的攝像機也只需要旋轉一個軸即可,左右看上去一般追求對稱性)
觀察上圖,假設現在攝像機位於空中的P點,已知AB為地圖的邊緣圍牆高度,BC為角色的高度,CP為跟蹤的攝像機到角色的距離,現在我們需要求出攝像機所在的X軸向的坐標,關鍵就是要求出AD的距離。
我們還知道一個數據就是攝像機的Fov,但是由於該Fov並非高度對應的值,所以我們先要進行一次轉換,以得到攝像機寬度視口的Fov角度。以下均為弧度計算:
1 //計算的角度均為弧度值,傳入縱向的(高)Fov的一半得到橫向的(寬)Fov的一半 2 public float GetHorizontalFovHalf(float vhfov, float aspect) 3 { 4 return Mathf.Atan(Mathf.Tan(vhfov) * aspect); 5 }
上面已經講過原理了這里就不在進行過多敘述了,簡單來說就是利用攝像機的深度值進行了一次轉換,因為無論是縱向還是橫向的Fov,它們的深度值都是相同的,讀者可以自行畫圖或腦補一下。
通過上面的方法我們就可以求得∠DPA的大小了,它正好就是橫向Fov的一半,那個∠α的大小就可以輕易求出,現在問題的關鍵就是要求出邊AP的長度,AP的長度得出的話,就可以利用∠α余弦求得AD,DP等。
利用正弦定理可以非常快速的解決上面的問題,當然你也可以設未知數利用勾股定律解一元二次方程,但當你寫程序的時候你可能會有想吐的沖動:
1 //計算軸向偏移值 2 private float GetSizeOffse(float fbangel, float distance, float wh, float followy) 3 { 4 //直角弧度值 5 var rightangel = 90 * Mathf.Deg2Rad; 6 //∠PAC 7 var disangel = fbangel + rightangel; 8 //求出正弦定理的比值 9 var sin = distance / Mathf.Sin(disangel); 10 //求∠APC的正弦值 11 var angelo = (wh - followy) / sin; 12 //三角形內角和求∠ACP 13 var angel = rightangel * 2 - Mathf.Asin(angelo) - disangel; 14 //計算AP利用α余弦返回AD 15 return sin * angel * Mathf.Cos(fbangel); 16 }
fbangel即為上圖中的∠α,distance即為上圖中的CP,wh即為上圖中的AB,followy即為上圖中的CB。
X軸向的偏移計算完畢后,Z軸的偏移也是類似的,只不過需要考慮旋轉值而已,接下來就是攝像機的高度(注意攝像機的高度是一個變量),這個很容易計算。下面給出生成攝像機運動區域的參考:
1 //計算並生成透視攝像機的運動區域 2 public void GenZone() 3 { 4 Camera = Camera.main; 5 6 //計算從地圖中心到邊緣的向量 7 var toedge = WidthHeight * UnitLength * .5f; 8 //左后 9 var lb = CenterPoint - toedge; 10 //右前 11 var rf = CenterPoint + toedge; 12 //牆高 13 var wh = WallHeight; 14 15 zone = new GameObject("CameraZone"); 16 17 var box = zone.AddComponent<BoxCollider>(); 18 var cvc = GetComponent<CinemachineVirtualCamera>(); 19 var cft = cvc.GetCinemachineComponent<CinemachineFramingTransposer>(); 20 21 var cvcs = cvc.m_Lens; 22 //攝像機跟蹤目標的高度 23 var followy = cvc.m_Follow.position.y; 24 //跟蹤距離 25 var distance = cft.m_CameraDistance; 26 //屏幕高對應的Fov的一半(真實Fov) 27 var hfov = cvcs.FieldOfView * .5f * Mathf.Deg2Rad; 28 //攝像機視口寬高比 29 var aspect = Camera.aspect; 30 //攝像機軸向旋轉值 31 var rotation = Camera.transform.eulerAngles.x * Mathf.Deg2Rad; 32 var rightangel = 90 * Mathf.Deg2Rad; 33 //屏幕寬對應的Fov的一半(轉化后的Fov) 34 var whfov = GetHorizontalFovHalf(hfov, aspect); 35 36 //攝像機當前高度 37 var height = Mathf.Sin(rotation) * distance + followy; 38 39 //計算左右偏移(對稱) 40 var lrangel = rightangel - whfov; 41 var widthh = GetSizeOffse(lrangel, distance, wh, followy); 42 var left = lb.x + widthh; 43 var right = rf.x - widthh; 44 var sizex = Mathf.Abs(left - right); 45 46 //計算前后偏移(帶旋轉值,非對稱) 47 var fangel = rotation - hfov; 48 var front = rf.y - GetSizeOffse(fangel, distance, wh, followy); 49 50 var bangel = rotation + hfov; 51 var back = lb.y - GetSizeOffse(bangel, distance, wh, followy); 52 53 var sizez = Mathf.Abs(front - back); 54 55 //設置攝像機運動范圍的大小,因為在XZ平面上,盒子的高度可以為一個常量 56 box.size = new Vector3(sizex, 5, sizez); 57 zone.transform.position = new Vector3((left + right) * .5f, height, (front + back) * .5f); 58 59 CC.m_BoundingVolume = zone.GetComponent<BoxCollider>(); 60 }
生成該盒子后,只需要將它賦值給CinemachineConfiner的BoundingVolume屬性即可:
為了更方便的進行測試和調試,可以寫一個Editor腳本在編輯器模式下生成:
1 using UnityEditor; 2 using UnityEngine; 3 4 [CustomEditor(typeof(CameraZoneCtrl))] 5 public class CameraZoneEditor : Editor 6 { 7 public override void OnInspectorGUI() 8 { 9 DrawDefaultInspector(); 10 CameraZoneCtrl ctrl = (CameraZoneCtrl)target; 11 if (GUILayout.Button("創建攝像機范圍")) 12 { 13 ctrl.GenZone(); 14 } 15 } 16 }
2020年7月9日更新:
攝像機帶y軸旋轉的處理方法
如上,攝像機不僅x軸有旋轉值,y軸也有旋轉,這時整個攝像機的運動范圍盒位置和大小都將發生變化,可以分別進行討論:
1.大小的變化
不再是以房間的邊緣為極限位置參考,而是改為以四個頂點;那是不是一定要從頭到尾重新計算一遍才行呢?
這里有一個更簡單的處理方式——計算原始矩形的旋轉包圍盒大小
如上圖,假設某矩形旋轉了α度,那么根據三角函數可以很快計算出變化后的包圍盒長寬:
x'=x*cos(α)+y*sin(α);
y'=y*cos(α)+x*sin(α);
用函數則是如下表示:
1 private Vector2 GetAxialRotation(Vector2 axial, float angle) 2 { 3 var x = axial.x * Mathf.Cos(angle) + axial.y * Mathf.Sin(angle); 4 var y = axial.y * Mathf.Cos(angle) + axial.x * Mathf.Sin(angle); 5 return new Vector2(x, y); 6 }
注意角度為弧度值。
2.旋轉后的位置值變化
本來應該是一個空間立方體在以前的基礎上繞房間中點的Y軸旋轉α度,但之前已經計算了大小值得變化,故此處只用計算中心點的前后旋轉值變換。
更進一步簡化則為,計算平面中任意一點繞另一點旋轉α度后的坐標:(逆時針)
1 public static Vector2 RotateByPos(this Vector2 pos, Vector2 rPos, float angle) 2 { 3 var x = (pos.x - rPos.x) * Mathf.Cos(angle) - (pos.y - rPos.y) * Mathf.Sin(angle) + rPos.x; 4 var y = (pos.y - rPos.y) * Mathf.Cos(angle) + (pos.x - rPos.x) * Mathf.Sin(angle) + rPos.y; 5 return new Vector2(x, y); 6 }
這里就不具體解釋證明過程了,如感興趣可見:
https://jingyan.baidu.com/article/2c8c281dfbf3dd0009252a7b.html?spm=0.0.0.0.YhLCCP
1 private void RotateYByPos(Vector2 r, Transform t, float angle) 2 { 3 var tempv2 = new Vector2(t.position.x, t.position.z); 4 var result = tempv2.RotateByPos(r, -angle); 5 t.position = new Vector3(result.x, t.position.y, result.y); 6 t.SetEulerAnglesY(angle * Mathf.Rad2Deg); 7 }
修改后的函數:
1 //計算並生成透視攝像機的運動區域 2 public void GenZone() 3 { 4 Camera = Camera.main; 5 //攝像機軸向旋轉值 6 var rotation = Camera.transform.eulerAngles.x * Mathf.Deg2Rad; 7 var sizeup = Camera.transform.eulerAngles.y * Mathf.Deg2Rad; 8 9 //計算從地圖中心到邊緣的向量 10 var toedge = WidthHeight * UnitLength * .5f; 11 12 //旋轉后的大小值變化(添加內容) 13 toedge= GetAxialRotation(new Vector2(toedge.x, toedge.y), sizeup); 14 15 //左后 16 var lb = CenterPoint - toedge; 17 //右前 18 var rf = CenterPoint + toedge; 19 //牆高 20 var wh = WallHeight; 21 22 zone = new GameObject("CameraZone"); 23 24 var box = zone.AddComponent<BoxCollider>(); 25 var cvc = GetComponent<CinemachineVirtualCamera>(); 26 var cft = cvc.GetCinemachineComponent<CinemachineFramingTransposer>(); 27 28 var cvcs = cvc.m_Lens; 29 //攝像機跟蹤目標的高度 30 var followy = cvc.m_Follow.position.y; 31 //跟蹤距離 32 var distance = cft.m_CameraDistance; 33 //屏幕高對應的Fov一半(真實Fov) 34 var hfov = cvcs.FieldOfView * .5f * Mathf.Deg2Rad; 35 //攝像機視口寬高比 36 var aspect = Camera.aspect; 37 var rightangle = 90 * Mathf.Deg2Rad; 38 //屏幕寬對應的Fov一半(轉化后的Fov) 39 var whfov = GetHorizontalFovHalf(hfov, aspect); 40 41 //攝像機當前高度 42 var height = Mathf.Sin(rotation) * distance + followy; 43 44 //計算左右偏移(對稱) 45 var lrangle = rightangle - whfov; 46 var widthh = GetSizeOffse(lrangle, distance, wh, followy); 47 var left = lb.x + widthh; 48 var right = rf.x - widthh; 49 var sizex = Mathf.Abs(left - right); 50 51 //計算前后偏移(帶旋轉值,非對稱) 52 var fangle = rotation - hfov; 53 var front = rf.y - GetSizeOffse(fangle, distance, wh, followy); 54 55 var bangle = rotation + hfov; 56 var back = lb.y - GetSizeOffse(bangle, distance, wh, followy); 57 58 var sizez = Mathf.Abs(front - back); 59 60 //設置攝像機運動范圍的大小,因為在XZ平面上,盒子的高度可以為一個常量 61 box.size = new Vector3(sizex, 5, sizez); 62 zone.transform.position = new Vector3((left + right) * .5f, height, (front + back) * .5f); 63 //位置值變化設置(添加內容) 64 RotateYByPos(CenterPoint, zone.transform, sizeup); 65 66 CC.m_BoundingVolume = zone.GetComponent<BoxCollider>(); 67 }