目录
前言
生活中,插值很实用,但是我们似乎没意识到。你在使用的PhotoShop、Krita等绘图软件绘制线条时,平滑的线条实际上就经过了插值处理。随着短视频的流行,剪映的出现,大家可以简单地剪辑视频,视频中过渡时也会用到插值。游戏开发过程中,实现一个角色的平滑移动,也用到了插值。本文将从游戏开发的角度介绍插值。 本项目可以从这里获取:插值
一、线性插值
插值:在两个值之间取一个值。(这个定义怎么这么粗略XD)
插值公式,万变不离其宗。
\(p_{current} = (1-u) \times p_{start} + u \times p_{end}\)上面的是最常用的插值公式,\(p_{current}\)为当前值,\(p_{start}\)为起始值,\(p_{end}\)为最终值,\(u\)为进度。
u是什么的进度?我的见解是该插值过程的进度。线性内插值中,u的取值范围从0到1。
若令u = 0,那么当前值等于起始值,也就意味着插值进展为0,还没开始;
若令u = 1,那么当前值等于最终值,也就意味着插值进展为100%(1),已经完成了。
如果我想让一个物体在指定时间内到达指定位置,应该如何做呢?
// 绑定至小方块上
public class Interpolation : MonoBehaviour
{
public Vector3 curPos; // 当前位置
public Vector3 startPos; // 起点位置
public Vector3 endPos = new Vector3(0,0,10); // 最终点的位置
public float startTime; // 开始运动的起始时间
public Transform testCubeTransform; // 这个小方块将从初始位置运动到(0,0,10)
public float timeDuration = 1f; // 插值过程(小方块移动到鼠标的位置这个过程)持续时间
public bool canMove = false; // 是否可以移动
private void Start()
{
// 获取小方块的Transform组件
testCubeTransform = this.GetComponent();
}
private void Update()
{
// 当按下空格键,可以开始测试;按下P键,退出测试
if (Input.GetKeyDown(KeyCode.Space))
{
canMove = true;
// 记录开始运动的时间
startTime = Time.time;
}
if (Input.GetKeyDown(KeyCode.P))
{
canMove = false;
}
// 没开始测试就不计算这些插值
if (!canMove)
{
return;
}
// 记录起始点
startPos = testCubeTransform.position;
// 计算u值
float u = (Time.time - startTime) / timeDuration;
// 限制u的值
if (u >= 1)
{
u = 1;
canMove = false;
}
// 计算当前位置
curPos = (1 - u) * startPos + u * endPos;
testCubeTransform.position = curPos;
}
}
在Unity中创建一个Cube对象,并设置其位置为(0,0,0),将上面的脚本附加到该对象上,点击运行后,按下空格即可开始测试,大概效果如图。
线性插值运动的小方块
二、使用插值让一个对象跟随另一个对象移动
小插曲:你听说过Zeno悖论吗?有一只青蛙,每次向前跳跃都只能到达距离前方的墙一半的位置,它永远也无法跳到墙上……
话不多说,直接附上代码。
public class FollowMouse : MonoBehaviour
{
public Transform cube; // 小方块的transform组件
private void Update()
{
Vector3 mouseScreenPos = Input.mousePosition; // 鼠标屏幕坐标
mouseScreenPos.z = 10; // 手动设置一下
if (Input.GetKeyDown(KeyCode.S))
Debug.Log(mouseScreenPos);
Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(mouseScreenPos); // 将屏幕坐标转换成世界坐标
if(Input.GetKeyDown(KeyCode.W))
Debug.Log(mouseWorldPos);
float u = 0.5f;
cube.position = (1 - u) * cube.position + u * mouseWorldPos;
}
}
效果如下:
方块跟随鼠标移动
三、更平滑的插值
1. 缓入;2. 缓出;3. 缓入缓出;4. Sin;5. Sin缓入;6. Sin缓出。
1. 缓入
缓入呈现出的效果是开始的运动较慢,逐渐加快运动。实现缓入的方法是通过令\(u = u^{easingModify}\),其中easingModify指的是缓入的平滑程度。接下来我将修改代码,让物体实现缓入的效果。
当easingModify为1时,实际上和前面提到的线性插值没有区别。
以下为修改后的代码:
// 绑定至小方块上
public class Interpolation : MonoBehaviour
{
public Vector3 curPos; // 当前位置
public Vector3 startPos; // 起点位置
public Vector3 endPos = new Vector3(0,0,10); // 最终点的位置
public float startTime; // 开始运动的起始时间
//----------新增的代码----------//
public float easingModify = 2; // 缓动的修饰值
//----------新增的代码----------//
public Transform testCubeTransform; // 这个小方块将从初始位置运动到(0,0,10)
public float timeDuration = 1f; // 插值过程(小方块移动到鼠标的位置这个过程)持续时间
public bool canMove = false; // 是否可以移动
private void Start()
{
// 获取小方块的Transform组件
testCubeTransform = this.GetComponent();
}
private void Update()
{
// 当按下空格键,可以开始测试;按下P键,退出测试并回到原点
if (Input.GetKeyDown(KeyCode.Space))
{
canMove = true;
// 记录开始运动的时间
startTime = Time.time;
}
if (Input.GetKeyDown(KeyCode.P))
{
canMove = false;
testCubeTransform.position = new Vector3(0, 0, 0);
}
// 没开始测试就不计算这些插值
if (!canMove)
{
return;
}
// 记录起始点
startPos = testCubeTransform.position;
// 计算u值
float u = (Time.time - startTime) / timeDuration;
// 限制u的值
if (u >= 1)
{
u = 1;
canMove = false;
}
//----------新增的代码----------//
float u2 = Mathf.Pow(u, easingModify);
//----------新增的代码----------//
// 计算当前位置,将u换成u2测试缓动
curPos = (1 - u2) * startPos + u2 * endPos;
testCubeTransform.position = curPos;
}
}
来看看效果如何,以下分别为easingModify为2和3的时候的效果:
easingModify = 2, \(u2 = u^{easingModify}\)
easingModify = 3, \(u2 = u^{easingModify}\)
2. 缓出
缓出的效果是物体快到达终点时速度变缓。实现的方式是令\(u=1-(1-u)^{easingModify}\),请定位到这一句代码并修改。
// 修改前的代码
float u2 = Mathf.Pow(u, easingModify);
// 修改后的代码
float u2 = 1 - Mathf.Pow(1-u,easingModify);
由于是缓出,为了更明显地看出区别,我将endPos设为了(0,0,30),timeDuration设为了4。读者可以调整至合适的数值以观察出区别。
easingModify = 1, \(u2=1-(1-u)^{easingModify}\)
easingModify = 2, \(u2=1-(1-u)^{easingModify}\)
当easingModify=1时,是最原始的线性插值;当easingModify=2时,是缓出更平滑,可以看到刚开始运动得快一些。
3. 缓入缓出
当\(u<=0.5\)时,令\(u=0.5 \times \frac{(2u)^{easingModify}}{2}\);当\(u>0.5\)时,令\(u=0.5 + 0.5 - \frac{[1-2(u-0.5)]^{easingModify}}{2}\)。
请修改u2的值。
float u2 = u;
if (u <= 0.5)
{
u2 = Mathf.Pow(u, easingModify);
}
else if(u>0.5)
{
u2 = 1 - Mathf.Pow(1 - u, easingModify);
}
下面是效果展示:
easingModify = 1, 当\(u<=0.5\)时,令\(u=0.5 \times \frac{(2u)^{easingModify}}{2}\);当\(u> 0.5\)时,令\(u=0.5 + 0.5 - \frac{[1-2(u-0.5)]^{easingModify}}{2}\)
easingModify = 2, 当\(u<=0.5\)时,令\(u=0.5 \times \frac{(2u)^{easingModify}}{2}\);当\(u> 0.5\)时,令\(u=0.5 + 0.5 - \frac{[1-2(u-0.5)]^{easingModify}}{2}\)
可以明显发现缓入缓出在开始和快到达终点时都较慢。
4. Sin
Sin,令\(u2=u+easingModify \times \sin(2\pi u) \)。
修改代码如下:
float u2 = u + easingModify * Mathf.Sin(Mathf.PI * u * 2);
效果如下图所示,timeDuration=1,endPos = (0,0,5),分别展示了easingModify = 1, easingModify =-0.2的情形。
easingModify = 1, \(u2=u+easingModify \times \sin(2\pi u) \)
easingModify = -0.2, \(u2=u+easingModify \times \sin(2\pi u) \)
可以发现,当easingModify = -0.2时,可以看见小方块先往后移动了一小段距离再往前运动,有一种蓄力助跑的感觉。
5. Sin缓入
Sin缓入,令\(u2=1-\cos(\frac{\pi}{2}u)\),你应该注意到了,这个效果中没有使用easingModify。
修改代码如下:
float u2 = 1 - Mathf.Cos(Mathf.PI * 0.5f * u);
\(u2=1-\cos(\frac{\pi}{2}u)\)
你能观察到效果还不错。
6. Sin缓出
Sin缓出,令\(u2=sin(\frac{\pi}{2}u)\)
修改代码如下:
float u2 = Mathf.Sin(Mathf.PI * 0.5f * u);
\(u2=sin(\frac{\pi}{2}u)\)
效果还行,开始比较快。
四、贝塞尔曲线
贝塞尔曲线实际上实现的是三个及以上的点的插值,有点像俄罗斯套娃。
举个例子,有三个点,分别为P1、P2和P3点。过程如下:
- 使用公式\(p_{current} = (1-u) \times p_{start} + u \times p_{end}\)计算P1点和P2点间的插值,记为P-12。
- 同理计算P2点和P3点间的插值,记为P-23。
- 最后计算P-12和P-23之间的插值,作为最终结果。
实际上这个过程进行了3次插值运算,再举个例子,有三个点,分别为P1、P2、P3和P4。插值过程如下:
- 使用公式\(p_{current} = (1-u) \times p_{start} + u \times p_{end}\)计算P1点和P2点间的插值,记为P-12。
- 同理计算P2点和P3点间的插值,记为P-23。
- 同理计算P3和P4间的插值,记为P-34。
- 接着计算P-12和P-23间的插值,记为P-123。
- 接着计算P-23和P-34间的插值,记为P-234。
- 最后计算P-123和P-234间的插值,记为P-1234,作为最终结果。
可以看到,四个点进行了6次插值运算。
接下来在Unity中看看效果如何。
public class InterpolationBezier : MonoBehaviour
{
public Transform cube; // 小方块
// 三个点均在检查器面板设置
public Vector3 pos1;
public Vector3 pos2;
public Vector3 pos3;
public float startTime;
public float timeDuration;
public bool canMove;
private void Update()
{
// 开始测试
if (Input.GetKeyDown(KeyCode.Space))
{
canMove = true;
startTime = Time.time;
}
// 回到原点
if (Input.GetKeyDown(KeyCode.P))
{
cube.position = Vector3.zero;
canMove = false;
}
if (!canMove)
return;
float u = (Time.time - startTime) / timeDuration;
if (u >= 1)
{
u = 1;
canMove = false;
}
Vector3 pos12 = (1 - u) * pos1 + u * pos2;
Vector3 pos23 = (1 - u) * pos2 + u * pos3;
Vector3 pos123 = (1 - u) * pos12 + u * pos23;
cube.position = pos123;
}
}
贝塞尔曲线,(0,0,0)->(10,0,10)->(0,0,30),timeDuration=1
效果还不错XD
总结
本文介绍了几种线性插值,利用这些插值实现平滑的移动吧!