Box2D C++教程11-旋转指定角度
Box2D C++教程11-旋转指定角度
转载自:http://www.ohcoder.com/post/2012-06-18/40027953487
旋转指定角度
这个话题就像之前那个话题,只不过把直线运动改成旋转运动。旋转一个物体既可以直接设置角度也可以通过设置扭矩/冲量方法来实现,对一个点直接改变其旋转角度,如果是这么做,那么其实并不算是真正的模拟物理场景。
为了做这个实验我们需要一个动态物体,最好没有重力的影响,那样物体可以停留在半空中。我们会给物体附加一个具有特定方向的定制器,我们可以查看物体是否面向右侧。让我们就像设置“顶点”一样先设置一下这个多边形吧:
b2Body* body;
FooTest() {
//body definition
b2BodyDef myBodyDef;
myBodyDef.type = b2_dynamicBody;
//hexagonal shape definition
b2PolygonShape polygonShape;
b2Vec2 vertices[6];
for (int i = 0; i < 6; i++) {
float angle = -i/6.0 * 360 * DEGTORAD;
vertices[i].Set(sinf(angle), cosf(angle));
}
vertices[0].Set( 0, 4 ); //change one vertex to be pointy
polygonShape.Set(vertices, 6);
//fixture definition
b2FixtureDef myFixtureDef;
myFixtureDef.shape = &polygonShape;
myFixtureDef.density = 1;
//create dynamic body
myBodyDef.position.Set(0, 10);
body = m_world->CreateBody(&myBodyDef);
body->CreateFixture(&myFixtureDef);
//zero gravity
m_world->SetGravity( b2Vec2(0,0) );
}
设置物体的点,使其朝向我们,然后设置鼠标输入方法:
//class member variable
//class member variable
b2Vec2 clickedPoint;
//in class constructor
clickedPoint = b2Vec2(0,20);//initial starting point
//override parent class
void MouseDown(const b2Vec2& p)
{
//store last mouse-down position
clickedPoint = p;
//do normal behaviour
Test::MouseDown( p );
}
//inside Step()
glPointSize(4);
glBegin(GL_POINTS);
glVertex2f( clickedPoint.x, clickedPoint.y );
glEnd();
如果你不明白Step()方法里的代码也不要紧,这段代码只是在屏幕上画出了鼠标单击的点。
这个点将会移到鼠标最后一次单击的地方。在场景中这也是一种默认的掠夺行为,虽然有一点点怪异但是还是比较好的满足了我们的需求。
直接设置角度
使用SetTransform方法来设置角度非常简单,但是首先我们需要知道设置到哪个角度,给定物体的坐标点以及需要设置的目标点。添加每帧都会调用的Step()方法:
//in Step() function
float bodyAngle = body->GetAngle();
b2Vec2 toTarget = clickedPoint - body->GetPosition();
float desiredAngle = atan2f( -toTarget.x, toTarget.y );
//view these in real time
m_debugDraw.DrawString(5, m_textLine, "Body angle %.3f", bodyAngle * RADTODEG);
m_textLine += 15;
m_debugDraw.DrawString(5, m_textLine, "Target angle %.3f", desiredAngle * RADTODEG);
m_textLine += 15;
现在用鼠标单击屏幕并进行旋转,看看在物体本身和目标点之间的角度是如何旋转的。注意如果物体在某个方向上持续旋转,那么角度会不停的变大甚至会超过360度,但是我们计算的目标点的角度会保持在-180和180之间。
现在我们看到是如何计算行为角度的,尝试使用SetTransform方法可以让物体指向目标点,而且你会看到物体自己会立刻指向目标点。
body->SetTransform( body->GetPosition(), desiredAngle );
试着抛弃物体,之后它会慢慢接近目标点,然后你会看到这个方法所带来负面影响,物体不会像真实的物理世界一样进行模拟。我发现物体要么会远离目标点,要么以某种轨迹绕着目标点进行旋转。这是因为质心稍稍偏离了的缘故,而且我认为当我们在上一帧直接设置角度并计算的时候,角速度已经变成了无效的。在任何情况下,我们都应该注意,通过设置角速度为零来消除上一帧所引起的问题,例如:
body->SetAngularVelocity(0);
让物体逐渐改变,只要在每一帧更新中限制角度的变更即可:
float totalRotation = desiredAngle - bodyAngle;
float change = 1 * DEGTORAD; //allow 1 degree rotation per time step
float newAngle = bodyAngle + min( change, max(-change, totalRotation));
body->SetTransform( body->GetPosition(), newAngle );
当目标位置在物体下面的时候,你发现异常了吗?角度在左边是正值,在右边是负值,刻度范围在-180~180之间。这也就意味着当目标在物体下方并直接横跨物体的时候,物体在179度到-179之间进行变动,几乎可以看做是在整圈的旋转(358度),之间的范围甚至可以是2度!我们可以通过一些变换让它旋转的范围永远都不能超过180度。当总的旋转角度超过180度的时候我们可以这样变换:
while ( totalRotation < -180 * DEGTORAD ) totalRotation += 360 * DEGTORAD;
while ( totalRotation > 180 * DEGTORAD ) totalRotation -= 360 * DEGTORAD;
这也可以防止旋转角度由于旋转很多次而变的很大。
使用扭矩
为了达到一个更真实的物理世界,可以使用扭矩来作用于物体让其旋转:我们可以这样开始:
float totalRotation = desiredAngle - bodyAngle;
while ( totalRotation < -180 * DEGTORAD ) totalRotation += 360 * DEGTORAD;
while ( totalRotation > 180 * DEGTORAD ) totalRotation -= 360 * DEGTORAD;
body->ApplyTorque( totalRotation < 0 ? -10 : 10 );
呃...不能正常工作吗?当物体面向正右方时,就会出现问题,当物体刚好转过目标点时就会立刻退回来,然后再过去再回来,就像荡秋千一样,永远都会这样。我们能做的就是减小作用力让其无限制的接近正确的角度...但是即便缩小力也不会从根本上解决这个问题,你可以试试。
既然问题是由当前角速度引起,并通过以后的时间步长所影响的。我们可以在没有力矩作用的情下,计算下一帧物体的角度-这就可以知道-在没有外界作用的情况下物体会怎样变化-然后将其用在当前角度(testbed默认是60Hz):
1
2
3
float nextAngle = bodyAngle + body->GetAngularVelocity() / 60.0;
float totalRotation = desiredAngle - nextAngle;//use angle in next time step
body->ApplyTorque( totalRotation < 0 ? -10 : 10 );
乍一看和之前差不多,但是如果你一下当物体面向正右方的时候,就会发现至少物体会立刻停住而不是永远的上下摇摆。虽然技术上完全解决了,但是对于大多数的应用来说还是不能让人满意。解决方案中,当物体到达正确角度的时候仍然超过了一帧的计算时间。我发现它就像磁罗盘针头一样,大概1/3秒的时间调整一次。
float nextAngle = bodyAngle + body->GetAngularVelocity() / 3.0;// 1/3 second
那么进行瞬间转动如何?就像之前所谈论的话题一样,在一个时间步长中用一个非常大的力矩,沿用之前那个具有“预见性”的方法。所使用的方程式可以由原来线性版本替换成扭矩版本,不同点是使用角速度和角质量。角质量的本质是转动惯量,通常用I来表示。
使用公式T=Iv/t,其中T是我们所知道的扭矩,I是物体的转动惯量,v是角速度以及t是我们将要使用扭矩作用的时间,像之前一样:
float nextAngle = bodyAngle + body->GetAngularVelocity() / 60.0;
float totalRotation = desiredAngle - nextAngle;
while ( totalRotation < -180 * DEGTORAD ) totalRotation += 360 * DEGTORAD;
while ( totalRotation > 180 * DEGTORAD ) totalRotation -= 360 * DEGTORAD;
float desiredAngularVelocity = totalRotation * 60;
float torque = body->GetInertia() * desiredAngularVelocity / (1/60.0);
body->ApplyTorque( torque );
注意虽然这不会非常精确的瞬间完成,但是要想让物体做正确的旋转通常会在2~3个时间步长范围内,对于实际应用来说,这种解决方案已经足够满足要求了。
更新:如果物体的质心没有在其原点上,那么这种方法将不能正确的工作,例如这里所实现的方式:(。当前Box2D API只能允许访问原点作为物体的中心,我们只能在质心上对物体施加扭矩。希望在Box2D以后的版本中API可以灵活设置质心,以此来调整惯性。
使用冲量
就像最近话题中所说的那样,类似于上面的代码使用冲量达到瞬时作用的效果,但是不加入时间因素:
float nextAngle = bodyAngle + body->GetAngularVelocity() / 60.0;
float totalRotation = desiredAngle - nextAngle;
while ( totalRotation < -180 * DEGTORAD ) totalRotation += 360 * DEGTORAD;
while ( totalRotation > 180 * DEGTORAD ) totalRotation -= 360 * DEGTORAD;
float desiredAngularVelocity = totalRotation * 60;
float impulse = body->GetInertia() * desiredAngularVelocity;// disregard time factor
body->ApplyAngularImpulse( impulse );
为了达到逐步改变的效果,只要允许每帧中对旋转角度做一定限制:
float nextAngle = bodyAngle + body->GetAngularVelocity() / 60.0;
float totalRotation = desiredAngle - nextAngle;
while ( totalRotation < -180 * DEGTORAD ) totalRotation += 360 * DEGTORAD;
while ( totalRotation > 180 * DEGTORAD ) totalRotation -= 360 * DEGTORAD;
float desiredAngularVelocity = totalRotation * 60;
float change = 1 * DEGTORAD; //allow 1 degree rotation per time step
desiredAngularVelocity = min( change, max(-change, desiredAngularVelocity));
float impulse = body->GetInertia() * desiredAngularVelocity;
body->ApplyAngularImpulse( impulse );
再次出现物体摆动现象的原因是由nextAngle变量所控制的,具体可以查看这个变量。