Chapter 12. 3D Rigid-Body Simulator

In this chapter we’ll show you how to make the leap from 2D to 3D by implementing a rigid-body simulation of an airplane. Specifically, this is a simulation of the hypothetical airplane model that we’ll discuss extensively in Chapter 15. This airplane is of typical configuration with its large wings forward, its elevators aft, a single vertical tail, and plain flaps fitted on the wings.

As with the 2D simulator in previous chapters, we’ll concentrate on the code that implements the physics part of the simulator and not the platform-specific GUI aspects of the simulations.

As in 2D, there are four main elements to this 3D simulation—the model, integrator, user input, and rendering. Remember, the model refers to your idealization of the thing—an airplane, in this case—that you are trying to simulate, while the integrator refers to the method by which you integrate the differential equations of motion. These two elements take care of most of the physics of the simulation. The user input and rendering elements refer to how you’ll allow the user to interact with and view your simulation.

In this simulation, the world coordinate system has its positive x-axis pointing into the screen, its positive y-axis pointing to the left of your screen, and the positive z-axis pointing up. Also, the local, or body-fixed, coordinate system has its positive x-axis pointing toward the front of the airplane, its positive y-axis pointing to the port side (left side), and its positive z-axis pointing up. Since this is a 3D simulation of an airplane, once you get it running, you’ll be able to fly in any direction, looping, banking, diving, and climbing, or performing any other aerobatic maneuver you desire.

Model

One of the most important aspects of this simulation is the flight model. We’ll spend all of Chapter 15 discussing the physics behind this flight model, so we won’t include that discussion here except to introduce a few key bits of code.

To implement the flight model, you first need to prepare a rigid-body structure to encapsulate all of the data required to completely define the state of the rigid body at any instant during the simulation. We’ve defined a structure called RigidBody for this purpose:

typedef struct _RigidBody {

     float         fMass;           // total mass
     Matrix3x3     mInertia;        // mass moment of inertia
                                    // in body coordinates

     Matrix3x3     mInertiaInverse; // inverse of mass moment of inertia
     Vector        vPosition;       // position in earth coordinates
     Vector        vVelocity;       // velocity in earth coordinates
     Vector        vVelocityBody;   // velocity in body coordinates
     Vector        vAngularVelocity;// angular velocity in body coordinates
     Vector        vEulerAngles;    // Euler angles in body coordinates
     float         fSpeed;          // speed (magnitude of the velocity)
     Quaternion    qOrientation;    // orientation in earth coordinates
     Vector        vForces;         // total force on body
     Vector        vMoments;        // total moment (torque) on body
} RigidBody, *pRigidBody;

You’ll notice that it is very similar to the RigidBody2D structure that we used in the 2D hovercraft simulation. One significant difference, however, is that in the 2D case, orientation was a single float value, and now in 3D it’s a quaternion of type Quaternion. We discussed the use of quaternions for tracking rigid-body orientation in the previous chapter, and Appendix C contains a complete definition of the Quaternion class.

The next step in defining the flight model is to prepare an initialization function to initialize the airplane at the start of the simulation. For this purpose, we’ve prepared a function called InitializeAirplane:

RigidBody    Airplane;    // global variable representing the airplane
.
.
.

void     InitializeAirplane(void)
{
     float iRoll, iPitch, iYaw;

     // Set initial position
     Airplane.vPosition.x = −5000.0f;
     Airplane.vPosition.y = 0.0f;
     Airplane.vPosition.z = 2000.0f;

     // Set initial velocity
     Airplane.vVelocity.x = 60.0f;
     Airplane.vVelocity.y = 0.0f;
     Airplane.vVelocity.z = 0.0f;
     Airplane.fSpeed = 60.0f;

     // Set initial angular velocity
     Airplane.vAngularVelocity.x = 0.0f;
     Airplane.vAngularVelocity.y = 0.0f;
     Airplane.vAngularVelocity.z = 0.0f;

     // Set the initial thrust, forces, and moments
     Airplane.vForces.x = 500.0f;
     Airplane.vForces.y = 0.0f;
     Airplane.vForces.z = 0.0f;
     ThrustForce = 500.0;

     Airplane.vMoments.x = 0.0f;
     Airplane.vMoments.y = 0.0f;
     Airplane.vMoments.z = 0.0f;

     // Zero the velocity in body space coordinates
     Airplane.vVelocityBody.x = 0.0f;
     Airplane.vVelocityBody.y = 0.0f;
     Airplane.vVelocityBody.z = 0.0f;

     // Set these to false at first,
     // you can control later using the keyboard
     Stalling = false;
     Flaps = false;

     // Set the initial orientation
     iRoll = 0.0f;
     iPitch = 0.0f;
     iYaw = 0.0f;
     Airplane.qOrientation = MakeQFromEulerAngles(iRoll, iPitch, iYaw);

     // Now go ahead and calculate the plane's mass properties
     CalcAirplaneMassProperties();
}

This function sets the initial location, speed, attitude, and thrust for the airplane and goes on to calculate its mass properties by making a call to CalcAirplaneMassProperties. You’ll see much more of this function in Chapter 15, so we won’t show the whole thing here. We do want to point out a portion of the code that is distinctly different from what you do in a 2D simulation, and that’s the calculation of the moment of inertia tensor:

void     CalcAirplaneMassProperties(void)
{
     .
     .
     .

     // Now calculate the moments and products of inertia for the
     // combined elements.
     // (This inertia matrix (tensor) is in body coordinates)
     Ixx = 0;     Iyy = 0;     Izz = 0;
     Ixy = 0;     Ixz = 0;     Iyz = 0;
     for (i = 0; i< 8; i++)
     {
          Ixx += Element[i].vLocalInertia.x + Element[i].fMass *
                 (Element[i].vCGCoords.y*Element[i].vCGCoords.y +
                  Element[i].vCGCoords.z*Element[i].vCGCoords.z);
          Iyy += Element[i].vLocalInertia.y + Element[i].fMass *
                 (Element[i].vCGCoords.z*Element[i].vCGCoords.z +
                  Element[i].vCGCoords.x*Element[i].vCGCoords.x);
          Izz += Element[i].vLocalInertia.z + Element[i].fMass *
                 (Element[i].vCGCoords.x*Element[i].vCGCoords.x +
                  Element[i].vCGCoords.y*Element[i].vCGCoords.y);
          Ixy += Element[i].fMass * (Element[i].vCGCoords.x *
                 Element[i].vCGCoords.y);
          Ixz += Element[i].fMass * (Element[i].vCGCoords.x *
                 Element[i].vCGCoords.z);
          Iyz += Element[i].fMass * (Element[i].vCGCoords.y *
                 Element[i].vCGCoords.z);
     }

     // Finally set up the airplane's mass and its inertia matrix and take the
     // inverse of the inertia matrix
     Airplane.fMass = mass;
     Airplane.mInertia.e11 = Ixx;
     Airplane.mInertia.e12 = -Ixy;
     Airplane.mInertia.e13 = -Ixz;
     Airplane.mInertia.e21 = -Ixy;
     Airplane.mInertia.e22 = Iyy;
     Airplane.mInertia.e23 = -Iyz;
     Airplane.mInertia.e31 = -Ixz;
     Airplane.mInertia.e32 = -Iyz;
     Airplane.mInertia.e33 = Izz;

     Airplane.mInertiaInverse = Airplane.mInertia.Inverse();
}

The airplane is modeled by a number of elements, each representing a different part of the airplane’s structure—for example, the tail rudder, elevators, wings, and fuselage (see Chapter 15 for more details). The code specified here takes the mass properties of each element and combines them, using the techniques discussed in Chapter 7 to come up with the combined inertia tensor for the entire aircraft. The important distinction between these calculations in a 3D simulation and the 2D simulation is that here the inertia is a tensor and in 2D it is a single scalar.

InitializeAirplane is called at the very start of the program. We found it convenient to make the call right after the application’s main window is created.

The final part of the flight model has to do with calculating the forces and moments that act on the airplane at any given instant in time during the simulation. As in the 2D hovercraft simulation, without this sort of function, the airplane will do nothing. For this purpose we’ve defined a function called CalcAirplaneLoads, which is called at every step through the simulation. This function relies on a couple of other functions—namely, LiftCoefficient, DragCoefficient, RudderLiftCoefficient, and RudderDragCoefficient. All of these functions are shown and discussed in detail in the section Modeling in Chapter 15.

For the most part, the code contained in CalcAirplaneLoads is similar to the code you’ve seen in the CalcLoads function of the hovercraft simulation. CalcAirplanLoads is a little more involved since the airplane is modeled by a number of elements that contribute to the total lift and drag on the airplane. There’s also another difference that we’ve noted here:

void     CalcAirplaneLoads(void)
{
     .
     .
     .

     // Convert forces from model space to earth space
     Airplane.vForces = QVRotate(Airplane.qOrientation, Fb);

     // Apply gravity (g is defined as −32.174 ft/s^2)
     Airplane.vForces.z += g * Airplane.fMass;

     .
     .
     .
}

Just about all of the forces acting on the airplane are first calculated in body-fixed coordinates and then converted to earth-fixed coordinates before the gravity force is applied. The coordinate conversion is effected through the use of the function QVRotate, which rotates the force vector based on the airplane’s current orientation, represented by a quaternion.[22]

Integration

Now that the code to define, initialize, and calculate loads on the airplane is complete, you need to develop the code to actually integrate the equations of motion so that the simulation can progress through time. The first thing you need to do is decide on the integration scheme that you want to use. In this example, we decided to go with the basic Euler’s method. We’ve already discussed some better methods in Chapter 7. We’re going with Euler’s method here because it’s simple and we didn’t want to make the code here overly complex, burying some key code that we need to point out to you. In practice, you’re better off using one of the other methods we discuss in Chapter 7 instead of Euler’s method. With that said, we’ve prepared a function called StepSimulation that handles all of the integration necessary to actually propagate the simulation:

void     StepSimulation(float dt)
{
     // Take care of translation first:
     // (If this body were a particle, this is all you would need to do.)

          Vector Ae;

          // calculate all of the forces and moments on the airplane:
          CalcAirplaneLoads();

          // calculate the acceleration of the airplane in earth space:
          Ae = Airplane.vForces / Airplane.fMass;

          // calculate the velocity of the airplane in earth space:
          Airplane.vVelocity += Ae * dt;

          // calculate the position of the airplane in earth space:
          Airplane.vPosition += Airplane.vVelocity * dt;


     // Now handle the rotations:
          float  mag;

          // calculate the angular velocity of the airplane in body space:
          Airplane.vAngularVelocity += Airplane.mInertiaInverse *
                                       (Airplane.vMoments -
                                       (Airplane.vAngularVelocity^
                                       (Airplane.mInertia *
                                         Airplane.vAngularVelocity)))
                                        * dt;

          // calculate the new rotation quaternion:
          Airplane.qOrientation += (Airplane.qOrientation *
                                    Airplane.vAngularVelocity) *
                                   (0.5f * dt);

          // now normalize the orientation quaternion:
          mag = Airplane.qOrientation.Magnitude();
          if (mag != 0)
               Airplane.qOrientation /= mag;

          // calculate the velocity in body space:
          // (we'll need this to calculate lift and drag forces)
          Airplane.vVelocityBody = QVRotate(~Airplane.qOrientation,
                                            Airplane.vVelocity);

          // calculate the air speed:
          Airplane.fSpeed = Airplane.vVelocity.Magnitude();

          // get the Euler angles for our information
          Vector u;

          u = MakeEulerAnglesFromQ(Airplane.qOrientation);
          Airplane.vEulerAngles.x = u.x;     // roll
          Airplane.vEulerAngles.y = u.y;     // pitch
          Airplane.vEulerAngles.z = u.z;     // yaw

}

The very first thing that StepSimulation does is call CalcAirplaneLoads to calculate the loads acting on the airplane at the current instant in time. StepSimulation then goes on to calculate the linear acceleration of the airplane based on current loads. Next, the function goes on to integrate, using Euler’s method, once to calculate the airplane’s linear velocity and then a second time to calculate the airplane’s position. As we’ve commented in the code, if you were simulating a particle this is all you would have to do; however, since this is not a particle, you need to handle angular motion.

The first step in handling angular motion is to calculate the new angular velocity at this time step, using Euler integration, based on the previously calculated moments acting on the airplane and its mass properties. We do this in body coordinates using the following equation of angular motion but rewritten to solve for dω:

Mcg = dHcg/dt = I (dω/dt) + (ω × (I ω))

The next step is to integrate again to update the airplane’s orientation, which is expressed as a quaternion. Here, you need to use the differential equation relating an orientation quaternion to angular velocity that we showed you in Chapter 11:

dq/dt = (1/2) ω q

Next, to enforce the constraint that this orientation quaternion be a unit quaternion, the function goes ahead and normalizes the orientation quaternion.

Since the linear velocity was previously calculated in global coordinates (the fixed coordinate system), and since CalcAirplaneLoads needs the velocity in the body-fixed (rotating) coordinates system, the function goes ahead and rotates the velocity vector, storing the body-fixed vector in the vVelocityBody member of the RigidBody structure. This is done here as a matter of convenience and uses the quaternion rotation function QVRotate to rotate the vector based on the airplane’s current orientation. Notice here that we use the conjugate of the orientation quaternion since we’re now rotating from global coordinates to body coordinates.

As another convenience, we calculate the air speed, which is simply the magnitude of the linear velocity vector. This is used to report the air speed in the main window title bar.

Lastly, the three Euler angles—roll, pitch, and yaw—are extracted from the orientation quaternion so that they can also be reported in the main window title bar. The function to use here is MakeEulerAnglesFromQ, which is defined in Appendix C.

Don’t forget, StepSimulation must be called once per simulation cycle.

Flight Controls

At this point, the simulation still won’t work very well because you have not implemented the flight controls. The flight controls allow you to interact with the airplane’s various controls surfaces in order to actually fly the plane. We’ll use the keyboard as the main input device for the flight controls. Remember, in a physics-based simulation such as this one, you don’t directly control the motion of the airplane; you control only how various forces are applied to the airplane, which then, by integration over time, affect the airplane’s motion.

For this simulation, the flight stick is simulated by the arrow keys. The down arrow pulls back on the stick, raising the nose; the up arrow pushes the stick forward, causing the nose to dive; the left arrow rolls the plane to the left (port side); and the right arrow rolls the plane to the right (starboard side). The X key applies left rudder action to cause the nose of the plane to yaw toward the left, while the C key applies right rudder action to cause the nose to yaw toward the right. Thrust is controlled by the A and Z keys. The A key increments the propeller thrust by 100 pounds, and the Z key decrements the thrust by 100 pounds. The minimum thrust is 0, while the maximum available thrust is 3,000 pounds. The F key activates the landing flaps to increase lift at low speed, while the D key deactivates the landing flaps.

We control pitch by deflecting the flaps on the aft elevators; for example, to pitch the nose up, we deflect the aft elevator flaps upward (that is, the trailing edge of the elevator is raised with respect to the leading edge). We control roll in this simulation by applying the flaps differentially; for example, to roll right, we deflect the right flap upward and the left flap downward. Finally, we control yaw by deflecting the vertical tail rudder; for example, to yaw left, we deflect the trailing edge of the tail rudder toward the left.

We’ve prepared several functions to handle the flight controls that should be called whenever the user is pressing one of the flight control keys. There are two functions for the propeller thrust:

void     IncThrust(void)
{
     ThrustForce += _DTHRUST;
     if(ThrustForce > _MAXTHRUST)
          ThrustForce = _MAXTHRUST;
}

void     DecThrust(void)
{
     ThrustForce -= _DTHRUST;
     if(ThrustForce < 0)
          ThrustForce = 0;
}

IncThrust simply increases the thrust by _DTHRUST checking to make sure it does not exceed _MAXTHRUST. We’ve defined _DTHRUST and _MAXTHRUST as follows:

#define    _DTHRUST      100.0f
#define    _MAXTHRUST    3000.0f

DecThrust, on the other hand, decreases the thrust by _DTHRUST checking to make sure it does not fall below 0.

To control yaw, we’ve prepared three functions that manipulate the rudder:

void     LeftRudder(void)
{
     Element[6].fIncidence = 16;
}

void     RightRudder(void)
{
     Element[6].fIncidence = −16;
}

void     ZeroRudder(void)
{
     Element[6].fIncidence = 0;
}

LeftRudder changes the incidence angle of Element[6], the vertical tail rudder, to 16 degrees, while RightRudder changes the incidence angle to −16 degrees. ZeroRudder centers the rudder at 0 degrees.

The ailerons, or flaps, are manipulated by these functions to control roll:

void     RollLeft(void)
{
     Element[0].iFlap = 1;
     Element[3].iFlap = −1;
}

void     RollRight(void)
{
     Element[0].iFlap = −1;
     Element[3].iFlap = 1;
}

void     ZeroAilerons(void)
{
     Element[0].iFlap = 0;
     Element[3].iFlap = 0;
}

RollLeft deflects the port aileron, located on the port wing section (Element[0]), upward, and the starboard aileron, located on the starboard wing section (Element[3]), downward. RollRight does just the opposite, and ZeroAilerons resets the flaps back to their undeflected positions.

We’ve defined yet another set of functions to control the aft elevators so as to control pitch:

void     PitchUp(void)
{
     Element[4].iFlap = 1;
     Element[5].iFlap = 1;
}

void     PitchDown(void)
{
     Element[4].iFlap = −1;
     Element[5].iFlap = −1;
}


void     ZeroElevators(void)
{
     Element[4].iFlap = 0;
     Element[5].iFlap = 0;
}

Element[4] and Element[5] are the elevators. PitchUp deflects their flaps upward, and PitchDown deflects their flaps downward. ZeroElevators resets their flaps back to their undeflected positions.

Finally, there are two more functions to control the landing flaps:

void     FlapsDown(void)
{
     Element[1].iFlap = −1;
     Element[2].iFlap = −1;
     Flaps = true;
}

void     ZeroFlaps(void)
{
     Element[1].iFlap = 0;
     Element[2].iFlap = 0;
     Flaps = false;
}

The landing flaps are fitted on the inboard wings sections, port and starboard, which are Element[1] and Element[2]. FlapsDown deflects the flaps downward, while ZeroFlaps resets them back to their undeflected position.

As we said, these functions should be called when the user is pressing the flight control keys. Further, they need to be called before StepSimulation is called so that they can be included in the current time step’s force and moment calculations. The sequence of calls should look something like this:

.
.
.

     ZeroRudder();
     ZeroAilerons();
     ZeroElevators();

     // pitch down
     if (IsKeyDown(VK_UP))
          PitchDown();

     // pitch up
     if (IsKeyDown(VK_DOWN))
          PitchUp();

     // roll left
     if (IsKeyDown(VK_LEFT))
          RollLeft();

     // roll right
     if (IsKeyDown(VK_RIGHT))
          RollRight();

     //  Increase thrust
     if (IsKeyDown(0x41)) // A
          IncThrust();

     //  Decrease thrust
     if (IsKeyDown(0x5A)) // Z
          DecThrust();

     // yaw left
     if (IsKeyDown(0x58)) // x
          LeftRudder();

     // yaw right
     if (IsKeyDown(0x43)) // c
          RightRudder();

     // landing flaps down
     if (IsKeyDown(0x46)) //f
          FlapsDown();

     // landing flaps up
     if (IsKeyDown(0x44)) // d
          ZeroFlaps();

     StepSimulation(dt);
.
.
.

Before StepSimulation is called, we check each of the flight control keys to see if it is being pressed. If so, then the appropriate function is called.

The function IsKeyDown, which checks whether a certain key is pressed, looks like this in a Windows implementation:

BOOL IsKeyDown(short KeyCode)
{

    SHORT    retval;

    retval = GetAsyncKeyState(KeyCode);

    if (HIBYTE(retval))
        return TRUE;

    return FALSE;
}

The important thing to note here is that the keys are being checked asynchronously because it is possible that more than one key will be pressed at any given time, and they must be handled simultaneously instead of one at a time (as would be the case in the standard Windows message processing function).

The addition of flight control code pretty much completes the physics part of the simulation. So far, you have the model, the integrator, and the user input or flight control elements completed. All that remains is setting up the application’s main window and actually drawing something that represents what you’re simulating. We’ll leave that part up to you, or you can look at the example we’ve included on the book’s website to see what we did on a Windows machine.



[22] QVRotate is defined in Appendix C.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.34.62