www.codeworx.org/opengl-tutorials/Tutorial 19: Partikel Engine

Lektion 19: Partikel Engine

Willkommen zu Tutorial 19. Es ist an der Zeit das bisher Gelernte praktisch und ein wenig spielerisch umzusetzen.
Es soll eine einigermaßen komplexe Partikel Engine herrauskommen. Grundsätzlich lassen sich damit diverse Effekte wie Fontänen, Feuer oder Rauch erstellen, die recht realistisch wirken.
Es kann sein das der Code etwas konfus wirkt, es ist meine erste Partikel Engine ;) . Dies ist mein persönlicher Ansatz solch eine Engine zu verwirklichen, am Anfang stand die Idee jedes Partikel als einzelnes Objekt zu behandeln was von A nach B fliegt, ein gängiges Verfahren. Der Code aus der ersten Lektion wird entsprechend erweitert werden.

5 neue Zeilen kommen schon am Anfang dazu. stdio.h ermöglicht den Zugriff auf Dateien. MAX_PARTICLES speichert wieviele Partikel ausgegeben werden sollen, der "Rainbow Mode", legt fest ob die Partikel ihre Farben wechseln sollen oder nicht, dazu aber später. sp und rp sollen speichern ob Return oder Enter gedrückt wurden.

#include <windows.h> // Header File For Windows
#include <stdio.h> // Header File For Standard Input/Output ( ADD )
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <gl\glaux.h> // Header File For The GLaux Library
#define MAX_PARTICLES 1000 // Number Of Particles To Create ( NEW )
HDC hDC=NULL; // Private GDI Device Context
HGLRC hRC=NULL; // Permanent Rendering Context
HWND hWnd=NULL; // Holds Our Window Handle
HINSTANCE hInstance; // Holds The Instance Of The Application
bool keys[256]; // Array Used For The Keyboard Routine
bool active=TRUE; // Window Active Flag Set To TRUE By Default
bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default
bool rainbow=true; // Rainbow Mode? ( ADD )
bool sp; // Spacebar Pressed? ( ADD )
bool rp; // Return Key Pressed? ( ADD )

Die Variable slowdown steuert die Geschwindigkeit der Partikel, je höher der Wert, destso langsamer werden diese. xspeed und yspeed steuern die Richtung des Partikelstrahls. Bei jedem Durchgang werden diese Werte zu den X- und Y-Positionen der Partikel addiert. Ist xspeed positiv, werden die Teilchen nach rechts bewegt, bei negativen Werten entsprechend nach links. yspeed steuert die Richtung analog dazu auf der Y-Achse. Je höher die Beträge der Werte destso schneller bewegen sich die Partikel (Es gibt aber noch einige andere Werte die die Partikel in ihrer Richtung und Geschwindigkeit beeinflussen. Dazu aber später mehr.) . zoom steuert, wie der Name schon vermuten läßt die Distanz des Betrachters zur Szene.

float slowdown = 2.0f; // Slow Down Particles
float xspeed;          // Base X Speed (To Allow Keyboard Direction Of Tail)
float yspeed;          // Base Y Speed (To Allow Keyboard Direction Of Tail)
float zoom = -40.0f;   // Used To Zoom Out

Die Variable loop wird nachher bei der Ausgabe der Partikel benötigt, col speichert deren aktuelle Farbe. delay wird beim "Rainbow Mode" benutzt. Man könnte die einzelnen Partikel auch durch Punkte darstellen lassen, aber texturierte Primitive sehen besser aus, da die Partikel dann ein ganz individuelles Aussehen haben können (Man denke an kleine Sterne, Fotos oder was auch immer) . Daher wird Platz für eine einzige Textur benötigt.

GLuint loop; // Misc Loop Variable
GLuint col; // Current Color Selection
GLuint delay; // Rainbow Effect Delay
GLuint texture[1]; // Storage For Our Particle Texture

So, jetzt zu den spannenden Sachen. Da alle Partikel die gleichen Eigenschaften haben sollen, bietet es sich an, deren Werte wie Lebensdauer, Position usw in eine Struktur zu packen.
Die erste Eigenschaft ist active. Ist active TRUE, ist das Partikel zu sehen und (normalerweise) auch in Bewegung. Bei FALSE abgeschaltet oder gar nicht genutzt. life und fade kontrollieren die Lebensdauer und die Helligkeit des Partikels. Die Lebensdauer und die Helligkeit hängen zusammen, je heller ein Partikel leuchtet destso länger wird es auch zusehen sein. Es wirkt natürlich realistischer wenn einige Partikel länger zu sehen sind als andere.

typedef struct // Create A Structure For Particle
{
   bool active; // Active (Yes/No)
   float life; // Particle Life
   float fade; // Fade Speed

Die Variablen r, g und b sind für die Farbe verantwortlich. Sie schwanken wie immer zwischen 0.0f und 1.0f und stehen stellvertretend für den Rot-, Grün- und Blauwert des Partikels.

   float r; // Red Value
   float g; // Green Value
   float b; // Blue Value

x, y und z speichern die Positionen der Partikel auf den entsprechenden Achsen.

   float x; // X Position
   float y; // Y Position
   float z; // Z Position

Die nächsten drei Variablen steuern die Geschwindigkeit und Richtung des Teilchens auf den entsprechenden Achsen und werden in jedem Durchgang zu den Positionswerten addiert.

   float xi; // X Direction
   float yi; // Y Direction
   float zi; // Z Direction

Gravitation spielt auch eine Rolle um der Realität etwas näher zu kommen. Ist yg zum Beispiel negativ, wird das Teilchen nach unten gezogen.

   float xg; // X Gravity
   float yg; // Y Gravity
   float zg; // Z Gravity
}
particles; // Particles Structure

Da wir ja mehrere Partikel auf dem Bildschirm haben wollen, muss auch ein entsprechendes Array aus Einzelpartikeln erstellt werden. MAX_PARTICLES wurde ja am Anfang auf 1000 gesetzt, daher gibt es hier jetzt auch 1000 Partikel.

particles particle[MAX_PARTICLES]; 
// Particle Array (Room For Particle Info)

Um später Schreibarbeit zu sparen, werden hier schonmal 12 Farbewerte gespeichert, die dann später sehr einfach als "colors" bei Bedarf eingesetzt werden können. Man beachte auch hier die drei Komponenten der Farben (RGB).

static GLfloat colors[12][3]= // Rainbow Of Colors
{
   {1.0f,0.5f,0.5f},{1.0f,0.75f,0.5f},{1.0f,1.0f,0.5f},{0.75f,1.0f,0.5f},
   {0.5f,1.0f,0.5f},{0.5f,1.0f,0.75f},{0.5f,1.0f,1.0f},{0.5f,0.75f,1.0f},
   {0.5f,0.5f,1.0f},{0.75f,0.5f,1.0f},{1.0f,0.5f,1.0f},{1.0f,0.5f,0.75f}
};
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc

Der Code zum Laden von Bitmaps aus den vergangen Lektionen ist der gleiche geblieben:

AUX_RGBImageRec *LoadBMP(char *Filename) // Loads A Bitmap Image
{
   FILE *File=NULL; // File Handle
   if (!Filename) // Make Sure A Filename Was Given
   {
      return NULL; // If Not Return NULL
   }
   File=fopen(Filename,"r"); // Check To See If The File Exists
   if (File) // Does The File Exist?
   {
      fclose(File); // Close The Handle
      return auxDIBImageLoad(Filename); // Load The Bitmap And Return A Pointer
   }

   return NULL; // If Load Failed Return NULL
}

Auch der Code zum Konvertieren der Texturen bleibt so wie er ist, es wird ein Bitmap geladen und als OpenGL-Textur gespeichert.

int LoadGLTextures() // Load Bitmaps And Convert To Textures
{
   int Status=FALSE; // Status Indicator
   AUX_RGBImageRec *TextureImage[1]; // Create Storage Space For The Texture
   memset(TextureImage,0,sizeof(void *)*1); // Set The Pointer To NULL
Our texture loading code will load in our particle bitmap and convert it to a linear filtered texture.
   if (TextureImage[0]=LoadBMP("Data/Particle.bmp")) // Load Particle    Texture
   {
      Status=TRUE; // Set The Status To TRUE
      glGenTextures(1, &texture[0]); // Create One Textures
      glBindTexture(GL_TEXTURE_2D, texture[0]);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
      glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY,    
                   0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
   }
   if (TextureImage[0]) // If Texture Exists
   {
      if (TextureImage[0]->data) // If Texture Image Exists
      {
         free(TextureImage[0]->data); // Free The Texture Image Memory
      }

      free(TextureImage[0]); // Free The Image Structure
   }

   return Status; // Return The Status
}

Hier eine kleine Änderung, es wird nicht 100.0f sondern 200.0f Einheiten ausgezoomt.

GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // Resize And Initialize The GL Window
{
   if (height==0) // Prevent A Divide By Zero By
   {
      height=1; // Making Height Equal One
   }
   glViewport(0, 0, width, height); // Reset The Current Viewport
   glMatrixMode(GL_PROJECTION); // Select The Projection Matrix
   glLoadIdentity(); // Reset The Projection Matrix
   // Calculate The Aspect Ratio Of The Window
   gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,200.0f); ( MODIFIED    )
   glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix
   glLoadIdentity(); // Reset The Modelview Matrix
}

InitGL muss noch dahingehend abgeändert werden, das die Textur geladen, das smooth shading und texturmapping aktiviert und der Hintergrund geschwärzt werden.

int InitGL(GLvoid) // All Setup For OpenGL Goes Here
{
   if (!LoadGLTextures()) // Jump To Texture Loading Routine
   {
      return FALSE; // If Texture Didn't Load Return FALSE
   }
   glShadeModel(GL_SMOOTH); // Enables Smooth Shading
   glClearColor(0.0f,0.0f,0.0f,0.0f); // Black Background
   glClearDepth(1.0f); // Depth Buffer Setup
   glDisable(GL_DEPTH_TEST); // Disables Depth Testing
   glEnable(GL_BLEND); // Enable Blending
   glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Type Of Blending To Perform
   glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST); // Really Nice Perspective Calculations
   glHint(GL_POINT_SMOOTH_HINT,GL_NICEST); // Really Nice Point Smoothing
   glEnable(GL_TEXTURE_2D); // Enable Texture Mapping
   glBindTexture(GL_TEXTURE_2D,texture[0]); // Select Our Texture

Der untenstehende Code geht durch das Partikelarray und aktiviert die Partikel nacheinander und setzt deren Lebensdauer auf 1.0f.

for (loop=0;loop<MAX_PARTICLES;loop++) // Initials All The Particles
{
   particle[loop].active=true; // Make All The Particles Active
   particle[loop].life=1.0f; // Give All The Particles Full Life

Jedes Partikel soll eine "zufällig" ermittelte Lebensdauer haben. life wird bei jedem Durchgang um den Wert von fade reduziert. fade soll einen sehr kleinen Wert haben, dieser liegt zwischen 0,003 und 0,102.

   particle[loop].fade=float(rand()%100)/1000.0f+0.003f; // Random Fade Speed

Jetzt zu den Farben. Am Anfang soll jedes Partikel eine der 12 vordefinierten Farbe bekommen. Dazu ist ein wenig Mathe erforderlich. Die loop-Variable wird mit der Farbanzahl (12) multipliziert und durch die Gesamtanzahl der Partikel geteilt. Das verhindert das Farbwerte herrauskommen die größer als 11 und kleiner als 0 sind.

Zwei kurze Bespiele: 900*(12/900)=12. 1000*(12/1000)=12, usw.

   particle[loop].r=colors[loop*(12/MAX_PARTICLES)][0]; // Select Red Rainbow Color
   particle[loop].g=colors[loop*(12/MAX_PARTICLES)][1]; // Select Red Rainbow Color
   particle[loop].b=colors[loop*(12/MAX_PARTICLES)][2]; // Select Red Rainbow Color

Jetzt wird die Richtung der Partikel festgelegt. Mit 10.0f wird multipliziert um beim Programmstart eine kleine Explosion zu erzeugen ;). Am Ende kommt eine positive oder negative Zufallszahl herraus, die Partikel haben dann unterschiedliche Geschwindigkeiten.

   particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Random Speed On X Axis
   particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Random Speed On Y Axis
   particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Random Speed On Z Axis

Gravitation soll es vorerst nur auf der Y-Achse geben, damit eine Art Erdanziehung nach simuliert wird. Die anderen Richtungen sollen noch keine Rolle spielen.

   particle[loop].xg=0.0f; // Set Horizontal Pull To Zero
   particle[loop].yg=-0.8f; // Set Vertical Pull Downward
   particle[loop].zg=0.0f; // Set Pull On Z Axis To Zero
  }

  return TRUE; // Initialization Went OK
}

Jetzt zum Kern der Partikelengine, der Teilchenbewegung. Die Modelview Matrix muss nur einmal zurückgesetzt werden. Die Partikel werden ohne glTranslatef an ihre Positionen gesetzt um nicht bei jedem Frame 1000 Mal an der Modelview Matrix rumschieben zu müssen. (Es werden lediglich die absoluten Positionen mit glVertex3f angegeben.)

int DrawGLScene(GLvoid) // Where We Do All The Drawing
{
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer
   glLoadIdentity(); // Reset The ModelView Matrix

Wieder wird eine Schleife durch alle Partikel benötigt.

   for (loop=0;loop<MAX_PARTICLES;loop++) // Loop Through All The Particles
   {

Ein Partikel soll nur bewegt werden wenn es auch aktiv ist. Ist das nicht der Fall muss auch nichts geupdates werden.

      if (particle[loop].active) // If The Particle Is Active
      {

x, y und z sind temporär um die aktuellen Positionen der Partikel zu berechnen. Zur z-Variable des aktuellen Partikels muss der zoom addiert werden, daher dieser kleine "Umweg".

         float x=particle[loop].x; // Grab Our Particle X Position
         float y=particle[loop].y; // Grab Our Particle Y Position
         float z=particle[loop].z+zoom; // Particle Z Pos + Zoom

Nun die Farbe des Partikels. Die Farbwerte werden in der richtigen Reihenfolge (RGB) übergeben, life wird für den Alpha-Wert genutzt. Je älter das Partikel ist, destso transparenter wird es. Sollen die Partikel generell länger "brennen", muss der fade-Faktor verkleinert werden, life sollte aber nicht größer als 1.0f gesetzt werden.

         // Draw The Particle Using Our RGB Values, Fade The Particle Based On It's Life
         glColor4f(particle[loop].r,
                   particle[loop].g,
                   particle[loop].b,
                   particle[loop].life);

Die Farbe ist festgesetzt und die Position berechnet, Zeit zur eigentlichen Ausgabe. Die Partikel sollen mit einem sogenannten "triangle strip" ausgegeben werden.

glBegin(GL_TRIANGLE_STRIP); // Build Quad From A Triangle Strip

Zitiert (und übersetzt) aus dem Red Book: "Ein triangle strip gibt eine Reihe von Dreicken (drei-seitigen Polygonen) aus, nach dem Schema V0, V1, V2, dann V2, V1, V3, weiter mit V2, V3, V4 usw (V steht jeweils für Vertex). Diese Reihenfolge garantiert das alle Dreiecke die gleiche Ausrichtung haben und der triangle strip eine Oberfläche beschreiben kann."

3 Punkte müssen mindestens übergeben werden. Und genau in der beschriebenen Reihenfolge werden die Dreiecke auch ausgegeben. Es gibt zwei Gründe warum hier ein triangle strip gewähöt wurde. Zum ersten: Nachdem die drei Punkte für das erste Dreieeck festgelegt wurden, wird für jedes weitere Dreieck nur noch jeweils ein einziger Punkt gebraucht. Die anderen beiden werden nocheinmal verwendet. Zweitens wird dadurch der Code verkürzt und die benötigten Daten veringert.
(Die Anzahl der ausgegebenen Dreiecke entspricht der Anzahl der Einzelpartikel minus 2. Der untere Code benötigt 4 statt 6 Punkte um 2 Dreiecke zu erzeugen.)

   glTexCoord2d(1,1); glVertex3f(x+0.5f,y+0.5f,z); // Top Right
   glTexCoord2d(0,1); glVertex3f(x-0.5f,y+0.5f,z); // Top Left
   glTexCoord2d(1,0); glVertex3f(x+0.5f,y-0.5f,z); // Bottom Right
   glTexCoord2d(0,0); glVertex3f(x-0.5f,y-0.5f,z); // Bottom Left

Zum Schluss wird OpenGL mitgeteilt das jetzt keine Punkte mehr übergeben werden.

glEnd(); // Done Building Triangle Strip

Jetzt können die Partikel bewegt werden. Der Code sieht ein wenig schwierig aus, ist er aber nicht. Zuerst wird zur momentanen x-Position die Wegstrecke xi, die pro Frame zurückgelegt werden soll, addiert. Das ganze wird durch 1000 divisiert, mit slowdown, dem "Bremswert", multipliziert. Je größer slowdown also ist, destso langsamer ist das Teilchen am Ende. Das Ganze passiert natürlich auch mit den anderen Komponenten, y und z.

   particle[loop].x+=particle[loop].xi/(slowdown*1000); // Move On The X Axis By X Speed
   particle[loop].y+=particle[loop].yi/(slowdown*1000); // Move On The Y Axis By Y Speed
   particle[loop].z+=particle[loop].zi/(slowdown*1000); // Move On The Z Axis By Z Speed

Die Gravitation fehlt noch und wird zur Geschwindigkeit dazuaddiert, da es sich um eine Beschleunigung handelt. Von Frame zu Frame werden die Partikel also immer schneller in die Gravitationsrichtung gezogen (was ja auch physikalisch durchaus Sinn macht).

   particle[loop].xi+=particle[loop].xg; // Take Pull On X Axis Into Account
   particle[loop].yi+=particle[loop].yg; // Take Pull On Y Axis Into Account
   particle[loop].zi+=particle[loop].zg; // Take Pull On Z Axis Into Account

Da die Partikel irgendwann "ausgebrannt" sein sollen, muss in jedem Frame ein wenig von der Lebensdauer abgezogen werden, nämlich genau der fade-Wert.

   particle[loop].life-=particle[loop].fade; // Reduce Particles Life By 'Fade'

Es wird auch gleich ermittelt ob das Partikel jetzt noch existiert.

   if (particle[loop].life<0.0f) // If Particle Is Burned Out
   {

Damit der Partikelstrom nicht irgendwann abreißt, wird ein neues Teilchen hinzugefügt

      particle[loop].life=1.0f; // Give It New Life
      particle[loop].fade=float(rand()%100)/1000.0f+0.003f; // Random Fade Value

Auch die Positionsangaben werden wieder auf 0 gesetzt

      particle[loop].x=0.0f; // Center On X Axis
      particle[loop].y=0.0f; // Center On Y Axis
      particle[loop].z=0.0f; // Center On Z Axis

Die Geschwindigkeit wird ebenfalls zurückgesetzt. Wieder werden Zufallszahlen genutzt damit nich alle Teilchen die gleiche Geschwindigkeit haben. Da es nur am Anfang zu einer Explosion kommen sollte, wird jetzt nicht mehr mit 10.0f multipliziert.

      particle[loop].xi=xspeed+float((rand()%60)-32.0f); // X Axis Speed And Direction
      particle[loop].yi=yspeed+float((rand()%60)-30.0f); // Y Axis Speed And Direction
      particle[loop].zi=float((rand()%60)-30.0f); // Z Axis Speed And Direction

Die Farbe soll auch erneuert werden. col speichert einen Wert zwischen 0 und 11, für die 12 Farben. Da ja jede der 12 Farben aus den drei Farbkomponenten Rot, Grün und Blau besteht. müssen diese auch in der richtigen Reihenfolge an die entsprechenden Variablen des Teilchens übergeben werden (Einfach nochmal einen Blick auf die Farbdeklaration am Anfang des Programm werfen!).

      particle[loop].r=colors[col][0]; // Select Red From Color Table
      particle[loop].g=colors[col][1]; // Select Green From Color Table
      particle[loop].b=colors[col][2]; // Select Blue From Color Table
   }

Der Benutzer soll ein paar Werte per Tastatur verändern können. Zuerst die Gravitation in y-Richtung mit der 8 auf dem Numpad. Allerdings werden dabei ein paar Grenzen gesetzt, so wird die Gravitation nur bis zu 1.5f erhöht (Höhere Werte sehen nicht gerade toll aus...). Es mag etwas merkwürdig aussehen, das diese Abfrage für jedes Partikel einzeln passieren soll, aber man müßte entweder mehr Variablen einführen oder noch eine weitere Schleife durch alle Partikel an anderer Stelle einbauen.

   // If Number Pad 8 And Y Gravity Is Less Than 1.5 Increase Pull Upwards
   if (keys[VK_NUMPAD8] && (particle[loop].yg<1.5f)) particle[loop].yg+=0.01f;

Die Y-Gravitation soll mit der 2 auf dem Numpad niedriger gestellt werden können.

   // If Number Pad 2 And Y Gravity Is Greater Than -1.5 Increase Pull Downwards
   if (keys[VK_NUMPAD2] && (particle[loop].yg>-1.5f)) particle[loop].yg-=0.01f;

Mit 6 wird der Partikelstrahl nach rechts gedrückt, also die Gravitation in X-Richung erhöht.

   // If Number Pad 6 And X Gravity Is Less Than 1.5 Increase Pull Right
   if (keys[VK_NUMPAD6] && (particle[loop].xg<1.5f)) particle[loop].xg+=0.01f;

Der gleiche Spaß mit der 4 in die entgegengesetzte Richtung:

   // If Number Pad 4 And X Gravity Is Greater Than -1.5 Increase Pull Left
   if (keys[VK_NUMPAD4] && (particle[loop].xg>-1.5f)) particle[loop].xg-=0.01f;

Mit der TAB-Taste kann die Explosion wiederholt werden, da es schade wäre diesen netten Effekt nur am Anfang sehen zu können. Alle Partikel werden zentriert und bekommen eine recht große Geschwindigkeit. Wenn die Lebensdauer der Partikel erschöpft ist, gehts normal weiter.

   if (keys[VK_TAB]) // Tab Key Causes A Burst
   {
      particle[loop].x=0.0f; // Center On X Axis
      particle[loop].y=0.0f; // Center On Y Axis
      particle[loop].z=0.0f; // Center On Z Axis
      particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Random Speed On X Axis
      particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Random Speed On Y Axis
      particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Random Speed On Z Axis
     }
    }
   }

   return TRUE; // Everything Went OK
}

KillGLWindow(), CreateGLWindow() und WndProc() werden nicht verändert, weiter zu WinMain(). Der Code steht zum besseren Verständnis ungekürzt da.

int WINAPI WinMain( HINSTANCE hInstance, // Instance
                    HINSTANCE hPrevInstance, // Previous Instance
                    LPSTR lpCmdLine, // Command Line Parameters

   int nCmdShow) // Window Show State
   {
      MSG msg; // Windows Message Structure
      BOOL done=FALSE; // Bool Variable To Exit Loop
      // Ask The User Which Screen Mode They Prefer

      if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", 
          "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)

      {
         fullscreen=FALSE; // Windowed Mode
      }
      // Create Our OpenGL Window

      if (!CreateGLWindow("NeHe's Particle Tutorial",640,480,16,fullscreen))
      {
         return 0; // Quit If Window Was Not Created
      }

Hier ein kleinere Modifikation. Sollte der Benutzer sich für den Vollbild-Modus entscheiden, wird slowdown auf 1.0f anstatt 2.0f gesetzt. Der Grund ist, dass das Programm im Fenster wesentlich langsamer läuft als im Vollbild. Bei schnelleren Computern kann das natürlich weggelassen werden.

      if (fullscreen) // Are We In Fullscreen Mode ( ADD )
      {
         slowdown=1.0f; // Speed Up The Particles (3dfx Issue) ( ADD )
      }
      while(!done) // Loop That Runs Until done=TRUE
      {

         if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Is There A Message Waiting?
         {
            if (msg.message==WM_QUIT) // Have We Received A Quit Message?
         {

         done=TRUE; // If So done=TRUE
      }

      else // If Not, Deal With Window Messages
      {
         TranslateMessage(&msg); // Translate The Message
         DispatchMessage(&msg); // Dispatch The Message
      }
   }

   else // If There Are No Messages
   {
      if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) // Updating View Only If Active
      {
         done=TRUE; // ESC or DrawGLScene Signalled A Quit
      }

      else // Not Time To Quit, Update Screen
      {
         SwapBuffers(hDC); // Swap Buffers (Double Buffering)

Der Benutzer soll auch auf die Geschwindikeit Einfluß nehmen können. Wenn "+" gedrückt wird und der "Bremsfaktor" slowdown nicht kleiner als 1.0f ist, wird slowdown verringert, die Partikelgeschwindigkeit damit letztendlich erhöht.

        if (keys[VK_ADD] && (slowdown>1.0f)) slowdown-=0.01f; // Speed Up Particles

Mit "-" wird abgebremst.

        if (keys[VK_SUBTRACT] && (slowdown<4.0f)) slowdown+=0.01f; // Slow Down Particles

Mit "Page Up" und "Page Down" kann ein- und ausgezoomt werden.

        if (keys[VK_PRIOR]) zoom+=0.1f; // Zoom In
        if (keys[VK_NEXT]) zoom-=0.1f; // Zoom Out

Drückt der Benutzer auf Enter und hat er das im Frame davor nicht getan, so wird der Rainbow-Mode aktiviert, bis das nächste Mal Enter gedrückt wird. Das sieht etwas umständlich aus, aber da diese Abfrage sehr häufig gemacht wird (Einmal pro Frame) würde der Rainbow-Mode kaum zu steuern sein, da niemand die Entertaste wirklich für nur 10 Millisektunden drücken kann.

        if (keys[VK_RETURN] && !rp) // Return Key Pressed
        {
           rp=true; // Set Flag Telling Us It's Pressed
           rainbow=!rainbow; // Toggle Rainbow Mode On / Off
        }

        if (!keys[VK_RETURN]) rp=false; // If Return Is Released Clear Flag

Die untere Bedingung sieht etwas konfus aus. Es wird geprüft ob die Leertaste gedrückt wurde, und nicht schon längere Zeit gehalten wird. Ist dann noch der Rainbow-Mode aktiviert und delay größer ist als 25, wird weiter gegangen. delay ist der Zähler für den Rregenbogen-Effekt. Würde die Farbe ständig verändert werden, wären die Partikel ziemlich bunt. Da aber soetwas wie ein Regenbogen entstehen soll, muss ein Zähler eingebracht werden, der immer einer Gruppe von Teilchen eine neue Farbe zuweist.

           if ((keys[' '] && !sp) || (rainbow && (delay>25))) // Space Or Rainbow Mode
           {

Wird die Leertaste gedrückt, soll der Rainbow-Mode deaktiviert werden, damit die Farben nicht mehr verändert werden.

              if (keys[' ']) rainbow=false; // If Spacebar Is Pressed Disable Rainbow Mode

sp (Leertaste gedrückt) wird TRUE und delay wird zurückgesetzt. Die Farbe wird außerdem geändert.

              sp=true; // Set Flag Telling Us Space Is Pressed 
              delay=0; // Reset The Rainbow Color Cycling Delay
              col++; // Change The Particle Color

col darf niemals größer als 12 werden.

              if (col>11) col=0; // If Color Is To High Reset It
           }

Wird die Leertaste losgelassen, muss auch sp wieder zurückgesetzt werden.

           if (!keys[' ']) sp=false; // If Spacebar Is Released Clear Flag

Auch die Bewegungsrichtung des Partikelstrahls kann noch beeinflußt werden, nämlich mit den Cursortasten. Man erinnere sich an xspeed und yspeed. Wenn die Partikel ausgestoßen werden, wird xspeed und yspeed dazuaddiert, die Bewegungsrichtungsrichtung damit schon am Anfang festgelegt (Die Gravitation wirkt ja erst danach).

           // If Up Arrow And Y Speed Is Less Than 200 Increase Upward Speed
           if (keys[VK_UP] && (yspeed<200)) yspeed+=1.0f;
           // If Down Arrow And Y Speed Is Greater Than -200 Increase Downward Speed
           if (keys[VK_DOWN] && (yspeed>-200)) yspeed-=1.0f;
           // If Right Arrow And X Speed Is Less Than 200 Increase Speed To The Right
           if (keys[VK_RIGHT] && (xspeed<200)) xspeed+=1.0f;
           // If Left Arrow And X Speed Is Greater Than -200 Increase Speed To The Left
           if (keys[VK_LEFT] && (xspeed>-200)) xspeed-=1.0f;

Nur noch delay muss um eins erhöht werden.

           delay++; // Increase Rainbow Mode Color Cycling Delay Counter

Da der Titel immernoch der alte der ersten lektion ist, kann dies hier noch verändert werden.

           if (keys[VK_F1]) // Is F1 Being Pressed?
              {
                 keys[VK_F1]=FALSE; // If So Make Key FALSE
                 KillGLWindow(); // Kill Our Current Window
                 fullscreen=!fullscreen; // Toggle Fullscreen / Windowed Mode
                 // Recreate Our OpenGL Window
           
                 if (!CreateGLWindow("NeHe's Particle Tutorial",640,480,16,fullscreen))
                 {
                    return 0; // Quit If Window Was Not Created

                 }
              }
           }
        }
    }

   // Shutdown
   KillGLWindow(); // Kill The Window
   return (msg.wParam); // Exit The Program
}

Ich habe versucht, alle Schritte die nötig sind eine wirklich schöne Partikel-Engine zu erstellen so anschaulich und detailiert wie möglich zu beschreiben. Man kann diese überall einsetzen als Feuer-, Wasser-, Explosions-, Schneeeffekt usw. Durch einfache veränderungen sind wirklich viele Möglichkeiten offen.

Vielen Dank an Richard Nutman, der die Idee hatte die Teilchen mit glVertex3f() zu positionieren und nicht jedesmal die Modelview Matrix zurücksetzen zu müssen und den Rest mit glTranslatef() zu erledigen. Beides funktioniert, aber die erste Methode ist eleganter und verbraucht nicht unnötig Ressourcen. Ebenfalls danke ich Antoine Valentim für die Idee triangle strips einzusetzen.

Jeff Molofee (NeHe) http://nehe.gamedev.net

Die Source Codes und Ausführbaren Dateien zu den Kursen liegen auf der Neon Helium Website

Übersetzt und leicht modifiziert von Hans-Jakob Schwer 28.10.2k2, www.codeworx.org