算法、數據結構、與設計模式等在游戲開發中的運用 (三):插值(Interpolation)
作者:Compasslg(李涵威)
1. 什么是插值
插值(Interpolation)其實是數學中的一種常用概念,他是利用一種給定函數來連接點的方式。在數學中,插值被用於通過將離散的點數據連接成連續的曲線,來達到補全函數圖像的目的。而在游戲開發中,插值則常常被運用於實現動畫(Animation)和 移動(motion)。
所謂插值,代表的是在離散點之間通過插入連續的“估值”來連接他們的概念,而不同的插值方法可以達到不同的連接效果。常用的插值有線性插值,三角函數插值,樣條插值等。不同的插值類型會造成在關鍵點附近圖像的平滑程度有所區別,但總的而言,給定的數據點都一定會在圖像上,這也是插值與數學中另一個常常被拿來討論的概念 擬合(Curve Fitting) 的區別。
- 線性插值是直接利用直線來連接點
- 非線性插值產生的圖像斜率變化得更為平滑
2. 如何實現和使用插值
插值的類型很多,但調用方式都大同小異,基本上都是給定數據點(起點和終點)以及當前自變量的值為參數,然后返回這個自變量所對應的插值。由於這篇博文主要討論的是插值在游戲中的應用而非每個插值的實現原理,這里我只以最簡單的線性插值和利用三角函數實現的非線性插值為例進行代碼實現。
線性插值的實現非常簡單,你可以把他想象成路程為(起點 - 終點),總時間為1的勻速直線運動。以下為范例代碼:
float LinearInterpolate(float startVal, float endVal, float t){
return startVal + t * (endVal - startVal);
}
非線性插值的主要優勢在於在比線性插值在數據點附近會更為平滑,實現例如在起點附近加速,終點附近減速的效果;但他同樣是t從0到1,返回值從起點運動到終點。也就是說,只要對t稍加處理,只要兩端的0和1不變,就可以達到這個平滑的效果。
我們都知道cos(t$\pi$)的函數圖像在$t \in [0, 1]$中y值是“平滑”的從1運動-1,在t = 0 附近加速變化,t=1附近減速變化,如下圖所示
所以我們只要稍加變化, 用 (1 - cos(t$\pi$)) / 2 就可以得到我們想要的效果,平滑的從起點運動到終點,如下圖所示
以下為范例代碼:
float CosInterpolate(float startVal, float endVal, float t){
float t_cos = (1 - Mathf.Cos(t * Mathf.PI)) / 2;
return startVal + t_cos * (endVal - startVal);
}
在unity以及各種有向量概念的游戲引擎中,你也可以直接將數據點參數改成向量類型。由於實現方式除了使用的數據類型以外基本相同,這里就不重復了。
Vector3 interpolate(Vector3 startpoint, Vector3 endpoint, float t);
插值函數具體的調用方法會在下面介紹。
3. 游戲開發中的應用(Unity)
在游戲開發中,插值主要被運用在下列幾個方面:
- 將時間作為參數,通過插值來補充某個數據(坐標點、顏色等)來實現平滑的直線運動或者顏色漸變的效果。
以下是Unity中用線性插值實現線性移動和漸變的代碼(非線性插值的使用同理,只要改變調用的函數即可;除了上一部分實現的cos插值以外,很多游戲引擎本身也有提供類似的函數,感興趣的可以去了解一下Unity中的SmoothStep 和 SmoothDamp)。
public class Mover : MonoBehaviour
{
// 在Inspector中設置起點和終點的位置
public Vector3 startpoint, endpoint;
// 從起點運動到終點所需要的時間(周期)
public float period;
// 當前時間參數t
private float t;
private SpriteRenderer spriteRenderer;
// Start is called before the first frame update
void Start()
{
t = 1;
spriteRenderer = GetComponent<SpriteRenderer>();
}
// Update is called once per frame
void Update()
{
// 按下空格鍵開始移動
if(Input.GetKeyDown(KeyCode.Space)){
t = 0;
}
// 更新當前時間,並通過插值獲取位置
if(t < period){
t += Time.deltaTime;
// 這里使用的是Unity Vector3中自帶的線性插值函數,效果相同只是直接作用在Vector3上
// 以時間為參數,用插值獲得起點到終點之間的位置
// 由於插值默認時間t在0..1之間(period = 1),這里需要用 t/period 來轉化成動畫播放的實際周期
transform.position = Vector3.Lerp(startpoint, endpoint, t / period);
// UnityEngine.Color也同樣有線性插值函數Lerp,實現方法一樣只是作用與Color(r,g,b,a)
//這里展示的是在移動中從白色轉變為黑色的過程
spriteRenderer.color = Color.Lerp(Color.white, Color.black, t / period);
}
}
}
- 假設你的精靈表單(Spritesheet)中有n個精靈(Sprite)是用於某一個動畫中的,那么你可以將時間作為參數,用插值的方法來獲得[0, n]之間的精靈索引值(Index)來控制精靈圖片的切換速率,實現精靈動畫(Sprite Animation)效果。
以下為Unity中的范例代碼
public class SpriteSheetAnimator : MonoBehaviour
{
// spritesheet中所有的sprites
public Sprite[] sprites;
// 動畫播放一遍所需要的時間(周期)
public float period;
// 當前動畫播放時間
private float t;
private SpriteRenderer spriteRenderer;
// Start is called before the first frame update
void Start()
{
t = period;
spriteRenderer = GetComponent<SpriteRenderer>();
}
// Update is called once per frame
void Update()
{
// 按下空格鍵重制時間,從頭播放
if(Input.GetKeyDown(KeyCode.Space)){
t = 0;
Debug.Log("Start");
}
// 更新 當前時間t 和 當前精靈圖片
if(t < period){
t += Time.deltaTime;
// 利用獲得以0為起點,(sprites總數-1)為終點的插值來計算當前精靈圖片的index
// 由於插值默認時間t在0..1之間(period = 1),這里需要用 t/period 來轉化成動畫播放的實際周期
int curIndex = Mathf.FloorToInt(interpolate(t / period, 0, sprites.Length - 1));
spriteRenderer.sprite = sprites[curIndex];
}
}
public float interpolate(float startVal, float endVal, float t){
return startVal + t * (endVal - startVal);
}
}
除了上述兩個被詳細介紹的方面以外,插值也可以用於碰撞檢測(實際上是通過使用參數方程來計算碰撞點是否存在,以后開單章介紹)以及曲線運動(基本和連接函數圖像一樣,這里就不贅述了)。同時,插值在圖形學中也有很多妙用。不過對於游戲開發來說,圖形學涉及的部分已經非常底層,這里也同樣略過了。
4. 總結
總體而言,插值在實現簡單的動態效果上是非常實用的,更何況他的實現方式也並不復雜,而且大部分游戲開發工具都會自帶插值函數。不過要學習更多有趣的插值函數和運用方法,不是短短一篇博客可以解決的,這篇文章也旨在幫助大家了解插值在游戲開發中最常用到的地方,所以如果有興趣進一步了解的話還是需要自己去學習。