Oh!Coder

Coding Life

Box2D C++ 教程-射线投射

| Comments

声明:本教程翻译自:Box2D C++ tutorials - Ray casting,仅供学习参考。

射线投射经常用来找出部分物理世界当中已经存在的物件。所谓射线就是一条直线,我们可以通过使用Box2D当中提供的方法来检测是否与某个定制器(fixture)有交点。我们也可以找出与定制器交点的法线。

下面就是上面我提到的方法,如果射线与某个定制器发生碰撞则返回true。这里需要注意的是此方法是b2Fixture类的一个成员,这也就是说首先我们满足一个射线对儿这个基本条件才行。

1
bool b2Fixture::RayCast(b2RayCastOutput* output, const b2RayCastInput& input);

现在让我们看一下输入、输出参数。直接从源代码中可以看出b2RayCastInput包含了哪些参数:

1
2
3
4
5
6
// Ray-cast input data. The ray extends from p1 to p1 + maxFraction * (p2 - p1).
struct b2RayCastInput
{
    b2Vec2 p1, p2;
    float32 maxFraction;
};

p1和p2点确定了射线的方向。maxFraction表示了射线检测到交点的距离。下面的图可能能够让人一目了然。maxFraction等于1意味着射线段是从p1点到p2点,在此例子中显然不能与多边形有交点,但是maxFraction等于2就可以。

pic

下面是b2RayCastOutput包括的参数:

1
2
3
4
5
6
7
// Ray-cast output data. The ray hits at p1 + fraction * (p2 - p1), where p1 and p2
// come from b2RayCastInput.
struct b2RayCastOutput
{
    b2Vec2 normal;
    float32 fraction;
};

如果射线确实与多边形相交,b2Fixture::RayCast将会返回true,而且我们可以通过这个输出结构体里的fraction参数找出交点段值(详见上图),还有在多边形定制器表面生成的法线:

pic

例子

为了尝试一下这个很方便实用的功能,让我们创建一个场景,场景的条件为失重状态,其中漂浮着一些多边形。现在创建这些你应该很顺手了,接下来我们在四周创建一些墙来代替四方盒子作为边界,为的是看起来更好看。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
FooTest() {

    //a static body
    b2BodyDef myBodyDef;
    myBodyDef.type = b2_staticBody;
    myBodyDef.position.Set(0, 0);
    b2Body* staticBody = m_world->CreateBody(&myBodyDef);

    //shape definition
    b2PolygonShape polygonShape;

    //fixture definition
    b2FixtureDef myFixtureDef;
    myFixtureDef.shape = &polygonShape;

    //add four walls to the static body
    b2Vec2 bl(-20, 0);
    b2Vec2 br( 20, 0);
    b2Vec2 tl(-20,40);
    b2Vec2 tr( 20,40);
    polygonShape.SetAsEdge( bl, br ); //ground
    staticBody->CreateFixture(&myFixtureDef);
    polygonShape.SetAsEdge( tl, tr);//ceiling
    staticBody->CreateFixture(&myFixtureDef);
    polygonShape.SetAsEdge( bl, tl );//left wall
    staticBody->CreateFixture(&myFixtureDef);
    polygonShape.SetAsEdge( br, tr );//right wall
    staticBody->CreateFixture(&myFixtureDef);

    myBodyDef.type = b2_dynamicBody;
    myBodyDef.position.Set(0,20);
    polygonShape.SetAsBox(2,2);
    myFixtureDef.density = 1;
    for (int i = 0; i < 5; i++)
        m_world->CreateBody(&myBodyDef)->CreateFixture(&myFixtureDef);

    //circles
    b2CircleShape circleShape;
    circleShape.m_radius = 2;
    myFixtureDef.shape = &circleShape;
    for (int i = 0; i < 5; i++)
        m_world->CreateBody(&myBodyDef)->CreateFixture(&myFixtureDef);

    //turn gravity off
    m_world->SetGravity( b2Vec2(0,0) );
}

pic

现在我们需要有一个射线与这些多边形发生碰撞。让我们在屏幕的中心生成一条四周投射的射线,并且缓慢旋转。我们需要唯一做的就是保持住射线的旋转角度,那么我们就不要为此特别封装一个类了,我们只需要有一个全局变量就OK了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//at global scope
float currentRayAngle = 0;

//in Step() function
currentRayAngle += 360 / 20.0 / 60.0 * DEGTORAD; //one revolution every 20 seconds

//calculate points of ray
float rayLength = 25; //long enough to hit the walls
b2Vec2 p1( 0, 20 ); //center of scene
b2Vec2 p2 = p1 + rayLength * b2Vec2( sinf(currentRayAngle), cosf(currentRayAngle) );

//draw a line
glColor3f(1,1,1); //white
glBegin(GL_LINES);
glVertex2f( p1.x, p1.y );
glVertex2f( p2.x, p2.y );
glEnd();

pic

现在在场景中你应该能够看到一条旋转的白线。现在我们可以就用RayCast方法来获取离射线最近的多边形,并画出线段的长度。我们将会对每个多边形中的每个定制器进行检测,当然这并不是最有效的办法,但是可以作为一个学习的例子(详见查询world话题)来学习。这样的话,我们就可以了解一下世界里的物件:

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
31
32
33
34
35
36
37
38
//in Step() function, continuing on from section above

    //set up input
    b2RayCastInput input;
    input.p1 = p1;
    input.p2 = p2;
    input.maxFraction = 1;

    //check every fixture of every body to find closest
    float closestFraction = 1; //start with end of line as p2
    b2Vec2 intersectionNormal(0,0);
    for (b2Body* b = m_world->GetBodyList(); b; b = b->GetNext()) {
        for (b2Fixture* f = b->GetFixtureList(); f; f = f->GetNext()) {

            b2RayCastOutput output;
            if ( ! f->RayCast( &output, input ) )
                continue;
            if ( output.fraction < closestFraction ) {
                closestFraction = output.fraction;
                intersectionNormal = output.normal;
            }
        }
    }

    b2Vec2 intersectionPoint = p1 + closestFraction * (p2 - p1);

    //draw a line
    glColor3f(1,1,1); //white
    glBegin(GL_LINES);
    glVertex2f( p1.x, p1.y );
    glVertex2f( intersectionPoint.x, intersectionPoint.y );
    glEnd();

    //draw a point at the intersection point
    glPointSize(5);
    glBegin(GL_POINTS);
    glVertex2f( intersectionPoint.x, intersectionPoint.y );
    glEnd();

你可能注意到现在我们在每个上方画出了两条线…为了清晰起见,删除第一条你就会看到类似下面的画面:

pic

上面所说的就是找出交点。我们也可以在输出结构中使用法线来实现一些有趣的想法,现在我们就可以试试看。首先,让我们把法线长什么样儿简单的画出来:

1
2
3
4
5
6
7
//draw intersection normal
b2Vec2 normalEnd = intersectionPoint + intersectionNormal;
glColor3f(0,1,0); //green
glBegin(GL_LINES);
glVertex2f( intersectionPoint.x, intersectionPoint.y );
glVertex2f( normalEnd.x, normalEnd.y );
glEnd();

pic

作为最后的压轴戏,我们可以把投射代码放到一个自有方法中,然后每次不断的递归调用直到射线的耗尽。这并不意味着Box2D怎么写都行,我只是觉得这么做代码看起来更整洁而已:)。

pic

代码:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//new function for FooTest class
void drawReflectedRay( b2Vec2 p1, b2Vec2 p2 )
{
    //set up input
    b2RayCastInput input;
    input.p1 = p1;
    input.p2 = p2;
    input.maxFraction = 1;

    //check every fixture of every body to find closest
    float closestFraction = 1; //start with end of line as p2
    b2Vec2 intersectionNormal(0,0);
    for (b2Body* b = m_world->GetBodyList(); b; b = b->GetNext()) {
        for (b2Fixture* f = b->GetFixtureList(); f; f = f->GetNext()) {

            b2RayCastOutput output;
            if ( ! f->RayCast( &output, input ) )
                continue;
            if ( output.fraction < closestFraction ) {
                closestFraction = output.fraction;
                intersectionNormal = output.normal;
            }
        }
    }
    b2Vec2 intersectionPoint = p1 + closestFraction * (p2 - p1);

    //draw this part of the ray
    glBegin(GL_LINES);
    glVertex2f( p1.x, p1.y );
    glVertex2f( intersectionPoint.x, intersectionPoint.y );
    glEnd();

    if ( closestFraction == 1 )
        return; //ray hit nothing so we can finish here
    if ( closestFraction == 0 )
        return;

    //still some ray left to reflect
    b2Vec2 remainingRay = (p2 - intersectionPoint);
    b2Vec2 projectedOntoNormal = b2Dot(remainingRay, intersectionNormal) * intersectionNormal;
    b2Vec2 nextp2 = p2 - 2 * projectedOntoNormal;

    //recurse
    drawReflectedRay(intersectionPoint, nextp2);
}

…然后在Step()方法中天假下面几行代码:

1
2
3
4
5
6
7
//calculate points of ray
float rayLength = 25;
b2Vec2 p1( 0, 20 ); //center of scene
b2Vec2 p2 = p1 + rayLength * b2Vec2( sinf(currentRayAngle), cosf(currentRayAngle) );

glColor3f(1,1,1); //white
drawReflectedRay(p1, p2);

Comments