基於向量插值的軌跡計算

前言

上一篇文章:
這一篇文章是填上次的遺留問題,算是一次填坑吧。可能大多數人對上次的感覺是,看得懂,可是使用的時候就有很多問題,這次我會詳細說明用法,期待大家能用在項目中。

簡單的插值運動

我們先從最簡單的運動說起:
我們的目的很簡單,就是要通過插值函數讓物體移動到目標點,下面我在Unity中創建好了兩個物體,綠色代表炮彈物體,紅色則代表目標物體;
在這裏插入圖片描述
這裏我們規定,發射位置爲綠色物體在遊戲開始運行時的位置,目標位置爲在遊戲開始運行時紅色物體的位置。
接下來我們開始插值計算:

	public float lerpSpeed;//插值速度
	
	public float lerpTime;//插值時間
	
	private Vector3 startPos;//開始位置
	
	private Vector3 targetPos;//目標位置
	
	public Transform self;//炮彈
	
	public Transform target;//目標
	
	// Update is called once per frame
	void Update () {
	
	    lerpTime += lerpSpeed * Time.deltaTime;
	
	    self.position = Vector3.Lerp(startPos, targetPos, lerpTime);
	}

運行後就可以看到炮彈朝目標移動,而且時勻速的。

插值運動的擴展-面向目標單位

從實用視角度出發,單純控制物體的位置並不能滿足我們的需要,因爲真實的炮彈一定是面向目標單位飛行的(當然,我們這裏沒有考慮軌跡的問題),這個問題也很好解決,我們只需要算出一個從發射位置看向目標的向量就好了,可以的話,這個向量最好是單位向量,因爲,我們只關心它的方向,而不是大小。獲得這個向量後,你要把它應用在炮彈物體上,將它的座標軸朝向強制改爲這個向量的朝向!可可以使用Unity官方自帶的 LookAt 函數,也可以將這個向量當作值,付給它的forward,(forward, right, up, 這三個變量是Transform下的分別代表物體的,前,右,上,三個方向;他們也對應這物體自身座標系下的,Z, X, Y, 軸。這點很重要,它們會幫助你完成很多計算,而且它們不止是隻讀的),下面是實現代碼:
版本1:

Vector3 dir = Vector3.Normalize(targetPos - self.transform.position);
self.transform.forward = dir;

版本2:

Vector3 dir = targetPos - self.transform.position;
self.lookAt(target);

讀者可以根據自己的需要取選擇一個合適的方法,他們的實現內部大致都是一樣的,所以這點不再給出,當然有興趣的讀者可以取研究研究,這裏我簡單說下思路;
實現思路:
根據獲取到的一個方向向量(單位向量),及計算出一組單位正交向量基,也就是一組新的座標朝向,再把這組新獲取的座標,應用在當前物體上,至於怎麼去應用和怎麼去計算一組單位正交向量基,讀者可以去看我講解的矩陣的含義一篇,就會明白其中的含義。
你足夠細心的話,就會發現通過使用這兩鍾方法得到的結果是不會導致物體出現萬向節鎖的問題的(你可以百度萬向節鎖)。至於爲什麼如果你不理解,也可以翻看我之前的文章加以理解。
想理解正交基如何應用到物體;
關於正交基的計算問題。

運行後你就會得到始終朝向目標的炮彈,這很簡單,這裏就不多說了。

差值運動的擴展-朝向運動軌跡

線性軌跡的插值,有時候仍然不能滿足我們的需求,如果我們要做的是以個類似於紅色警戒中的V3火箭車的發射呢?顯然上面的方法是不具有實用性的,所以我們要尋求一種新的解決辦法,保證它的運動在任何情況下都是可行的,且是合理的。
下來我們來一步步的思考,我會引導讀者來完成這個過程,而不是直接告訴你們答案是怎麼樣的。

我們想要的結果是讓炮彈沿着他自己的運動軌跡運動,而不是一直看向目標,也就是說,我們希望炮彈的朝向是一個動態可變的向量,且實在自己的運動軌跡上的,在此我們把一個複雜的問題簡化爲,求出一個朝向的向量;
上面我們把問題簡化爲求出一個在炮彈需要朝向的向量。我們繼續結合要求去分析,炮彈的朝向向量,不僅是個簡單的朝向,而且這個向量因爲要符合炮彈運動的軌跡,所以一定要根據炮彈的位置去計算,這樣纔會符合我們的需求;

那麼向量的計算可以通過炮彈的位置得出,所以如果我們計算的向量是通過炮彈已經運動過的位置計算的,那麼得出的向量一定符合我們的要求!我們的思路已經清晰了,只需要記錄不事件炮彈走過的位置,用這些位置去計算出一個炮彈的朝向就可以了,下面是實現代碼:

Vector3 lastPos;//上一幀的位置

Vector3 currentPos;//當前幀的位置

void LookAtBulletDir()
{
	currentPos = self.position;
	self.forward = Vector3.Normalize(currentPos - lastPos);
	lastPos = currentPos;//跟更新位置
}

我們把 lastPos 的值放在最後更新,以確保在 lastPos 時,使用的是上一幀的值,這裏不再多做解釋。
但是讀者應該會發現這個方法存在一個問題,也就是在第一幀的時候我們使用了爲賦值的 lastPos ,那麼它的值會默認是zero, 炮彈的方向也會出現奇怪的情況,但是這是一個可以規避的問題,讀者可以自己優化它,在這裏我不需要在做太多解釋,我這裏主要提供的還是一個解決思路,也許你還有比我更好的方法,不是嗎?

插值運動的擴展-插值下的軌跡運動

講了那麼多,我們終於可以進入主題了,這次我們來解決上篇文章最後遺留的一個問題。是用插值的運動軌跡問題。如果你還對向量計算熟悉,可能會難以理解,但如果你一旦學會,所有問題就會迎刃而解。下面我們開始吧!
我們在前兩部分解決了,炮彈的位置和朝向問題,這是不夠的。下來我會教大家使用一種方法,去實現炮彈的任意軌跡移動:

  • 運動的軌跡函數
    首先,要達成我們的目標,我們必須先得到一個軌跡函數,用來表示炮彈的運動匯軌跡,雖然我們是在3D世界下運行的,但是這並不代表我們就需要求一個3D世界座標系下的運動函數,普通的直角座標系下的函數就完全夠用了,比如,我們找到的函數是 Cos(x * PI * 0.5f) * x, 這個函數的圖像爲:
    在這裏插入圖片描述
    這個函數可以模擬一個炮彈的軌跡,雖然不是很精確,但是已經夠用了。

    從圖像我們不難看出,這個函數從0 - 1的變化值爲0 - 某一值 - 0;這是他的變化趨勢,我們也正要這種變化趨勢的函數,因爲我們的插值數值在 0 - 1,也就是說當插值量爲 0 時, 我們開始運動, 爲 1 時, 我們就要結束這個運動,所以要把這裏的“軌跡”函數, 不能看作是軌跡函數,而是一個代表位置的變化趨勢的函數,或者一個代表炮彈的高度的變化趨勢的函數;

    我們有了炮彈的飛行高度的變化趨勢,就要把它運用進來,它的變化量從 0 - 1,正好是 0 - 任意值 - 0, 也就是說,這個變化函數,可以讓炮彈的高度在發射時爲0, 發射中時爲任意值 ,擊中時爲0, 這正是我們要的,所以我們要獲取一個增量的方向,可以讓炮彈的高度在這個方向上變化。
    因爲炮彈是在垂直於世界的XZ平面的方向上移動的,也就是Y軸,所以,我們直接取,Vector3.up,作爲高度的變化方向,所以我們要給原本的插值過程中加上一個方向的變化量,來取得一個以原位置爲基礎,在Y軸上偏移了 Cos(x * PI * 0.5f) * x 大小的實際位置,然後把這個位置給炮彈(x代表插值函數中的t),然後隨着插值時間X的增大,炮彈就會沿着 Cos(x * PI * 0.5f) * x,的點所組成的軌跡運動了。
    下面是實現代碼:

void Motion()
{

	self.position = Vector3.Lerp(startPos, targetPos, lerpTime) 
	+ Vector3.up * Mathf.Cos(lerpTime * Mathf.PI * 0.5) * lerpTime; 
	
}

讀者可以自行補充其他代碼,然後運行起來看看結果,無論你怎麼改變位置和方向,他都會有一個類似拋物線的運動軌跡,而且在 lerpTime > 1時一定會擊中目標,這樣一來我們就只用判斷 lerpTime大不大於 1,就可以知道有沒有擊中,而不是直接的去判斷距離了,如果時判斷距離還需要關心,炮彈距離的擊中距離是多少,而且如果炮彈速度過快,也有可能出現和碰撞失效一樣的結果,但是用插值就不會出現這個結果,因爲,當 lerpTime 超過1時,炮彈一定會撞上目標。如果你想讓運動更真實,那你只需要找一個更加貼近炮彈運動的軌跡函數就可以了,但是有一點必須注意,那就是這個函數的因變量,要在區間 [0, 1] 內的變化趨勢時, 0 -> 任意值 -> 0,這樣才能保證,當時間大於等於 1 時,炮彈已經擊中了目標。

插值運動的擴展-任意的發射方向,任意的速度

我們已經做出了類似拋物線的運動,但是可能還不夠,因爲,我們有的時候想做一個可以在任意方向發射的飛彈,比如說,如果我需要從飛機投擲一個飛彈,這個飛彈在空中飛行的軌跡類似一個拋物線,也就是這樣:
在這裏插入圖片描述
我們不難看出,這時炮彈的速度方向,不再是單純的垂直於世界水平面了,而是一個和自己飛行速度方向相同的方向,那麼這時我們怎麼做呢?其實還是一樣的,我們只需要取出它的飛行方向,然後加上插值位置就可以了,但是我們仍需要給這個方向乘上一個變化量,以便他能在時間等於 1 的時候,擊中物體,所以下面我們這麼做:

 代碼:
 void AnyOffDir()
 {
	self.position = Vector3.Lerp(startPos, targetPos, lerpTime) +
	speedDir * Mathf.Cos(lerpTime * Mathf.PI * 0.5f) * lerpTime ;
}

我們定義 speedDir 爲飛行的速度方向,這樣我們就可以得到一個僞拋物線了,
綜上所述,我們不難看出這種做法的原理,下面時原理圖:

在這裏插入圖片描述
其實很簡單,Vector3.Lerp的1插值是個線性插值,也就是說他返回的一系列點都是在一條線上的,我們不能去改變這條線上的數據,因爲它代表了從開始位置到結束位置的過程,也就是說,這些點是我們給炮彈運動軌跡做任何改動的基礎。我們得到所謂的炮彈軌跡只不過時在原來的線性插值得到的點的基礎上,進行了偏移而已,這也就是把複雜的問題簡單化地過程。

我這裏把複雜的曲線運動,進行了一個分解,先求出最簡單的直線運動,再在這條直線上畫出原圖形進行分析;

明白了這一個原理,你甚至可以繪製出一條閃電!

下面是任意的發射方向代碼:

 	float temp = Random.Range(-2f, 2f);
 	float sinT = Mathf.Sin(temp * Mathf.PI);
 	float cosT = Mathf.Cos(temp* Mathf.PI);

	void AnyOffDir()
	{
	
	Vector3 offDir = Vector3.Normalize(targetPos - startPos);//計算髮射方向
	Vector3 up = this.Step(0.99, offDir .y) * Vector3.Up + (1 - this.Step(0.99f, offDir )) * Vector3.forward;//這裏這麼做是爲了不讓發射方向和 Y , Z 軸平行
	Vector3 right = Vector3.Normalize(Vector.Cross(up, offDir ));
	up = Vector3.Cross(right, offDir );
	
	offDir = right * cosT + sinT * up;//得到一個任意的偏移方向
	
	}
	
	int Step(float a, float x)
	{
		return x >= a ? 0 : 1; 
	}
	

任意偏移方向的計算原理是,通過先求出極座標系,再將極座標系轉化爲直角座標的思想來得到一個圓上隨機點的位置,然後來用作偏移方向。注意,方向的計算應該在發射炮彈時就計算好!

  • 變速的運動
    雖然我們得到我們想要的了,但是你會發現,這些運動的變化速度過於均勻。也就是說,無論是什麼運動,他都是一個速度進行的,下面是我們之前的炮彈速度變化圖:
    對他是一個勻速變化的,但是我們並不像這樣,因爲我們知道,在實際當中,不存摺這樣運動的炮彈,所以我們要改變炮彈的速度,要求他的速度是由 0 變快的,
    比如說,做自由落體運動的物體的速度變化趨勢:
    這時我們並不需要速度在時間爲 1 時,它爲 0 , 這樣的話,顯然是不符合邏輯的!所以我們將這個公式 0.5 * Pow(x, 2) 運用到我們的運動當中去:
 lerpTime += Time.delatTime;  float tempTime = 0.5f *
 Mathf.Pow(lerpTime, 2);

直接將上面的代碼作爲插值 t , 填入,運行,就可以看到結果了,炮彈的速度會逐漸加快,而不是知識簡單的勻速運動了。

結尾

將問題簡化的思想是重要的,我們不能把一個問題看的太複雜,要學會分解運動,去看透一個現象的本質是什麼。而不是關心它的表象。如果這篇文章幫助到了你,或者有什麼疑問,請在下方評論,我會及時回覆。當然如果有錯誤也歡迎提出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章