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.
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]
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.
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.
3.129.42.134