Partikelengine und diskrete Geschwindigkeitsfelder

Parallel zu meinen Ausarbeitungen des aktuell anstehenden Seminars, bei dem ich mich mit der Thematik "GPU Simulation and Rendering of Volumetric Effects" beschäftige, habe ich noch einmal eine kleine Partikelengine implementiert. Diesmal in C++ und vor dem Hintergrund diesmal gezielte Kräfte an den Partikeln anzusetzen, die sich aus einem räumlich angelegten Gitter ableiten, welches ein Geschwindigkeitsfeld simulieren soll.
In der ersten Version ist die Beziehung einseitig: Das Gitter gibt seine Richtungsimpule an die Partikel ab, diese breiten sich entsprechend im Raum aus. Eine Reaktion seitens der Partikel auf das Gitter - zum Beispiel aufgrund von simulierten Druck- oder Temperaturschwankungen - wird nicht berechnet.

Sicher gibt es bessere Techniken für Partikelengines und ich bin auch kein Programmierguru, aber um einen Einblick in die Funktionsweise einer einfachen Partikelengine zu bekommen habe ich in den verschiedenen Entwicklungsstufen ein paar Screenshots genommen und die grundsätzliche Vorgehensweise skizziert. Grundsätzlich gibt es natürlich auch ganz andere Ansätze wie "richtiges" volumetrisches Rendern über die 3D_TEXTURE Extensions oder ähnliches. Was ich hier zeige ist ein einfacher "Billboard- und Blending" Ansatz.

Entwicklung: Einfache Billboard und Blending Engine

Die Objektstruktur ist in meinem Ansatz zunächst dreigeteilt:

1. Das Partikel
Das Partikel selbst ist eine Struktur die im wesentlichen Angaben zur eigenen Raumposition (x, y, z), Gechwindigkeit (vx, vy, vz), Größe und Farbe (r, g, b, a) sowie der TTL - Time to Live - besitzt.
Diese Angaben habe ich noch um Beschleunigung, Rotation, Rotationsgeschwindikeit und einen "Verglühen-Faktor" ergänzt. Im wesentlichen erhält man in C etwas wie:

struct Particle {
  GLint   ttl;          
  GLfloat burnoutfactor;          
  GLfloat size;
  GLfloat rot;
  GLfloat rotspeed;
  GLfloat position[3];
  GLfloat speed[3];
  GLfloat speedup[3];
  GLfloat color[4];
};
Im wesentlichen soll das Partikel zunächst in den Raum gesetzt werden, eine gewisse Lebenspanne lang mit seiner Farbe in der Gegend herumfliegen und dann je nach Burnout verglühen. Verglühen heisst hier soviel wie den Alphawert mehr oder weniger schnell gegen Null laufen lassen.

2. Der Partikelgenerator
Ein Partikelgenerator ist ein Objekt, das einen ganzen Haufen von Partikeln generiert sie dann initialisiert (Position, Farbe, Geschwindigkeit, TTL...) und nachdem ein Partikel seinen Lebensweg abgeschlossen hat und "verglüht" ist, dieses - falls gewünscht - wieder neu initialisiert.
Der Partikelgenerator braucht dazu die Angaben wo er sich im Raum befindet (dort beginnt die Reise der generierten Partikel), wieviele Partikel er verwalten soll, aus welchen Farb-, Geschwindigkeits- und TTL-Bereichen er den Partikeln beim Initialisieren zufällig Werte zuordnen soll und ob er dauerhaft arbeitet oder nur ein gewisses Kontingent an Partikel generieren soll - also wieviel "Schuss" er zur Verfügung hat...
Ausserdem stellt das Partikelgeneratorobjekt eine Routine zum Rendern der zugehörigen Partikel (mit der entsprechenden Textur!) zur Verfügung, sowie eine Routine zur Berechnung eines Zeitschritts. Diese erhält als Eingabeparameter die verstrichene Zeit seit dem letzten Aufruf und aktualisiert entsprechend die Parameter der Partikel. Das Outline in meinem konkreten Fall sieht wie folgt aus:

class ParticleGenerator {
  private:
    Particle*       particles;
    GLint           numParticles;
    
    GLint           lifestate;        // 1 = ready, 2 = generating particles, 3 = fading out, 0 = done
    bool            infinite;         // true if infinite ammount of particles must be generated
    GLint           ammo;             // if zero no particles will be generated anymore, 
                                      // lifestate = 3 and pgburnout begins to decrease
    GLint           pgburnout;        // worst case ttl+fadeout, if < 0 lifestate becomes 0

    GLfloat         pos[3];
    
    GLuint          texture;                //texture 
                                            //several ranges for particle init:
    GLfloat         rgbaMinColor[4];        //color start range
    GLfloat         rgbaMaxColor[4];        //color end range...
    GLint           particleColorScheme;    //...
    GLint           minParticleTTL;         //...
    GLint           maxParticleTTL;
    GLint           minParticleBurnout;
    GLint           maxParticleBurnout;
    GLfloat         minParticleSize;
    GLfloat         maxParticleSize;
    GLfloat         minParticleSpeed;
    GLfloat         maxParticleSpeed;
    GLfloat         minParticleRotSpeed;
    GLfloat         maxParticleRotSpeed;


  public:
                    ParticleGenerator(GLfloat position[3],         bool    slowstart,
                                      bool    infinite,            GLint   ammo, 
                                      GLint   numParticles,        GLint   particleColorScheme,
                                      GLfloat minColor[4],         GLfloat maxColor[4],
                                      GLint   minParticleTTL,      GLint   maxParticleTTL,
                                      GLint   minParticleBurnout,  GLint   maxParticleBurnout,
                                      GLfloat minParticleSize,     GLfloat maxParticleSize,
                                      GLfloat minParticleSpeed,    GLfloat maxParticleSpeed,
                                      GLfloat minParticleRotSpeed, GLfloat maxParticleRotSpeed,
                                      char*   texturefile,         char*   alphafile);

                   ~ParticleGenerator();

    GLint           getLifestate();
    void            start();
    void            stop();
    void            processPhysics(int ticks);
    void            render(GLfloat cam_rot[3], int blendType);
};
Da der Objektansatz das "Bombebasteln" ja geradezu fördert ergab sich nun noch ein drittes Objekt:

3. Der ParticleGeneratorContainer (PGContainer)
Ein PGContainer dient dazu mehrere verschiedene Partikelgeneratoren aufzunehmen und sie alle gleichzeitig zu bedienen - unterstützt also Funktionen wie startAll(), stopAll() oder renderAll(). Um PartikelGeneratoren aufzunehmen bietet er die Funktion insert(ParticleGenerator* newPG) an.
Das konkrete Outline meinerseits wieder als Beispiel:

class PGContainer {
  private:
    ParticleGenerator*  pg;
    PGContainer*        nextPGC;
  public:
                        PGContainer(ParticleGenerator* newPG = NULL);
                       ~PGContainer();
    void                insert(ParticleGenerator* newPG);
    void                startAll();
    void                stopAll();
    void                processPhysicsAll(int ticks);
    void                renderAll(GLfloat cam_rot[3], int blendType);
    void                setParticleColorAll(GLfloat minr, GLfloat ming, GLfloat minb, GLfloat mina,
                                            GLfloat maxr, GLfloat maxg, GLfloat maxb, GLfloat maxa);
};    
Intuitiv funktioniert ein PGContainer wie ein Feuerwerkskörper:

1. Feuerwerks-Hüllkörper (new PGContainer) herstellen
2. Feuerwerkseffekt (new PartikelGenerator) herstellen und einstellen (farblich, etc...)
3. Vorbereiteten Partikelgenerator in den Hüllkörper einfügen ( PGC->insert(newPG); )
4. beliebig oft ab Schritt 2 wiederholen
5. Den gesamten Effekt auf einmal "abbrennen" (PGC->startAll()) In der Hauptschleife des Programms müssen dann die Zeiten gemessen werden und PGC->processPhysicsAll(); und PGC->renderAll(); aufgerufen werden.

Ein Wort noch zu den Aufrufparametern: Der Physiksimulationschritt benötigt wie gesagt die verstrichene Zeit seit der letzten Aktualisierung um korrekte Ergebnisse liefern zu können, die Renderroutine erhält in meinem Fall die Kamerarotation um ein "Pseudo-Billboard" herstellen zu können. Alle Partikel zeigen mit "ihrer Breitseite" dann also in etwa in Richtung des Betrachters...


 

 
Um die Implementation nachvollziehen zu können hier meine "Development-Galerie":

Schritt 1:

Der Partikelgenerator erzeugt Partikel, die eine TTL und Geschwindigkeit haben, berechnet Schritt für Schritt die neue Position der Partikel, setzt sie nach Ablauf ihrer Lebenszeit in die Ursprungsposition zurück und rendert natürlich die gesamte Szene.

In diesem Stadium bestehen die Partikel zunächst einfach aus den gezeichneten Vertices.

Schritt 2:

Die Partikel werden jetzt mit Hilfe von GL_QUADS als vollständige ebene Quadrate gerendert. Partikelgröße und Rotation werden vom Generator per Zufall ermittelt und entsprechend über die Zeit mitberechnet

Gut zu sehen ist, wie die willkürliche Drehung der Partikel die Orientierung bezüglich des Betrachters beeinflusst. Die Partikel stehen also selbst wenn die Kamera frontal auf die Simulation zeigt teilweise seitlich.

Schritt 3:

Die Render Routine wurde um den Aufrufparameter cam_rot erweitert. Dieser wird nun dazu genutzt die Richtung des Betrachters zu ermitteln und die Partikel immer frontal zur Kamera hin auszurichten.

Statt eines echten Billboardings, bei dem jedes Partikel genau zum Betrachter zeigt wurde hier aus Geschwindigkeitgründen nur eine Bilboarddrehung genutzt, die für alle Partikel aus Lokalitätsgründen in etwa gleiche Ergebnisse produziert...

Schritt 4:

Der Partikelgenerator dekrementiert die TTL (Time To Live) auf 0 und beginnt dann statt einer Neuinitialisierung den Alphawert des Teilchens je nach Burnoutfactor richtung 0 hin zu verändern.

Das alleinige Lebensende eines Teilchen ruft nun nicht mehr seine Neuinitialisierung hervor, sondern leitet den Prozess des "Verglühen" durch Ausblendung ein. Erst nachdem der Alphakanal 0 erreicht hat und das Partikel auch optisch verschwunden ist wird neu initialisiert. Ergebnis: Ein weicher Übergang an den Randgebieten der Effektausdehnung.

Möglich, aber nicht implementiert: Der Alpha-Übergang könnte mit der Größe des Teilchens korrelieren...

Schritt 5:

Die mit GL_QUADS gerenderten Partikel werden nun mit einer entsprechenden Textur gerendert, die selbst zum Rand hin im Alphakanal abnimmt und in sich auch leicht transparent ist.
Um die Partikel darzustellen wird das entsprechende Quad mit aktivierter Textur auch mit aktiviertem Blendings gerendert.
Die Textur selbst wird zur Laufzeit aus einer weissen Basistexur und einer Alphakanaltextur gemischt:

+

Schritt 6:

Die erhaltene Effektwolke kann nun über den entsprechend eingestellten ParticleGenerator gefärbt werden. Der ParticleGenerator hat dabei zwei RGBA Werte und errechnet für jedes Partikel eine Zufallsfarbe, die innerhalb des angegebenen Farbbereichs liegt.

Schritt 7:

Obwohl die Ergebnisse bis hierhin im allgemeinen schon recht passabel sind mindere ich an dieser Stelle die erhaltene Partikelwolke nun noch einmal in ihren Farbintensitätswerten.
Der Grund ist der gleich folgende Einsatz der Blendfunktion glBlendFunc(GL_SRC_ALPHA, GL_ONE). Diese kumuliert die einzelnen Farbkanäle schneller auf und erzeugt so ein leuchtenderes Farbenspiel, was ich in meinem Fall für "Feuereffekte" besser nutzen kann, als glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); für die etwas stumpferen, Nebel-effekte.

Schritt 8:

glBlendFunc(GL_SRC_ALPHA, GL_ONE); ist aktiviert und die Partikelengine ist in sich soweit abgeschlossen.
Beispiel für glBlendFunc(GL_SRC_ALPHA_SATURATE, GL_ONE); und glBlendFunc(GL_SRC_ALPHA_SATURATE, GL_ONE_MINUS_SRC_ALPHA);


 

 
Erweiterung um ein Grid

Obwohl der Nutzen für ein kleines Spiel oder als zusätzlicher Effekt in einer Graphikanwendung soweit schon vorhanden ist, habe ich mich jetzt an einem gridbasiertes Simulationssystem versucht.

Im Raum liegt jetzt ein Gitter, das an jedem Punkt einen Geschwindigkeitsvektor vorhält. Während der Simulationsschritte erhält der ParticleGenerator dann als weiteren Parameter das Grid und fragt so die Raumvektoren des Geschwindigkeitsfeldes während der Simulation ab bzw. lässt diese in die Berechnung einfliessen.

Zunächst wurden die Vektoren alle in die gleiche Richtung initialisiert und haben entsprechend nur einen Geschwindigkeitsimpuls in eine Richtung abgegeben. Die Unterschiede im Partikelstrom ergeben sich durch die unterschiedlichen Startimpulse.

Einige Bilder aus dem Programm:

Nachdem die Anwendung der Vektoren des Grids auf die Partikelsimulation soweit funktioniert, können inhomogene Richtungen und Länge (Intensität) der Vektoren in die Berechnung mit einfließen:

Um nun auch die Auswirkungen verschiedene Vektorfelder auf die Partikelfluktuation zu visualisieren habe ich einige funktionale Setups integriert. Hier zu sehen: Die Ausbreitung der Partikel in einem "gebogenen" Vektorfeld, sowie ein x-sinus-/y-konstant/z-cosinus-förmiges Feld (Spirale). Dort wo die Partikel den Wirkungsbereich des Grids verlassen wirkt nur noch ein Nullvektor.

Ein Zufallsvektorfeld:


Das Programm kann hier geladen werden: .download

Wie immer bei meinen Quelltexten - keine Kommentare, dafür ineffizienter Code.... :)
Das Programm ist ja auch eher als Anrgung und für mich als Programmierübung entstanden.