Oh!Coder

Coding Life

Box2D C++ 教程-旋转到指定角度

| Comments

声明:本教程翻译自:Box2D C++ tutorials-Rotating to a given angle,仅供学习参考。

  • 旋转指定角度

这个话题和之前的那个话题类似,只不过把直线运动改成旋转运动。旋转一个物体既可以直接设置角度也可以通过设置扭矩/冲量方法来实现,同样需要注意的问题是,如果直接设置物体的角度,其实并不算是真正的模拟物理场景。

为了做这个实验我们需要一个动态物体,如果没有重力的影响效果更好,那样物体可以停留在屏幕上。我们会给物体附加一个具有不同方向的定制器,这样我们可以查看物体是否面向正确的方向。让我们就像设置“尖尖的”定点一样设置一个多边形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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) );
}

pic

设置物体的点,使其朝向我们,然后设置鼠标输入方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//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()方法里的代码也不要紧,这段代码只是在屏幕上画出了鼠标单击的点。

pic

这个点将会移到鼠标最后一次单击的地方。在场景中这也是一种默认的拖拽动作,虽然有一点点怪异但是还是比较好的满足了我们的需求。

  • 直接设置角度

使用SetTransform方法来设置角度非常简单,但是首先我们需要知道设置多少个角度,给定物体本地的坐标点以及本地的目标点。然后添加到每帧都会调用的Step()方法:

1
2
3
4
5
6
7
8
9
10
//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之间。

pic

现在我们看到是如何计算行为角度的,尝试使用SetTransform方法可以让物体指向目标点,而且你会看到物体自己会立刻指向目标点。

1
body->SetTransform( body->GetPosition(), desiredAngle );

pic

试着抛出物体,之后它会慢慢接近目标点,然后你会看到这个方法所带来负面影响,物体不会像真实的物理世界一样进行模拟。我发现物体要么会远离目标点,要么以某种轨迹绕着目标点进行旋转。这是因为质心稍稍偏了的缘故,而且我认为当我们在上一帧 直接设置角度并计算的时候,角速度已经变成了无效的。在任何情况下,我们都应该注意,通过设置角速度为零来消除上一帧所引起的问题,例如:

1
body->SetAngularVelocity(0);

让物体逐渐改变,只要在每一帧更新中限制角度的变更即可:

1
2
3
4
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度的时候我们可以这样变换:

1
2
while ( totalRotation < -180 * DEGTORAD ) totalRotation += 360 * DEGTORAD;
while ( totalRotation >  180 * DEGTORAD ) totalRotation -= 360 * DEGTORAD;

这也可以防止旋转角度由于旋转很多次而变的很大。

  • 使用扭矩

为了达到一个更真实的物理世界,可以使用扭矩来作用于物体让其旋转:我们可以这样开始:

1
2
3
4
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秒的时间出一次结果。

1
float nextAngle = bodyAngle + body->GetAngularVelocity() / 3.0;// 1/3 second

那么进行瞬间转动如何?就像之前所谈论的话题一样,在一个时间步长中用一个非常大的力矩,沿用之前那个具有“预见性”的方法。所使用的方程式可以由原来线性版本替换成扭矩版本,不同点是使用角速度和角质量。角质量的本质是转动惯量,通常用I来表示。

使用公式T=Iv/t,其中T是我们想要知道的扭矩,I是物体的转动惯量,v是角速度以及t是我们将要使用扭矩作用的时间,像之前一样:

1
2
3
4
5
6
7
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可以灵活设置质心,以此来调整惯性。

  • 使用冲量

就像最近话题中所说的那样,类似于上面的代码使用冲量可以达到瞬间移动的效果,但是不加时间系数:

1
2
3
4
5
6
7
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 );

为了达到逐步改变的效果,只要在每帧中对旋转角度做一定限制:

1
2
3
4
5
6
7
8
9
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变量所控制的,具体可以查看这个变量。

Comments