--- ./hole.cpp.save.orig Wed Oct 4 11:21:15 2006 +++ ./hole.cpp.save Wed Oct 4 11:22:13 2006 @@ -0,0 +1,1122 @@ +// ---------------------- +// OpenGL Black Hole Simulator. +// +// Written by and Copyright Chris Halsall (chalsall@chalsall.com). +// First published on the O'Reilly Network on Linux.com +// (oreilly.linux.com). September 2000. All rights reserved. +// +// This code is licensed under the GNU GPL Version 2.0. +// See (URL: http://www.gnu.org/copyleft/gpl.html ) for details. +// +// Coded to the groovy tunes of Fluke: Risotto. +// +// Dedicated to Stephen W. Hawking, one of the greatest explorers +// of our time. + +#define PROGRAM_TITLE "O'Reilly Net: Black Hole -- C.Halsall" + + +#include // Always a good idea. +#include // For RAND_MAX. +#include // For our FPS stats. +#include // For M_PI +#include // OpenGL itself. +#include // GLU support library. +#include // GLUT support library. + + +// A quick macro to populate a 1x3 vector array. +#define ourVectInit(a,x,y,z) { (a)[0]=x; (a)[1]=y; (a)[2]=z; } + +// Structure to hold all the data for each particle. +typedef struct { + int Running; + float Pos[3]; // Position. + float Vel[3]; // Velocity. + float Grav[3]; // Acceleration. + float Color[3]; +} Particle; + + +// ------ +// Some global variables. + +// Window and Texture IDs, Window sizes. +int Texture_ID; +int Window_ID; +int Window_Width=640; +int Window_Height=480; + +// We'll request a sphere from the GLU library at run-time. +struct GLUquadric *Black_Hole; + +// "Name" for first (and only) OpenGL display list. +#define STAR_FIELD 1 + + +// Pointer for allocated array of Particles. +Particle *Parts; + +// Particle status variables. +int Parts_Running=0; +int Parts_Allocated=0; +int Parts_LastUnused=1; + +// Number of parts initially in the system. Make sure there's at least +// one over a hundred (101, 801), so our Root particle doesn't get deleted. +int Parts_Num = 801; + +float Parts_Brightness = 0.15; + + +// Drawing flags. +int Draw_Axis = 0; +int Draw_Vectors = 0; +int Draw_Stars = 1; +int Heads_Up = 0; +int Texture_On = 1; + + +// Particle Gun variables. +float Gun_PX = 0; +float Gun_PY = 0; +float Gun_PZ = 4; + +float Gun_VX =-0.135; +float Gun_VY = 0.0125; +float Gun_VZ = 0.0; +float Gun_R = 0.005; +float Gun_OffR = 0.050; + +// Backwards firing and off-target probabilities. +float Gun_Back = 0.9, Gun_Off = 0.9; + +// '.' key toggels between keypad adjusting gun position and eject vect. +int Gun_Control = 1; + + +// Orbit and motion settings. +int On_Orbit=1; +int Move_Enable=1; +int Move_Step=0; + +// Observer initial placement. +float Obs_Angle=114.0; +float Obs_Height=.2; +float Obs_Dist=4.6; + +// Calculated observer location. +float Obs[3]; + +// How quickly do the orbits decay? +// Lower number (limit 1) is faster decay. + +int Decay_Factor = 6000; + +// The force of gravity exterted by the black hole. +float Grav = 0.075; + +// Somewhat arbitrary values for Event and Escape horizons. + +#define EVENT_HORIZON_GRAV .5 + +#define ESCAPE_HORIZON 30 +#define ESCAPE_HORIZON_SQR (ESCAPE_HORIZON * ESCAPE_HORIZON) + + +// ------ +// Frames per second (FPS) statistic variables and routine. + +#define FRAME_RATE_SAMPLES 50 +int FrameCount=0; +float FrameRate=0; + +static void ourDoFPS( + void +) +{ + static clock_t last=0; + clock_t now; + float delta; + + if (++FrameCount >= FRAME_RATE_SAMPLES) { + now = clock(); + delta= (now - last) / (float) CLOCKS_PER_SEC; + last = now; + + FrameRate = FRAME_RATE_SAMPLES / delta; + FrameCount = 0; + } +} + + +// ------ +// String rendering routine; leverages on GLUT routine. + +static void ourPrintString( + void *font, + char *str +) +{ + int i,l=strlen(str); + + for(i=0;i Num) + Parts_LastUnused = Num; + + Parts_Running = 0; + for (i = 0; i < Num; i++) + if (P[i].Running) + Parts_Running++; + + Parts_Allocated = Num; + Parts = P; + + return P; +} + + +// ------ +// Function to return a random floating number between 0 and the passed +// parameter. + +float ourRand( + float Max +) +{ + return( (Max * rand()) / RAND_MAX ); +} + + +// ------ +// Builds a Display List containing a random star field. +// +// Note: this could also be done by calculating the star points in this +// routine, which would be faster than having OpenGL perform two +// rotations (matrix multiplications) for each star. However, this +// technique is simpler and faster for the programmer, and demonstrates +// how successive transformations can be a powerful tool. + +void ourBuildStarfield( + int Stars +) +{ + int Cnt; + + glNewList(STAR_FIELD, GL_COMPILE); + + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + + for ( Cnt = 0; Cnt < Stars; Cnt++) { + + // Vary the color for each star. + glColor4f( + 0.8 + ourRand(0.2), + 0.8 + ourRand(0.2), + 0.8 + ourRand(0.2), + .95); + + // Vary the size. Ensure integer sizes to avoid alias shimmering. + glPointSize(ourRand(2) > 1 ? 1.0 : 2.0); + + // Spin your Universe, round and round.... + glRotatef(ourRand(100),0.0f,1.0f,0.0f); + glRotatef(ourRand(100),1.0f,0.0f,0.0f); + + glBegin(GL_POINTS); + glVertex3f(15.0, 0.0f, 0.0f); + glEnd(); + } + + glPopMatrix(); + glEndList(); +} + + +// ------ +// Function builds a simple alpha channel texture of a dot, +// and then creates mipmaps. This could instead load textures from +// graphics files from disk, or render textures based on external +// input. + +void ourBuildTextures( + void +) +{ + GLenum gluerr; + GLubyte tex[128][128]; + int x,y,t; + int hole_size = 3300; // ~ == 57.45 ^ 2. + + // Generate a texture index, then bind it for future operations. + glGenTextures(1,&Texture_ID); + glBindTexture(GL_TEXTURE_2D,Texture_ID); + + // Iterate across the texture array. + + for(y=0;y<128;y++) { + for(x=0;x<128;x++) { + + // Make a round dot in the texture's alpha-channel. + + // Calculate distance to center (squared). + t = (x-64)*(x-64) + (y-64)*(y-64) ; + + if ( t < hole_size) // Don't take square root; compare squared. + tex[x][y]= 240 - (240 * t) / hole_size + ourRand(15); + else + tex[x][y]=0; // Outside of the dot, it's transparent. + + } + } + + // The GLU library helps us build MipMaps for our texture. + + if ((gluerr=gluBuild2DMipmaps(GL_TEXTURE_2D, 1, 128, 128, GL_ALPHA, + GL_UNSIGNED_BYTE, (void *)tex))) { + + fprintf(stderr,"GLULib%s\n",gluErrorString(gluerr)); + exit(-1); + } + + // Some pretty standard settings for wrapping and filtering. + glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT); + glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT); + + glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR); + glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); + + // We start with GL_MODULATE mode. + glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE); +} + + +// ------ +// Callback routine executed whenever our window is resized. Lets us +// request the newly appropriate perspective projection matrix for +// our needs. Try removing the gluPerspective() call to see what happens. + +void cbResizeScene( + int Width, + int Height +) +{ + // Let's not core dump, no matter what. + if (Height == 0) + Height = 1; + + glViewport(0, 0, Width, Height); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.05f,100.0f); + + glMatrixMode(GL_MODELVIEW); + + Window_Width = Width; + Window_Height = Height; +} + + +// ------ +// Fires the Particle Gun, or, sets up the passed Particle to be +// placed at the Particle Gun location, and fired in a direction +// specified in Gun_Va, with 'a' being "X", "Y", or "Z". +// +// The particles are normally fired with a randomness of Gun_R. +// Occationally (by default 10%) a larger randomness of Gun_OffR is +// added. Also, 10% of the time, the particles are fired backwards. +// This is used to introduce a bit of non-uniformity. Gun_R and +// Gun_OffR can be controled with the '3' and '6', and '/' and '*' +// keys, respectively. If set to zero, effects are removed. + +static void ourFireParticleGun( + Particle *p +) +{ + float r; + int c; + int Dir = 1; + + if (!p->Running) { + p->Running=1; + Parts_Running++; + } + + if (p == Parts) { // Root part. + ourVectInit(p->Color,1,1,0); // Bright Yellow + ourVectInit(p->Pos,5.0,0,0); // Location + ourVectInit(p->Vel,0,Gun_VY,-Gun_VX*0.95); // Velocity + return; + } + + // Regular particle. + + ourVectInit(p->Pos,Gun_PX,Gun_PY,Gun_PZ); + r = Gun_R; + +// This creates a few a very unpredicatable trajectories. It actually +// works out to be much less than a full 10 percent, as many are eatten +// or escape within a very short period of time. Only a few enter a +// stable orbit. + + if (ourRand(1) > Gun_Off) r += Gun_OffR; + + if (ourRand(1) > Gun_Back) Dir = -1; + + ourVectInit(p->Vel, + (Gun_VX + r-ourRand(2*r)) * Dir, + (Gun_VY + r-ourRand(2*r)) * Dir, + (Gun_VZ + r-ourRand(2*r)) * Dir); + + c = (int)(ourRand(5) + 1.5); // Range of 1 to 6. + + // The last set of numbers bias the colors to blue. Red is nice too. + + ourVectInit(p->Color, + (c & 0x01 ? 0.9 : 1.0) * 0.7, + (c & 0x02 ? 0.9 : 1.0) * 0.7, + (c & 0x04 ? 0.9 : 1.0) * 1.0 + ); +} + + +// ------ +// Calculates the next position based on the current velocity vector, +// then calculates the new velocity vector based on the particle's +// proximity to the black hole. +// +// We do the motion calculation before updating the velocity vector +// (and calculating the acceleration-because-of-gravity vector) so +// that our Vector Display option will be correct. If we didn't do +// this, the gravity vector would not point towards (0,0,0) when +// we drew it, outside of this function. + +static void ourMoveParticle( + Particle *p +) +{ + float dp2, dsx, dsy, dsz, G, d; + + // Used to randomly kill and re-create particles. + if (p != Parts) + if (ourRand(1) > 0.9998) { + ourFireParticleGun(p); + return; + } + + // We're actually going to move this particle... + + // We first move it based on the LAST iteration's + // calculation of the Velocity... + p->Pos[0] += p->Vel[0]; + p->Pos[1] += p->Vel[1]; + p->Pos[2] += p->Vel[2]; + + // ...and then proceed to calculate the force of gravity at the new + // position, and update our velocity vector. + + dsx = p->Pos[0] * p->Pos[0]; + dsy = p->Pos[1] * p->Pos[1]; + dsz = p->Pos[2] * p->Pos[2]; + + // Calculate the square of the distance. + dp2 = dsx + dsy + dsz; + + if (dp2) { + // May wish to scale dp2 (change 1.0); effects gravity gradiant. + G = Grav / (dp2 * 1.0); + d = sqrt(dp2); + } + + // If the force of gravity is too strong, our algorithim breaks + // down and conservation of energy isn't maintained. We consider + // this the event horizon, and recycle the particle. + if (G > EVENT_HORIZON_GRAV) { + ourFireParticleGun(p); + return; + } + + if (dp2 > ESCAPE_HORIZON_SQR) { + // Particle escaped; lucky it. + ourFireParticleGun(p); + return; + } + + // OK, this particle is staying in the system. Calculate the + // vectors.... + + // We store the components of the force of gravity for + // our Vectors display. Note the negative magnitude; the vector + // must point _from_ our particle _towards_ (0,0,0). + p->Grav[0] = - G * p->Pos[0] / d; + p->Grav[1] = - G * p->Pos[1] / d; + p->Grav[2] = - G * p->Pos[2] / d; + + // Simply add the gravity vector to the current velocity vector. + p->Vel[0] += p->Grav[0]; + p->Vel[1] += p->Grav[1]; + p->Vel[2] += p->Grav[2]; + + if (p != Parts) { + // This handles orbit decay; not correctly, but well enough. + // (Decay should be a ratio to the vector length, applied to each + // vector component here, rather than each component being effected + // based on its individual size. The effect is to circlurize the + // orbit, which we want anyway.) + + p->Vel[0] -= p->Vel[0] / Decay_Factor; + p->Vel[1] -= p->Vel[1] / Decay_Factor; + p->Vel[2] -= p->Vel[2] / Decay_Factor; + } +} + + +// ------ +// Angle to Radian conversion. + +float ourA2R( + float Angle +) +{ + return Angle * M_PI/180; +} + + +// ------ +// Calculates the observer's XYZ position from their Distance from +// the origin, the angle and the height. + +static void ourCalcObs(void) +{ + Obs[0]=Obs_Dist * sin(ourA2R(Obs_Angle)); + Obs[1]=Obs_Height; + Obs[2]=Obs_Dist * cos(ourA2R(Obs_Angle)); +} + + +// ------ +// Draws the X, Y, and Z axis lines. + +void ourRenderAxis( + void +) +{ + glBegin(GL_LINES); + + glColor4f(0.5,0.5,0.0,1.0); // Mid-level yellow. + + // Three primary axis lines. + glVertex3f(100,0,0); + glVertex3f(-100,0,0); + glVertex3f(0,100,0); + glVertex3f(0,-100,0); + glVertex3f(0,0,100); + glVertex3f(0,0,-100); + + glColor4f(0.25,0.25,0.0,1.0); // Low-level yellow. + + // Two pairs of secondary lines for X and Z axis. + glVertex3f(100,1,0); + glVertex3f(-100,1,0); + glVertex3f(100,-1,0); + glVertex3f(-100,-1,0); + + glVertex3f(0,1,100); + glVertex3f(0,1,-100); + glVertex3f(0,-1,100); + glVertex3f(0,-1,-100); + + glColor4f(0.0,0.5,0.0,1.0); // Mid-level green. + + // Lable the X axis. + glVertex3f(1.0,0.9,0); + glVertex3f(1.1,0.8,0); + glVertex3f(1.1,0.9,0); + glVertex3f(1.0,0.8,0); + + // And the Z. + glVertex3f(0,0.9,1.0); + glVertex3f(0,0.9,1.1); + glVertex3f(0,0.9,1.1); + glVertex3f(0,0.8,1.0); + glVertex3f(0,0.8,1.0); + glVertex3f(0,0.8,1.1); + + glEnd(); +} + + +// ------ +// Draws the Gravity and Velocity Vectors for each active particle. + +void ourRenderVectors( + void +) +{ + int i; + + glBegin(GL_LINES); + + for (i=0; iPos[0], Parts->Pos[1],Parts->Pos[2]); + glRasterPos2i(6,-16); + ourPrintString(GLUT_BITMAP_HELVETICA_12,buf); + + // And the Root Particle's velocity. + sprintf(buf,"RootV: ( %-.4f, %-.4f, %-.4f)", + Parts->Vel[0], Parts->Vel[1],Parts->Vel[2]); + glRasterPos2i(6,-32); + ourPrintString(GLUT_BITMAP_HELVETICA_12,buf); + + // Done with this special projection matrix. Throw it away. + glPopMatrix(); +} + + +// ------ +// Routine which actually does the drawing + +static void cbRenderScene(void) +{ + Particle *p; + int i; + + // For the first few objects, we want full depth-buffer testing. + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + + // Clear the screen. + glClearColor(0.00,0.00,0.00,1.0); + glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); + + // Ensure we're working with the model matrix. + glMatrixMode(GL_MODELVIEW); + + // Reset to 0,0,0; no rotation, no scaling. + glLoadIdentity(); + + // Are we on-orbit, or wandering around? + if (On_Orbit) { + gluLookAt(Parts->Pos[0],Parts->Pos[1],Parts->Pos[2], + 0.0,0.0,0.0, + 0.0,1.0,0.0); + } else { + gluLookAt(Obs[0],Obs[1],Obs[2], + 0.0,0.0,0.0, + 0.0,1.0,0.0); + } + + // No texturing. + glDisable(GL_TEXTURE_2D); + + // Black holes are BLACK! + glColor4f(0.0,0.0,0,1.0); + gluSphere(Black_Hole,0.02,8,8); + + if (Draw_Stars) + glCallList(STAR_FIELD); + + if (Draw_Vectors) + ourRenderVectors(); + + if (Draw_Axis) + ourRenderAxis(); + + // We don't want any of the particles to obscure any others, but + // we DO want the black hole to block any particles behind it. + // Note that GL_DEPTH_TEST is still enabled. + glDepthMask(GL_FALSE); + + // Enable the dot texture. "Oh no! Not THE DOT!" + if (Texture_On) + glEnable(GL_TEXTURE_2D); + + // Iterate through the array of particles, drawing all that are + // active. For those that aren't active, 0.03% of the time, we + // introduce them into the system. + + for(i=0; iRunning) { + if (Move_Enable && ourRand(1) > 0.9997) + ourFireParticleGun(p); + } else { + // Set the part's color. + glColor4f(p->Color[0],p->Color[1],p->Color[2], + Parts_Brightness); + + // Draw two intersecting quads, along XY and ZY axis. + glBegin(GL_QUADS); + + glTexCoord2f(0.0,0.0); + glVertex3f(p->Pos[0]-.00,p->Pos[1]-.10,p->Pos[2]-.10); + glTexCoord2f(1.0,0.0); + glVertex3f(p->Pos[0]-.00,p->Pos[1]+.10,p->Pos[2]-.10); + glTexCoord2f(1.0,1.0); + glVertex3f(p->Pos[0]-.00,p->Pos[1]+.10,p->Pos[2]+.10); + glTexCoord2f(0.0,1.0); + glVertex3f(p->Pos[0]-.00,p->Pos[1]-.10,p->Pos[2]+.10); + + glTexCoord2f(0.0,0.0); + glVertex3f(p->Pos[0]-.10,p->Pos[1]-.10,p->Pos[2]-.00); + glTexCoord2f(1.0,0.0); + glVertex3f(p->Pos[0]-.10,p->Pos[1]+.10,p->Pos[2]-.00); + glTexCoord2f(1.0,1.0); + glVertex3f(p->Pos[0]+.10,p->Pos[1]+.10,p->Pos[2]+.00); + glTexCoord2f(0.0,1.0); + glVertex3f(p->Pos[0]+.10,p->Pos[1]-.10,p->Pos[2]+.00); + + glEnd(); + + if (Move_Enable) + ourMoveParticle(p); + } + } + + + if (Heads_Up) + ourRenderHeadsUp(); + + // All done drawing. Let's show it. + glutSwapBuffers(); + + // This handles our single-step function. + if (Move_Step) + Move_Step = Move_Enable = 0; + + // Collect the FPS statistics. + ourDoFPS(); +} + + +// ------ +// Callback function called when a normal key is pressed. + +void cbKeyPressed( + unsigned char key, + int x, int y +) +{ + int t; + + switch (key) { + case 27: + case 'q': case 'Q': + exit(0); + break; + + // Toggle drawing. + case 'a': case 'A': + Draw_Axis = !Draw_Axis; + break; + case 'v': case 'V': + Draw_Vectors = !Draw_Vectors; + break; + case 's': case 'S': + Draw_Stars = !Draw_Stars; + break; + + // Adjust particle brightness. + case 'b': + Parts_Brightness+=0.01; + break; + case 'B': + Parts_Brightness-=0.01; + break; + + // Toggle being on-orbit. + case 'o': case 'O': + On_Orbit = ! On_Orbit; + break; + + // Toggle Texture. + case 't': case 'T': + Texture_On = !Texture_On; + break; + + // Impart an impulse on the Root particle. + case 'x': + Parts->Vel[0]+=.0001; + break; + case 'X': + Parts->Vel[0]-=.0001; + break; + case 'c': case 'y': + Parts->Vel[1]+=.0001; + break; + case 'C': case 'Y': + Parts->Vel[1]-=.0001; + break; + case 'z': + Parts->Vel[2]+=.0001; + break; + case 'Z': + Parts->Vel[2]-=.0001; + break; + + case 'r': case 'R': + ourFireParticleGun(Parts); + + // Single-step through motion calculations. + case 'm': + Move_Step = 1; + Move_Enable = 1; + break; + + // Normal motion. + case 'M': + Move_Enable= !Move_Enable; + break; + + // Heads up display. + case 'h': case 'H': + Heads_Up = !Heads_Up; + break; + + // Inject one (or all) free particle(s). + case 'i': case 'I': + for (t=Parts_LastUnused; t 100) { + Parts_Num -= 100; + ourAllocParticles(Parts_Num); + } + break; + + case '+': + Parts_Num += 100; + ourAllocParticles(Parts_Num); + break; + + default: + printf("No action for %d\n", key); + + } +} + + +// ------ +// Callback Function called when a special key is pressed. + +static void cbSpecialKeyPressed(int key, int x, int y) +{ + switch (key) { + case GLUT_KEY_PAGE_UP: + Obs_Dist -= 0.05f; + break; + + case GLUT_KEY_PAGE_DOWN: + Obs_Dist += 0.05f; + break; + + case GLUT_KEY_LEFT: + Obs_Angle-=2.0; + break; + + case GLUT_KEY_RIGHT: + Obs_Angle+=2.0; + break; + + case GLUT_KEY_DOWN: + Obs_Height-=0.05; + break; + + case GLUT_KEY_UP: + Obs_Height+=0.06; + break; + } + + // We don't know anything changed, but it never hurts. + ourCalcObs(); +} + + +// ------ +// Does everything needed before losing control to the main +// OpenGL event loop + +void ourInit( + int Width, + int Height +) +{ + ourBuildTextures(); + ourBuildStarfield(500); + + glEnable(GL_BLEND); + glDisable(GL_ALPHA_TEST); + + // Enable flat shading -- no need for smooth. + glShadeModel(GL_FLAT); + + // Blending mode used for fire, lit gas, etc. + glBlendFunc(GL_SRC_ALPHA,GL_ONE); + + // Calculate the non-on-orbit observer's position. + ourCalcObs(); + + // Load up the correct perspective matrix; using a callback directly. + cbResizeScene(Width, Height); + + if (!(Black_Hole = gluNewQuadric())) + exit; + + // Allocate our first block of particles. + ourAllocParticles(Parts_Num); + + // Fire off the first (Root) Particle. + ourFireParticleGun(Parts); + +} + + +// ------ +// The main() function. Inits OpenGL. Calls our own init function, +// then passes control onto OpenGL. + +int main(int argc,char **argv) +{ + glutInit(&argc,argv); + + // To see OpenGL drawing, take out the GLUT_DOUBLE request. + glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); + glutInitWindowSize(Window_Width,Window_Height); + + // Open a window + + if (!(Window_ID=glutCreateWindow( PROGRAM_TITLE ))) { + fprintf(stderr,"Error opening a window.\n"); + exit(-1); + } + + // Register the callback function to do the drawing. + glutDisplayFunc(&cbRenderScene); + + // If there's nothing to do, draw. + glutIdleFunc(&cbRenderScene); + + // It's a good idea to know when our window's resized. + glutReshapeFunc(&cbResizeScene); + + // And let's get some keyboard input. + glutKeyboardFunc(&cbKeyPressed); + glutSpecialFunc(&cbSpecialKeyPressed); + + // OK, OpenGL's ready to go. Let's call our own init function. + ourInit(Window_Width, Window_Height); + + // Print out a bit of help dialog. + printf("\n" PROGRAM_TITLE "\n\n\ +Use arrow keys to rotate around or move along Y axis.\n\ +Page up/down will move observer towards/away from the Y axis.\n\n\ +'O' toggles observer onto non-decaying orbit of yellow root particle.\n\ +'H' toggles heads-up-display of various status variables.\n\n\ +'x'/'X', 'c'/'C', 'z'/'Z' thrusts root particle along X, Y, Z axis\n\ +in positive/negative directions, respectively; 'R' resets.\n\n\ +Numerical Keypad controls Particle Gun parameters. '.' key switches\n\ +between effecting Gun Velocity Vector and Position.\n\n\ +'+' and '-' add to or remove particles from the system.\n\n\ +Use first letter of shown display mode settings to alter.\n\n\ +Q or [Esc] to quit; OpenGL window must have focus for input.\n"); + + // Pass off control to OpenGL. + // Above functions are called as appropriate. + glutMainLoop(); + + // Free our allocated particle array. + ourAllocParticles(0); + + return 1; +} +