Skip to content

Collision detection

Jean-Milost Reymond edited this page Mar 31, 2022 · 31 revisions

Introduction

In a 3d engine, the collision detection techniques are a very broad subject, which covers a range from the simple bounding box collisions to Binary Space Partitioning (BSP) trees, passing by the polygon intersection techniques.

As CompactStar is a tiny engine, I experienced some that were useful to me, but by far not all of them.

However I'll expose below the common techniques I know and I implemented in the CompactStar source code, which may be found in huge majority in the csrIntersect3() function, located in the CSR_Geometry.c and CSR_Geometry.h files.

Bounding boxes

The bounding boxes are the simpler and faster way to evaluate a collision. The principle consists in enclosing a complex mesh in a mathematical shape, which encompasses approximately, but as much as possible, the shape of the mesh. The closer the shape is to the mesh, the more precise the detection will be.

Of course, a such test is not enough to resolve a complex collision detection system, however it's a very important technique, because due to its speed, it is commonly used as a preliminary test to determine if a further and more precise collision detection is required on the involved meshes.

The bounding box collision detection tests are often simple algorithms to apply, and there are many mathematical shapes which may be used, so I'll briefly expose below the 3 most commonly used in the 3d engines.

Box-box collision

The box-box collision is simply an extension of the rectangle to rectangle algorithm in 2d, but including the 3rd dimension. It consists to calculate if the space of the first box intersects the space of the second.

return !(pBox1->m_Min.m_X <= pBox2->m_Max.m_X &&
         pBox1->m_Max.m_X >= pBox2->m_Min.m_X &&
         pBox1->m_Min.m_Y <= pBox2->m_Max.m_Y &&
         pBox1->m_Max.m_Y >= pBox2->m_Min.m_Y &&
         pBox1->m_Min.m_Z <= pBox2->m_Max.m_Z &&
         pBox1->m_Max.m_Z >= pBox2->m_Min.m_Z);

Sphere-sphere collision

The sphere-sphere collision is the simplest algorithm of all, just need to calculate the distance between the 2 sphere centers. If this distance is lower than the sum of the both sphere radius, then the spheres are in collision.

CSR_Vector3 dist;
float       length;

// calculate the distance between the both sphere centers
dist.m_X = fabs(pSphere1->m_Center.m_X - pSphere2->m_Center.m_X);
dist.m_Y = fabs(pSphere1->m_Center.m_Y - pSphere2->m_Center.m_Y);
dist.m_Z = fabs(pSphere1->m_Center.m_Z - pSphere2->m_Center.m_Z);

// calculate the length between the both sphere centers
csrVec3Length(&dist, &length);

// the spheres are in collision if the length between their centers is lower than or equal to
// the sum of the both sphere radius
return (length <= (pSphere1->m_Radius + pSphere2->m_Radius));

Capsule-capsule collision

This shape is a little particular, because capsules are often used to simulate a body, e.g. in a game with a first person view, and not just to determine if a more complex collision test should be performed.

Contrary to the previous one, this test is one of the more complex to perform, because a capsule is composed by several simpler shapes, i.e 2 spheres and a cylinder. For that reason several scenarios may happen and should be considered.

The following article explains in details how a capsule collision detection works and how to implement it.

Anyway, below is the code I use:

CSR_Vector3  firstLineDir;
CSR_Vector3  firstLineEndOffset;
CSR_Vector3  firstTop;
CSR_Vector3  firstBottom;
CSR_Vector3  secondLineDir;
CSR_Vector3  secondLineEndOffset;
CSR_Vector3  secondTop;
CSR_Vector3  secondBottom;
CSR_Vector3  v0;
CSR_Vector3  v1;
CSR_Vector3  v2;
CSR_Vector3  v3;
CSR_Vector3  bestCandidate;
CSR_Vector3  secondBestCandidate;
CSR_Vector3  penetrationNormal;
CSR_Segment3 line;
CSR_Segment3 secondLine;
float        d0;
float        d1;
float        d2;
float        d3;
float        len;
float        penetrationDepth;

// perfect collision (rare, but may happen)
if ((pCapsule1->m_Top.m_X    == pCapsule2->m_Top.m_X    &&
     pCapsule1->m_Top.m_Y    == pCapsule2->m_Top.m_Y    &&
     pCapsule1->m_Top.m_Z    == pCapsule2->m_Top.m_Z)   ||
    (pCapsule1->m_Bottom.m_X == pCapsule2->m_Bottom.m_X &&
     pCapsule1->m_Bottom.m_Y == pCapsule2->m_Bottom.m_Y &&
     pCapsule1->m_Bottom.m_Z == pCapsule2->m_Bottom.m_Z))
    return 1;

// this capsule
csrVec3Sub      (&pCapsule1->m_Top,    &pCapsule1->m_Bottom, &firstLineDir);
csrVec3Normalize(&firstLineDir,        &firstLineDir);
csrVec3MulVal   (&firstLineDir,         pCapsule1->m_Radius, &firstLineEndOffset);
csrVec3Sub      (&pCapsule1->m_Top,    &firstLineEndOffset,  &firstTop);
csrVec3Add      (&pCapsule1->m_Bottom, &firstLineEndOffset,  &firstBottom);

// second capsule
csrVec3Sub      (&pCapsule2->m_Top,    &pCapsule2->m_Bottom, &secondLineDir);
csrVec3Normalize(&secondLineDir,       &secondLineDir);
csrVec3MulVal   (&secondLineDir,        pCapsule2->m_Radius, &secondLineEndOffset);
csrVec3Sub      (&pCapsule2->m_Top,    &secondLineEndOffset, &secondTop);
csrVec3Add      (&pCapsule2->m_Bottom, &secondLineEndOffset, &secondBottom);

// vectors between line endpoints
csrVec3Sub(&secondBottom, &firstBottom, &v0);
csrVec3Sub(&secondTop,    &firstBottom, &v1);
csrVec3Sub(&secondBottom, &firstTop,    &v2);
csrVec3Sub(&secondTop,    &firstTop,    &v3);

// squared distances
csrVec3Dot(&v0, &v0, &d0);
csrVec3Dot(&v1, &v1, &d1);
csrVec3Dot(&v2, &v2, &d2);
csrVec3Dot(&v3, &v3, &d3);

// select best candidate for endpoint on first capsule
if (d2 < d0 || d2 < d1 || d3 < d0 || d3 < d1)
    bestCandidate = firstTop;
else
    bestCandidate = firstBottom;

line.m_Start       = firstBottom;
line.m_End         = firstTop;
secondLine.m_Start = secondBottom;
secondLine.m_End   = secondTop;

// select best candidate for point on second capsule line segment nearest to best potential endpoint on first capsule
csrSeg3ClosestPoint(&secondLine, &bestCandidate, &secondBestCandidate);

// do the same for first capsule segment
csrSeg3ClosestPoint(&line, &secondBestCandidate, &bestCandidate);

// calculate penetration normal and length
csrVec3Sub   (&bestCandidate,     &secondBestCandidate, &penetrationNormal);
csrVec3Length(&penetrationNormal, &len);

if (len == 0.0f)
    return 0;

// normalize
csrVec3DivVal(&penetrationNormal, len, &penetrationNormal);

// calculate the penetration depth
penetrationDepth = (pCapsule1->m_Radius + pCapsule2->m_Radius) - len;

return (penetrationDepth > 0.0f);

Polygon-ray intersection

The polygon-ray intersection is, from my point of view, one of the most useful collision detection technique. I use it for example to detect if the mouse hovers an object in the 3d world, or to follow the contours of the ground. But it may be also used in several situations, e.g. to detect where exactly a bullet hits a model, ...

Contrary to the majority of bounding box collisions, this technique is more an algorithm than a formula, it involves indeed several steps to get the final result, as you can see on the below animation:

Ray polygon collision

Get the polygon plane

The first step to detect if a ray intersects a polygon is to get the polygon plane from its vertices. This may be achieved in the following manner:

void csrPlaneFromPointNormal(const CSR_Vector3* pP, const CSR_Vector3* pN, CSR_Plane* pR)
{
    float d;

    // the a, b, and c components are only the normal of the plane
    pR->m_A = pN->m_X;
    pR->m_B = pN->m_Y;
    pR->m_C = pN->m_Z;

    // calculate plane d component using the aX + bY + cZ + d = 0 formula
    csrVec3Dot(pN, pP, &d);
    pR->m_D = -d;
}

void csrPlaneFromPoints(const CSR_Vector3* pV1,
                        const CSR_Vector3* pV2,
                        const CSR_Vector3* pV3,
                              CSR_Plane*   pR)
{
    CSR_Vector3 e1;
    CSR_Vector3 e2;
    CSR_Vector3 normal;

    // calculate edge vectors
    csrVec3Sub(pV2, pV1, &e1);
    csrVec3Sub(pV3, pV1, &e2);

    // calculate the normal of the plane
    csrVec3Cross(&e1, &e2, &normal);
    csrVec3Normalize(&normal, &normal);

    // calculate and return the plane
    csrPlaneFromPointNormal(pV1, &normal, pR);
}

Get the point where the ray intersects the plane

The second step is to find the point where the ray intersects the plane. This may be achieved in the following manner:

CSR_Vector3 n;
float       dot;
float       nDot;
float       temp;

// get the figures to check
const CSR_Ray3*  pRay   = (CSR_Ray3*)pFirst;
const CSR_Plane* pPlane = (CSR_Plane*)pSecond;

// get the normal of the plane
n.m_X = pPlane->m_A;
n.m_Y = pPlane->m_B;
n.m_Z = pPlane->m_C;

// calculate the angle between the line and the normal to the plane
csrVec3Dot(&n, &pRay->m_Dir, &dot);

// if normal to the plane is perpendicular to the line, then the line is either parallel
// to the plane and there are no solutions or the line is on the plane in which case
// there are an infinite number of solutions
if (!dot)
    return 0;

csrVec3Dot(&n, &pRay->m_Pos, &nDot);

temp = ((pPlane->m_D + nDot) / dot);

// calculate the intersection point
if (pR1)
{
    pR1->m_X = (pRay->m_Pos.m_X - (temp * pRay->m_Dir.m_X));
    pR1->m_Y = (pRay->m_Pos.m_Y - (temp * pRay->m_Dir.m_Y));
    pR1->m_Z = (pRay->m_Pos.m_Z - (temp * pRay->m_Dir.m_Z));
}

return 1;

Determine if the point is inside the polygon

Once the intersection point between the ray and the plane composed by the 3 vertices of the polygon was found, the only remaining step is to determine if this point is inside or outside the polygon.

To achieve that, we need to calculate the segment between the point and each polygon vertices, then to calculate the angle between each of these segments, and sum them.

If the sum is equals to 2 * π, then the angle is inside the polygon, in this case the ray hits the polygon, otherwise it is outside, in this case the ray misses the polygon.

int csrInsidePolygon3(const CSR_Vector3* pP, const CSR_Polygon3* pPo)
{
    CSR_Vector3 nPToV1;
    CSR_Vector3 nPToV2;
    CSR_Vector3 nPToV3;
    float       a1;
    float       a2;
    float       a3;
    float       angleResult;

    /*
    * check if the point p is inside the polygon in the following manner:
    *
    *                  V1                         V1
    *                  /\                         /\
    *                 /  \                       /  \
    *                / *p \                  *P /    \
    *               /      \                   /      \
    *            V2 -------- V3             V2 -------- V3
    *
    * calculate the vectors between the point p and each polygon vertex, then calculate the angle
    * formed by each of these vectors. If the sum of the angles is equal to a complete circle, i.e.
    * 2 * PI in radians, then the point p is inside the polygon limits, otherwise the point is
    * outside. It is assumed that the point to check belongs to the polygon's plane
    */
    csrVec3Sub(&pPo->m_Vertex[0], pP, &nPToV1);
    csrVec3Sub(&pPo->m_Vertex[1], pP, &nPToV2);
    csrVec3Sub(&pPo->m_Vertex[2], pP, &nPToV3);
    csrVec3Normalize(&nPToV1, &nPToV1);
    csrVec3Normalize(&nPToV2, &nPToV2);
    csrVec3Normalize(&nPToV3, &nPToV3);

    // calculate the angles using the scalar product of each vectors with the following formulas:
    // A1 = NPToV1.x * NPToV2.x + NPToV1.y * NPToV2.y + NPToV1.z * NPToV2.z
    // A2 = NPToV2.x * NPToV3.x + NPToV2.y * NPToV3.y + NPToV2.z * NPToV3.z
    // A3 = NPToV3.x * NPToV1.x + NPToV3.y * NPToV1.y + NPToV3.z * NPToV1.z
    csrVec3Dot(&nPToV1, &nPToV2, &a1);
    csrVec3Dot(&nPToV2, &nPToV3, &a2);
    csrVec3Dot(&nPToV3, &nPToV1, &a3);

    // limit a1 to avoid rounding errors
    if (a1 > 1.0f)
        a1 = 1.0f;
    else
    if (a1 < -1.0f)
        a1 = -1.0f;

    // limit a2 to avoid rounding errors
    if (a2 > 1.0f)
        a2 = 1.0f;
    else
    if (a2 < -1.0f)
        a2 = -1.0f;

    // limit a3 to avoid rounding errors
    if (a3 > 1.0f)
        a3 = 1.0f;
    else
    if (a3 < -1.0f)
        a3 = -1.0f;

    // calculate the sum of all angles
    angleResult = acos(a1) + acos(a2) + acos(a3);

    // if sum is equal or higher to 6.28 radians then point P is inside polygon
    if (angleResult >= 6.28f)
        return 1;

    return 0;
}

Aligned-axis bounding box trees

An aligned-axis bounding box tree (often summarized in AABB tree), is a technique allowing to simplify a complex collision system, like a mesh, by dividing it in smaller group of polygons, bounded by a collision box aligned on the x, y and z axis.

Depending on which bounding box is hit, it's easy to find which polygons may potentially be in collision, and to test them further with a more complex algorithm.

In CompactStar a such algorithm is implemented in the CSR_Collision.c and CSR_Collision.h files.

Take a look to the CSR_AABBNode structure:

/**
* Aligned-axis bounding box tree node
*/
typedef struct CSR_tagAABBNode
{
    struct CSR_tagAABBNode*          m_pParent;
    struct CSR_tagAABBNode*          m_pLeft;
    struct CSR_tagAABBNode*          m_pRight;
           CSR_Box*                  m_pBox;
           CSR_IndexedPolygonBuffer* m_pPolygonBuffer;
} CSR_AABBNode;

The m_pBox member contains the bounding box owning the node. The m_pParent represents the parent node in which this node is contained, meaning that the bounding box is a sub-division of the parent box. The m_pLeft and m_pRight members contain the sub-divided nodes, meaning that their bounding boxes fit in the current node bounding box. Finally the m_pPolygonBuffer contains all the polygons owned by the node bounding box.

In CompactStar, an AABB tree may be built using the following function:

int csrAABBTreeFromIndexedPolygonBuffer(const CSR_IndexedPolygonBuffer* pIPB,
                                              CSR_AABBNode*             pNode)
{
    size_t                    i;
    size_t                    j;
    CSR_Box                   leftBox;
    CSR_Box                   rightBox;
    CSR_Polygon3              polygon;
    CSR_IndexedPolygon*       pNewPolygons    = 0;
    CSR_IndexedPolygonBuffer* pLeftPolygons   = 0;
    CSR_IndexedPolygonBuffer* pRightPolygons  = 0;
    int                       boxEmpty        = 1;
    int                       insideLeft      = 0;
    int                       insideRight     = 0;
    int                       canResolveLeft  = 0;
    int                       canResolveRight = 0;
    int                       result          = 0;

    // no indexed polygon buffer?
    if (!pIPB)
        return 0;

    // no node?
    if (!pNode)
        return 0;

    // initialize node content
    pNode->m_pParent        = 0;
    pNode->m_pLeft          = 0;
    pNode->m_pRight         = 0;
    pNode->m_pBox           = (CSR_Box*)malloc(sizeof(CSR_Box));
    pNode->m_pPolygonBuffer = csrIndexedPolygonBufferCreate();

    // succeeded?
    if (!pNode->m_pBox || !pNode->m_pPolygonBuffer)
    {
        csrAABBTreeNodeContentRelease(pNode);
        return 0;
    }

    // create the polygon buffers that will contain the divided polygons
    pLeftPolygons  = csrIndexedPolygonBufferCreate();
    pRightPolygons = csrIndexedPolygonBufferCreate();

    // succeeded?
    if (!pLeftPolygons || !pRightPolygons)
    {
        csrIndexedPolygonBufferRelease(pLeftPolygons);
        csrIndexedPolygonBufferRelease(pRightPolygons);
        csrAABBTreeNodeContentRelease(pNode);
        return 0;
    }

    // iterate through polygons to divide
    for (i = 0; i < pIPB->m_Count; ++i)
    {
        // using its index, extract the polygon from its vertex buffer
        csrIndexedPolygonToPolygon(&pIPB->m_pIndexedPolygon[i], &polygon);

        // extend the bounding box to include the polygon
        csrBoxExtendToPolygon(&polygon, pNode->m_pBox, &boxEmpty);
    }

    // divide the bounding box in 2 sub-boxes
    csrBoxCut(pNode->m_pBox, &leftBox, &rightBox);

    // iterate again through polygons to divide
    for (i = 0; i < pIPB->m_Count; ++i)
    {
        // get the concrete polygon (i.e. with physical coordinates, not indexes)
        csrIndexedPolygonToPolygon(&pIPB->m_pIndexedPolygon[i], &polygon);

        insideLeft  = 0;
        insideRight = 0;

        // check which box contains the most vertices
        for (j = 0; j < 3; ++j)
            // is vertex inside left or right sub-box?
            if (csrInsideBox(&polygon.m_Vertex[j], &leftBox))
                ++insideLeft;
            else
                ++insideRight;

        // check at which sub-box the polygon belongs (and thus to which buffer it should be added)
        if (insideLeft >= insideRight)
        {
            // allocate the memory to add a new polygon in the left polygon buffer
            pNewPolygons = (CSR_IndexedPolygon*)csrMemoryAlloc(pLeftPolygons->m_pIndexedPolygon,
                                                               sizeof(CSR_IndexedPolygon),
                                                               pLeftPolygons->m_Count + 1);

            // succeeded?
            if (!pNewPolygons)
            {
                csrIndexedPolygonBufferRelease(pLeftPolygons);
                csrIndexedPolygonBufferRelease(pRightPolygons);
                csrAABBTreeNodeContentRelease(pNode);
                return 0;
            }

            // update the buffer content
            pLeftPolygons->m_pIndexedPolygon = pNewPolygons;
            ++pLeftPolygons->m_Count;

            // copy the polygon index content in the left buffer
            pLeftPolygons->m_pIndexedPolygon[pLeftPolygons->m_Count - 1] = pIPB->m_pIndexedPolygon[i];
        }
        else
        {
            // allocate the memory to add a new polygon in the left polygon buffer
            pNewPolygons = (CSR_IndexedPolygon*)csrMemoryAlloc(pRightPolygons->m_pIndexedPolygon,
                                                               sizeof(CSR_IndexedPolygon),
                                                               pRightPolygons->m_Count + 1);

            // succeeded?
            if (!pNewPolygons)
            {
                csrIndexedPolygonBufferRelease(pLeftPolygons);
                csrIndexedPolygonBufferRelease(pRightPolygons);
                csrAABBTreeNodeContentRelease(pNode);
                return 0;
            }

            // update the buffer content
            pRightPolygons->m_pIndexedPolygon = pNewPolygons;
            ++pRightPolygons->m_Count;

            // copy the polygon content inside its buffer
            pRightPolygons->m_pIndexedPolygon[pRightPolygons->m_Count - 1] = pIPB->m_pIndexedPolygon[i];
        }
    }

    canResolveLeft  = (pLeftPolygons->m_Count  && pLeftPolygons->m_Count  < pIPB->m_Count);
    canResolveRight = (pRightPolygons->m_Count && pRightPolygons->m_Count < pIPB->m_Count);

    // leaf reached?
    if (!canResolveLeft && !canResolveRight)
    {
        // iterate through left polygons to copy to the leaf polygon buffer
        for (i = 0; i < pLeftPolygons->m_Count; ++i)
        {
            // allocate the memory to add a new polygon in the leaf node
            pNewPolygons = (CSR_IndexedPolygon*)csrMemoryAlloc(pNode->m_pPolygonBuffer->m_pIndexedPolygon,
                                                               sizeof(CSR_IndexedPolygon),
                                                               pNode->m_pPolygonBuffer->m_Count + 1);

            // succeeded?
            if (!pNewPolygons)
            {
                csrIndexedPolygonBufferRelease(pLeftPolygons);
                csrIndexedPolygonBufferRelease(pRightPolygons);
                csrAABBTreeNodeContentRelease(pNode);
                return 0;
            }

            // update the buffer content
            pNode->m_pPolygonBuffer->m_pIndexedPolygon = pNewPolygons;
            ++pNode->m_pPolygonBuffer->m_Count;

            // copy the polygon content inside the polygon buffer
            pNode->m_pPolygonBuffer->m_pIndexedPolygon[pNode->m_pPolygonBuffer->m_Count - 1] =
                    pLeftPolygons->m_pIndexedPolygon[i];
        }

        // iterate through right polygons to copy to the leaf polygon buffer
        for (i = 0; i < pRightPolygons->m_Count; ++i)
        {
            // allocate the memory to add a new polygon in the leaf node
            pNewPolygons = (CSR_IndexedPolygon*)csrMemoryAlloc(pNode->m_pPolygonBuffer->m_pIndexedPolygon,
                                                               sizeof(CSR_IndexedPolygon),
                                                               pNode->m_pPolygonBuffer->m_Count + 1);

            // succeeded?
            if (!pNewPolygons)
            {
                csrIndexedPolygonBufferRelease(pLeftPolygons);
                csrIndexedPolygonBufferRelease(pRightPolygons);
                csrAABBTreeNodeContentRelease(pNode);
                return 0;
            }

            // update the buffer content
            pNode->m_pPolygonBuffer->m_pIndexedPolygon = pNewPolygons;
            ++pNode->m_pPolygonBuffer->m_Count;

            // copy the polygon content inside the polygon buffer
            pNode->m_pPolygonBuffer->m_pIndexedPolygon[pNode->m_pPolygonBuffer->m_Count - 1] =
                    pRightPolygons->m_pIndexedPolygon[i];
        }

        // release the left and right polygon buffers, as they will no longer be used
        csrIndexedPolygonBufferRelease(pLeftPolygons);
        csrIndexedPolygonBufferRelease(pRightPolygons);

        return 1;
    }

    // do create left node?
    if (canResolveLeft)
    {
        // create the left node
        pNode->m_pLeft = (CSR_AABBNode*)malloc(sizeof(CSR_AABBNode));

        // populate it
        result |= csrAABBTreeFromIndexedPolygonBuffer(pLeftPolygons, pNode->m_pLeft);

        // set node parent. IMPORTANT must be done after the node is populated (because this value
        // will be reseted while the node is filled by csrAABBTreeFromIndexedPolygonBuffer())
        pNode->m_pLeft->m_pParent = pNode;

        // delete left polygon buffer, as it will no longer be used
        csrIndexedPolygonBufferRelease(pLeftPolygons);
    }

    // do create right node?
    if (canResolveRight)
    {
        // create the right node
        pNode->m_pRight = (CSR_AABBNode*)malloc(sizeof(CSR_AABBNode));

        // populate it
        result |= csrAABBTreeFromIndexedPolygonBuffer(pRightPolygons, pNode->m_pRight);

        // set node parent. IMPORTANT must be done after the node is populated (because this value
        // will be reseted while the node is filled by csrAABBTreeFromIndexedPolygonBuffer())
        pNode->m_pRight->m_pParent = pNode;

        // delete right polygon buffer, as it will no longer be used
        csrIndexedPolygonBufferRelease(pRightPolygons);
    }

    return result;
}

This function will calculate the closest bounding box surrounding the polygons, then it will divide the box on the longest axis and distribute the polygons between the left and right subdivided boxes according to which space they occupies. Then the function will executes itself recursively on each sub-divided box and polygons, until reaching a region which can no longer be sub-divided.

To resolve the AABB tree, the following function may be used:

int csrAABBTreeResolve(const CSR_Ray3*           pRay,
                       const CSR_AABBNode*       pNode,
                             size_t              deep,
                             CSR_Polygon3Buffer* pPolygons)
{
    #ifdef _MSC_VER
        unsigned      i;
        int           leftResolved   = 0;
        int           rightResolved  = 0;
        CSR_Polygon3* pPolygonBuffer = 0;
        CSR_Figure3   ray            = {0};
        CSR_Figure3   box            = {0};
    #else
        unsigned      i;
        int           leftResolved  = 0;
        int           rightResolved = 0;
        CSR_Polygon3* pPolygonBuffer;
        CSR_Figure3   ray;
        CSR_Figure3   box;
    #endif

    // no ray?
    if (!pRay)
        return 0;

    // no node to resolve?
    if (!pNode)
        return 0;

    // no polygon buffer to contain the result?
    if (!pPolygons)
        return 0;

    // is the first iteration?
    if (!deep)
    {
        // ensure the polygon buffer is initialized, otherwise this may cause hard-to-debug bugs
        pPolygons->m_pPolygon = 0;
        pPolygons->m_Count    = 0;
    }

    // is leaf?
    if (!pNode->m_pLeft && !pNode->m_pRight)
    {
        // iterate through polygons contained in leaf
        for (i = 0; i < pNode->m_pPolygonBuffer->m_Count; ++i)
        {
            // allocate memory for a new polygon in the buffer
            pPolygonBuffer = (CSR_Polygon3*)csrMemoryAlloc(pPolygons->m_pPolygon,
                                                           sizeof(CSR_Polygon3),
                                                           pPolygons->m_Count + 1);

            // succeeded?
            if (!pPolygonBuffer)
                return 0;

            // update the polygon buffer
            pPolygons->m_pPolygon = pPolygonBuffer;
            ++pPolygons->m_Count;

            // copy the polygon content
            if (!csrIndexedPolygonToPolygon(&pNode->m_pPolygonBuffer->m_pIndexedPolygon[i],
                                            &pPolygons->m_pPolygon[pPolygons->m_Count - 1]))
                return 0;
        }

        return 1;
    }

    // convert ray to geometric figure
    ray.m_Type    = CSR_F3_Ray;
    ray.m_pFigure = pRay;

    // node contains a left child?
    if (pNode->m_pLeft)
    {
        // convert left box to geometric figure
        box.m_Type    = CSR_F3_Box;
        box.m_pFigure = pNode->m_pLeft->m_pBox;

        // check if ray intersects the left box
        if (csrIntersect3(&ray, &box, 0, 0, 0))
            // resolve left node
            leftResolved = csrAABBTreeResolve(pRay, pNode->m_pLeft, deep + 1, pPolygons);
    }

    // node contains a right child?
    if (pNode->m_pRight)
    {
        // convert right box to geometric figure
        box.m_Type    = CSR_F3_Box;
        box.m_pFigure = pNode->m_pRight->m_pBox;

        // check if ray intersects the right box
        if (csrIntersect3(&ray, &box, 0, 0, 0))
            // resolve right node
            rightResolved = csrAABBTreeResolve(pRay, pNode->m_pRight, deep + 1, pPolygons);
    }

    return (leftResolved || rightResolved);
}

This function will test which box the ray hits. It will repeat the test until find a leaf, in other words the smallest possible region hit by the ray. If several boxes are hit, their polygons will be added to the result. The function will returns a polygon list to test, which contains the polygons with which a collision with the ray is possible. The other polygons composing the mesh may be discarded.

Sliding against walls

There are many ways to react to a collision, but one of the most common (especially for games like FPS) is to calculate a new position for the model which would give the impression that the model slides against the obstacle it collides.

In a commercial game engine, the sliding may be a very complex task to perform, but a such effect may be not so complex to reach if simple objects like a sphere and a plane are used. It's this approach wich is retained when a sliding should be implemented with the CompactStar engine.

Consider the following situation:

Player hits a wall

On the above image, a sphere collider hits a wall after the player has moved forward. The current position is known, as well as the bounding sphere and the wall plane.

From there, a ray starting from the center of the bounding sphere (which should match with the current position), and oriented in the direction of the wall, may be calculated.

Calculate ray

The point where the ray crosses the plane may be calculated.

Calculate where ray crosses the plane

Finally, the new position can be calculated by finding the point along the inverted center-to-plane ray located at the distance equals to the bounding sphere radius, from the calculated point on the plane.

Move to new pos

Below is the implementation of this algorithm, available in the CSR_Collision.c and CSR_Collision.h files.

void csrSlidingPoint(const CSR_Plane*   pSlidingPlane,
                     const CSR_Vector3* pPosition,
                           float        radius,
                           CSR_Vector3* pR)
{
    float        distanceToPlane;
    CSR_Plane    plane;
    CSR_Vector3  planeRatio;
    CSR_Vector3  pointBeyondPlane;
    CSR_Vector3  pointOnPlane;
    CSR_Segment3 segment;
    CSR_Figure3  segmentFigure;
    CSR_Figure3  planeFigure;

    plane = *pSlidingPlane;

    // calculate the distance between the center of the sphere and the plane
    csrPlaneDistanceTo(pPosition, &plane, &distanceToPlane);

    // check if value is negative
    if (distanceToPlane < 0.0f)
    {
        // invert the plane
        plane.m_A = -plane.m_A;
        plane.m_B = -plane.m_B;
        plane.m_C = -plane.m_C;
        plane.m_D = -plane.m_D;
    }

    // calculate the direction of the line segment position - plane
    planeRatio.m_X = radius * plane.m_A;
    planeRatio.m_Y = radius * plane.m_B;
    planeRatio.m_Z = radius * plane.m_C;

    // calculate who the line segment perpendicular to the plane, from the center
    // of the sphere, cross the collision sphere. Normally this point is beyond
    // the plane
    pointBeyondPlane.m_X = pPosition->m_X - planeRatio.m_X;
    pointBeyondPlane.m_Y = pPosition->m_Y - planeRatio.m_Y;
    pointBeyondPlane.m_Z = pPosition->m_Z - planeRatio.m_Z;

    // configure the line segment to test
    segment.m_Start = *pPosition;
    segment.m_End   =  pointBeyondPlane;

    // build a figure containing the line segment
    segmentFigure.m_Type    =  CSR_F3_Segment;
    segmentFigure.m_pFigure = &segment;

    // build a figure containing the plane
    planeFigure.m_Type    = CSR_F3_Plane;
    planeFigure.m_pFigure = pSlidingPlane;

    // calculate the point where the segment "center of the sphere - point beyond
    // the plane" cross the collision plane
    csrIntersect3(&segmentFigure, &planeFigure, &pointOnPlane, 0, 0);

    // from point calculated above, add the sphere radius and return the value
    pR->m_X = pointOnPlane.m_X + planeRatio.m_X;
    pR->m_Y = pointOnPlane.m_Y + planeRatio.m_Y;
    pR->m_Z = pointOnPlane.m_Z + planeRatio.m_Z;
}

Clone this wiki locally