Welcome to my tutorial on cylinder-based collision detection and response. I will attempt to provide you with an alternative to the ever popular sphere-based collision detection that is abundant out there. Why a cylinder you may ask, because I handle my collision detection in two different ways. I use cylinder based collision detection (CD) for my horizontal collision and response and I handle up/down gravity as a separate entity. It is actually easier to break it up this way than to fight with a sphere rolling on the ground. I will try and prove this to you , but please note that I may not be the best tutorial writer in the world. If you every have a question, please feel free to email me at spiritking@lycos.com . On to the beginning.....


Setup and Definitions

I make my game world with the X-axis being left and right, Z-axis near and far, and Y-axis is Up and Down. If you use anything different , you will have to adjust accordingly.

The Player in the game world is represented by a single point (X,Y,Z) I like to call Position.The position is the place the player is standing in the world, meaning it is usually a point on the ground. First we will handle the Cylinder (XZ) collision and response and then we will handle the Gravity (Y) aspect.

The Cylinder has 3 variables that make it up. It's radius (R), beginning height (H1) and it's ending height (H2). The radius should encompase your player but not really exceed to far out from him. The H1 is always a little above the player position, how much determines the slope he can travel before he will collide with it. The H2 is always at the top of the player. To determine H1 use some good ole triangle math to determine the angle.

Using Tan (a)=H1/R you can find the max angle you player will walk up.


On to more setup

Now that we know what a cylinder is and what values we use to set it up, let's place it in our world.

Now we start with the players Original Position (oPos) and we move him by whatever velocity vector we want and come up with the New Pos (nPos). We need both of these for the CD routine and remember we move our player's XZ components only, the Y is separate, start thinking 2-D from the top down.

So we now have our oPos (X,Z) and our nPos (X,Z), also the R, H1, H2. Here's what we do with it.


Clipping the World

We take all the world geometry that is close by, that we could possible collide with, and we clip it all at the H1 and H2 mark.We discard anything not in this range, as it could not possible effect or CD.

Before

After

So now that we have clipped our world to only the things that matter, we still have one more step. Since everything thats in range of H1 and H2 will collide the same no matter where it hits the cylinder, we flatten all the clipped geometry to have y=0. Do you got that, we clip to fit in the range of the cylinder, then we flatten it. We are working in 2 dimensions now.

There is no side view, it's all flat.


What Now...

 So far, there has not been any code, and there won't be yet. I'm saving that for the end. Here's what we do now that we have the following, oPos, nPos, R, H1, H2, All close by geometry clipped within the range of H1 and H2, you have all these right...right. Well, now you loop through all the clipped geometry checking each edge to the npos using a closestpointonline() function. If the distance is less than R, you have a collision, save the closestpointonline() result and distance. Continue loop through all the geometry checking for any points closer, if there is than that become your new saved point and distance.

tut6 image


Response

Now that we have the collision point (if we have one that is), what do we do...Simple,
we calculate a normal from the nPos to the collision point , normalize the normal and move the nPos , R units in the normal direction. Did you get all that? Good, if not we are getting to the code soon.



To code or not to code...

Here's the part we all been waiting for, the main event. The "I'm to darned lazy to code it myself" section.... But first , let me explain why you've not seen any code up until now.  Through my sheer genius, I have reduced the whole process into 1 mere function... which would be kinda pointless to butcher it up and sprinkle it here and there throughout the tutorial. I t would just get in the way and be messy. Let me give you and overview of the function...One, it does not tell you what geometry should be sent to it, it will process anything you send to it, it is your job to setup a good partition of your world weather bsp,oct/quad or whatever. Send only the geometry that could possible be collided with, no more or less. It will work with more, but that is a waste of valuable CPU. Two it can fail if your player moves greater than Radius each frame.... I have left that check and solution out of this... you have to work for something...this will work fine though if your player does not move > than R each frame. I do have the solution , but it is mine...sorry, but here's a hint, there is a reason to send oPos into the function. This function accepts a triangle from your world , clips it to the H1 and H2, flattens it, and checks it against the nPos and send back the collision point. It's that darned amazing. So here it is....
// h1 is H1 ...duh
// h2 is H2 ...ditto
// r is radius....n/c
// pos is nPos
// opos is oPos
// vPoly[] has to be a triangle
// res is the returned collision
// function returns true if a collision else returns false
//
bool TriInRange(float h1, float h2, float r, D3DXVECTOR3 pos,D3DXVECTOR3 opos, D3DXVECTOR3 vPoly[], D3DXVECTOR3 *res)
{
    int inr,i;
    int lowwer,higher;
   
    int numverts;
    D3DXVECTOR3 verts[6];
       
    D3DXVECTOR3 closest,org,cpl,normal,oorg;
    float cdis,ndis,bignum;

//// org  is nPos
    org=pos;
    org.y=0;

//// oorg is oPos
    oorg=opos;
    oorg.y=0;

    D3DXVECTOR3 hold;


    lowwer=0;
    higher=0;
    inr=0;
    numverts=0;

    if (vPoly[0].y<h2) lowwer=1;
    if (vPoly[1].y<h2) lowwer=1;
    if (vPoly[2].y<h2) lowwer=1;

    if (vPoly[0].y>h1) higher=1;
    if (vPoly[1].y>h1) higher=1;
    if (vPoly[2].y>h1) higher=1;

    if ((higher==1)&&(lowwer==1))
    {
        //======== AB Group ==========================
        if ((vPoly[0].y>h2)&&(vPoly[1].y<h2)){
            hold.y=0;//h2;
            hold.x=vPoly[0].x+((h2-vPoly[0].y)*(vPoly[1].x-vPoly[0].x)/(vPoly[1].y-vPoly[0].y));
            hold.z=vPoly[0].z+((h2-vPoly[0].y)*(vPoly[1].z-vPoly[0].z)/(vPoly[1].y-vPoly[0].y));
            verts[numverts]=hold;
            numverts++;
        }//clip a top
       
        if ((vPoly[0].y<h1)&&(vPoly[1].y>h1)){
            hold.y=0;//h1;
            hold.x=vPoly[0].x+((h1-vPoly[0].y)*(vPoly[1].x-vPoly[0].x)/(vPoly[1].y-vPoly[0].y));
            hold.z=vPoly[0].z+((h1-vPoly[0].y)*(vPoly[1].z-vPoly[0].z)/(vPoly[1].y-vPoly[0].y));
            verts[numverts]=hold;
            numverts++;
        }//clip a bot

        if ((vPoly[1].y>h2)&&(vPoly[0].y<h2)){
            hold.y=0;//h2;
            hold.x=vPoly[1].x+((h2-vPoly[1].y)*(vPoly[0].x-vPoly[1].x)/(vPoly[0].y-vPoly[1].y));
            hold.z=vPoly[1].z+((h2-vPoly[1].y)*(vPoly[0].z-vPoly[1].z)/(vPoly[0].y-vPoly[1].y));
            verts[numverts]=hold;
            numverts++;
        }//clip b top
       
        if ((vPoly[1].y<h1)&&(vPoly[0].y>h1)){
            hold.y=0;//h1;
            hold.x=vPoly[1].x+((h1-vPoly[1].y)*(vPoly[0].x-vPoly[1].x)/(vPoly[0].y-vPoly[1].y));
            hold.z=vPoly[1].z+((h1-vPoly[1].y)*(vPoly[0].z-vPoly[1].z)/(vPoly[0].y-vPoly[1].y));
            verts[numverts]=hold;
            numverts++;
        }//clip b bot
        //======== BC Group ==========================
        if ((vPoly[1].y>h2)&&(vPoly[2].y<h2)){
            hold.y=0;//h2;
            hold.x=vPoly[1].x+((h2-vPoly[1].y)*(vPoly[2].x-vPoly[1].x)/(vPoly[2].y-vPoly[1].y));
            hold.z=vPoly[1].z+((h2-vPoly[1].y)*(vPoly[2].z-vPoly[1].z)/(vPoly[2].y-vPoly[1].y));
            verts[numverts]=hold;
            numverts++;
        }//clip b top
       
        if ((vPoly[1].y<h1)&&(vPoly[2].y>h1)){
            hold.y=0;//h1;
            hold.x=vPoly[1].x+((h1-vPoly[1].y)*(vPoly[2].x-vPoly[1].x)/(vPoly[2].y-vPoly[1].y));
            hold.z=vPoly[1].z+((h1-vPoly[1].y)*(vPoly[2].z-vPoly[1].z)/(vPoly[2].y-vPoly[1].y));
            verts[numverts]=hold;
            numverts++;
        }//clip b bot

        if ((vPoly[2].y>h2)&&(vPoly[1].y<h2)){
            hold.y=0;//h2;
            hold.x=vPoly[2].x+((h2-vPoly[2].y)*(vPoly[1].x-vPoly[2].x)/(vPoly[1].y-vPoly[2].y));
            hold.z=vPoly[2].z+((h2-vPoly[2].y)*(vPoly[1].z-vPoly[2].z)/(vPoly[1].y-vPoly[2].y));
            verts[numverts]=hold;
            numverts++;
        }//clip c top
       
        if ((vPoly[2].y<h1)&&(vPoly[1].y>h1)){
            hold.y=0;//h1;
            hold.x=vPoly[2].x+((h1-vPoly[2].y)*(vPoly[1].x-vPoly[2].x)/(vPoly[1].y-vPoly[2].y));
            hold.z=vPoly[2].z+((h1-vPoly[2].y)*(vPoly[1].z-vPoly[2].z)/(vPoly[1].y-vPoly[2].y));
            verts[numverts]=hold;
            numverts++;
        }//clip c bot

        //======== CA Group ==========================
        if ((vPoly[2].y>h2)&&(vPoly[0].y<h2)){
            hold.y=0;//h2;
            hold.x=vPoly[2].x+((h2-vPoly[2].y)*(vPoly[0].x-vPoly[2].x)/(vPoly[0].y-vPoly[2].y));
            hold.z=vPoly[2].z+((h2-vPoly[2].y)*(vPoly[0].z-vPoly[2].z)/(vPoly[0].y-vPoly[2].y));
            verts[numverts]=hold;
            numverts++;
        }//clip c top
       
        if ((vPoly[2].y<h1)&&(vPoly[0].y>h1)){
            hold.y=0;//h1;
            hold.x=vPoly[2].x+((h1-vPoly[2].y)*(vPoly[0].x-vPoly[2].x)/(vPoly[0].y-vPoly[2].y));
            hold.z=vPoly[2].z+((h1-vPoly[2].y)*(vPoly[0].z-vPoly[2].z)/(vPoly[0].y-vPoly[2].y));
            verts[numverts]=hold;
            numverts++;
        }//clip c bot

        if ((vPoly[0].y>h2)&&(vPoly[2].y<h2)){
            hold.y=0;//h2;
            hold.x=vPoly[0].x+((h2-vPoly[0].y)*(vPoly[2].x-vPoly[0].x)/(vPoly[2].y-vPoly[0].y));
            hold.z=vPoly[0].z+((h2-vPoly[0].y)*(vPoly[2].z-vPoly[0].z)/(vPoly[2].y-vPoly[0].y));
            verts[numverts]=hold;
            numverts++;
        }//clip a top
       
        if ((vPoly[0].y<h1)&&(vPoly[2].y>h1)){
            hold.y=0;//h1;
            hold.x=vPoly[0].x+((h1-vPoly[0].y)*(vPoly[2].x-vPoly[0].x)/(vPoly[2].y-vPoly[0].y));
            hold.z=vPoly[0].z+((h1-vPoly[0].y)*(vPoly[2].z-vPoly[0].z)/(vPoly[2].y-vPoly[0].y));
            verts[numverts]=hold;
            numverts++;
        }//clip a bot

        //repeat first point
        verts[numverts]=verts[0];
       
        closest=verts[0];
        cdis=10000;
        D3DXVECTOR3 resnc;
//******************************************Find closest point on lines
        for (i=0;i<numverts;i++){
                cpl=ClosestPointOnLine(verts[i],verts[i+1],org);
                ndis=Distance(cpl,org);
                if (ndis<cdis){
                    cdis=ndis;
                    closest=cpl;
                }//closer

        }
       
        //check to see if it is less than radius
        if (cdis<r){
            *res=closest;
            return true;
           
        }else return false;
    }
    else return false;

}
//// end function

D3DXVECTOR3 ClosestPointOnLine(D3DXVECTOR3 vA, D3DXVECTOR3 vB, D3DXVECTOR3 vPoint)
{
    // Create the vector from end point vA to our point vPoint.
    D3DXVECTOR3 vVector1 = vPoint - vA;

    // Create a normalized direction vector from end point vA to end point vB
    D3DXVECTOR3 vVector2 = Normalize(vB - vA);

    // Use the distance formula to find the distance of the line segment (or magnitude)
    float d = Distance(vA, vB);

    // Using the dot product, we project the vVector1 onto the vector vVector2.
    // This essentially gives us the distance from our projected vector from vA.
    float t = Dot(vVector2, vVector1);

    // If our projected distance from vA, "t", is less than or equal to 0, it must
    // be closest to the end point vA.  We want to return this end point.
    if (t <= 0)
        return vA;

    // If our projected distance from vA, "t", is greater than or equal to the magnitude
    // or distance of the line segment, it must be closest to the end point vB.  So, return vB.
    if (t >= d)
        return vB;
 
    // Here we create a vector that is of length t and in the direction of vVector2
    D3DXVECTOR3 vVector3 = vVector2 * t;

    // To find the closest point on the line segment, we just add vVector3 to the original
    // end point vA. 
    D3DXVECTOR3 vClosestPoint = vA + vVector3;

    // Return the closest point on the line segment
    return vClosestPoint;
}


It really is simple, now for the code within your program to use this and dela with collision reponse..... you will have to figure your own hooks out...
    D3DXVECTOR3 vTri2[3];
    float radius=4.0f;
    float oldPos=Player.pos;
    float newPos=Player.pos+Time_Elapsed*Player.velocity;
       
        for (ict=0;ict<Level->numobj;ict++){

                for(j=0; j<Level->obj[ict].numtri; j++ )
                {
                     //load the triangle to send to the function
                    vTri2[0] = Level->obj[ict].verts[Level->obj[ict].index[3*j+0]];
                    vTri2[1] = Level->obj[ict].verts[Level->obj[ict].index[3*j+1]];
                    vTri2[2] = Level->obj[ict].verts[Level->obj[ict].index[3*j+2]];
                   
                
      if (TriInRange(player.pos.y+3,Player.pos.y+9,radius,newPos,oldPos,vTri2,&res))
                    {
                        bnorm=newPos-res;

                        bnorm.y=0;

                        bignum=sqrt((bnorm.x*bnorm.x)+(bnorm.z*bnorm.z));
                        bnorm.x/=bignum;
                        bnorm.z/=bignum;
                        newPos=res+radiusf*bnorm;
                       
                    }//tri hit
                }//for j numtris
          
        }//loop obj
// only update XZ, Y is done by itself
   Player.pos.x=newPos.x;
    Player.pos.z=newPos.z;
Player.pos.y= GetYValueAtPosition(newPos.x,newPos.z,etc...);


What about Y

Ok, so for now all we have been effecting is the XZ axis of our player. I am only gonna describe the GetYValueAtPosition() function. Maybe my next revision will have code. it really is simple if you are thinking in 2-D, think that you are looking down from above. Loop all close triangles find the triangle that your player pos is in and find the Y value of that triangle at that position.... ok. Just a little code....
BOOL GetYValueAtPosition( const float& xp,const float& zp, D3DXVECTOR3& v0,D3DXVECTOR3& v1, D3DXVECTOR3& v2,float *ddd)
{
   
        float d,dh,yh;
        int ct=0;

        if (!IsPossible(xp,zp,v0,v1,v2))return FALSE;

        if (( (v0.x>=xp)&&(v1.x<xp) )||( (v0.x<xp)&&(v1.x>=xp)  ))
        {
          d=v0.z+(xp-v0.x)*(v1.z-v0.z)/(v1.x-v0.x);   
          if (d>zp){ ct++; dh=d;yh=v0.y+(xp-v0.x)*(v1.y-v0.y)/(v1.x-v0.x); }
        }
        if (( (v1.x>=xp)&&(v2.x<xp) )||( (v1.x<xp)&&(v2.x>=xp)  ))
        {
          d=v1.z+(xp-v1.x)*(v2.z-v1.z)/(v2.x-v1.x);   
          if (d>zp){ ct++; dh=d; yh=v1.y+(xp-v1.x)*(v2.y-v1.y)/(v2.x-v1.x);}
        }
        if (( (v2.x>=xp)&&(v0.x<xp) )||( (v2.x<xp)&&(v0.x>=xp)  ))
        {
          d=v2.z+(xp-v2.x)*(v0.z-v2.z)/(v0.x-v2.x);   
          if (d>zp){ ct++; dh=d; yh=v2.y+(xp-v2.x)*(v0.y-v2.y)/(v0.x-v2.x); }
        }

        if (ct==1) {
           
       
            float x[3],y[3],z[3];
            int hm=0;

            if ( ((v0.x>=xp)&&(v1.x<xp)) || ((v1.x>=xp)&&(v0.x<xp)) )
            {
                z[hm]=v0.z+(xp-v0.x)*(v1.z-v0.z)/(v1.x-v0.x);
                y[hm]=v0.y+(xp-v0.x)*(v1.y-v0.y)/(v1.x-v0.x);
                x[hm]=xp;
                hm++;

            }

            if ( ((v1.x>=xp)&&(v2.x<xp)) || ((v2.x>=xp)&&(v1.x<xp)) )
            {
                z[hm]=v1.z+(xp-v1.x)*(v2.z-v1.z)/(v2.x-v1.x);
                y[hm]=v1.y+(xp-v1.x)*(v2.y-v1.y)/(v2.x-v1.x);
                x[hm]=xp;
                hm++;

            }

            if ( ((v2.x>=xp)&&(v0.x<xp)) || ((v0.x>=xp)&&(v2.x<xp)) )
            {
                z[hm]=v2.z+(xp-v2.x)*(v0.z-v2.z)/(v0.x-v2.x);
                y[hm]=v2.y+(xp-v2.x)*(v0.y-v2.y)/(v0.x-v2.x);
                x[hm]=xp;
                hm++;

            }
   
            if (hm<2) return FALSE;
            *ddd = y[0] + (zp-z[0])*(y[1]-y[0])/(z[1]-z[0]);

           
            return TRUE;
        }

        return FALSE;
   

}


Send the triangles in and if  the x and z value are in the triangle , it returns true and ddd returns the y value at that location. You have to decide which one is the right one if more than 1 triangle is over/under the player....

 Last update: 9-8-04

Copyright to Vincent Lancon (VoodooMage) 2004.
Main Web Page: http://www.spiritedtechnology.com

 


Free Site Counter
Netflick