基于向量插值的轨迹计算

前言

上一篇文章:
这一篇文章是填上次的遗留问题,算是一次填坑吧。可能大多数人对上次的感觉是,看得懂,可是使用的时候就有很多问题,这次我会详细说明用法,期待大家能用在项目中。

简单的插值运动

我们先从最简单的运动说起:
我们的目的很简单,就是要通过插值函数让物体移动到目标点,下面我在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 , 填入,运行,就可以看到结果了,炮弹的速度会逐渐加快,而不是知识简单的匀速运动了。

结尾

将问题简化的思想是重要的,我们不能把一个问题看的太复杂,要学会分解运动,去看透一个现象的本质是什么。而不是关心它的表象。如果这篇文章帮助到了你,或者有什么疑问,请在下方评论,我会及时回复。当然如果有错误也欢迎提出。

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