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