计算刚体运动轨迹

没错,Box2D可以帮我们轻松的解决物理碰撞模拟的问题,但是人类是贪婪的,我们不满足于此,并希望能够快Box2D一步,预先知道下一步或者将来,刚体的运动轨迹,就像在《愤怒的小鸟》中,当我们拉动弹弓后,可以看到小鸟将要飞行的轨迹。

愤怒的小鸟

又或者,可以让刚体听我们的话,指哪打哪,就像《弹弹堂》中攻击敌人可以百发百中。

弹弹堂

这一节,我们就来学习一下如何对刚体运动轨迹未卜先知,进而百发百中。

本节知识点

为实现以上两个游戏中的效果。本节将相关内容分解为一下3个知识点

(该知识点源自iforce2d.net,对英文该兴趣的同学,点击 这里 查看原文)

  1. 计算刚体的位置。第n个timeStep后,刚体的位置。用于描绘小鸟飞行的轨迹。
  2. 计算刚体最大高度。以初始速度v0飞出时,可以达到的最大高度。
  3. 计算刚体初始速度。为达到某个指定位置,而需要的初始速度,实现《弹弹堂》中的百发百中。

计算刚体的位置

初中的时候,我们都学过自由落体运动,假设重力加速度为a,那么经过t秒后,物体下落的距离h可以用下面的公式计算出来

自由落体公式

但是在Box2D的世界里,使用这个公式是不准确的。因为Box2D是一个以delta为频率的数字采样世界,无法完美无缺的还原世界中的运动轨迹。
我们知道,在物体以a为加速度进行加速运动时,t秒后物体的速度v,可以用下面的公式计算出来。

速度公式
v和t之间以加速度为斜率,成连续的线性关系,如下图所示。

现实中的v和t呈线性关系
而在Box2D的数字世界里,每个delta之间,物体是以当时的速度vt进行匀速运动的,所以随着时间t的不断增加,速度v是以delta为单位称阶梯形上升。如下图所示:

Box2D中v和t阶梯型关系
要知道,图中的速度曲线与坐标轴形成的形状面积,就是经过时间t后,刚体运动的距离。所以Box2D模拟出来的运动距离,要比实际少一些。而缺少的部分刚好是阶梯形状缺口的面积,如下图所示:

现实和Box2D中移动距离差异

所以,接下来针对Box2D中物体运动的距离,我们要做的是,计算阶梯形状的面积。为此,可以讲锯齿分解成一个个小的矩形,每个矩形的宽为delta,高度为vn=a*delta*n,计算出每个矩形的面积dn,然后累加起来,如下图所示:

Box2D中的移动距离公式
将图中每个矩形的面积累加起来,将vn替换为tna,同时t=ndelta后,所以刚体移动的距离转换成下面的公式:

Box2D中距离计算公式

如果,刚体有初始速度v0的话,那么要在以上公式d的基础上,累加初始速度移动的距离da = v0*n*delta。公式如下:

带有初始速度的距离计算公式
把这个公式,定义到名为getPositionWhen()的函数中,转换成代码如下所示:

    private function getPositionWhen(pos:b2Vec2, v0:b2Vec2, n:Number):b2Vec2{
        var newPos:b2Vec2 = new b2Vec2();
        var dx:Number,dy:Number;

        var delta:Number = 1/stage.frameRate;
        var a:Number = world.GetGravity().y;
        dy = v0.y*delta*n + delta*delta*a*(n+1)*n/2;

        dx = v0.x * delta*n;

        newPos.y = pos.y + dy;
        newPos.x = pos.x + dx;
        return newPos;
    }

参数说明如下:

  • pos:刚体当前的坐标位置
  • v0:刚体移动的初始速度
  • n:经过的timestep数

计算最大高度

虽然,在移动距离上,Box2D模拟出的结果与实际有些差异。但在速度上,经过时间t = n * delta后,刚体的速度还是符合下面的公式的:

速度公式

我们知道,因为受到重力的作用,刚体在上升过程中,会渐渐慢下来,最终速度vt=0,此时刚体到达最大高度。根据上面的公式,可以计算出vt=0时,经过的timestep数量n:

timestep数量

然后将计算出的n作为参数,传递一个getPositionWhen(),既可以返回最大高度位置。
把以上计算过程,定义到名为getHighestPosition()函数中,代码如下所示:

    private function getHighestPoint(pos:b2Vec2,v0:b2Vec2):b2Vec2{
        var delta:Number = 1/stage.frameRate;
        var a:Number = world.GetGravity().y;
        var n:Number = -v0.y/delta /a;
        return getPositionWhen(pos,v0,n);
    }

计算刚体初始速度

人类得到的越多,就越是贪婪。已经知道了如何计算刚体的位置,以及最大高度,但我们更想知道,如果已知某个坐标位置p,要以多大的速度发射炮弹,可以百步穿杨,击中目标位置p。
实现这一点并不困难,我们假定炮弹到达目标位置时,速度刚好为0,即目标位置为最大高度。那么根据getHighestPosition()函数中的公式,我们可以得知,初始速度v0和到达目标位置,所经过的timestep数n的关系为:
timestep数量
假设炮弹发射位置与目标位置p的垂直距离为d,根据getPositionWhen()中的公式,可以得到n,与距离d的关系为:

距离公式

将n替换为 –v0/a/delta后,可以到只包含未知数v0的一个一元二次方程,具体如下:

速度与距离公式
根据一元二次方程的求解公式:
一元二次方式求解
将速度与距离公式对应整理成一元二次方程求解公式形式,可以轻松的得到v0的求解结果。
对应一元二次方程
解方程后,我们可以得到两个结果,分别表示向上和向下的速度。因为AS3的坐标系统中,y>0是向下的,所以这里v0.y<0的结果。
将以上计算过程,集成到名为getVelocityToPosition()函数中,代码如下:

    private function getVelocityForPosition(from:b2Vec2,to:b2Vec2):b2Vec2{
        var dy:Number = to.y-from.y;
        var dx:Number = to.x -from.x;

        if ( dy >= 0 )
            return new b2Vec2();

        var delta:Number = 1 / stage.frameRate;
        var aGravity:Number = delta * delta * world.GetGravity().y; // m/s/s

        var a:Number = 0.5 / aGravity;
        var b:Number = 0.5;
        var c:Number = dy;

        var quadraticSolution1:Number = ( -b - Math.sqrt( b*b - 4*a*c ) ) / (2*a);
        var quadraticSolution2:Number = ( -b + Math.sqrt( b*b - 4*a*c ) ) / (2*a);

        var vy:Number = quadraticSolution1;
        if ( vy > 0  ){
            vy = quadraticSolution2;
        }

        var vx:Number = dx/(-vy/aGravity*delta);

        return new b2Vec2(vx,vy*stage.frameRate);
    }
}

举个栗子

好了,明白了计算刚体运动估计的算法和公式,下面该上示例了。

下载本节源文件,运行CalcForceFromPosition.swf文件,效果如下图所示,点击舞台任意位置,左下角的小气会向鼠标位置抛出,并准确的击中鼠标位置。
与此同时,在小球抛出之前,它的运动轨迹已经被计算好,并用黑色的圆圈绘制出来,小球会不偏不倚的从绘制好的路径上飘过。
另外,红色点是小球运动的最高点
Demo

大痔过程

关于初始速度、和最高点,使用前面介绍的getVelocityForPosition()和getHighestPoint()都可以直接获取,这里重点说明一下轨迹的绘制。
我们知道,通过前面介绍的getPositionWhen()函数,可以获取任意时刻刚体的位置。那么,如果使用for循环,从1到50(或者更多)多次调用getPositionWhen()计算敢提位置,就会得到连续的刚体移动轨迹,然后再在update()函数中,在这些位置上绘制圆圈,就可以看到连续的轨迹了。代码如下所示:

        override protected function update(e:Event):void{
            super.update(e);

            var v:b2Vec2 =velocity;
            var p:b2Vec2 = new b2Vec2(100/30,300/30);
            for (var i:int = 0; i < 50; i++) 
            {
                var np:b2Vec2 = getPositionWhen(p,v,i);
                LDEasyDebug.debug.DrawCircle(np,3/30,new b2Color(0,0,0));
            }
            var highestPoint:b2Vec2 = getHighestPoint(p,v);
            LDEasyDebug.debug.DrawSolidCircle(highestPoint,5/30,new b2Vec2(),new b2Color(1,0,0));
        }

后记

另外还有一个问题,文章中没有提及。这里我们介绍的是刚体没有发生碰撞时的移动轨迹计算,如果在移动过程,刚体与其他对象发生了碰撞,就无法使用教程中的方法,实现轨迹计算了。
这种情况下,会牵扯到碰撞计算的模拟,这是一个复杂的过程,此时可以创建另外一个与当前Box2D世界完全相同的world(但不对其进行渲染),然后在这个world中预先调用step(),并将运行的结果和数据,在当前的world中渲染出来。
不过这是一个非常消耗性能的做法,因为程序会进行两次(当前world,不渲染的world)渲染,当世界中刚体较多时,会加大CPU的负担,不推荐。

联系作者

公众号:拉小登 | 微博:拉登Dony | B站:拉小登Excel

5 Replies to “计算刚体运动轨迹”

  1. 请问怎么控制刚体的运动轨迹,比如一个手形状的刚体,模拟手在舞台上抹除画布上的物体,运动轨迹怎么设置,直接对刚体施加一个力,还是设置刚体的线速度和角速度,或者添加关节控制手的运动,刚学box2d,想做个类似手绘视频的项目,请拉登大叔指教!

  2. boss让做出真实的效果,涉及到物体间的碰撞等等,让我用这个引擎实现,手的运动轨迹我能通过运动轨迹上的坐标直接在每帧loop的时候直接SetPosition吗?

  3. 你好,我现在在用egret和p2.js参考这篇帖子做运动轨迹效果,运行结果是球开始会沿着轨迹走,但慢慢就会偏离轨迹向下,最终就是球的落点和实际轨迹不符
    代码中用到的转换p2和egret位置长度宽度等单位的factor=30, stageFrame=60, gravity=[0,10], loop(){ world.step(1/60) }, 不知道问题出在哪里

回复 指尖

您的电子邮箱地址不会被公开。 必填项已用*标注