演算法、資料結構、與設計模式等在遊戲開發中的運用 (三):插值(Interpolation)

Compasslg 發表於 2021-04-24

演算法、資料結構、與設計模式等在遊戲開發中的運用 (三):插值(Interpolation)

作者:Compasslg(李涵威)

1. 什麼是插值

插值(Interpolation)其實是數學中的一種常用概念,他是利用一種給定函式來連線點的方式。在數學中,插值被用於通過將離散的點資料連線成連續的曲線,來達到補全函式影像的目的。而在遊戲開發中,插值則常常被運用於實現動畫(Animation)和 移動(motion)

所謂插值,代表的是在離散點之間通過插入連續的“估值”來連線他們的概念,而不同的插值方法可以達到不同的連線效果。常用的插值有線性插值,三角函式插值,樣條插值等。不同的插值型別會造成在關鍵點附近影像的平滑程度有所區別,但總的而言,給定的資料點都一定會在影像上,這也是插值與數學中另一個常常被拿來討論的概念 擬合(Curve Fitting) 的區別。

  • 線性插值是直接利用直線來連線點
    線性插值
Matlab中的線性插值補全函式影像


  • 非線性插值產生的影像斜率變化得更為平滑
    樣條插值
Matlab中的樣條插值補全函式影像


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附近減速變化,如下圖所示
在這裡插入圖片描述

cos(t * pi)的影像


所以我們只要稍加變化, 用 (1 - cos(t$\pi$)) / 2 就可以得到我們想要的效果,平滑的從起點運動到終點,如下圖所示

在這裡插入圖片描述

(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)

在遊戲開發中,插值主要被運用在下列幾個方面:

  1. 將時間作為引數,通過插值來補充某個資料(座標點、顏色等)來實現平滑的直線運動或者顏色漸變的效果

線性插值移動

Unity中用線性插值在1秒內從 (-1,-1,0) 移動到 (1,1,0),從白色漸變為黑色


在這裡插入圖片描述

改用第2部分實現的 cos插值來移動達到的效果


以下是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);
        }
    }
}

  1. 假設你的精靈表單(Spritesheet)中有n個精靈(Sprite)是用於某一個動畫中的,那麼你可以將時間作為引數,用插值的方法來獲得[0, n]之間的精靈索引值(Index)來控制精靈圖片的切換速率,實現精靈動畫(Sprite Animation)效果

泡泡爆炸spritesheet

泡泡爆炸精靈表單(Spritesheet)

泡泡爆炸gif

泡泡爆炸精靈動畫播放效果



以下為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. 總結

總體而言,插值在實現簡單的動態效果上是非常實用的,更何況他的實現方式也並不複雜,而且大部分遊戲開發工具都會自帶插值函式。不過要學習更多有趣的插值函式和運用方法,不是短短一篇部落格可以解決的,這篇文章也旨在幫助大家瞭解插值在遊戲開發中最常用到的地方,所以如果有興趣進一步瞭解的話還是需要自己去學習。

相關文章