Friday, November 11, 2011

Ballistic trajectory to travel between two points, and rotating a point around an axis

The formula for calculating ballistic trajectory is tricky, luckily one doesn't have to reinvent the wheel to do it  because someone has already done it for us. If you would like to see the math behind how it is calculated, click here. Be aware this does not account for wind or air resistance, so if your game is realistic enough to have either, you'll probably want to look here. Today we're just looking at the version without air resistance.

Ballistic trajectory is the angle required to launch an object from one point to another. For example if you wanted to fire a cannonball out of a cannon to a reticule that the player gets to aim, you would need something like this:
// Returns true if 'end' can be reached at the given 'speed', otherwise
// it returns false.
bool CalculateTrajectory(const Vector3& start, const Vector3& end,
                     const float speed, const float gravity,
                     const bool bUseHighArc, Vector3& outTrajectory, 
                     float& outAngle)
    bool canHit = false;

    // We use doubles instead of floats because we need a lot of
    // precision for some uses of the pow() function coming up.
    double term1 = 0.0f;
    double term2 = 0.0f;
    double root = 0.0f;

    Vector3 diffVector = destination - origin;

    // A horizontally-flattened difference vector.
    Vector3 horzDiff = Vector3(diffVector.x, 0.0f, diffVector.y);
    // We shrink our values by this factor to prevent too much
    // precision loss.
    const float factor = 100.0;

    // Remember that Unitize returns length
    float x = horz.Unitize() / factor; 
    float y = diffVector.y / factor;
    float v = speed / factor;
    float g = gravity / factor;

    term1 = pow(v, 4) - (g * ((g * pow(x,2)) + (2 * y * pow(v,2))));

    // If term1 is positive, then the 'end' point can be reached
    // at the given 'speed'.
    if ( term1 >= 0 )
        canHit = true;

        term2 = sqrt(term1);

        double divisor = (g * x);

        if ( divisor != 0.0f )
            if ( bUseHighArc )
                root = ( pow(v,2) + term2 ) / divisor;
                root = ( pow(v,2) - term2 ) / divisor;

            root = atan(root);

            angleOut = static_cast<float>(root);

            Vector3 rightVector = horz.Cross(Vector3::UnitY);

            // Rotate the 'horz' vector around 'rightVector' 
            // by '-angleOut' degrees.
            RotatePointAroundAxis(rightVector, -angleOut, horz); 

        // Now apply the speed to the direction, giving a velocity
        outTrajectory = horz * speed;

    return canHit;

Bear in mind that the above function assumes an 'up' direction of +Y, and also assumes gravity to be -Y, which is why only a float is needed to represent gravity, rather than a vector.

The formula used by this function gives two trajectories to reach the 'end' point, the highest possible trajectory, and the lowest possible trajectory. The 'bUseHighArc' variable passed in is what determines which result is used.

You'll notice a reference to a new function called 'RotatePointAroundAxis' in there, I have not yet gone over matrix mathematics in my blog, and rather than get into that now I will supply you with the math required create a rotation matrix and use that to rotate our point. If you think of the example I gave earlier about firing a cannon ball, imagine that the cannon was spun around to face the direction it needs to fire, but hasn't yet been elevated to the correct firing angle, this function is what we're using to rotate our currently flat 'horz' vector into the air.

Vector3 RotatePointAroundAxis( const Vector3& axis, const float
                               radians, const Vector3& point )
    float matrix[3][3];

    float sn = sinf(radians);
    float cs = cosf(radians);

    float xSin = axis.x * sn;
    float ySin = axis.y * sn;
    float zSin = axis.z * sn;  
    float oneMinusCS = 1.0f - cs;
    float xym = axis.x * axis.y * oneMinusCS;
    float xzm = axis.x * axis.z * oneMinusCS;
    float yzm = axis.y * axis.z * oneMinusCS;

    matrix[0][0] = (axis.x * axis.x) * oneMinusCS + cs;
    matrix[0][1] = xym + zSin;
    matrix[0][2] = xzm - ySin;
    matrix[1][0] = xym - zSin;
    matrix[1][1] = (axis.y * axis.y) * oneMinusCS + cs;
    matrix[1][2] = yzm + xSin;
    matrix[2][0] = xzm + ySin;
    matrix[2][1] = yzm - xSin;
    matrix[2][2] = (axis.z * axis.z) * oneMinusCS + cs;

    return Vector3
        matrix[0][0] * point.x + matrix[0][1] * point.y + matrix[0][2] * point.z,

        matrix[1][0] * point.x + matrix[1][1] * point.y + matrix[1][2] * point.z,

        matrix[2][0] * point.x + matrix[2][1] * point.y + matrix[2][2] * point.z

Normally you wouldn't have to create your own function to rotate a point around an axis, this would generally be part of the Matrix class you would find in a math library. What you'll often find when programming is that you need to understand the concept behind which functions you're using so that you can make an educated decision about which functions you will need to perform a task. There is little need for you to memorize the exact function above, because you'll have it as part of an engine or you can look it up online, but knowing how the function works is something you might want to do some day. For now I'll skip the lesson on matrix math.


  1. That's really would be erally really helpful if that function could calculate the 3d velocity needed to hit a target but also clearing a fence positioned at a certain distance.
    I wasn't able to find anything on the web and I'm stuck with that :(

  2. Hi!

    Just the code i needed, thanks a lot.

    @Forrest: I dont know if it make any sense, but maybe if you decompose the problem in two parts:
    1) find the velocity to hit the fence at its top.
    2) try to calculate the new trajectory to the target, using as initial velocity the result from step 1).



  3. Thanks for sharing this code, it has saved me a lot of time. By the way, you have several undefined variables there (just using other names).