算法、数据结构、与设计模式等在游戏开发中的运用 (三):插值(Interpolation)

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

在游戏开发中,插值主要被运用在下列几个方面:

将时间作为参数,通过插值来补充某个数据(坐标点、颜色等)来实现平滑的直线运动或者颜色渐变的效果

线性插值移动

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); } } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpzpzg.html