Looking at Vehicle Dynamics


Vehicle dynamics is the subset of physics concerning the motion of vehicles (planes, trains, automobiles, boats, etc.) through space as well as how they interact with other objects. Other objects include the medium that they are moving through or on as well as anything that they might collide with. The mathematics involves the mass of the vehicle as well as the forces that act upon the vehicle.

Note

You might remember from high school physics class that mass is not the same as weight. Weight is the force that mass exerts when the acceleration of gravity is applied.

The key equation of motion in physics is the basic force equation that follows and its derivatives. In order to make an object move from one location to another in a realistic manner, we must apply forces to the object. Those forces create an acceleration of the object along some vector. By integrating the acceleration we can determine the object s new velocity, and by integrating the velocity we can determine a change in position.

Force=Mass * Acceleration

Acceleration = Force/Mass

Multiple forces will be involved in our vehicle dynamics. Gravity will apply a downward force on our vehicle. Assuming that the vehicle s tires are in contact with the terrain, an upward force will be applied to the tires by the ground surface. When the engine is causing the tires to rotate, a frictional force between the tires and the ground will create a linear force that will serve to accelerate or decelerate the vehicle. As the vehicle moves through the air, the friction between the vehicle and the air will create a drag force that serves to limit the vehicle s maximum speed. As we proceed through the chapter, we will address these forces as well as others to develop a basic dynamics model for our game.

Defining Supporting Classes

Before we dive into the actual physics code, we need to look at a few support classes that will be used in the CarDynamics class. The VehicleDynamics assembly is a stand-alone library that will be used by the game engine. As such, it is designed to exist separately from DirectX and could be used just as easily with an OpenGL rendering engine. Because it was written this way, we will need to redefine two classes that we have been working with in the game engine.

Redefining the Vector Class

The first of these classes is the Vector class. Within the game engine we were able to rely on the Vector3 class that comes with DirectX 9. Listing 10 “1 contains the entire definition of the Vector class that is used within this library. Within the vehicle dynamics library, we will work almost exclusively with double-precision floating-point variables . The mathematics of physics tend to rely on a greater level of precision than we find necessary within the game engine itself.

Listing 10.1: Vector Class
start example
 using System;  namespace VehicleDynamics  {     public class Vector     {        public   Vector() {x=0.0; y=0.0; z=0.0;}        public   Vector (Vector other) {x=other.x; y=other.y; z=other.z;}        public   Vector (double new_x, double new_y, double new_z)        {x=new_x; y=new_y; z=new_z;}        ~Vector () {}        // Operator functions        public static bool operator <(Vector first, Vector second)        {           return (first.x < second.x && first.y < second.y && first.z < second.z);        }        public static bool operator >(Vector first, Vector second)        {           return (first.x > second.x && first.y > second.y && first.z > second.z);        }        public static bool operator ==(Vector first, Vector second)        {           return first.x==second.x && first.y==second.y && first.z==second.z;        }        public bool Equals(Vector second)        {           return x==second.x && y==second.y && z==second.z;        }        public static bool operator !=(Vector first, Vector second)        {           return first.x!=second.x   first.y!=second.y   first.z!=second.z;        }        public static Vector operator   (Vector first)        {           return new Vector(   first.x,   first.y,   first.z);        }        public static Vector operator   (Vector first, Vector second)        {           return new Vector(first.x   second.x, first.y   second.y,                  first.z   second.z);        }        public static Vector operator + (Vector first, Vector second)        {           return new Vector(first.x+second.x, first.y+second.y,                  first.z + second.z);}        public static Vector operator / (Vector first, float value)        {           return new Vector(first.x/value, first.y/value, first.z/value);        }        public static Vector operator * (Vector first, float value)        {           return new Vector(first.x*value, first.y*value, first.z*value);        }        public static double operator * (Vector first, Vector second)        {           return (first.x*second.x+first.y*second.y+first.z*second.z);        }        // Accessor functions        public double X { get {return x;} set { x = value; } }        public double Y { get {return y;} set { y = value; } }        public double Z { get {return z;} set { z = value; } }        // Set functions        public    Vector Set (double new_x, double new_y, double new_z)        {           x= new_x; y= new_y; z= new_z; return this;        }        public    Vector SetX (double new_x)        {           x= new_x; return this;        }        public    Vector SetY (double new_y)        {           y= new_y; return this;        }        public    Vector SetZ (double new_z)        {           z= new_z; return this;        }        public    Vector SetToZero ()        {           x= 0; y= 0; z= 0; return this;        // Limiting functions        public    Vector Limit (Vector value, Vector limit)        {           return new Vector(Math.Min(limit. X, Math.Max(limit. X, value. X)),              Math.Min(limit. Y, Math.Max(limit. Y, value. Y)),              Math.Min(limit. Z, Math.Max(limit. Z, value. Z)));}        public    Vector Limit (Vector limit)        {           LimitX(limit.X);           LimitY(limit.Y);           LimitZ(limit.Z); return this;        }        public    double LimitX (double min_value, double max_value)        {           return Math.Min(max_value, Math.Max(min_value, x));        }        public    double LimitX (double value)        {           return Math.Min(value, Math.Max(   value, x));        }        public    double LimitY (double min_value, double max_value)        {           return Math.Min(max_value, Math.Max(min_value, y));        public    double LimitY (double value)        {           return Math.Min(value, Math.Max(   value, y));        }        public    double LimitZ (double min_value, double max_value)        {           return Math.Min(max_value, Math.Max(min_value, z));        }        public    double LimitZ (double value)        {           return Math.Min(value, Math.Max(   value, z));        }        public    Vector Delta (Vector from)        {           return new Vector(x   from.x, y   from.y, z   from.z);        }        public    double DeltaX (Vector from)        {           return x   from.x;        }        public    double DeltaY (Vector from)        {           return y   from.y;        }        public    double DeltaZ (Vector from)        {           return z   from.z;        }        public    Vector ABSDelta (Vector from)        {           return new Vector(Math.Abs(x   from.x), Math.Abs(yfrom.y),                  Math.Abs(z   from.z));        }        public    double ABSDeltaX (Vector from)        {           return Math.Abs(x   from.x);        }        public    double ABSDeltaY (Vector from)        {           return Math.Abs(y   from.y);        }        public double ABSDeltaZ (Vector from)        {           return Math.Abs(z   from.z);        }        protected double x;        protected double y;        protected double z;        private double DistanceFrom (Vector from)        {           return Math.Sqrt((x   from.x)*(x   from.x) + (y   from.y)*(y   from.y) +                  (z   from.z)*(z   from.z));        }        public double otherXYDistanceFrom (Vector from)        {           return Math.Sqrt((x   from.x)*(x   from.x) + (y   from.y)*(y   from.y));        }        public double otherDistanceFrom ()        {           return Math.Sqrt((x*x) + (y*y) + (z*z));        }        public double DistanceFrom ()        {           return Math.Sqrt((x*x) + (y*y) + (z*z));        }        public double XYDistanceFrom (Vector from)        {           return Math.Sqrt((x   from.x)*(x   from.x) + (y   from.y)*(y   from.y));        }        public double XYDistanceFrom ()        {           return Math.Sqrt((x*x) + (y*y));        }        public double otherXYDistanceFrom ()        {           return Math.Sqrt((x*x) + (y*y));        }        public Vector Normalize ()        {           float temp = (float)Math.Sqrt(x*x + y*y + z*z);           if (temp != 0.0f)              return new Vector(x/temp, y/temp, z/temp);           return new Vector (0.0f, 0.0f, 0.0f);        }        Vector CrossProduct (Vector that)        {           return new Vector(y*that.z   z*that.y, z*that.x   x*that.z,                  x*that.y   y*that.x);        }        public double otherElevBrg (Vector other)        {           double xy_length= XYDistanceFrom(other);           double elev_brg= Math.Atan2(DeltaZ(other), xy_length);           while (elev_brg > Math.PI) elev_brg   = Math.PI;           while (elev_brg <   Math.PI) elev_brg += Math.PI;           return elev_brg;        }        public double otherRelBrg (Vector other)        {           return Math.Atan2(Y   other.Y, X   other.X);        }        bool otherIsParallelWith (Vector other)        {           return CrossProduct(other) == new Vector(0.0f, 0.0f, 0.0f);        }        public void IncrementX(double value)        {           x += value;        }        public void IncrementY(double value)        {           y += value;        }        public void IncrementZ(double value)        {           z += value;        }     };  } 
end example
 

I will not be describing the class in Listing 10 “1 to the same level of detail that I ve used for the game engine. Suffice it to say that the class holds three doubles and provides a number of properties and methods for manipulating the three values as a vector. It is included here for completeness. Although you could enter all of this code manually from the book, I urge you instead to download all of the sample code from the Apress Web site (http://www.apress.com).

Redefining the Attitude Class

The second class that is somewhat a redefinition from the game engine is the Euler class, which is an expansion of the Attitude structure used within the game engine. This class holds not only the heading, pitch, and roll values (psi, theta, and phi), but also the sines and cosines of these values as well as a rotation matrix based on these values. The goal of this class is to be as efficient as possible and to take advantage of the interrelations between the angular values and the methods that rely on them. That is why the trigonometric values and the rotation matrix are included within the class. Rather than rebuild the matrix each pass based on the latest angular information, it can check a flag to see if the matrix is current. The rotation matrix is only recalculated if an angle has changed and the flag has been cleared indicating that the matrix needs to be recalculated. The same holds true with the sine and cosine of each angle. Rather than calling the computationally expensive trigonometric functions every pass, they are called only when the associated angle is modified.

Listing 10 “2 is the complete Euler class. Because it is a support class that is not specific to the game engine, I will not go into a more complete explanation of this code. All of the properties and methods within this class are quite simple and should be readily understandable. The only methods that are not obvious at a glance might be the AEPCPI methods, which are angle end-point check methods that maintain an angle between zero and two pi.

Listing 10.2: Euler Class
start example
 using System;  namespace VehicleDynamics  {     public class Euler     {        private  double    psi;        private  double    theta;        private  double    phi;        private  double    cpsi;        private  double    ctheta;        private  double    cphi;        private  double    spsi;        private  double    stheta;        private  double    sphi;        private    double[,] mat = new double[3,3];        private    bool matrix_current;        // The default class constructor        public    Euler() {psi = 0.0; theta = 0.0; phi = 0.0;}        // A constructor that accepts three floats defining the angles        public    Euler(double x_psi, double x__theta, double x_phi)        {Psi = x_psi; Theta=x_theta; Phi=x_phi;}        // A copy constructor        public    Euler (Euler x_angle) {psi = x_angle.Psi; theta=x_angle.Theta; phi=x_angle.Phi;}        // The class destructor        ~Euler () {}        // Calculates the difference between two copies of the class        public    Euler Delta (Euler from)        { return new Euler(psi   from.psi, theta   from.theta, phi   from.phi); }        public     double DeltaPsi (Euler from)        { return psi   from.psi; }        public    double DeltaTheta (Euler from)        { return theta   from.theta; }        public    double DeltaPhi (Euler from)           { return phi   from. phi; }        public    Euler ABSDelta (Euler from)           { return new Euler(Math.Abs(psi   from. psi),           Math.Abs(theta   from.theta),           Math.Abs(phi   from. phi)); }        public    double ABSDeltaPsi (Euler from)           { return Math.Abs(psi   from.psi); }        public    double ABSDeltaTheta (Euler from)           { return Math.Abs(theta   from.theta); }        public    double ABSDeltaPhi (Euler from)           { return Math.Abs(phi   from. phi); }        public  static  Euler operator * (Euler first, float value)        {return new  Euler(first.psi*value, first. theta*value, first. phi*value);}        public  static Euler operator + (Euler first, Euler second)        {return new Euler(first.psi+second.psi, first. theta+second.theta,                  first.phi+second.phi);}        // Accessor functions        public     double Psi        {           get {return psi;}           set           {              matrix_current = psi == value;              psi = value;              cpsi=(float)Math.Cos(psi);              spsi=(float)Math.Sin(psi);           }        }       public     double Theta        {           get {return theta;}           set           {              matrix_current = theta == value;              theta = value;              ctheta=(float)Math.Cos(theta);              stheta=(float)Math.Sin(theta);           }        }        public     double Phi        {           get {return phi;}           set           {              matrix_current = phi == value;              phi=value;              cphi=(float)Math.Cos(phi);              sphi=(float)Math.Sin(phi);           }        }        public     double cosPsi { get {return cpsi;} }        public     double cosTheta { get {return ctheta;} }        public     double cosPhi { get {return cphi;} }        public     double sinPsi { get {return spsi;} }        public     double sinTheta { get {return stheta;} }        public     double sinPhi { get {return sphi;} }        public     double PsiAsDegrees { get {return (psi*l80.0/Math.PI);} }        public     double ThetaAsDegrees { get {return (theta*l80.0/Math.PI);} }        public     double PhiAsDegrees { get {return (phi*180.0/Math.PI);} }        // Set functions        public    Euler SetToZero () {psi= 0; theta= 0; phi= 0; return this;}        //=====================================================================        public    Euler SetPsiAsDegrees (double    x_psi) {Psi = (x_psi*Math. PI/180.0); return this;}        //=====================================================================        public    Euler SetThetaAsDegrees (double x_theta) {The   ta = (x_theta*Math. PI/180.0); return this;}        //=====================================================================        public    Euler SetPhiAsDegrees (double x_phi) {   Phi = (x_phi*Math. PI/180.0); return this;}        //=====================================================================        public   float AEPCPI(float angle)        {          while (angle > (Math.PI+Math.PI))              angle   = (float) (Math.PI+Math.PI);           while (angle < 0.0f)              angle += (float) (Math.PI+Math.PI);           return angle;        }        //=================================================        public double AEPCPI(double angle)        {           while (angle > (Math.PI+Math.PI))              angle   = (float)Math.PI;           while (angle < 0.0)              angle += (Math.PI+Math.PI);           return angle;        }        //=================================================        public void Limits ()        {           // Flip heading and roll when we go over the top or through the bottom.           if (theta > (Math. PI/2.0))           {              theta=Math. PITheta;              psi = AEPCPI(Psi + Math.PI);              phi = AEPCPI(Phi + Math.PI);           }           else if (theta <   (Math.PI/2.0))           {              theta =   Math.PI   Theta;              psi = AEPCPI(Psi + Math.PI);              phi = AEPCPI(Phi + Math.PI);           {           else           }              psi = AEPCPI(Psi);              phi = AEPCPI(Phi);           }        } // End Limits        //=================================================        public void Limits (Euler results)        {           // Flip heading and roll when we go over the top or through the bottom.           if (results.Theta > (Math.PI/2.0))            {               theta = (float)Math.PI   results. Theta;               psi = (float)AEPCPI(results.Psi + (float)Math.PI);               phi = (float)AEPCPI(results.Phi + (float)Math.PI);            }            else if (results.Theta <   (Math.PI/2.0))            {               theta =   (float)Math.PI   results.Theta;               psi = (float)AEPCPI(results.Psi + (float) Math.PI);               phi = (float)AEPCPI(results.Phi + (float)Math.PI);            }            else            {              theta=results. Theta;              psi = (float)AEPCPI(results.Psi);              phi = (float)AEPCPI(results.Phi);            }         } // End Limits         //=================================================         public void Limits (float x_psi, float x_theta, float x_phi)         {             // Flip heading and roll when we go over the top or through the bottom.             if (x_theta > (Math. PI/2.0))             {                theta = (float)Math.PI   x_theta;                psi = (float)AEPCPI(x_psi+Math.PI);                phi = (float) AEPCPI(x_phi+Math.PI);             }             else if (x_theta <   (Math.PI/2.0))             {                theta =   (float)Math.PI   x_theta;                psi = (float)AEPCPI(x_psi + Math.PI);                phi = (float)AEPCPI(x_phi + Math.PI);             }             else             {                theta = x_theta;                psi = (float)AEPCPI(x_psi);                phi = (float)AEPCPI(x_phi);             }         } // End Limits         //===============================================         public float AngularDifference(float ang1, float ang2)         {             float result;             result = ang1   ang2;             if (result < 0.0)             {                result *=   1.0f;             }             return result;         }         //===============================================         public void RotateAtoE(Vector num)         {            double[] temp = new double[3];            if (!matrix_current) CalcMatrix();            temp[0] = mat[0,0] * num.X + mat[0,1] * num.Y + mat[0,2 ] * num.Z;            temp[1] = mat[1,0] * num.X + mat[1,1] * num.Y + mat[1,2 ] * num.Z;            temp[2] = mat[2,0] * num.X + mat[2 ,1] * num.Y + mat[2 ,2 ] * num.Z;            num.X = temp[0];            num.Y = temp[1];            num.Z = temp[2];         }         //===============================================         public void RotateEtoA(Vector num)         {           double [] temp = new double[3];            if (!matrix_current) CalcMatrix();            temp[0] = mat[0,0] * num.X + mat[1,0] * num.Y + mat[2,0] * num.Z;            temp[1] = mat[0,1] * num.X + mat[1,1] * num.Y + mat[2,1] * num.Z;            temp[2] = mat[0,2] * num.X + mat[1,2] * num.Y + mat[2,2] * num.Z;            num.X = temp[0];            num.Y = temp[1];            num.Z = temp[2];         }         //===========================================        public void CalcMatrix()         {            mat[0,0] = ctheta * cpsi;            mat[0,1] = sphi * stheta * cpsi   cphi * spsi;            mat[0,2] = cphi * stheta * cpsi + sphi * spsi;            mat[1,0] = ctheta * spsi;            mat[1,1] = sphi * stheta * spsi + cphi * cpsi;            mat[1,2] = cphi * stheta * spsi   sphi * cpsi;            mat[2,0] =   stheta;            mat[2,1] = sphi * ctheta;            mat[2,2] = cphi * ctheta;            matrix_current=true;         }      }   } 
end example
 

Adding Support for Data Curves

There is one more support class to investigate before we get into the actual dynamics code: the LFI class. LFI is short for linear function interpolation. At times a calculation is based on physical information supplied in the form of a data curve. Some sample uses for this can be found in coefficient of lift-anddrag data versus angle of attack for flight equations of motion. We will use this interpolation for the engine torque curve for our car dynamics.

Data points along the curve are supplied to the class with two values. The class assumes that the index axis of the data curve is linear and based at zero, with as many as 100 data points along this axis. In order to achieve these requirements, the class includes a slope and intercept value that may be set to scale the index values into this range.

The code for this class is shown in Listing 10 “3. To calculate the return value, the input value is first scaled using the slope and offset. The integer portion of this number is the index into the data table. The fractional portion of this number is the portion of the delta between this index value and the next .

Listing 10.3: LFI Class
start example
 using System;  namespace VehicleDynamics  {     ///<summary>     ///Class for linear function interpolation     ///</summary>     public class LFI     {        private double[] data = new double[101];        private double slope = 1.0;        private double intercept = 0.0;        public double Slope        {           get { return slope; }           set { slope = value; }        }        public double Intercept        {           get { return intercept; }           set { intercept = value; }        }    ///<summary>     ///Method to place curve data into the class     ///</summary>     public bool SetDataPoint(double index_value, float data_point)  {    bool result=false;     int index = (int)(index_value / slope   intercept);     if (index >= 0 && index <= 100)     {        data [index]=data_point;        result = true;     }           return result;        }     ///<summary>     ///Method to interpolate linearly to get a value from a data curve     ///</summary>     public double Interpolate(double index_value)     {         double delta;        double result=0.0;        try        {           double scaled_value = index_value / slope   intercept;           int index = (int)scaled_value;           delta=data[index+1]data[index];           result = data[index] + delta * (scaled_value   index);        }        catch (Exception e)        {           System.Diagnostics.Debug.WriteLine(e.Message);        }       }     };  } 
end example
 

Getting Dynamic-Car Physics Classes

Now we are ready to get into the actual dynamics classes.

Putting Wheels on Our Car

The first of these classes will be the Wheel class, which represents one wheel of a vehicle and its associated suspension. All aspects of the wheel are tracked and manipulated through this class. The attributes for the Wheel class are found in Listing 10 “4. The class includes an enumeration that declares which corner of the vehicle the wheel belongs in. As you can see from the enumeration, this class assumes a four-wheeled vehicle. If we wanted to support tractor- trailers with this class, we would need to expand the enumeration.

Listing 10.4: Wheel Class Attributes
start example
 using System;  namespace VehicleDynamics  {     public enum WhichWheel { LeftFront=0, RightFront, LeftRear, RightRear };     public struct Wheel     {        public   Vector        offset;        public  Vector       earth_location;        private   double       rel_heading;        public    double      radius;        public    double      ground_height;        public    double      altitude;        public    double      height_above_ground;        private    double      weight_over_wheel;        private    double      static_weight_over_wheel;        public    double      suspension_offset;        public    double      max_suspension_offset;        private    double      upwards_force;        public    double      friction;        private    double      stiction;        private    double      sliding_friction;        public    bool      bottomed_out;        public    bool      touching_ground;        public    bool      squealing;        public    bool      sliding;        public    bool      drive_wheel;        public  WhichWheel  position; 
end example
 

The offset attribute denotes the position of the wheel relative to the center of gravity of the vehicle. The earth_location attribute is the wheel location relative to the terrain frame of reference. We need this position when we want to query the terrain for the height of the ground beneath the wheel. The rel_heading attribute is the relative heading between the vehicle and the wheel itself. The Car class would adjust this if this were one of the front wheels as a function of the steering actions.

The origin for the wheel is at its point of rotation in the center. The radius attribute lets us know how far below this point we would be contacting the ground. The ground_height attribute is the height of the ground beneath the wheel. The vehicle s position plus the earth_location offset minus the radius gives us the altitude attribute. Together with the ground_height attribute, it can determine if the wheel is in contact with the ground, off of the ground, or currently pressed into the ground (which would demand additional force upwards on the suspension).

We need to figure out the various forces working on the wheel. One of the most important considerations is how much weight is pressing down on the wheel. Two attributes are used to represent this. The static_weight_over_wheel attribute represents the portion of the vehicle s weight the wheel supports when the vehicle is at rest. The weight_over_wheel attribute, on the other hand, is the amount of weight that the wheel is currently supporting. You will see later in this chapter how external forces working on the vehicle can change the distribution of weight on the wheels.

The next set of attributes to consider involves the forces that oppose the force of the weight. These are the suspension forces. The suspension_offet attribute represents the current position of the suspension referenced from its static position. This attribute can go both positive and negative, since the spring can be either stretched or compressed depending on the forces working against it. The max_suspension_off set attribute represents the limit to which the suspension can be stretched or compressed. The upwards_force attribute is the force created by the suspension. This is normally in the upward direction opposing the weight of the vehicle, unless the vehicle has lost contact with the ground. If contact has been lost, there would be a negative force until the suspension reached its static position or contact is restored.

The final forces acting on the wheel are the friction forces. Three types of friction are involved:

  • The friction of the surface that the wheel is moving over is represented by the friction attribute.

  • The static friction of the wheel is represented by the stiction attribute.

  • Finally, a reduced friction value occurs if the wheel has lost adhesion to the ground surface and is now sliding across the surface. This attribute is called sliding_friction .

All of these friction values are expressed as the number of gravities (Gs) of force that the friction supplies .

A number of status flags are also held within the structure. The bottomed_out flag is true if the suspension has been compressed to its limit and may be used by the game code to trigger an appropriate sound effect. The touching_ground flag is true if the wheel is in contact with the ground. The squealing attribute is also made available for use in triggering sound effects. If the wheel is on the verge of sliding, it will start to make a squealing sound. If it has broken loose, the sliding flag is set. There is also a flag that indicates whether this wheel is a drive wheel or not. If the car is front-wheel drive, this flag will be set for the front tires and cleared for the rear. Finally, the WhichWheel enumeration value indicates the wheel s position on the vehicle.

Several properties for the Wheel class, shown in Listing 10 “5, enforce readonly or write-only status for several of the attributes. The RelHeading property provides write-only access to the corresponding attribute. On the other hand, the UpwardsForce property is read only. The Stiction property not only sets the stiction attribute, but also sets sliding_friction to 60 percent of the static friction.

Listing 10.5: Wheel Class Properties
start example
 #region Properties  public double RelHeading { set { rel_heading = value; } }  public double UpwardsForce { get { return upwards_force; } }  public double WeightOverWheel {            set { weight_over_wheel = value; }            get { return weight_over_wheel; } }  public double StaticWeightOverWheel {            set { static_weight_over_wheel = value; } }  public double Stiction  {     set     {        stiction = new_value;        sliding_friction = 0.6f * stiction;     }  }  #endregion 
end example
 

The constructor for the Wheel structure (shown in Listing 10 “6) is fairly straightforward. It simply initializes each of the attributes to a reasonable initial value. The vehicle class that has the wheels must set most of these values. That class (or a parent class that holds an instance of the class) will have the knowledge to set them to the proper values for that vehicle. We will assume that the vehicle begins with all of the wheels touching the ground.

Listing 10.6: Wheel Class
start example
 public Wheel(WhichWheel where)  {     position=where;     offset = new Vector (0.0, 0.0, 0.0);     earth_location = new Vector(0.0, 0.0, 0.0);     rel_heading = 0.0;     radius = 0.5;     circumference = 0.0;     height_above_ground = 0.0;     rpm = 0.0;     ground_height = 0.0;     friction = 1.0;     weight_over_wheel = 0.0;     static_weight_over_wheel =0.0;     suspension_offset = 0.0;     max_suspension_offset = 0.25;     altitude = 0.0;     bottomed_out = false;     drive_wheel = false;     sliding = false;     sliding_friction = 0.0;     squeeling = false;     stiction = 1.0;     upwards_force = 0.0;     touching_ground = true;  } 
end example
 

The method that performs that actual work for the wheels is the Process method shown in Listings 10 “7a through 10 “7e. Listing 10 “7a shows the arguments to the method as well as the calculations of the wheel s position. The first argument ( delta_t ) is the amount of time that has passed since the last time this method has been called. It will be used for integrating values as a function of time. The remaining arguments provide the status of the vehicle that the wheel belongs to. This includes the vehicle s attitude, position velocities, and accelerations.

Listing 10.7a: Wheel Class Process Method
start example
 public void Process (float delta_t, Euler attitude, Vector acceleration,      Vector velocity, Vector position)   {      double   temp;      double   susp_delta;      double   squeel_force;      double   slide_force;      double   grab_force;      double   tire_side_force;      earth_location.X = offset.X;      earth_location.Y = offset.Y;      earth_location.Z = offset.Z + suspension_offset;      attitude.RotateAtoE(earth_location);      altitude = position.Z + earth_location.Z   radius;      height_above_ground=altitudeground_height;      touching_ground = height_above_ground <= 0.0f; 
end example
 
Listing 10.7b: Wheel Class Process Method
start example
 if (touching_ground)  {     suspension_offset =   height_above_ground;  }  else  {     suspension_offset =   max_suspension_offset;  susp_delta = (suspension_offset + height_above_ground) * delta_t;  if (Math.Abs(upwards_force   weight_over_wheel) < 2.0)  {     suspension_offset   = susp_delta;  }  if (suspension_offset > max_suspension_offset)  {     suspension_offset=max_suspension_offset;  {  else if (suspension_offset <   max_suspension_offset)  {     suspension_offset =   max_suspension_offset;  }  bottomed_out = suspension_offset == max_suspension_offset; 
end example
 
Listing 10.7c: Wheel Class Process Method
start example
 temp = (0.9f * (suspension_offset * suspension_offset) /     (max_suspension_offset * max_suspension_offset));  if (suspension_offset < 0.0f)  {     temp *=   1.0f;  }  if (Math.Abs(suspension_offset) < 0.3f)  {     temp = 0.0f; // Suspension neutral  }  temp += 1.0f;  if (!touching_ground)  {     temp = 0.0f;  }  upwards_force = static_weight_over_wheel * temp; 
end example
 
Listing 10.7d: Wheel Class Process Method
start example
 if ((upwards_force   weight_over_wheel) > 2. 0f)  {     suspension_offset   = 0.5f * delta_t;  }  else if ((upwards_forceweight_over_wheel) <   2.0f)  {     suspension_offset += 0.5f * delta_t; 
end example
 
Listing 10.7e: Wheel Class Process Method (Conclusion)
start example
 slide_force = 32.0f * stiction * friction;          squeel_force=0.9f * slide_force;           grab_force = 32. 0f * sliding_friction;           if ((acceleration. Y > 0.0f && rel_heading > 0.0f)                (acceleration.Y < 0.0f && rel_heading < 0.0f))           {              tire_side_force = (float)Math.Abs (acceleration.Y *                 (1.0f   Math.Cos (rel_heading)));           }           else              tire_side_force = (float)Math.Abs(acceleration.Y *                Math.Cos (rel_heading));           }           squeeling=false;           if (drive_wheel && acceleration.X >= slide_force)           {              sliding = true;           }           if ((acceleration.X < -squeel_force && acceleration.X >   slide_force)                (tire_side_force > squeel_force && tire_side_force < slide_force))           {              squeeling = true;           }           if (acceleration.X <=   slide_force   tire_side_force >= slide_force)           {              sliding = true;           }           if (Math.Abs(acceleration.X) < grab_force &&              Math.Abs(acceleration.Y)< grab_force &&              tire_side_force < grab_force)           {              sliding = false;           }        }     };  } 
end example
 

Taking the body-relative offsets and the current suspension offset and rotating it into earth frame using the vehicle s current attitude calculates the location of the wheel in the terrain frame of reference. The altitude at the base of the tire is this offset applied to the vehicle s height minus the radius of the wheel. The height above the ground is the difference between the ground elevation and the bottom of the tire. The tire is touching the ground if the height above the ground is less than or equal to zero.

The next section of the Process method (shown in Listing 10 “7b) deals with determining the new suspension offset. If the wheel is touching the ground, the offset will be the inverse of the height above ground. In other words, we will compress the suspension enough that the base of the wheel will be resting on the surface of the ground in the next frame. If the wheel has lost contact with the ground, the weight of the wheel will extend the suspension to its negative limit. A suspension delta is calculated as the integrated sum of the suspension offset and the height above the ground. This delta is applied if the upward force applied to the vehicle is at least 2 pounds greater than the weight of the vehicle applied to the wheel. This will give us a small increase in the suspension extension to help balance out the forces. The suspension offset is limited to the range of plus and minus the maximum suspension offset. The suspension on the wheel has bottomed out if it has reached the positive limit of its movement.

The next section of the method (shown in Listing 10 “7c) is the simplified calculation of the force that the wheel is exerting on the vehicle. The temp value represents the fractional portion of the vehicle s weight that is being countered by the wheel and its suspension. It is calculated as 90 percent of the ratio of the suspension offset squared to the maximum offset squared. Squaring the suspension offset removes the direction of the offset. If the offset is negative, the value of temp is negated. If the suspension is roughly centered, temp will be set to zero, since this is the offset from the static force required to hold up the vehicle. One is then added to the value to get the complete percentage of the base force that will be applied this time. If the wheel is not in contact with the ground, then there will be no upward force applied by the wheel. The actual force is the static weight over the wheel multiplied by the value of temp .

The next section of the method (shown in Listing 10 “7d) is a second adjustment to the suspension offset based on the new upward force. If the upward force is at least 2 pounds greater than the actual weight over the wheel, then the suspension is extended at the rate of .5 per second. Likewise, if the weight is at least 2 pounds greater, then the upward force the suspension is compressed at the same rate.

The final section of the Process method (shown in Listing 10 “7e) deals with the forward and lateral forces applied to the wheel and the interaction of the wheel with the ground. The force required to make the wheel slide is the product of the ground friction and the tire static friction converted from Gs to acceleration. The wheel will begin to squeal at 90 percent of the sliding value. The acceleration at which the wheel will stop sliding is the sliding friction converted from Gs to acceleration.

The side force applied to the tire is a function of the lateral acceleration of the vehicle and any relative heading applied to the wheel. If the wheel is turning into the direction of the acceleration, the wheel is turning somewhat with the force, and the force is reduced based on the amount of the relative heading. If the tire is steering away from the force, the tire sees much more of the acceleration as side force.

If the wheel is a drive wheel and the forward acceleration is greater than the slide force, the tire will start to slide (i.e, laying rubber, spinning, etc.). If the forward acceleration is between the squeal force level and the sliding force level or the side force is between those same extremes, then the tire will squeal. If either the forward or the lateral forces exceed the slide force level, then the wheel will be sliding rather than gripping the surface. If any of these accelerations drop below the grab force threshold, the wheel will grab the surface again and stop sliding.

Simulating the Car

That completes our look at the Wheel structure. Now we get down to the serious work of vehicle dynamics with the CarDynamics class. The attributes and enumerations for this class are shown in Listings 10 “8a and 10 “8b. The IgnitionState enumeration in Listing 10 “8a defines the current position of the key in the ignition. This allows the class to support the ability to start and stop the vehicle s engine. The second enumeration, GearState , defines the state of the transmission. Our sample game will only utilize the drive gear of the transmission, but the class will support either an automatic or three-speed manual transmission.

Listing 10.8a: CarDynamics Enumerations and Attributes
start example
 using System;  using System.Threading;  using System.Diagnostics;  namespace VehicleDynamics  {     public class CarDynamics : IDisposable     {        public enum IgnitionState { IgnitionOff, IgnitionOn, IgnitionStart };        public enum GearState { Park=0, Reverse=1, Neutral=2, Drive=3,           FirstGear=4, SecondGear=5, ThirdGear=6 };     #region Attributes        private   Euler          attitude = new Euler();        private   Euler          attitude_rate = new Euler();        private   Euler          ground_slope = new Euler();        private   Vector          position = new Vector();        private   Vector          velocity = new Vector();        private   Vector          earth_velocity = new Vector();        private   Vector          acceleration = new Vector();        private   Vector          body_gravity = new Vector();        private   Wheel[]          wheel = new Wheel[4];        private   double          weight;        private   double          cg_height;        private   double          wheel_base;        private   double          wheel_track;        private   double          wheel_angle;        private   double          wheel _max_angle;        private   double          weight_distribution;        private   double          front_weight;        private   double          back_weight;        private   double          wheel_pos;        private   double          throttle;        private   double          brake;        private   double          engine_rpm;        private   double          wheel_rpm;        private   double          wheel_force;        private   double          net_force;        private   double          drag;        private   double          rolling_resistance;        private   double          eng_torque;        private   double          mph; 
end example
 
Listing 10.8b: CarDynamics Enumerations and Attributes (Conclusion)
start example
 private double       brake_torque;        private double       engine_loss;        private double       idle_rpm;        private double       max_rpm;        private double       target_rpm;        private double       engine_horsepower;        private double       max_engine_torque;        private double       air_density;        private double       drag_coeff;        private double       frontal_area;        private double       wheel_diameter;        private double       mass;      // In slugs        private double       inverse_mass;      // In slugs        private float        centripedal_accel;        private bool       running;        private bool       front_wheel_drive = false;        private GearState       gear;        private GearState       auto_gear = GearState.Drive;        private double[]       gear_ratio = new double[7];        // percent max torque   index by % max RPM * 10        private    LFI                 torque_curve = new LFI();        private    IgnitionState    ignition_state = IgnitionState.IgnitionOn;        private   bool            was_sliding = false;        private Thread          m_process_thread;        private bool              thread_active = true;        private Mutex           mutex = new Mutex();  #endregion 
end example
 

The Attitude attribute holds the current heading, pitch, and roll of the vehicle. Attitude_rate is the rate of change of each of these angles. The ground_slope attribute represents the current pitch and roll of the ground under the vehicle in the vehicle s frame of reference. This will be important since the impact of gravity is a function of this slope so that the vehicle will tend to accelerate while going downhill and slow when going uphill .

Five vectors are held by the class. These vectors contain the position, velocities, and accelerations for the vehicle. We also have the gravity vector in the vehicle s body frame as well as an array of four wheels for our car. The weight attribute is the overall weight of the car. The cg_height attribute, which represents the height of the car s center of gravity above the ground, is used in calculating the offset of the wheel s positions based on the car in its static condition. The wheelbase of the vehicle is the side-to-side separation between the wheels, and the wheel track is the forward-and-back separation between the wheels. It is assumed that the wheels are evenly distributed around the center of the vehicle.

The wheel angle is the current relative heading of the front wheels due to steering inputs. The wheel_max_angle attribute represents the limits to which the front wheels may be turned. The weight_distribution attribute is the fraction of the vehicle s overall weight that is over the rear wheels when the vehicle is not moving. Accelerations will result in the actual weight distribution to shift once the vehicle starts moving. These accelerations will cause the front_weight and back_weight to change dynamically. These weights will be divided up and passed to each wheel as the weight over that wheel.

The next three attributes are the control values passed in from the parent class that control the basic driving of the vehicle. The wheel_pos attribute is the steering wheel position normalized to 1 with positive steering to the right. The throttle is the gas pedal position normalized, with 0 meaning no depression of the pedal and 1 being pedal to the metal. The brake attribute works the same way for the brake pedal, with 0 being pedal up and 1 being pedal fully depressed.

The engine_rpm is the current speed of the engine. This value is a function of the throttle position and any losses fed back from the interactions with the road. If the vehicle is going uphill, for example, the vehicle will slow and reduce engine speed until the transmission drops to a lower gear. The wheel_rpm indicates how fast the wheels are turning based on the engine RPM and the current transmission gear ratio. The wheel_force specifies the amount of the force developed by the engine that is passed to the wheels to move the vehicle. The engine torque (in the eng_torque attribute) is the force developed by the engine. The net_force attribute, calculated from the summation of all of the forces acting on the vehicle, defines the net force that changes the vehicle s acceleration. The drag attribute, which acts to slow the vehicle due to air impacting the front of the vehicle as it is moving, is one of several forces that act to limit the top speed of a vehicle. Another force acting against the movement of the vehicle is the rolling resistance. This is energy that deforms the tires as they rotate. The mph attribute is the vehicle s speed in the forward direction expressed in miles per hour .

The remaining attributes are shown in Listing 10 “8b. The brake_torque and engine_loss attributes are two forces that act in opposition to the engine torque. The normal RPM when the engine is idling is held in the idle_rpm attribute, and the maximum RPM is contained in the max_rpm attribute. The target_rpm indicates the engine speed that is currently being demanded by the throttle position. The engine_horsepower and max_engine_torque determine how much power the engine can produce.

The next three attributes calculate the aerodynamic drag against the vehicle. The air_density is the thickness of the air that the vehicle is passing through. The standard value for air density at sea level is 14.7 pounds per square inch. The air_density attribute will hold the inverse of this value (0.068). The drag coefficient ( drag_coeff ) is a factor representing the amount of drag that a given shape has when moving through the atmosphere. Without a physical model of our vehicle and a wind tunnel, it is difficult to determine a specific number for this coefficient. Instead, we will just pick a value and adjust it until everything seems realistic. The last factor in the calculation is the frontal area of the vehicle, the cross-section of the vehicle that impacts the air as it moves forward.

The diameter of the wheels is involved in the calculations for wheel torque as well as for passing the radius of the wheel to each wheel for its own calculations. The mass of the vehicle is required to change the forces working on the vehicle into accelerations. Remember that acceleration is force divided by mass. To make the calculation more efficient, we will also hold the inverse of the mass so we can multiply instead of divide. The acceleration that works laterally on the vehicle due to inertia is called centripetal acceleration. Flags will also exist that state whether or not the engine is running and if the vehicle has front-wheel or rear-wheel drive.

There are two instances of the GearState enumeration. The first attribute, gear, is the gear that has been selected by the driver. A second attribute, auto_gear , is the state of the automatic transmission. This is because an automatic transmission shifts between the forward gears as a function of engine RPM and torque. The gear ratios in each of the transmission s gears are held within an array of double-precision variables. There is also an instance of the LFI class, which holds the torque curve for the engine. The data in the curve is the percentage of maximum engine torque as a function of the engine RPM. We also have an instance of the IgnitionState enumeration, which indicates the current state of the engine s ignition. A flag indicates whether the vehicle s tires were sliding on the previous pass.

The final attributes in the class manage a separate processing thread for the class. There is a reference to the thread itself, a flag that controls if the thread should continue to cycle, and a Mutex object for thread-safe data transfers between the thread process and the remainder of the class.

The properties for the CarDynamics class (shown in Listing 10 “9) provide the public interface for the class. All of the control inputs from the containing class will come in through these properties. The SteeringWheel property sets the wheel position as a double-precision value between 1. The Throttle property specifies the gas pedal position and the Brake property does the same for the brake pedal. The Gear property is used for shifting gears and the Ignition property for turning the key. A bit of logic is involved with the Ignition property; it checks the current gear and starts the engine if the Start value is set and the gear is set to Park or Neutral . Likewise, if the Off state is requested the engine is stopped . There are also properties to retrieve the current RPM and running state.

Listing 10.9: CarDynamics Properties
start example
 #region Properties     public double SteeringWheel {        get { return wheel_pos; }        set { wheel_pos = value; } }     public double Throttle { get { return throttle; } set { throttle = value; } }     public double Brake { get { return brake; } set { brake = value; } }     public GearState Gear { get { return gear; } set { gear = value; } }     public IgnitionState Ignition { get { return ignition_state; }        set        {           ignition_state=value;           if (ignition_state == IgnitionState.IgnitionStart &&              (gear == GearState.Park   gear == GearState.Neutral))           {              running = true;           }           else if (ignition_state == IgnitionState.IgnitionOff)           {              running=false;           }        }     }     public double EngineRPM { get { return engine_rpm; } }     public bool EngineRunning { get { return running; } }     public double Roll{        get { return attitude.Phi; }        set { attitude.Phi=value; } }     public double Pitch {        get { return attitude.Theta; }        set { attitude.Theta = value; } }     public double Heading {        get { return attitude.Psi; }        set { attitude.Psi = value; } }     public double North {        get { return position.X; }        set { position.X = value; } }     public double East {        get { return position.Y; }        set { position.Y = value; } }     public double Height {        get { return position.Z; }        set { position.Z = value; } }     public double NorthVelocity {        get { return earth_velocity.X; }        set { velocity.X = value; } }     public double EastVelocity {        get { return earth_velocity.Y; }        set { velocity.Y = value; } }     public double VerticalVelocity {        get { return earth_velocity.Z; }        set { velocity.Z = value; } }     public double ForwardVelocity { get { return velocity.X; } }     public double SidewaysVelocity { get { return velocity.Y; } }     public double WheelRadius {        get { return wheel[(int)WhichWheel.LeftFront].radius; }        set        {           wheel_diameter = value * 2                                     .0;           wheel[(int)WhichWheel.LeftFront].radius = value;           wheel[(int)WhichWheel.LeftRear].radius = value;           wheel[(int)WhichWheel.RightFront].radius = value;           wheel[(int)WhichWheel.RightRear],radius = value;}     }    public double HorsePower {        get { return engine_horsepower; }        set { engine_horsepower = value; } }     public double LFGroundHeight {        set { wheel[(int)WhichWheel.LeftFront].ground_height = value; } }     public double RFGroundHeight {        set { wheel[(int)WhichWheel.RightFront].ground_height = value; } }     public double LRGroundHeight {        set { wheel[(int)WhichWheel.LeftRear].ground_height = value; } }     public double RRGroundHeight {        set { wheel[(int)WhichWheel.RightRear].ground_height = value; } }     public double MPH { get { return mph; } }     public bool Running { get { return running; } }  #endregion 
end example
 

Various properties get and set the vehicle s attitude and position within the world. There are also properties to access the vehicle s velocities in both the world frame of reference as well as the body frame.

Note

Notice that we are using North, East, and Vertical instead of X, Y, and Z for the world-related values. The X, Y, and Z values have different meanings in different places. In physics equations, we tend to think of X going to the north, Y moving to the east, and Z moving up. DirectX, on the other hand, uses a different axis convention. To eliminate confusion, we use a public interface that employs world- related terms.

The property that accesses the size of the wheels assumes that all four wheels of our vehicle are the same size . Therefore, we can return the left-front wheel s radius in the Get method and set all wheels to the same value in the Set method. We also set the diameter of the wheel with the same property. The HorsePower property changes the power of the engine for the vehicle being simulated. The height of the ground under each tire is likely to be different. Because of this, a separate property exists for each wheel. There are also properties to access the vehicle s speed in miles per hour and a flag that indicates whether or not the engine is running.

The constructor for the class (shown in Listing 10 “10) initializes the attributes that do not change over time. This includes the horsepower of the engine as well as the engine s torque curve. It also resets the members of the wheel array so that each wheel is initialized knowing its position on the vehicle. The constructor then calls the Reset method to complete the initialization. Placing the other attribute initialization in a separate method allows us to reset the vehicle to a know state when needed. The final task for the constructor is to create and start the thread that will perform the dynamics processing. The integrations involved in these calculations tend to be very rate dependent. If they are not processed quickly enough, they can become unstable and oscillate out of control. To prevent this from happening, we will run the dynamics processing in a separate thread that will cycle at a nominal 100 hertz.

Listing 10.10: CarDynamics Constructor
start example
 public CarDynamics()  {        engine_horsepower = 70.0f;        torque_curve.SetDataPoint(0, 0.0f);        torque_curve.SetDataPoint(10, 0.13f);        torque_curve.SetDataPoint(20, 0.32f);        torque_curve.SetDataPoint(30, 0.5f);        torque_curve.SetDataPoint(40, 0.72f);        torque_curve.SetDataPoint(50, 0.9f);        torque_curve.SetDataPoint(60, 1.0f);        torque_curve.SetDataPoint(70, 0.98f);        torque_curve.SetDataPoint(80, 0.89f);        torque_curve.SetDataPoint(90, 0.5f);        torque_curve.SetDataPoint(100, 0.13f);        wheel[(int)WhichWheel.LeftFront] = new Wheel(WhichWheel.LeftFront);        wheel[(int)WhichWheel.RightFront] = new Wheel(WhichWheel.RightFront);        wheel[(int)WhichWheel.LeftRear] = new Wheel(WhichWheel.LeftRear);        wheel[(int)WhichWheel.RightRear] = new Wheel(WhichWheel.RightRear);        Reset();        m_process_thread = new Thread(new ThreadStart(Process));        m_process_thread.Start();  } 
end example
 

The method that will run in the processing thread is the Process method (shown in Listing 10 “11), which has no arguments. Since the thread will be running at about 100 hertz, the time delta will be the inverse of the frequency, or 0.01 seconds per pass. The method will iterate until the thread_active flag becomes cleared. Each pass, it will call the normal Process method and then sleep for 10 milliseconds . It is this idle time produced by the Sleep method that gives us the iteration rate we want.

Listing 10.11: CarDynamics Thread Process Method
start example
 public void Process()  {        float delta_t = 0.01f;        Debug.WriteLine("car physics thread started");        while (thread_active)        {              Process (delta_t);              Thread.Sleep(10);        }        Debug.WriteLine("car physics thread terminated");  } 
end example
 

The Reset method (shown in Listing 10 “12) places the state of the dynamics into a basic starting condition. The attitude, velocities, and accelerations are all reset to zero. The maximum wheel angle defaults to 40 degrees. Since all angular work within the software is done in radians, we will set it to the radian equivalent of 40 degrees. The maximum RPM for the engine will be set to 7000 with the idle speed set to 10 percent of this maximum. The current RPM will be set to zero since it will be recalculated when the engine starts.

Listing 10.12: CarDynamics Reset Method
start example
 public void Reset()  {        wheel_max_angle = 0.69813170079773183076947630739545; // 40 degrees        idle_rpm = 700. 0f;        max_rpm = 7000. 0f;        engine_rpm = 0.0f;        attitude.Theta = 0.0f;        attitude.Phi = 0.0f;        attitude.Psi = 0.0f;        attitude_rate.Theta = 0.0;        attitude_rate.Phi = 0.0;        attitude_rate.Psi = 0.0;        position.X = 0.0;        position.Y = 0.0;        position.Z = 0.0;        velocity.X = 0.0;    // Body velocity        velocity.Y = 0.0;    // Body velocity        velocity.Z = 0.0;    // Body velocity        earth_velocity.X = 0.0;    // Earth velocity        earth_velocity.Y = 0.0;    // Earth velocity        earth_velocity.Z = 0.0;    // Earth velocity        acceleration.X = 0.0;    // Body accelerations        acceleration.Y = 0.0;    // Body accelerations        acceleration.Z = 0.0;    // Body accelerations        cg_height           =2.0f        wheel_base          = 5.0;        wheel_track         = 8.0;        weight              = 2000.0f;        wheelRadius        = 1.25;       wheel[(int)WhichWheel.LeftFront].offset.Set(wheel_track / 2  .0f,   wheel_base / 2.0f,   cg_height + wheel[(int)WhichWheel.LeftFront].radius);       wheel[(int)WhichWheel.RightFront].offset.Set(wheel_track / 2 .0f,           wheel_base / 2.0f,   cg_height + wheel[(int)WhichWheel.LeftFront].radius);       wheel[(int)WhichWheel.LeftRear ].offset.Set(   wheel_track / 2  .0f,   wheel_base / 2.0f,   cg_height + wheel[(int)WhichWheel.LeftFront].radius);       wheel[(int)WhichWheel.RightRear ].offset.Set(   wheel_track / 2 .0f,           wheel_Base / 2.0f,   cg_height + wheel[(int)WhichWheel.LeftFront].radius);       for (int i=0; i<4; i++)       {         wheel[i].SetStaticWeightOverWheel(weight / 4.0f);       }       weight_distribution   = 0.5f;       front_weight         = weight * (1.0   weight_distribution);       back_weight          = weight * weight_distribution;       wheel_pos            = 0.0f;       throttle             = 0.0f;       brake                = 0.0f;       engine_rpm           = 0.0f;       wheel_rpm            = 0.0f;       wheel_force          = 0.0f;       net_force            = 0.0f;       mph                  = 0.0f;       drag                 = 0.0f;        rolling_resistance = 0.0f;        eng_torque = 0.0f;        brake_torque = 0.0f;        engine_loss = 0.0f;        air_density = 0.068;        drag_coeff = 0.4f;        frontal_area = 20.0f;        mass = weight * 0.031080950172; // In slugs        inverse_mass = 1.0 / mass;        running = true;        front_wheel_drive = false;        gear = GearState.Drive;        gear_ratio[(int)GearState.Park] = 0.0f;        gear_ratio[(int)GearState.Reverse] =   80.0f;        gear_ratio[(int)GearState.Neutral] = 0.0f;        gear_ratio[(int)GearState.Drive] = 45.0f;        gear_ratio[(int)GearState.FirstGear] = 70.0f;        gear_ratio[(int)GearState.SecondGear] = 50.0f;        gear_ratio[(int)GearState.ThirdGear] = 30.0f;        ignition_state = IgnitionState.IgnitionOn;        max_engine_torque = engine_horsepower * 550.0f;        if (front_wheel_drive)        {           wheel[(int)WhichWheel.LeftFront].drive_wheel = true;           wheel[(int)WhichWheel.RightFront].drive_wheel = true;           wheel[(int)WhichWheel.LeftRear].drive_wheel = false;           wheel[(int)WhichWheel.RightRear].drive_wheel = false;        }       else        {           wheel[(int)WhichWheel.LeftFront ].drive_wheel = false;           wheel[(int)WhichWheel.RightFront].drive_wheel = false;           wheel[(int)WhichWheel.LeftRear ].drive_wheel = true;           wheel[(int)WhichWheel.RightRear ].drive_wheel = true;        }     } 
end example
 

The height of the center of gravity defaults to 2 feet off of the ground and the wheelbase and track default to 5 feet and 8 feet, respectively. We will also default to a weight of 1 ton, which is 2000 pounds. The offset to each of the wheels is calculated from the wheelbase, wheel track, center of gravity height, and the wheel radius. Since we are assuming that the weight of the vehicle is evenly distributed over the wheels, one quarter of the weight is passed to each wheel as its static weight over the wheel. This also gives us a weight distribution of one half and the associated portions of the weight assigned to the front and rear of the vehicle.

The mass of the vehicle needs to be in slugs for the English system of measurement. The weight is converted to slugs for the mass and inverted for the inverse_mass attribute. We will assume that we reset to a rear-wheel drive vehicle with the engine running and in drive, ready to start racing. The gear ratios are the complete ratio of engine RPM to the corresponding wheel rotation speed. This includes the gearing in the rear hub with the transmission ratios. Note that the value for reverse is negative to reverse the direction of the wheel rotation. The final action in the method is to inform each of the wheels whether or not they are a drive wheel based on the front_wheel_drive flag.

The IntegratePosition method (shown in Listing 10 “13) performs the integrations that change accelerations into velocities and velocities into modifications in position. This method of integration is known as Euler s method. Since acceleration is feet per second squared and we are multiplying by seconds per pass, we get resulting units of feet per second per pass. Since feet per second represents a velocity, we can add this product to our velocity last pass to get our new velocity. The same holds true for taking our attitude rate in radians per second and integrating with our time delta to get the number of radians each attitude angle changes in a frame. Since we want to ensure that our attitude angles remain in the proper range, we will call the Attitude class s Limit method to handle any integration through zero or the maximum value for a given attitude angle.

Listing 10.13: CarDynamics IntegratePosition Method
start example
 private void IntegratePosition (float delta_t)  {    velocity.IncrementX(delta_t * acceleration.X);     velocity.IncrementY(delta_t * acceleration.Y);     velocity.IncrementZ(delta_t * acceleration.Z);     attitude = attitude + (attitude_rate * delta_t);     attitude.Limits();     earth_velocity.X = velocity.X;     earth_velocity.Y = velocity.Y;     earth_velocity.Z = velocity.Z;     attitude.RotateAtoE(earth_velocity);     position.IncrementX(delta_t * earth_velocity.X);     position.IncrementY(delta_t * earth_velocity.Y);     position.IncrementZ(delta_t * earth_velocity.Z);     mph = (float)velocity.X * 3600.0f / 5280.0f;  } 
end example
 

The terms in the velocity vector are in the vehicle s frame of reference. Before we can integrate our position in the world, we must rotate this vector into the world coordinate system. The vector is copied into another vector that is rotated based on the vehicle s attitude. The earth velocity is integrated to get the change in world position. We will also calculate the miles per hour version of our velocity by taking the forward velocity and converting it from feet per second to miles per hour.

The net force that acts on the vehicle is not being applied through the vehicle s center of gravity (CG). It is actually applied at the interface between the wheels and the ground. Because the force is not being applied at the CG, there is an apparent transfer of the vehicle s weight in both the forward and lateral directions. I m sure you have experienced this while driving in a car. If the car is accelerated rapidly , the front of the car rises. If you slam on the brakes, the nose of the car dips. If you make a quick turn, the car rolls toward the outside of the turn . These are all examples of weight transfer due to the accelerations acting on the car. Remember that weight is an acceleration. The weight transferred to the front of the vehicle is the forward acceleration multiplied by the ratio of the center of gravity height and the wheelbase. This value is added to the front of the vehicle and subtracted from the rear. The same calculation holds true for the lateral acceleration. Instead of dividing by the wheelbase, though, we will use the wheel track since it is the distance between the tires in the lateral direction. Using these relationships, we can calculate the weight over each of the four tires and pass the values to the associated wheel (see Listing 10 “14).

Listing 10.14: CarDynamics CalcWeightTransfer Method
start example
 private void CalcWeightTransfer()  {        front_weight = (1.0f   weight_distribution) * weight +           (float)acceleration.X * cg_height / wheel_base;        back_weight = weight   front_weight;        wheel[(int)WhichWheel.LeftFront].weight_over_wheel =                      0.5f * front_weight   (float)acceleration.Y * cg_height / wheel_track;        wheel[(int)WhichWheel.RightFront].weight_over_wheel =                      front_weight   wheel [(int)WhichWheel.LeftFront].weight_over_wheel;        wheel [(int)WhichWheel.LeftRear].weight_over_wheel = 0.5f *                       back_weight   (float)acceleration.Y * cg_height / wheel_track;        wheel[(int)WhichWheel.RightRear].weight_over_wheel =                      back_weight   wheel[(int)WhichWheel.LeftRear].weight_over_wheel;     } 
end example
 

The SetFriction method (shown in Listing 10 “15) provides the interface to set the friction of the surface under a specific wheel. The arguments to the method state which wheel is being set and what the new value should be. This value is simply passed to the requested wheel.

Listing 10.15: CarDynamics SetFriction Method
start example
 public void Set Friction (WhichWheel the_wheel, float friction)  {        wheel[(int)the_wheel].friction = friction;  } 
end example
 

The SetGearRatio method (shown in Listing 10 “16) provides a programmatic way to change the gears in the transmission. This is how the initialization of a class that uses the CarDynamics can tailor the transmission for a specific vehicle. The combination of different horsepower engine and different gear ratios can be used to add vehicles with varying performance.

Listing 10.16: CarDynamics SetGearRatio Method
start example
 public void SetGearRatio(GearState state, float ratio)  {        gear_ratio[(int)state] = ratio;  } 
end example
 

The current position of a given wheel may be queried using the WheelNorth method shown in Listing 10 “17. The enumeration value for a wheel is passed in as an argument and the position in the north axis is returned.

Listing 10.17: CarDynamics WheelNorth Method
start example
 public float WheelNorth(WhichWheel the_wheel)  {     return (float)wheel[(int)the_wheel].earth_location.X;  } 
end example
 

The east position of a wheel is obtained the same way using the WheelEast method and the vertical position of the wheel with the WheelHeight method. Both of these methods are shown in Listing 10 “18.

Listing 10.18: CarDynamics WheelEast Method
start example
 public float WheelEast(WhichWheel the_wheel)  {     return (float)wheel[(int)the_wheel].earth_location.Y;  }  public float WheelHeight (WhichWheel the_wheel)  {     return (float)wheel [(int)the_wheel].earth_location.Z;  } 
end example
 

The height of the ground beneath a wheel is set using the SetWheelAltitude method shown in Listing 10 “19. The altitude and the selection of which wheel to set are passed in as arguments. The enumeration value is used as an index into the wheel array and the associated wheel s ground height is set to the supplied altitude.

Listing 10.19: CarDynamics SetWheelAltitude Method
start example
 public void SetWheelAltitude(WhichWheel the_wheel, float altitude)  {     wheel[(int)the_wheel].ground_height = altitude;  } 
end example
 

The SetWheelOffset method (shown in Listing 10 “20) adjusts the position of each wheel independently. The three supplied offsets are set within the requested wheel s data structure.

Listing 10.20: CarDynamics SetWheelOffset Method
start example
 public void SetWheelOffset(WhichWheel the_wheel, float forward,     float right, float up)  {     wheel[(int)the_wheel].offset.X = forward;     wheel[(int)the_wheel].offset.Y = right;     wheel[(int)the_wheel].offset.Z = up;  } 
end example
 

The Dispose method (shown in Listing 10 “21) has only one task. Its job is to terminate the processing thread. By setting the thread_active flag to false, the thread will terminate itself when it reaches the end of its while loop. A 20-millisecond sleep is included before the Dispose methods terminates. This is to ensure that the thread has time to terminate before we deallocate the class by passing out of scope.

Listing 10.21: CarDynamics Dispose Method
start example
 public void Dispose()  {    Debug.WriteLine("car physics Dispose");    thread_active=false;    Thread.Sleep(20);  } 
end example
 
Note

If we fail to use Dispose on a class like this one, which has spawned a thread, it is possible that the thread will not get the message that the application has terminated, and the thread will continue. This will cause the application to remain partially active.

The Process method (shown in Listings 10 “22a through 10 “22e) is where the real work of the vehicle dynamics is done. Among the local variables declared for this method is the gravity vector, which is the acceleration in the down direction that will be applied to the vehicle in addition to the other forces that affect the vehicle. The method begins, as shown in Listing 10 “22a, by processing the steering of the vehicle. The angular offset of the front wheels is a function of the current steering wheel position and the maximum turn single for the wheels. This wheel angle is passed to each of the front wheels as their new relative heading. The turn rate for the vehicle is a function of this wheel angle, the distance between the wheels, and the current forward speed of the vehicle. If both of the front wheels are sliding, then we have lost the ability to steer the vehicle, and the turn rate is set to zero. The Greek symbol used to represent heading is psi. The attitude rate for psi is set to this turn rate.

Listing 10.22a: CarDynamics Process Method
start example
 void Process (float delta_t)  {     double temp;     double delta_rpm;     double current_gear_ratio = 0.0;     double brake_force;     double percent_rpm;     double turn_rate;     bool shift_up;     bool shift_down;     double delta_psi;     Vector   gravity = new Vector (0.0f, 0.0f, 32.0f);     wheel_angle = wheel_pos * wheel_max_angle;     wheel[(int)WhichWheel.LeftFront].SetRelHeading(wheel_angle);     wheel[(int)WhichWheel.RightFront].SetRelHeading(wheel_angle);     turn_rate = (Math.Sin(wheel_angle) * velocity.X / wheel_track) / 10.0f;     if (wheel[(int)WhichWheel.LeftFront].sliding &&              wheel[(int)WhichWheel.RightFront].sliding)     {        turn_rate = 0.0f;     }     attitude_rate.Psi = turn_rate;     delta_psi = turn_rate * delta_t;     centripedal_accel = (float) (2.0 * velocity.X *               Math.Sin(delta_psi)) / delta_t;     wheel_rpm = 60.0f * velocity.X / (Math. PI * wheel_diameter);     rolling_resistance = 0.696f * (float)Math.Abs(velocity.X);     drag = 0.5f * drag_coeff * frontal_area * air_density *              Math.Abs(velocity.X * velocity.X);     brake_force = brake * 32.0; // Max braking 1G     if (mph < 0.0)     {         brake_force *=   1.0;     }     if (wheel[(int)WhichWheel.LeftFront].sliding &&               wheel[(int)WhichWheel.RightFront].sliding &&               wheel[(int)WhichWheel.RightRear].sliding &&               wheel[(int)WhichWheel.RightRear].sliding)     {     brake_force = 0.0f;     } 
end example
 

The centripetal acceleration is a function of the forward speed and the turn rate. The rotational speed of the wheels is a function of the circumference of the wheel and the forward speed of the vehicle. This assumes that the wheels are not slipping. If the wheels were slipping, there would no longer be a direct relationship between forward velocity and tire rotational speed. Instead, the relationship would be between engine RPM and tire rotational speed. The rolling resistance is also a function of the forward velocity. Since the direction of the velocity doesn t affect the resistance, only the magnitude, we can calculate the resistance as the product of a resistance coefficient and the absolute value of the velocity. Likewise, the aerodynamic drag is a function of the drag coefficient, the frontal area of the vehicle, the air density, and the square of the forward velocity. The vehicle s brakes represent another force that will affect the speed of the vehicle. The acceleration that will be applied by the brakes is a function of the brake pedal position and a maximum braking acceleration of 32 feet per second (1 G of acceleration). If the vehicle is traveling backward, the braking force is negated. If all of the wheels are sliding, there is no braking force, and the acceleration is set to zero.

The next section of code, shown in Listing 10 “22b, concerns the simulation of the vehicle s transmission. The first thing to do in this section of code is to determine the current percentage of the maximum engine RPM. The percentage of maximum RPM will come into play when deciding the proper time for the automatic transmission to shift. A switch statement based on the current gear determines what the transmission will do. If Park is selected, there will be maximum braking force until the vehicle is almost completely stopped, and then the velocity will be set to zero to complete the process. If Reverse is selected, the associated gear ratio is selected, and the automatic gear is set to the same value. The same holds true for Neutral . If the transmission is in Drive , we must be using an automatic transmission. This means that the transmission will automatically shift up and down through the forward gears as necessary. If the engine RPM is in the top 20 percent of its range, and the automatic gear is less than its top gear, the shift_up flag is set. Likewise, if the automatic gear is in Neutral and the engine is running faster them idle, it will also shift up. The transmission will downshift if the percent RPM drops below 40 percent and the automatic gear is greater than first gear.

Listing 10.22b: CarDynamics Process Method
start example
 percent_rpm = engine_rpm / max_rpm;  switch (gear)  {     case GearState.Park:        if (mph > 1.0  mph <   1.0)        {           brake_force = 32.0;        }        else        {           velocity.SetX(0.0f);        }        auto_gear = GearState.Park;        break;     case GearState.Reverse:        auto_gear = GearState.Reverse;        break;     case GearState.Neutral:        auto_gear = GearState.Neutral;        break;     case GearState.Drive:        shift_up = false;        shift_down = false;        if ((percent_rpm > 0.8 && auto_gear < GearState.Drive)             (percent_rpm > 0.1 && auto_gear == GearState.Neutral))        {           shift_up = true;        }        if (percent_rpm < 0.4 && auto_gear >= GearState.FirstGear)        {            shift_down = true;        }     switch (auto_gear)     {        case GearState.Neutral:           if (shift_up)           {              auto_gear = GearState.FirstGear;           }           break;        case GearState.Drive:           if (shift_down)           {              auto_gear = GearState.ThirdGear;           }           break;        case GearState.FirstGear:           if (shift_up)           {              auto_gear = GearState.SecondGear;           }           else if (shift_down)           {              auto_gear = GearState.Neutral;           }           break;        case GearState.SecondGear:           if (shift_up)           {              auto_gear = GearState.ThirdGear;           }           else if (shift_down)           {              auto_gear = GearState.FirstGear;           }           break;        case GearState.ThirdGear:           if (shift_up)           {              auto_gear = GearState.Drive;           }           else if (shift_down)           {              auto_gear = GearState.SecondGear;           }           break;     }        break;     case GearState.FirstGear:        auto_gear = GearState.FirstGear;        break;     case GearState.SecondGear:        auto_gear = GearState.SecondGear;        break;     case GearState.ThirdGear:        auto_gear = GearState.ThirdGear;        break;  }  current_gear_ratio = gear_ratio[(int)auto_gear]; 
end example
 

Embedded within the Drive state is a second switch based on the automatic gear. If the transmission is currently in Neutral and we are shifting up, the automatic gear is changed to first. It is changed to third gear if we are in Drive and downshifting. If the transmission is in First gear, we can either shift up to Second or downshift to Neutral . If the transmission is in Second gear, we can either shift up to Third or downshift to First . Finally, if the transmission is in Third , we can shift up to Drive or downshift into Second . If the manually selected gear is first through third, than the automatic gear is set to match. The current gear ratio is selected from the gear ratio array based on the current automatic gear.

The next section of the Process method (shown in Listing 10 “22c) deals with the speed of the engine and the force that it creates. If the engine is running and the engine is not up to idle speed yet, then we will set idle RPM as our target. On the other hand, if the engine has been turned off, we need to target zero RPM. If neither of these conditions is met, then the target RPM is based on the gas pedal position, demanding an RPM somewhere between idle and maximum RPM. The delta RPM is the difference between the current engine speed and this target RPM. We will limit the change in RPM to 3000 RPM per second. This delta is integrated into the current engine RPM using the time delta.

Listing 10.22c: CarDynamics Process Method
start example
 if (running && target_rpm < idle_rpm)  {     target_rpm = idle_rpm;  }  else if (!running)  {     target_rpm = 0.0f;  }  else  {     target_rpm = idle_rpm + throttle * (max_rpm   idle_rpm);  }  delta_rpm = target_rpm   engine_rpm;  if (delta_rpm > 3000.0f)  {     delta_rpm = 3000.0f;  }  else if (delta_rpm <   3000.0f)  {     delta_rpm =   3000.0f;  }  if (delta_rpm < 1.0f && delta_rpm >   1.0f)  {     delta_rpm = 0.0f;  }  engine_rpm += (delta_rpm * delta_t);  if (auto_gear == GearState.Neutral   gear == GearState.Park)  {     eng_torque = 0;  }  else  {     eng_torque = torque_curve.Interpolate(percent_rpm * 100.0) *            max_engine_torque;  }  engine_loss = Math.Max(((engine_rpm/20) * (engine_rpm/20) + 45), 0.0);  brake_torque = brake_force * mass;  temp = (eng_torque   engine_loss   brake_torque);  if (temp < 0.0 && Math.Abs(mph) < 0.1)  {     temp = 0.0;  }  if (current_gear_ratio != 0.0)  {     wheel_force = temp;  }  else  {     wheel_force = 0.0f;  } 
end example
 

If the transmission is in either Park or Neutral , the engine will not produce any torque. Otherwise, we will get the engine s torque from the torque curve LFI based on the engine RPM and the engine s maximum torque. Counteracting the engine torque will be the losses within the engine and the braking force. The engine loss is the sum of a constant loss as well as a portion that increases with engine speed. The braking losses are calculated from the braking acceleration we determined earlier. Since force is mass times acceleration, we need to multiply by the vehicle mass to get the braking torque. An intermediate force is calculated by subtracting these two losses from the engine torque. If this intermediate force is negative, the losses exceed the engine torque, and the speed is close to zero, we will zero this force for stability. If the gear ratio is nonzero, this will become our wheel force. Otherwise, our wheel force is zero, since no force is connecting through to the drive wheels.

The next section of the method (shown in Listing 10 “22d) covers the calculation of the net force working on the vehicle. The net force is the wheel force minus the drag and rolling resistance. If the transmission is in reverse, we will negate the force so that it acts in the opposite direction. The next step is to determine the effect of gravity on the vehicle. For this, we rotate a gravity vector from earth to vehicle reference frame. If the car is not in park, we will apply the forces to the vehicle s forward acceleration. We need to divide the net force by the vehicle mass to get the resulting acceleration. From this, we will subtract the braking acceleration and add in the acceleration due to gravity. The vertical component of the gravity is applied to the vertical acceleration. We will assume that the lateral component of gravity is not enough to make a difference and will ignore it. If this iteration s portion of the braking force is greater than the current forward velocity, then we will zero the velocities and accelerations to bring the car to a complete stop.

Listing 10.22d: CarDynamics Process Method
start example
 net_force = wheel_force   drag   rolling_resistance;  if (gear == GearState.Reverse)  {     net_force *=   1.0f;     // Force in reverse is in opposite direction.  }  ground_slope.RotateEtoA(gravity);  body_gravity=gravity;  if (gear != GearState.Park)  {     acceleration. X = (net_force / mass   brake_force) + body_gravity.X;  }  acceleration. Z -= body_gravity.Z;  if (velocity.X < (delta_t * brake_force) &&            velocity.X > (delta_t *   brake_force))  {     mph = 0.0f;     velocity.X = 0.0;     acceleration.X = 0.0;     brake_force = 0.0;  } 
end example
 

The final portion of the method (shown in Listing 10 “22e) closes the loop on the engine speed and calls additional methods to complete the processing. The speed of the engine not only creates the power that drives the vehicle, the speed of the vehicle also feeds back to back-drive the speed of the engine. If the transmission is in park or neutral, there is no connection between the wheels and the engine, so the engine speed remains a direct relation to the throttle position. Otherwise, we calculate a value based on the vehicle speed and the current gear ratio. This value is limited between the idle and the maximum RPM, and becomes the new target RPM. To complete the processing, we will call the CalcWeightTransfer method to get the current weight over each wheel. The ProcessWheels method calls the Process method for each wheel and applies the forces from the wheels to the vehicle. The ProcessAttitude method determines the new attitude rate based on the suspension forces. The final step is to call the IntegratePosition method to integrate the new velocity and position.

Listing 10.22e: CarDynamics Process Method
start example
 if (auto_gear == GearState.Neutral   gear == GearState.Park)     {        temp = idle_rpm + (max_rpm-idle_rpm) * throttle;     }     else     {        temp = velocity.X * current_gear_ratio;     }     if (temp >= (idle_rpm * 0.9f))     {        if (temp > max_rpm)        {           target_rpm = max_rpm;        }        else        {           target_rpm = temp;        }     }     else     {           target_rpm = idle_rpm;     }        CalcWeightTransfer();        ProcessWheels(delta_t);        ProcessAttitude(delta_t);        IntegratePosition(delta_t);  } 
end example
 

The SetAttitude method (shown in Listing 10 “23) provides the means of setting the attitude from another class. This would typically be used while initializing the vehicle to a new place prior to starting a new level.

Listing 10.23: CarDynamics SetAttitude Method
start example
 void SetAttitude(float roll, float pitch, float heading)  {     attitude.Phi = roll;     attitude.Theta = pitch;     attitude.Psi = heading;  } 
end example
 

The SetPosition method and SetVelocity method (shown in Listing 10 “24) handles the positioning and velocity portion of setting the vehicle to a new position and velocity.

Listing 10.24: CarDynamics SetPosition and SetVelocity Methods
start example
 void SetPosition(float north, float east, float height)  {     position.X = north;     position.Y = east;     position.Z = height;  }  void SetVelocity(float north, float east, float vertical)  {     velocity.X = north;     velocity.Y = east;     velocity.Z = vertical;  } 
end example
 

There are two methods (shown in Listing 10 “25) for setting the ground height under the wheels. The first is SetGroundHeight , which can be used to set the height under a specified wheel. The second method, SetAllGroundHeights , takes all four elevations as arguments in order to set the values for all four wheels in one call.

Listing 10.25: CarDynamics SetGroundHeight and SetAllGroundHeights Methods
start example
 void SetGroundHeight (WhichWheel the_wheel, float height)  {     wheel[(int)the_wheel].ground_height = height;  }  void SetAllGroundHeights (float left_front, float right_front,     float left_rear, float right_rear)  {     wheel[(int)WhichWheel.LeftFront].ground_height = left_front;     wheel[(int)WhichWheel.RightFront].ground_height = right_front;     wheel[(int)WhichWheel.LeftRear].ground_height = left_rear;     wheel [(int)WhichWheel.RightRear].ground_height = right_rear;  } 
end example
 

The WheelAngle method (shown in Listing 10 “26) provides the means of getting the relative angle of the front wheels in either degrees or radians. If the Boolean argument is true, the return value is converted from radians to degrees.

Listing 10.26: CarDynamics WheelAngle Method
start example
 double WheelAngle(bool in_degrees)  {     double result;     if (in_degrees)     {        result = (wheel_angle * 180.0 / Math.PI);     }     else     {        result = wheel_angle;     }     return result;  } 
end example
 

There are similar methods for accessing pitch and roll shown in Listing 10 “27. The GetPitch method returns pitch in radians or degrees, and the GetRoll method does the same for roll.

Listing 10.27: CarDynamics GetPitch and GetRoll Methods
start example
 double GetPitch(bool in_degrees)  {     double result;     if (in_degrees)     {        result = attitude.ThetaAsDegrees;     }     else     {        result = attitude.Theta;     }     return result;  }  double GetRoll(bool in_degrees)  {     double result;     if (in_degrees)     {        result = attitude.PhiAsDegrees;     }     else     {        result = attitude.Phi;     }     return result;  } 
end example
 

The IsTireSquealing method (shown in Listing 10 “28) provides the ability to query each tire to see if it is currently in a squealing state.

Listing 10.28: CarDynamics IsTireSquealing Method
start example
 bool IsTireSquealing(WhichWheel the_wheel)  {     return wheel[(int)the_wheel].squeeling;  } 
end example
 

The IsTireLoose method (shown in Listing 10 “29) provides similar access to the flag that indicates an individual tire is sliding.

Listing 10.29: CarDynamics IsTireLoose Method
start example
 bool IsTireLoose(WhichWheel the_wheel)  {     return wheel[(int)the_wheel].sliding;  } 
end example
 

The SetTireStiction method (shown in Listing 10 “30) is the means of altering the static friction for the wheels. It is assumed that all four tires are the same and have the same friction characteristics.

Listing 10.30: CarDynamics SetTireStiction Method
start example
 void SetTireStiction(float new_value)  {     wheel[(int)WhichWheel.LeftFront].SetStiction(new_value);     wheel[(int)WhichWheel.RightFront].SetStiction(new_value);     wheel[(int)WhichWheel.LeftRear].SetStiction(new_value)     wheel[(int)WhichWheel.RightRear].SetStiction(new_value  } 
end example
 

The ProcessWheels method (shown in Listing 10 “31) was mentioned in the description of the Process method. This is where we update each wheel and its suspension and the resulting forces that act on the vehicle. The lateral acceleration on the vehicle is the opposite of the centripetal force. The method loops through the four tires in the array. Each wheel s Process method is called. The upward force from the four tires is accumulated along with the suspension offsets and ground heights. If any of the wheels have bottomed out or are touching the ground, a local flag is set to reflect this fact. This method also counts the number of tires that are sliding. After all four wheels have been processed, the accumulated suspension offset and ground height are divided by four to get the average values of each. If the suspension is roughly centered, the vertical speed of the vehicle will be reduced by 80 percent to provide damping. If the force is close to zero, we will zero both the force and the velocity. The acceleration is the force divided by the mass. If the wheels are in contact with the ground, we will divide the vertical velocity by two as further damping of the vertical velocity. If any of the wheels have bottomed out, we will set the elevation of the vehicle to ensure that it remains above the ground. If it was moving down when it bottomed out, the vertical velocity will be zeroed as well. The final portion of the method deals with the lateral accelerations on the vehicle. If more than two wheels are sliding and this is the first pass that this has happened , we will break the tires loose and begin moving laterally. If we are sliding to the right, a 1 G acceleration will be applied to the left, representing the friction of the tires serving to slow the slide. If sliding to the left, the acceleration will be applied to the right. If we aren t sliding to the side, the lateral speed and acceleration will be zeroed because the tires are maintaining their grip on the ground.

Listing 10.31: CarDynamics ProcessWheels Method
start example
 void ProcessWheels(float delta_t)  {     int      i;     double   accel;     double   total_upwards_force = 0.0;     bool   bottomed_out = false;     bool   on_ground = false;     int      sliding = 0;     double   avg_suspension = 0.0;     double   delta_force;     double   avg_ground_height = 0.0;     acceleration.SetY(   centripedal_accel);     for (i=0; i<4; i++)     {        wheel[i].Process(delta_t, attitude, acceleration, velocity,              position);        total_upwards_force += wheel[i].UpwardsForce();        avg_suspension += wheel[i].suspension_offset;        avg_ground_height += wheel[i].ground_height;        if (wheel[i].bottomed_out)        {           bottomed_out = true;        }        if (wheel[i].touching_ground)        {           on_ground = true;        }        if (wheel[i].sliding)        {           sliding++;        }     }     avg_suspension /= 4.0f;     avg_ground_height /= 4.0f;     if (Math.Abs(avg_suspension) < 0.1f)     {        velocity.Z = velocity.Z * 0.2;     }     delta_force = total_upwards_force   weight;     if (Math.Abs(delta_force) < 1.0)     {        delta_force = 0.0;        velocity.Z = 0.0;     }     accel = delta_force / mass;     acceleration.Z = accel;     if (on_ground)     {        velocity.Z = velocity.Z * 0.5;     }     if (bottomed_out)     {        position.Z = avg_ground_height + wheel[0].offset.X + wheel[0].radius;     }     if (bottomed_out && velocity.Z < 0.0f)     {        velocity.Z = 0.0;     }     if (sliding > 2 && !was_sliding)     {        was_sliding = true;        velocity.Y = acceleration.Y;     }     if (sliding > 2 && velocity.Y > 0.0)     {        accelerations =   32.0;     }     else if (sliding > 2 && velocity.Y < 0.0)     {        accelerations = 32.0;     }     else     {        velocity.Y = 0.0;        accelerations = 0.0;        was_sliding = false;     }  } 
end example
 

The ProcessAttitude method (shown in Listing 10 “32) determines the attitude rates based on the suspension offsets and the slope of the ground under the vehicle. The method begins by determining the pitch of the ground under the vehicle. The Terrain class has a method for returning ground slope for a given point. Unfortunately, the slope at a single point on the terrain can give erroneous results compared to the average heights of the four points under the wheels. If, for example, there were a sharp crest in the terrain below the vehicle, the tires could be straddling that point. To get a correct value as experienced by the vehicle, we will take the average height below the front wheels and the average height below the rear wheels. The arcsine of the difference between these heights and the wheelbase of the vehicle is the pitch of the terrain as experienced by the vehicle. The roll angle is calculated the same way. In this case, we will use the arcsine of the difference between the average height on the left and the average height on the right, divided by the wheel track. The attitude rates will be a fraction of the difference between the current attitude angles and those that we just calculated.

Listing 10.32: CarDynamics ProcessAttitude Method
start example
 void ProcessAttitude(float delta_t)  {     double avg_front;     double avg_rear;     double pitch;     double avg_left;     double avg_right;     double roll;     // First do ground slope.     avg_front = (wheel[(int)WhichWheel.LeftFront].ground_height +                  wheel[(int)WhichWheel.RightFront].ground_height) / 2.0;     avg_rear  = (wheel[(int)WhichWheel.LeftRear].ground_height +                  wheel[(int)WhichWheel.RightRear].ground_height) / 2.0;     pitch = Math.Asin((avg_rear   avg_front) / wheel_base);     ground_slope.Theta = pitch;     avg_left = (wheel[(int)WhichWheel.LeftFront].ground_height +                 wheel[(int)WhichWheel.LeftRear].ground_height) / 2.0;     avg_right  = (wheel[(int)WhichWheel.RightFront].ground_height +                   wheel[(int)WhichWheel.RightRear].ground_height) / 2.0;     roll = Math.Asin((avg_right   avg_left) / wheel_track);     ground_slope.Phi = roll;     // Now do vehicle attitude     avg_front = (wheel[(int)WhichWheel.LeftFront].suspension_offset +                  wheel[(int)WhichWheel.RightFront].suspension_offset) / 2.0f;     avg_rear  = (wheel[(int)WhichWheel.LeftRear].suspension_offset +                  wheel[(int)WhichWheel.RightRear].suspension_offset) / 2.0f;     pitch = Math.Asin((avg_front   avg_rear) / wheel_base);     attitude_rate.Theta = ((ground_slope.Theta+pitch)   attitude.Theta) * 0.025;     avg_left = (wheel[(int)WhichWheel.LeftFront].suspension_offset +                 wheel[(int)WhichWheel.LeftRear].suspension_offset) / 2. 0f;     avg_right  = (wheel[(int)WhichWheel.RightFront].suspension_offset +                   wheel[(int)WhichWheel.RightRear].suspension_offset) / 2. 0f;     roll = Math.Asin((avg_right   avg_left) / wheel_track);     attitude_rate.Phi = ((ground_slope.Phi+roll)   attitude.Phi) * 0.05;  } 
end example
 

The MinorCollision method (shown in Listing 10 “33) will be called whenever the vehicle hits something small that will affect the vehicle s speed only slightly ” things like bushes, shrubs, and other small objects. This reduction in speed will be accomplished by making a 10 percent reduction in the vehicle s velocity vector.

Listing 10.33: CarDynamics MinorCollision Method
start example
 public void MinorCollision()  {     velocity = velocity * 0.9f;  } 
end example
 

When the vehicle hits something major, a MinorCollision method won t be enough. The MajorCollision method (shown in Listing 10 “34) will be used when we hit something that we can t just drive over and continue on our way. In order to react to the object we hit, we need to have some information about the objects involved. We need to know the difference in the velocity vectors of the two objects involved. This will tell us how hard we hit the other object. The other thing we need is the distance vector between the two objects, which gives us the direction between the two objects. Since this library and the game engine use different classes for vectors, we need to pass the components of the vectors individually. The relative velocity vector is built from the velocity components , and a collision normal vector is built from the position delta. Since this needs to be a normal vector, we normalize it so that it will be a unit vector in the direction of the collision.

Listing 10.34: CarDynamics Major Collision Method
start example
 public void MajorCollision(float delta_x_velocity,        float delta_y_velocity,        float delta_z_velocity,        float delta_x_position,        float delta_y_position,        float delta_z_position)     {       Vector RelativeVelocity = new Vector(delta_x_velocity,                  delta_y_velocity, delta_z_velocity);        Vector CollisionNormal = new Vector(delta_x_position,                  delta_y_position, delta_z_position);        CollisionNormal.Normalize();        double collisionSpeed = RelativeVelocity * CollisionNormal;        float impulse = (float)((2.50 * collisionSpeed) /           ((CollisionNormal * CollisionNormal) *           (inverse_mass)));        velocity = (CollisionNormal * impulse) * (float) inverse_mass;        engine_rpm = idle_rpm;     }  };  } 
end example
 

Normally when a car hits a solid object, we have what is referred to as an inelastic collision . The energy of the collision is dispersed by the deformation of the car. Unless we want the game to be over on impact, we need to take a step away from reality for the major collision. Instead, we will have what is called an elastic collision . In an elastic collision, the force is expended, driving the objects along the reverse of the normal of the collision. We will in fact exaggerate the force of the collision. This will cause the objects to bounce away from each other.

The first step is to determine how hard the vehicle hit the object. This is the dot product of the relative velocity and the collision normal. The impulse that will be applied to the car would normally be calculated as the impact speed divided by the dot product of the collision normal with itself and divided by the mass of the vehicle. To exaggerate the bounce, we will use two-and-a-half times the force. To convert the impulse into a new velocity for the car, we multiply the impulse by the collision normal vector to put it in the correct direction. We also divide this value by the mass of the vehicle to convert the force to an acceleration that is applied immediately as the vehicle s new velocity.

Waving the Flag

The first part of this chapter has dealt with rigid body dynamics. Now we will look at the dynamics of flexible systems. These techniques can be applied to cloth, hair, grass, or other flexible objects that will move and deform based on forces applied to them. We will use such a system for a rectangle of cloth that happens to be a flag. The flexibility of the object is accomplished by creating an array of control points or nodes as a regularly spaced grid. Each node is connected to its adjacent nodes with a spring/damper connection.

Dividing the Cloth Surface into Nodes

The definition of the Node structure that specifies each of these nodes is shown in Listing 10 “35. Each node will have an associated mass. This is the portion of the overall mass of the cloth that is represented by the node. Although the mass is in truth evenly distributed in the spaces between the nodes, we will simplify the problem by isolating the mass at the node locations. As you might guess from this, there is a tradeoff regarding the number of nodes that are used. The greater the number of nodes, the closer the simulation gets to reality. Unfortunately, the amount of processing required increases sharply as the number of nodes increases. For efficiency, we will also hold the inverse of the mass so that we only need to perform the division once during initialization.

Listing 10.35: Cloth Class Node Structure Attributes
start example
 using System;  using System.Collections;  using System.Drawing;  using System.Threading;  using Microsoft.DirectX;  using Microsoft.DirectX.Direct3D;  namespace GameEngine  {      /// <summary>      /// Summary description for Cloth      /// </summary>      public class Cloth : Object3D, IDynamic      {         private struct Node         {            public float  mass;            public float  inverse_mass;            public Vector3 position;            public Vector3 velocity;            public Vector3 acceleration;            public Vector3 force;            public bool    constrained; 
end example
 

Each node will have a position in space that will be used when we render the cloth. The velocity of a node is included for integration as well as the acceleration and force vectors that will modify the velocity. Each node also has a flag that defines whether or not the position of the node is constrained. Nodes that are fastened to a parent object will not move due to the forces acting on the rest of the cloth. They will only move as the parent object moves.

The constructor for the Node structure is shown in Listing 10 “36. The mass and position of the node are supplied as arguments as well as the flag that defines whether or not the node is constrained to remain in one place. This information is used to initialize each of the attributes of the structure. The velocities, accelerations, and forces are all initialized to zero. If the cloth was in zero gravity and no other forces were applied to the cloth, it would maintain its original rectangular shape. The forces of wind and gravity will change the shape of the cloth.

Listing 10.36: Cloth Class Node Constructor
start example
 public Node(float mass_, double x, double y, double z,                bool fixed_in_place)      {         mass = mass_;         inverse_mass = 1.0f / mass;         position.X = (float)x;         position.Y = (float)y;         position.Z = (float)z;         velocity.X = 0.0f;         velocity.Y = 0.0f;         velocity.Z = 0.0f;         acceleration.X = 0.0f;         acceleration.Y = 0.0f;         acceleration.Z = 0.0f;         force.X = 0.0f;         force.Y = 0.0f;         force.Z = 0.0f;         constrained = fixed_in_place;      } } 
end example
 

The springs that connect the nodes will need to know which nodes are at each end of the spring. Since the nodes will be laid out in a grid, we will be able to index them by row and column. The NodeIndex structure (shown in Listing 10 “37) will be used for this purpose.

Listing 10.37: Cloth Class NodeIndex Structure
start example
 private struct NodeIndex  {     public int row;     public int column;  } 
end example
 

The Spring structure (shown in Listing 10 “38) will encapsulate the spring/ damper system that will connect the nodes. The structure holds two instances of the NodeIndex structure that point to the nodes at each end of the spring. There is also a spring constant that defines the force exerted by the spring as it is stretched or compressed and the damping coefficient that counteracts the force of the spring for stability. The last attribute of the structure is the rest length of the spring. When this distance separates the two nodes, there will be no force exerted by the spring.

Listing 10.38: Cloth Class Spring Structure
start example
 private struct Spring  {     public NodeIndex node1;     public NodeIndex node2;     public float    spring_constant;     public float     damping;     public float     length;  } 
end example
 

Weaving Our Cloth

Now that we ve seen all of the support structures that will be used by the Cloth class, we can look at the Cloth class itself. We will begin with the attributes and constants for the class (shown in Listing 10 “39) that will be used by the class. An array of vertices render the cloth. The node positions will become the positional part of the vertices. The class also includes a vertex and an index buffer. By using the combination of two buffers, we can have each node represented by a single vertex in the buffer. The sharing of the vertices is accomplished by the references to the vertices in the index buffer. Since the contents of an index buffer default to short integers, we will create an array of shorts that will be used to populate the index buffer. The final attribute required for the rendering of the cloth is the texture that will be applied to the cloth. In the case of our sample program, this will be an image of the US flag.

Listing 10.39: Cloth Class Attributes and Constants
start example
 #region Attributes  private CustomVertex.PositionNormalTextured[] m_vertices;  private VertexBuffer m_VB = null;   // Vertex buffer  private IndexBuffer m_IB = null;    // Index buffer  // Indices buffer  private short[] indices;  private Texture      m_Texture;   // Image for face  private Node[,] nodes;  private int num_springs;  private int num_faces;  private int num_rows;  private int num_columns;  private int num_nodes;  private Spring[] springs;  private Vector3 m_vOffset = new Vector3(0.0f, 0.0f, 0.0f);  private Attitude m_AttitudeOffset = new Attitude();  private bool m_bValid = false;  private Thread         m_physics_thread;  private bool thread_active = true;  private Mutex mutex = new Mutex();  // Global variables  private static Vector3 wind = new Vector3();  // Constants  private static float gravity =   32.174f;  private static float spring_tension = 40.10f;  private static float damping = .70f;  private static float drag_coefficient = 0.01f;  #endregion 
end example
 

The next section of attributes are those required to manage the nodes that control the shape of the cloth. A two-dimensional array of node structures forms the basic grid. We will also keep track of the number of springs that connect the nodes, the number of faces that will exist between the nodes, the number of rows and columns in the grid, and the overall number of nodes. A one-dimensional array will hold all of the springs that connect the nodes.

The final section of normal attributes are those concerned with the attachment of the cloth to a parent object and the updating of the cloth. The Cloth class inherits from the Object3D class. Because of this, it can be attached as a child of another object. We will include a position and attitude offset that may be used to adjust the cloth s placement relative to its parent. If the initialization of the class is successful, the valid flag is set to true. This prevents unneeded processing later if the cloth could not be properly created. As in the CarDynamics class, we must run the dynamics for the cloth at a high iteration rate. This entails spawning a processing thread with its associated active flag and Mutex for safe communications.

One global attribute exists for the Cloth class. A wind vector defines the current wind direction and speed that will affect all instances of the class. The attributes end with a set of static constants that are used in the physics calculations: the acceleration of gravity that will tend to make the nodes fall towards the earth, the spring constant that will work to hold the nodes together, and the damping constant that will prevent the spring forces from oscillating. Finally, we will have a drag coefficient that calculates the force of the wind against the fabric.

The properties for the Cloth class are shown in Listing 10 “40. The Offset property is available for checking and altering the object s offset from its parent object, and there are two static properties for controlling the global wind vector.

Listing 10.40: Cloth Class Properties
start example
 #region Properties  public Vector3 Offset {     set { m_vOffset = value; }     get { return m_vOffset; } }  public static float EastWind {     set { wind.X = value; }     get { return wind.X; } }  public static float NorthWind {     set { wind.Z = value; }     get { return wind.Z; } }  #endregion 
end example
 

The constructor for the Cloth class (shown in Listings 10 “41a through 10 “41e later in this section) is quite involved. Half of the work in simulating a complex spring/damper system is in the initialization. To construct a piece of cloth for our flag, we need a number of arguments. Since the class inherits from Object3D , we need a name for each instance of the class. The name is passed down to the base class. We also need the name of the texture that will be applied to the surface as well as the size of the cloth defined by the number of rows and columns in the node grid and the separation distance between the nodes. Finally, we need the mass of the piece of cloth in order to perform the physics calculations.

Listing 10.41a: Cloth Class Constructor
start example
 public Cloth(string name, string texture_name, int rows, int columns,               double spacing, float mass) : base(name)  {     try     {         num_rows = rows;         num_columns = columns;         m_Texture =            GraphicsUtility.CreateTexture(CGameEngine.Device3D,            texture_name);         nodes = new Node[rows+1,columns+1];         num_nodes = (rows+1) * (columns+1);         num_faces = rows * columns * 2;         num_springs = columns * (rows+1) + rows * (columns+1) +             columns*rows*2;         springs = new Spring[num_springs];         wind.X = 0.0f;         wind.Y = 0.0f;         wind.Z = 0.0f; 
end example
 
Listing 10.41b: Cloth Class Constructor
start example
 m_vertices = new CustomVertex.PositionNormalTextured[num_nodes];  float mass_per_node = mass / num_nodes;  for (int r=0; r<=rows; r++)  {     for (int c=0; c <=columns; C++)     {        nodes [r,c] = new Node(mass_per_node,   (c*spacing),   (r*spacing), 0.0, c==0 && (r==0   r == rows));     }  } 
end example
 
Listing 10.41c: Cloth Class Constructor
start example
  //  Create a buffer for rendering the cloth.  m_VB = new VertexBuffer(typeof(CustomVertex.PositionNormalTextured), num_nodes,     CGameEngine.Device3D, Usage.WriteOnly ,     CustomVertex.PositionNormalTextured. Format,     Pool.Default);  m_IB = new IndexBuffer(typeof(short), num_faces * 3, CGameEngine.Device3D,     Usage.WriteOnly, Pool.Managed);  indices = new short [num_f aces * 3];  m_IB.Created += new System.EventHandler (this.PopulateIndexBuffer);  this.PopulateIndexBuffer(m_IB, null) ;  m_VB.Created += new System.EventHandler (this.PopulateBuffer);  this.PopulateBuffer(m_VB, null); 
end example
 
Listing 10.41d: Cloth Class Constructor
start example
 // Create the springs  int index = 0;  for (int r=0; r<=rows; r++)  {      for (int c=0; c <=columns; C++)      {         if (c < columns)         {            springs[index].node1.row = r;            springs[index].node1.column = c;            springs[index].node2.row = r;            springs[index],node2.column = c+1;            springs[index],spring_constant = spring_tension;            springs[index],damping = damping;           Vector3 length = nodes[r,c].position   nodes[r,c+1].position;            springs[index].length = length.Length();            index++;         }         if (r < rows)         {            springs[index].node1.row = r;            springs[index].node1.column = c;            springs[index].node2.row = r+1;            springs[index].node2.column = c;            springs[index].spring_constant = spring_tension;            springs[index].damping = damping;           Vector3 length = nodes[r,c].position   nodes[r+1,c].position;            springs [index].length = length.Length();            index++;         }         if (r < rows && c < columns)         {            springs[index].node1.row = r;            springs[index].node1.column = c;            springs[index].node2.row = r+1;            springs[index].node2.column = c+1;            springs[index].spring_constant = spring_tension;            springs[index].damping = damping;           Vector3 length = nodes[r,c].position   nodes[r+1,c+1].position;            springs[index].length = length.Length();            index++;         }         if (r < rows && c > 0)         {            springs[index].node1.row = r;            springs[index].node1.column = c;            springs[index].node2.row = r+1;            springs[index].node2.column = c   1;            springs[index].spring_constant = spring_tension;            springs[index].damping = damping;            Vector3 length = nodes[r,c],position   nodes[r+1,c   1].position;            springs[index].length = length.Length();            index++;         }     } } 
end example
 
Listing 10.41e: Cloth Class Constructor (Conclusion)
start example
 m_physics_thread = new Thread(new ThreadStart(DoPhysics));        m_physics_thread.Start() ;        m_bValid = true;     }     catch (DirectXException d3de)     {        Console.AddLine("Unable to create cloth for " + name);        Console.AddLine(d3de.ErrorString);     }     catch (Exception e)     {        Console.AddLine("Unable to create cloth for " + name);        Console.AddLine(e.Message);     }  } 
end example
 

The first portion of the constructor is shown in Listing 10 “41a. The number of rows and columns are saved away in the associated attributes for later reference and the texture is loaded from the requested file. The working code in the constructor is enclosed in a Try/Catch block so that errors such as the inability to load the texture will terminate the constructor without crashing the application. We will consider the rows and columns specified to be the areas between the nodes. Therefore, we need one more node in each direction. The array of nodes is allocated with this increased count. The number of faces that will be rendered is twice the product of the rows and columns, since it requires two triangles for each square piece of the cloth. The number of springs required is even greater. Springs will connect each node with its neighbors in the vertical, horizontal, and diagonal directions. Once the required number of springs is calculated, the array of springs will be allocated. We will also clear the global wind vector within the constructor so that there will be no wind unless specifically set by the game application.

The next section of the constructor (shown in Listing 10 “41b) allocates the array of vertices and all of the nodes. The array of vertices is allocated to be as large as the total number of nodes that we will be using. The vertex format includes position, normal, and texture coordinate information. The mass at each node is also calculated. Since we stated that the mass would be evenly distributed across the nodes, this is simply the total mass divided by the number of nodes.

To construct the nodes, we will loop through all of the rows and columns of our grid. The position of each node is a function of the row and column number and the spacing distance. We will constrain the top and bottom nodes of the first column. Since we are modeling a flag, these will be the points at which the flag is attached to the parent object.

The next section of the constructor (shown in Listing 10 “41c) covers the creation and population of the index and vertex buffers. These are buffers that are allocated on the video card if at all possible. The vertex buffer is created large enough to hold one vertex for each of the nodes. The index buffer is large enough to hold the indices of the three vertices for each face making up the surface of the cloth. The indices array that holds the local image of the index buffer is also allocated as an array of short integers the same size.

An event handler will be set up to repopulate the buffers if they become recreated at some point in the future. To complete the initial population of the buffers, we will call these event handlers manually at this point.

The next section of the constructor (shown in Listing 10 “41d) initializes all of the springs that connect the nodes. To do this, we will loop through all the rows and columns of our grid again. If the current column is not the last column, we attach a spring between the current node and the one in the next column. The row and column indices of the two nodes are recorded, and the spring and damping constants are copied into the structure. We need to know the rest length of the spring. It is the magnitude of the distance vector that connects the two nodes. We will connect another spring down to the node below this one if the current node is not in the last row. Another spring will be connected diagonally down and to the right if the current node is not in the last row or column. One last spring will be connected to this node down and to the left if there is a node in that direction. This will give us springs connecting every node with every adjacent node in each direction, which will be what holds the fabric of our cloth together.

The final portion of the constructor (shown in Listing 10 “41e) fires off the thread that will calculate the movement of the cloth. A new thread is created that will use the DoPhysics method as its code. The thread is then started. If we have reached this point without throwing an exception, then we know that the cloth has been successfully initialized. The validity flag can now be safely set. If an exception is thrown during the construction, a message will be posted to the console.

The PopulateBuffer method that is called by the constructor and will be called any time the vertex buffer changes is shown in Listing 10 “42. The method loops through all of the nodes, copying the position of the node into the corresponding entry in the local vertex array. The texture coordinates for each vertex can be calculated as the ratio of the current row and column with the maximum value of each. Once the local array of vertices has been filled, we need to transfer the data to the vertex buffer in the video card s memory. For thread safety, we don t want to do this while the card s vertex buffer is in use. To prevent this, we will set the Mutex object. Once the Mutex object is locked for us by its WaitOne method, we can copy the data safely to the vertex buffer. When the copying is completed, we will release the Mutex object to signal we are done.

Listing 10.42: Cloth Class PopulateBuffer Method
start example
 public void PopulateBuffer(object sender, EventArgs e)  {     VertexBuffer vb = (VertexBuffer) sender;     int index = 0;     for (int r=0; r<(1+num_rows); r++)     {         for (int c=0; c <(1+num_columns); C++)         {            m_vertices[index].SetPosition(nodes[r,c].position);            m_vertices[index].SetNormal(new Vector3 (0.0f, 0.0f, 1.0f));            m_vertices[index].Tv = (float)r / (float)(num_rows);            m_vertices[index].Tu = (float)c / (float)(num_columns);            index++;         }    }    // Copy vertices into vertex buffer.     mutex.WaitOne();     vb.SetData(m_vertices, 0, 0);     mutex.ReleaseMutex();  } 
end example
 

The PopulateIndexBuffer method (shown in Listing 10 “43) works in a similar manner. In this case, the data that is placed in the local index buffer is calculated from the row and column numbers themselves . Two sets of three indices are placed in the buffer for each face. Once the local buffer has been filled, the data is copied over to the card s index buffer using the same procedure we followed for the vertex buffer.

Listing 10.43: Cloth Class PopulateIndexBuffer Method
start example
 public void PopulateIndexBuffer(object sender, EventArgs e)  {     int index = 0;     IndexBuffer g = (IndexBuffer)sender;     for (int r=0; r<num_rows; r++)     {        for (int c=0; c<num_columns; C++)        {           indices[index] = (short)((r) *(1+num_columns) + (c));           indices[index+1] = (short)((r+1)*(1+num_columns) + (c));           indices[index+2] = (short)((r) *(1+num_columns) + (c+1));           indices[index+3] = (short)((r) *(1+num_columns) + (c+1));           indices[index+4] = (short)((r+1)*(1+num_columns) + (c));           indices[index+5] = (short)((r+1)*(1+num_columns) + (c+1));           index += 6;        }     }     mutex.WaitOne();    g.SetData(indices, 0, 0);     mutex.ReleaseMutex();  } 
end example
 

The Render method of the Cloth class (shown in Listing 10 “44) is an override of the method from the base class. The method will not render anything if the class does not complete construction and set itself as valid. If it is valid, the process begins by determining the world matrix required to render the cloth in the proper location. If a parent exists, we will use the product of the class s matrix with the parent s matrix. Otherwise, we will just use the class s matrix. This matrix is passed to the device so it may translate the vertices we will send to the correct location. A plain white material, the texture, and the buffers are passed to the device. We will wrap the actual rendering call with the Mutex code to ensure that we don t try to render with buffers that are being changed. Since the class not only can be a child but may also have children of its own, we need to render any children held by the class. If children exist, we loop through the list of children and call the Render method for each one.

Listing 10.44: Cloth Class Render Method
start example
 public override void Render(Camera cam)     {        if (m_bValid)        {           Matrix world_matrix;           if (m_Parent != null)           {              world_matrix = Matrix.Multiply(m_Matrix, m_Parent.WorldMatrix);           }           else           {              world_matrix = m_Matrix;           }           CGameEngine.Device3D.Transform.World = world_matrix;           Material mtrl = new Material();           mtrl.Ambient = Color.White;           mtrl.Diffuse = Color.White;           CGameEngine.Device3D.Material = mtrl;           CGameEngine.Device3D.SetStreamSource(0, m_VB, 0);           CGameEngine.Device3D.VertexFormat =                  CustomVertex.PositionNormalTextured.Format;           CGameEngine.Device3D.RenderState.CullMode = Cull.None;           // Set the texture.           CGameEngine.Device3D.SetTexture(0, m_Texture);           // Set the indices.           CGameEngine.Device3D.Indices = m_IB;           // Render the face.           mutex.WaitOne();           CGameEngine.Device3D.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, num_nodes, 0, num_faces);           mutex.ReleaseMutex();           if (m_Children. Count > 0)           {              Object3D obj;              for (int i=0; i<m_Children. Count; i++)              {                  obj = (Object3D)m_Children.GetByIndex(i);                  obj.Render(cam);              }           }        }    } 
end example
 

The Update method (shown in Listing 10 “45) is another method that is declared by the base class and needs to be overridden for completeness. The update process for the cloth itself is done in the processing thread. If an additional Update method has been attached to the cloth, it will be called from this method. If the class has any children, then their Update method will be called as well.

Listing 10.45: Cloth Class Update Method
start example
 public override void Update (float Delta!)  {     if (m_UpdateMethod != null)     {        m_UpdateMethod((Object3D)this, DeltaT);     }     if (m_Children. Count > 0)     {        Object3D obj;        for (int i=0; i<m_Children.Count; i++)        {           obj = (Object3D)m_Children.GetByIndex(i);           obj.Update(DeltaT);        }     }  } 
end example
 

The DoPhysics method that runs in the processing thread (shown in Listings 10 “46a through 10 “46d) is where the actual cloth dynamics takes place. The method has a local vector that will hold the inverse speed of the parent if a parent exists (see Listing 10 “46a). This represents the apparent wind caused by the movement of the parent. An instance of the System.Random class is instantiated . Random numbers are generated to represent turbulence in the wind interacting with the cloth. This method loops within the thread until the thread_active flag is cleared. The code within the loop is encapsulated within a Try/Catch block to make sure that any exceptions are caught and don t terminate the thread prematurely. If the class has a parent, its velocity is inverted and saved as the parent_wind as mentioned earlier. A world translation matrix based on the object attitude, offsets, and position is created next.

Listing 10.46a: Cloth Class DoPhysics Method
start example
 private void DoPhysics()  {     Vector3 parent_wind = new Vector3(0.0f, 0.0f, 0.0f);     System.Random rand = new System.Random();     while (thread_active)     {        try        {           if (m_Parent != null)           {              parent_wind = m_Parent.Velocity *   1.0f;           }           m_Matrix = Matrix.Identity;           m_Matrix = Matrix.RotationYawPitchRoll(Heading+              m_AttitudeOffset.Heading,              Pitch+m_AttitudeOffset.Pitch,Roll+m_AttitudeOffset.Roll);           Matrix temp = Matrix.Translation(m_vPosition);           m_Matrix.Multiply(temp); 
end example
 
Listing 10.46b: Cloth Class DoPhysics Method
start example
 for (int r=0; r<=num_rows; r++)  {     for (int c=0; c <=num_columns; C++)     {        nodes [r,c], force.X = 0.0f;        nodes [r,c].force.Y = 0.0f;        nodes [r,c].force.Z = 0.0f;     }  }  // Process external forces  for (int r=0; r<=num_rows; r++)  {     for (int c=0; c <=num_columns; C++)     {        if (!nodes[r,c].constrained)        {           // Gravity           nodes [r,c]. force.Y += (float) (gravity * nodes[r,c].mass);           // Drag           Vector3 drag = nodes[r,c].velocity;           drag.Multiply(   1.0f);           drag.Normalize();           drag.Multiply((nodes[r,c].velocity.Length() *               nodes[r,c].velocity.Length()) * drag_coefficient);           nodes[r,c].force += drag;           // Wind           Vector3 turbulence = new Vector3 ((float)rand.NextDouble(), 0.0f,                (float)rand.NextDouble());           Vector3 total_wind = wind + turbulence + parent_wind;           nodes[r,c].force += total_wind;        }     }  } 
end example
 
Listing 10.46c: Cloth Class DoPhysics Method
start example
 // Spring forces  for (int i=0; i<num_springs; i++)  {      int row1    = springs [i].node1.row;      int column1 = springs[i].node1.column;      int row2    = springs [i].node2.row;      int column2 = springs[i].node2.column;      Vector3 distance = nodes [row1, column1].position   nodes[row2,column2].position;      float spring_length = distance.Length();      Vector3 normalized_distance = distance;      normalized_distance.Multiply(1.0f / spring_length);      Vector3velocity = nodes[row1,column1].velocity   nodes[row2,column2].velocity;      float length = springs[i].length;      float spring_force = springs[i].spring_constant *                 (spring_Length   length);      float damping_force = springs[i].damping *                Vector3.Dot(velocity,distance) / spring_length;      Vector3force2 = (spring_force + damping_force) *               normalized_distance;      Vector3force1 = force2;      force1.Multiply(   1.0f);      if (!nodes[row1,Column1].constrained)      {         nodes[row1,column1].force += force1;      }      if (!nodes[row2,column2].constrained)      {         nodes[row2,column2].force += force2;      }  } 
end example
 
Listing 10.46d: Cloth Class DoPhysics Method
start example
 // Integrate position                for (int r=0; r<=num_rows; r++)                {                  for (int c=0; c <=num_columns; C++)                {                  float x;                  Vector3 accel = nodes[r,c].force;                  accel.Multiply(nodes[r,c].inverse_mass);                  nodes[r,c].acceleration = accel;                  nodes[r,c].velocity.X += accel.X * 0.01f;                  nodes[r,c].velocity.Y += accel.Y * 0.01f;                  nodes[r,c].velocity.Z += accel.Z * 0.01f;                  nodes[r,c].position.X += nodes[r,c].velocity.X * 0.01f;                  nodes[r,c].position.Y += nodes[r,c].velocity.Y * 0.01f;                  nodes[r,c].position.Z += nodes[r,c].velocity.Z * 0.01f;                  x=1;                }            }            PopulateBuffer((object)m_VB, null);        }        catch (DirectXException d3de)        {           Console.AddLine("Unable to update a Model " + Name);           Console.AddLine(d3de.ErrorString);        }        catch (Exception e)        {           Console.AddLine("Unable to update a Model " + Name);           Console.AddLine(e.Message);        }        Thread.Sleep(10);     }  } 
end example
 

Making the Cloth Move

The next section of the DoPhysics method (shown in Listing 10 “46b) concerns the external forces that are acting on the cloth. It begins by zeroing the force vector for every node. We will be accumulating forces into this vector. We will then loop through all of the nodes again. If the node is constrained, we will not calculate forces for the node. We will start by adding in the force due to gravity in the vertical direction. Next, we will calculate the drag force acting on the node. The first step is to get the normal of the velocity vector inverted to point in the direction opposite the current velocity of the node. We will do this because drag is always in opposition with the current velocity. The drag force is the square of the magnitude of the velocity times the drag coefficient applied along this normalized vector.

The final external force acting on the nodes is any wind. A turbulence vector is created using three random numbers. This turbulence along with global wind and the wind from the parent form the total wind force acting on the node.

The internal forces from the springs are calculated in the next section of the method shown in Listing 10 “46c. This section loops through the array of springs. It begins by getting the indices of the nodes at each end of the spring. The distance vector between the two nodes is calculated. The magnitude of this vector is the current length of the spring. A copy of this distance vector is divided by this length in order to normalize the vector. A vector based on the difference in the velocities of the two nodes is also computed. The force due to the spring is calculated as the product of the spring coefficient and the deviation of the spring length from its rest length. The damping force that opposes the spring force is the damping coefficient times the dot product of velocity delta vector and the distance vector divided by the current spring length. The sum of these two forces, oriented with the normalized vector, becomes the force acting on one of the nodes. The inverse of the force acts on the other node. The nodes are either pulled together by the spring force or repelled, depending on whether the spring is stretched or compressed. If the nodes are not constrained, the associated force is applied to each of these nodes.

The final step in the processing is shown in Listing 10 “46d. This step is to apply the forces to the nodes and integrate the new accelerations, velocities, and positions for each node. Once again, we loop through all of the nodes. The new acceleration is calculated by multiplying the current force by the inverse of the node s mass. Since we are iterating at a nominal 100 hertz, we will calculate the new velocity by adding in .01 of the acceleration. The position is changed by .01 of the new velocity. The last thing we need to do is update the vertex buffer with the new node positions. We have reached the end of the processing for one pass and will sleep for 10 milliseconds before starting it all again.

The last method in the class is the Dispose method shown in Listing 10 “47. The only task for this method is to terminate the processing thread. Clearing the thread_active flag and waiting 100 milliseconds to ensure that the thread has terminated accomplishes this.

Listing 10.47: Cloth Class Dispose Method
start example
 public override void Dispose()        {           thread_active = false;           Thread.Sleep(100);        }     }  } 
end example
 



Introduction to 3D Game Engine Design Using DirectX 9 and C#
Introduction to 3D Game Engine Design Using DirectX 9 and C#
ISBN: 1590590813
EAN: 2147483647
Year: 2005
Pages: 98

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net