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 }
複制