(轉)Unity3D 之插值計算
在unity3D中經常用線性插值函數Lerp()來在兩者之間插值,兩者之間可以是兩個材質之間、兩個向量之間、兩個浮點數之間、兩個顏色之間,其函數原型如下:
Material.Lerp 插值
function Lerp (start : Material, end : Material, t : float) : void
在兩個材質之間插值
Vector2.Lerp 插值
static function Lerp (from : Vector2, to : Vector2, t : float) : Vector2
兩個向量之間的線性插值。按照數字t在form到to之間插值。
t是夾在0到1之間。當t=0時,返回from。當t=1時,返回to。當t=0.5時放回from和to之間的平均數。
Vector3.Lerp 插值
static function Lerp (from : Vector3, to : Vector3, t : float) : Vector3
兩個向量之間的線性插值。按照數字t在from到to之間插值。
Vector4.Lerp 插值
static function Lerp (from : Vector4, to : Vector4, t : float) : Vector4
兩個向量之間的線形插值。按照數字t在from到to之間插值。t是夾在[0...1]之間的值。,當t = 0時,返回from。當t = 1時,返回to。當t = 0.5 返回from和to的平均數。
Mathf.Lerp 插值
static function Lerp (from : float, to : float, t : float) : float
基於浮點數t返回a到b之間的插值,t限制在0~1之間。當t = 0返回from,當t = 1 返回to。當t = 0.5 返回from和to的平均值。
Color.Lerp 插值
static function Lerp (a : Color, b : Color, t : float) : Color
通過t在顏色a和b之間插值。
"t"是夾在0到1之間的值。當t是0時返回顏色a。當t是1時返回顏色b。
插值,從字面意思上看,就是在其間插入一個數值,這種理解是否正確呢?我們先從最簡單的浮點數插值函數來分析:
Mathf.Lerp 插值
static function Lerp (from : float, to : float, t : float) : float
基於浮點數t返回a到b之間的插值,t限制在0~1之間。當t = 0返回from,當t = 1 返回to。當t = 0.5 返回from和to的平均值。
首先,我們來做一個試驗,啟動Unity3D,任建一個腳本文件,在其Start()中輸入內容如下:
void Start () {
print(Mathf.Lerp(0.0f, 100.0f, 0.0f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 0.1f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 0.2f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 0.3f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 0.4f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 0.5f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 0.6f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 0.7f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 0.8f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 0.9f).ToString());
print(Mathf.Lerp(0.0f, 100.0f, 1.0f).ToString());
}
運行Unity,在控制臺將打印出:
這個實驗是在0到100之間插值,插入什麽值,取決於第3個參數,從打印結果可看出,第3個參數是個比例因數,是0.1時表示0到100這個長度的十分之一,同理,0.2表示十分之二,依此類推。從這點上看來,我們起初從字面上所理解的插值就是插入一個數值是可以這樣理解的。
如果我們把上面那個腳本裏的插值函數裏的第一個參數變為100.0f,第二個參數變為110.0f,第三個參數保持不變,大家想想其運行結果該是什麽呢?可不要認為是0、1、2、3、4、5、6、7、8、9、10了喲,實際結果是100、101、102、103、104、105、106….,因插值是把值插在原來的兩數之間,這說明這個函數首先是根據第三個參數所給定的比例算出凈增量,再加上起始數,最終算出插值值的。
在Unity3D遊戲開發中,應用最多的是Vector3.Lerp 向量插值,下面我們以此插值來猜推其內部實現機理以及一些應用。
如圖,在空間中存在兩點A(0,10,0)與B(10,0,-10),我們在A、B兩點間插入一C點,假設C點的位置在AB的五分之二處,即AC/AB=0.4,根據相似圖形對應邊成比例的初中幾何知識可知,在⊿ABO中AC/AB=OD/OB,同理在⊿OBF中OD/OB=OE/OF,所以AC/AB=OD/O=OE/OF = 0.4,則C點的X坐標值為:OE=0.4*OF=0.4*10=4。
根據上圖,還可知ED/FB=0.4,所以C點的Z坐標值DE=0.4*BF=0.4*(-10)=-4。
C點的Y坐標值請看下圖:
EO/AO=DF/AF=CB/AC=1-0.4=0.6,則C點的Y坐標值EO=0.6*AO=0.6*10=6。
綜上所述,C點的三維坐標為C(4,6,-4)。
下面我們利用Unity3D中的Vector3.Lerp 插值函數:static function Lerp (from : Vector3, to : Vector3, t : float) : Vector3來計算上面演算的插值。
我們把先前腳本中的Start()函數改寫成:
void Start()
{ print(Vector3.Lerp(new Vector3(0, 10, 0), new Vector3(10, 0, -10), 0.4f).ToString()); }
其運行結果為:
這與我們的演算結果是一致的。
上面的演算,我們為了簡便,A、B兩點取得較特殊,降低了演算的復雜度。而對普通的A、B兩點,如下圖所示:
我們同樣可以得到三角形EGL與三角形EFK,使用同樣的方法可計算出HI的長度,再加上OH的長度就是C點的X坐標值了。同樣的方法可推演出Y與Z的坐標。
手工計算是很復雜的,而Lerp函數可以高效地為我們返回這個插值的,我們在這裏做出的演算,只是幫助我們來推測Lerp這個函數的內部實現機理而也,實際運用中,一切工作都是交於Lerp函數去完成。
Lerp函數在遊戲開發過程使用較多,在Unity的幫助文檔裏就有為我們列舉了Vector3.Lerp的兩個應用的例子,一個是在1秒時間動畫位置移動從start.position開始到end.position結束:
using UnityEngine;
using System.Collections;
public class example : MonoBehaviour {
public Transform start;
public Transform end;
void Update() {
transform.position = Vector3.Lerp(start.position, end.position, Time.time);
}
}
另一個例子:
//像彈簧一樣跟隨目標物體
using UnityEngine;
using System.Collections;
public class example : MonoBehaviour {
public Transform target;
public float smooth = 5.0F;
void Update() {
transform.position = Vector3.Lerp(transform.position, target.position, Time.deltaTime * smooth);
}
}
這個例子中的transform.position是去跟隨的那個物體的空間坐標,target.position是目標物體的空間坐標,整句的結果是讓跟隨物體的坐標不斷地變化為它們兩者之間的插值,然而隨著時間的推移,第三個參數的值最終會為1,所以最終跟隨物體的位置會與目標物體重合的。我們以前所玩的遊戲中,主人公身上依附著一只寵物如鷹,主人公移動時,鷹會跟隨著飛動,主人公移動得快它就飛行跟動得快,始終不會離開主人公,使用Lerp插值函數就可實現。
下面我們來看另一個應用實例。
這是酷跑遊戲場景,囚犯沿著一條森林道路向前奔跑,後面有警車追趕,前面有路障,在遊戲過程中,我們要在囚犯奔跑的固定路線上隨機產生路障,而道路不是平直的,既左右彎曲,又上下起伏,由程序隨機生成的路障怎樣確定其空間位置呢?這時,Lerp函數就派上了用場。
先根據道路的彎曲與起伏,在轉折處設置一個空物體,此空物體的Position值即空間坐標與此處道路一致,我們把這些空物體所在的點稱為道路轉折點,這些點連接而成的線段所組成的多段折線貼合在路面上,是這條道路的近似路徑,這些點取得越多、越準確,這條路徑與道路的相似程度就越高。
現在我們用那條路徑來代替那條道路,把隨機產生的路障放在這條路徑上也就是放在道路上了。
假設我們想每隔100米至200米之間產生一個路障,用變量z += Random.Range(100, 200)記錄下該路障的Z坐標值(因囚犯總體上是沿著Z軸往前跑)然後根據此Z坐標值判斷該坐標值在前面所設置的轉折點中的哪兩個點之間,找到後就在這兩個點之間插值,其插值的比例因數(Lerp()函數的第3個參數)可由兩個轉折點與這個插值點這三個點中已知的Z坐標值算出來,這樣Vector3.Lerp (from : Vector3, to : Vector3, t : float)函數中的三個參數值便都是已知的了,它就可計算出這個插值點的空間坐標了,根據前面的設計,這兩個轉折點之間的線段是貼合在路面上的,那麽此插值的坐標也就是在路面上了,根據此插值放置的路障也就不會偏離道路,且會隨著道路的左轉而左轉,右轉而右轉,上坡而上坡,下坡而下坡了。
具體設計過程如下。導入道路模型,假設命名為forest_1。模型設計時就確定好了其長度為3000、坐標原點在其終端上了的。導入後我們將其沿Z軸正方向放置在場景中,讓其Transorm.Position的X、Y值均為0。我們可以導入多段同類型的道路模型,通過控制它們的Z值來把它們拼接成長長的森林道路。在此道路物體上新建一個空物體作為它的子物體,命名為waypoint,再在其下建立多個為空的孫物體,分別命名為waypoint_01、waypoint_02……,把它們放在道路的轉折處,並通過放大、旋轉場景圖後細調這些孫物體的坐標值,使它們與道路路面貼合,如下圖所示:說明:圖中的綠色按鈕狀塊就是這些孫物體,因它們是空物體,不能顯示在場景中,是通過屬性面板
給它們設置了一個供編輯時顯示使用的圖標標示。
這樣,我們便把彎彎曲曲的道路分成了一段一段的直路段,並記錄下來了各段路段兩端的特征點的坐標值。有了這些特征點,也就有了與道路相近的路線了。這是化曲為直的方法,把彎曲、起伏的道路化成了與此相近的一段一段的線段。這樣的點越多,其相似程度越高。
在waypionts上創建一個腳本組件waypionts.cs:
using UnityEngine;
using System.Collections;
public class waypoints : MonoBehaviour {
public Transform[] points;
void OnDrawGizmos(){
iTween.DrawPath (points);
}
}
public Transform[] points;該句所定義的points就是存放那些特征點的數組,因它是public,可在Unity編輯界面中為其賦值,其操作方法是先在Hierarchy視圖中選中waypoints控件,然後在其Inspector視圖中點擊圖標鎖住其Inspector面板,然後在Hierarchy視圖中全選waypoint_01至waypiont_11後拖到屬性面板上的數組名points上即可完成賦值,如下圖:
接下來,在這個森林道路上建立的Forestcs.cs腳本組件裏添加生成路障的腳本:
using UnityEngine;
using System.Collections;
public class Forest : MonoBehaviour {
public GameObject[] obstacles; //路障物體數組
public float startLength = 50; //路障在道路上出現的開始位置
public float minLength = 100; //路障距上一個路障的最小距離
public float maxLength = 200; //路障距上一個路障的最大距離
private Transform player; //遊戲主人公-奔跑者的Transform組件
private waypoints wayPoints; //與路面相貼合的路線上的腳本組件
void Awake() {
player = GameObject.FindGameObjectWithTag(Tags.player).transform; //找到遊戲主人公-奔跑者並獲得它的Transform組件
wayPoints = transform.Find("waypoints").GetComponent<waypoints>(); //找到與路面相貼合的路線上的腳本組件
}
// Use this for initialization
void Start()
{
GenerateObstacle(); //當森林道路被創建出來時,就會自動調用此Start()方法,從而調用此GenerateObstacle()方法
}
// 如果主人公跑完了這段道路,則通知GenerateForest類開始運行產生新的道路,並銷毀已跑完的這條道路
void Update () {
if (player.position.z > transform.position.z+100) {
Camera.main.SendMessage("GenerateForest");
GameObject.Destroy(this.gameObject);
}
}
void GenerateObstacle(){
float startZ = transform.position.z - 3000; //當前道路在場景中的起始Z坐標
float endZ = transform.position.z; //當前道路在場景中的結束Z坐標
float z = startZ + startLength; //將要產生的路障的Z坐標
while (true) {
z += Random.Range(100, 200); //每隔100多米的距離產生一個路障
if (z > endZ) //如果將要產生路障的位置超出了這條道路則退出路障產生循環,否則產生路障
{
break;
}
else {
Vector3 position = GetWayPosByz(z); //調用GetWayPosByz()方法計算路障位置坐標
int obsIndex = Random.Range(0, obstacles.Length); //產生一個從路障數組裏取路障的隨機序數
GameObject.Instantiate(obstacles[obsIndex], position, Quaternion.identity);//實例化路障
}
}
}
Vector3 GetWayPosByz(float z) {
Transform[] points = wayPoints.points; //在道路上設置的轉折點的集合
int index = 0; //轉折點在集合中的序數號
for (int i = 0; i < points.Length-1; i++) { //根據要插入路障的Z值在集合中尋找在哪兩個點之間,找到後記下序數號
if(z<=points[i].position.z && z>= points[i+1].position.z){
index = i;
break;
}
}
//使用Lerp函數計算出插入路障處的空間坐標值
return Vector3.Lerp(points[index + 1].position, points[index].position, (z - points[index + 1].position.z) / (points[index].position.z - points[index + 1].position.z));
}
}
(轉)Unity3D 之插值計算