Tutorial Partikel1

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Partikel Systeme I

Übersicht

Dies ist das erste von zwei Tutorials zum Thema Partikel Systeme. Zuerst werde ich den Begriff Partikel Systeme und deren Funktionsweise erklären.Anhand des beiliegenden Beispiel-Programms erläutere ich die Programmierung eines einfachen Partikel-Systems. Dieses Tutorial wird hauptsächlich auf die verwendeten Konzepte eingehen, die Umsetzung ist dem Beispiel zu entnehmen. Es empfiehlt sich deshalb, das Gelesene sofort anhand des Source-Codes nachzuvollziehen.

Das zweite Tutorial baut auf dem ersten auf und erweitert das System um Effekte wie Gravitation oder Luftwiderstand. Außerdem werde ich noch ein paar Probleme und deren Lösung ansprechen, damit der eigenen Partikel-Engine nichts mehr im Wege steht!

Von Chaos, Ordnung und Explosionen

Wer sich mit Grafikprogrammierung beschäftigt, hat meist das Ziel, Szenen oder Grafiken zu erschaffen, die realen Vorbildern möglichst exakt nachempfunden sind. In Computerspielen zum Beispiel ist die Atmosphäre und damit auch der Unterhaltungswert abhängig von Detailgrad und Stimmigkeit der simulierten virtuellen Welt.

Es ist also notwendig Methoden zu finden, um reale Objekte und Phänomene möglichst genau nachzubilden. Während Dinge wie Personen oder Gebäude von festlegbarer Form sind und sich sehr gut aus Polygonen und Texturen nachbilden lassen, dürfte auch der beste Modeller bei Effekten wie Feuer, Dampf oder Wasser das Handtuch werfen. Sieht man sich zum Beispiel eine Explosion genauer an, so lassen sich keine festen Formen erkennen, die man nachbauen könnte. Stattdessen fliegen hier Funken, Feuerzungen blähen sich auf und fallen zusammen, Teile fliegen auseinander und Rauch steigt auf. Tausende kleiner Partikel verhalten sich scheinbar chaotisch und bilden doch im Zusammenspiel ein stimmiges Bild.

Um solche Effekte nachbilden zu können, braucht man ein anderes Konzept, nämlich das der Partikel-Systeme. So komplex eine Explosion auch aussehen mag, das Verhalten der einzelnen Komponenten lässt sich durch physikalische Gesetze erklären. Sind die Zusammenhänge erst einmal erkannt, so lassen sie sich auch auf virtuelle Systeme übertragen. Ein Partikel-System besteht also aus einer Reihe von Regeln, die das Verhalten von unzähligen dynamischen Partikeln beeinflussen. Um ein möglichst natürliches, chaotisches Resultat zu erhalten, werden zusätzlich zufällige Elemente eingeflochten.

Das Ergebnis sind chaotische, dynamische Gebilde, die trotzdem kontrollierbar bleiben. Durch die Kombination verschiedenster Regeln/Effekte mit variierten Parametern wird der Kreativität kaum Grenzen gesetzt.

Am Anfang war nur Staub

Nachdem jetzt klar ist, was Partikel Systeme sind und wofür sie gebraucht werden, zeige ich an einem einfachen Beispiel, woraus solch ein System aufgebaut sein muss und wie es sich implementieren lässt.

Eins jedoch noch vorweg: Ich werde hier keine ultraschnelle und flexible Partikel-Engine mit dutzenden von Effekt-Templates und Schießmichtot-Features entwickeln. Am Ende der Tutorials sollte der Grundstein für die Entwicklung einer echten Engine gelegt sein, aber diese Arbeit nehme ich euch nicht ab! ;)

Das allerwichtigste an einem Partikel System sind natürlich die Partikel (siehe Unit pfxCore). Deshalb sollten wir uns zunächst Gedanken darüber machen, welche Attribute diese ausmachen.Zuerst fallen einem da die physikalischen Grundeigenschaften ein, die jeder Körper besitzt: Position, Geschwindigkeit (und Richtung), Masse, Dichte (des Materials) und natürlich eine bestimmte Größe. Eigenschaften wie Masse oder Dichte scheinen auf den ersten Blick unwichtig zu sein, sind aber für bestimmte Effekte wie Gravitation oder Wind unverzichtbar.Außerdem ordnen wir jedem Partikel eine individuelle Lebensspanne zu, die jeden Frame nach unten korrigieren wird. Von ähnlicher Bedeutung ist das Alter, das in Millisekunden angegeben wird.

Zuletzt spendieren wir unseren Partikeln noch eine Farbe, die Zeiten der Schwarz-Weiß Effekte sind schließlich vorbei!Nun dürfte auch die Implementation keine Fragen mehr aufwerfen - na ja, fasst keine! Man könnte sich fragen, ob man lieber ein Partikel-Record oder eine Partikel-Klasse macht, aber wenn man bedenkt, dass eine Klassen-Instanz wesentlich mehr Speicher belegt als ein schlichtes Record und wir mit hunderten von Partikel hantieren wollen, sollte die Entscheidung klar sein.

Ein Partikel-Typ könnte also folgendermaßen definiert sein:

TPfxParticle = record
  Position : TPfxVector;
  Velocity : TPfxVector;
  Density  : single;
  Mass     : single;
  Size     : single;
  Color    : TPfxColor;
  LiveSpan : integer;
  Age      : integer;
end;

Die Typen TPfxVector und TPfxColor müssen wir natürlich auch noch definieren.TPfxVektor enthält eine x, y und z Koordinate und eignet sich damit prächtig zum speichern von Orts- oder Richtungsvektoren.

TPfxColor definiert beliebige Farben, indem es die Farbintensität der Grundfarben angibt. Üblicherweise verwendet man dazu drei Byte, so dass jede Grundfarbe eine Intensität von 0 bis 255 haben kann. OpenGL benutzt jedoch Fließkommazahlen (Single), bei denen die Intensität zwischen 0 und 1 angegeben wird.

TPfxVector = record
  x     : single;
  y     : single;
  z     : single;
end;

TPfxColor = record
  r     : single;
  g     : single;
  b     : single;
end;

Container auf - Partikel rein - Container zu

Nun haben wir festgelegt, wie ein Partikel auszusehen hat, aber ein Partikel allein macht noch keinen eindrucksvollen Effekt. Für etwas Aufwendiges wie z.B. eine Explosion sollten es schon einige hundert Partikel sein mit gänzlich verschiedenen Eigenschaften. So müssen sich Rauch-Partikel anders verhalten als ihre Kollegen bei den sprühenden Funken, oder züngelnden Flammen.

Um da nicht die Übersicht zu verlieren, fassen wir gleiche Partikel-Typen zu Gruppen zusammen, die über eine Container-Klasse verwaltet werden.Unsere Container-Klasse hat ein Interface, über das sich Partikel hinzufügen und löschen lassen. Diese Partikel werden in einem dynamischen Array gespeichert, das dem Benutzer Zugriff auf die Attribute aller Partikel gewährt. Außerdem aktualisiert die Container-Klasse die Partikel in Bezug auf Position und Alter und gibt bei abgelaufener Lebensspanne den Speicher frei.

Ein einfaches Container Interface könnte z.B. so aussehen:

TPfxContainer = class(TObject)
protected
  FnumParticles : word;
  function GetSize : word;
public
  Particles  : array of TPfxParticle;
  property numParticles : word read FnumParticles;
  property Size : word read GetSize;
  constructor Create(aSize : word); overload;
  function Add(var aParticle : TPfxParticle) : integer;
  procedure Delete(aIndex : integer);
  procedure Advance(aTime : integer);
  function Clean : word;
  procedure Clear;
end;

Wie gesagt, dieses Interface ist einfach - und genauso einfach ist die Implementation. Ein Blick auf den Quellcode in der Unit pfxCore sollte die meisten Fragen beantworten. Die Speicherverwaltung ist zwar effektiv, aber noch lange nicht perfekt. Wenn die Instanz eines Containers erstellt wird, muss die Anzahl der zur Verfügung stehenden Plätze mit angegeben werden. Danach ändert sich daran nichts mehr, denn das Zuweisen und Freigeben von Speicher ist sehr rechenintensiv.

numParticles gibt an, wie viele Plätze belegt sind. Dabei ist es egal, ob es sich um aktive Partikel handelt oder "tote", bei denen LiveSpan bereits negativ ist. Für ein neues Partikel wird numParticels einfach um eins erhöht und dient gleichzeitig als Index für das dynamische Array.

Und was machen wir, wenn alle Plätze belegt sind (numParticles = Size-1)? Wir rufen die Funktion Clean auf, die das gesamte Array einmal durch geht und in sich selbst kopiert, dabei aber Partikel mit negativer LiveSpan außen vor lässt. Die Anzahl der kopierten Partikel ist damit auch die Anzahl der belegten Plätze und wird in numParticles geschrieben.

Die Funktion Advance geht alle Partikel im Array durch und aktualisiert sie. aTime gibt dabei die Zeit seit der letzten Aktualisierung an - dadurch wird die Bewegung von der Framerate unabhängig. Die Position wird anhand der alten Position und des Bewegungsvektors neu festgelegt und die Lebensspanne wird um aTime gesenkt, während das Alter entsprechend erhöht wird.

Wie bereits erwähnt, sind Partikel mit LiveSpan kleiner eins nicht mehr aktiv (und werden damit nicht mehr gerendert), auch wenn sie noch bewegt werden. Um ein Partikel zu löschen, reicht es demnach, LiveSpan auf Null zu setzen.

Klappe … und Action!

Es wird Zeit für etwas Farbe auf dem Bildschirm, und deshalb werden wir uns jetzt einen kleinen Beispiel-Effekt basteln, der Feuer nachempfunden ist.Dazu erstellen wir in der Unit PfxImp eine Effekt-Klasse, um im praktischen Einsatz möglichst wenig Aufwand mit dem Effekt zu haben. Diese sollte über eine Methode zum Rendern des Effekts und eine für die Aktualisierung verfügen. Außerdem brauchen wir einen Container für die Partikel, eine Partikel-Schablone, eine Textur und eine Hilfsvariable, die die Emission steuert.

TExampleFX = class(Tobject)
protected
  Container : TPfxContainer;
  Particle : TPfxParticle;
  EmissionTime : integer;
  FireTex : GluInt;
public
  constructor Create;
  destructor Destroy; override;
  procedure Render;
  procedure Advance(aTime : integer);
end;

Zu Constructor und Destructor gibt es nicht viel zu sagen… interessant ist vor allem die Methode Advance, die das eigentliche Herz des Effekts ist. Sie fügt neue Partikel in den Container ein und sorgt dafür, dass der Inhalt des Containers per Advance-Methode aktualisiert wird. Da Anzahl und Eigenschaften der erstellten Partikel nicht von der Framerate abhängen sollen, brauchen wir wieder die Zeit seit der letzten Aktualisierung. Advance macht zunächst einmal nichts anderes, als die Zeit seit der letzten Aktualisierung (aTime) zu der Zeit seit der letzten Partikel Emission (EmissionTime) hinzuzuaddieren. Erst wenn ein bestimmtes Limit überschritten ist, werden Partikel erstellt. Dieses Limit ist übrigens über die Konstante EMISSION_RATE festgelegt. Ist es Zeit für eine Emission, so werden neue Partikel erstellt, und zwar so viele, wie der Wert von PARTICLES_PER_EMISSION vorschreibt.

Um ein chaotisches Aussehen zu erreichen, werden die Attribute der zu erstellenden Partikel innerhalb bestimmter Grenzen vom Zufall ausgewählt und in der Schablone gespeichert. Da sich alle Partikel einer Emission von den Attributen ähnlich sein sollen - denn das sorgt für einen realistischeren Flammen-Effekt -, wird die Schablone nur einmal pro Emission generiert und dann für jedes Partikel der Emission leicht variiert. Mit der Add-Methode des Containers fügen wir eine Kopie der Schablone den Partikeln im Container hinzu.

Da der Container die Partikel von nun an selbstständig verwaltet, brauchen wir uns nur noch um das Zeichnen der Partikel zu kümmern. Dies geschieht in der Prozedur Render. Wie ein Partikel gerendert wird, hängt natürlich vor allem davon ab, um was für ein Partikel es sich handelt. Die einfachste Möglichkeit einen Partikel zu zeichnen wäre, mit GL_POINTS an die Position des Partikels einen Punkt zu setzen. Meistens bedient man sich jedoch des so genannten Billboards, wodurch sich wesentlich vielseitigere Effekte realisieren lassen. Jeder Partikel wird als Quad gerendert, das genau senkrecht zur Blickrichtung der Kamera positioniert ist. Durch den Einsatz unterschiedlicher Texturen, Farben, Größen, Formen und natürlich Blending lassen sich so mit relativ wenigen Partikeln eindrucksvolle und vielseitige Effekte schaffen.

In unserem Bespiel Effekt werden alle Quads gleichgroß sein und dieselbe Textur (eine Feuerzunge in Schwarz-Weiß) haben - lediglich Farbe und Transparenz der Partikel unterscheiden sich. Da es sich bei Feuer im Wesentlichen um Licht handelt, nutzen wir, wie bei fast allen Lichteffekten, additives Blending, bei dem die Farbwerte des Quads zu den Farbwerten im Buffer addiert werden. Außerdem errechnen wir aus dem Alter der Partikel einen Alphawert, der die nötige Transparenz angibt.

Bevor wir aber das erste Mal Rendern können, müssen gewisse Vorbereitungen getroffen werden. Natürlich muss Blending aktiviert und die richtige Blendfunktion gesetzt werden, aber auch der Tiefen-Puffer muss deaktiviert sein, denn wir wollen ja auch mehrere Quads übereinanderzeichnen.

glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);

Jetzt können wir uns der eigentlichen Render-Methode der Effekt-Klasse zuwenden: In einer Schleife werden alle Partikel des Containers durchlaufen. Ist die Lebensspanne größer 0, so ist das Partikel noch aktiv und muss gerendert werden. Dazu wird als erstes die Farbstärke (sat von Saturation = Sättigung) berechnet. In den ersten 100ms steigt sat auf 1 an, um dann bis zum Ablauf der Lebensspanne wieder auf 0 zu sinken. Nun können wir die Farbe des aktuellen Partikels setzen. Als Alphawert setzen wir sat ein, wobei das Partikel transparenter wird, je kleiner sat ist.

Bevor wir das Partikel rendern, müssen wir mit glTranslate die Modelview-Matrix auf die aktuelle Position des Partikels setzen. Davor sichern wir jedoch die Matrix, denn die Transformationen sind für jeden Partikel unterschiedlich. Nach der Transformation entfernen wir mit FXBillboardBegin alle Rotationen aus der Matrix d.h. wir ersetzen die ersten 3 Felder der ersten drei Zeilen durch eine 3x3 Einheitsmatrix. Das texturierte Quad, das wir nun zeichnen, steht damit parallel zum Bildschirm.

Am Ende jeden Schleifendurchlaufs wird die im Stack gesicherte Matrix wieder hergestellt, die Matrix wird also nicht dauerhaft verändert.

Jetzt brauchen wir die Partikel-Klasse nur noch irgendwo einzubauen. (Für die Demo habe ich das OpenGL-Template genommen) Nachdem die Klasseninstanz erstellt ist, müssen wir der Prozedur glDraw nur noch folgende Methoden-Aufrufe hinzufügen:

Effect.Advance(round(1000/FPS));
glLoadIdentity;
glTranslate(0,-1,-3);
Effect.Render;

Und schon haben wir eine lustig flackernde Flamme auf unserem geliebten Bildschirm!

Tutorial Partikel1 fire.gif

Das Wort zum Sonntag

Tja, es ist Sonntag und ich habe mein erstes Tutorial für die DGL fertig! Ich hoffe, es hat dem ein oder anderen gefallen. Die Materie ist teilweise nicht ganz einfach, aber dafür umso spannender! ^^ Ich schlage vor, ihr nehmt das soeben Gelesene als Grundstock für eigene Versuche auf dem Gebiet der Partikel-Systeme. Wie eingangs erwähnt, werde ich im zweiten Tutorial das heute entwickelte System erweitern und auf besonders quälende Fragen und häufige Probleme eingehen. Das setzt natürlich voraus, dass ich von den Problemen und Fragen erfahre. Also her mit dem Feedback - sonst gibt's kein zweites Tut! *fg*

Happy Coding!

Thomas aka Lithander

Dateien


Vorhergehendes Tutorial:
Tutorial_Nebel
Nächstes Tutorial:
Tutorial_BumpMap

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.