Entwicklung eines Partikelsystems auf Basis moderner 3D

Transcription

Entwicklung eines Partikelsystems auf Basis moderner 3D
Entwicklung eines Partikelsystems auf
Basis moderner 3D-Grafikhardware
Studienarbeit
Vorgelegt von
Philipp Pätzold
Institut für Computervisualistik
Arbeitsgruppe Computergraphik
Betreuer:
Dipl.-Inform. Matthias Biedermann
Prüfer:
Prof. Dr.-Ing. Stefan Müller
September 2005
Inhaltsverzeichnis
1
2
3
4
5
Einleitung
2
1.1
Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
1.2
Ziel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
Partikelsysteme
3
2.1
Partikel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
2.2
Emitter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
2.3
Zustandslose und zustandserhaltende Systeme . . . . . . . . . . . . . . . . . . . . . . .
6
Moderne 3D-Grafikhardware
9
3.1
Fließkommaleistung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
3.2
Programmierbare Grafikpipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
3.2.1
Vertexprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
3.2.2
Fragmentprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
3.2.3
Shadersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
3.3
Speicherobjekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
3.4
Pointsprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
Entwurf und Design
15
4.1
Anforderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
4.2
Das Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
4.3
Der Namespace effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
4.3.1
Die Klasse Particle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
4.3.2
Die Klasse ParticleSet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
4.3.3
Die Klasse ParticleEmitter . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
4.3.4
Die Klasse CPUParticleEmitter . . . . . . . . . . . . . . . . . . . . . . . . .
22
4.3.5
Die Klasse GPUParticleEmitter . . . . . . . . . . . . . . . . . . . . . . . . .
22
4.3.6
Die Klasse ParticleSystem . . . . . . . . . . . . . . . . . . . . . . . . . . .
22
4.3.7
Die Klasse XMLParticleSystemParser . . . . . . . . . . . . . . . . . . . . .
24
4.4
Ein Datenformat zur Beschreibung des Partikelsystems . . . . . . . . . . . . . . . . . .
24
4.5
Die Darstellung der Partikel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
Implementierung
29
5.1
Verwendete Bibliotheken, APIs und Werkzeuge . . . . . . . . . . . . . . . . . . . . . .
29
5.2
Die CPU-Variante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
5.2.1
Die Methode create() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
5.2.2
Die Methode update() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
5.2.3
Die Methode render() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
32
i
5.3
6
Der Vertexshader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
5.2.5
Der Fragmentshader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
Die GPU-Variante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
5.3.1
Die Methode create() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
5.3.2
Die Methode update() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
5.3.3
Die Methode render() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
5.3.4
Der Vertexshader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
37
5.3.5
Der Fragmentshader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
Ergebnisse
39
6.1
Leistungsuntersuchungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
39
6.1.1
Leistungsvergleich der beiden Varianten . . . . . . . . . . . . . . . . . . . . . .
39
6.1.2
Leistungsvergleich: immediate mode und Vertex Buffer Objects . . . . . . . . .
41
6.1.3
Leistungsvergleich beider Varianten bezüglich der Füllrate . . . . . . . . . . . .
42
Die Testumgebung ParticleSystemViewer . . . . . . . . . . . . . . . . . . . . . . . . .
44
6.2
7
5.2.4
Ausblick
46
ii
Abbildungsverzeichnis
1
Eine Filmszene aus „Star Trek II - The Wrath Of Khan“ . . . . . . . . . . . . . . . . . .
3
2
Ein Partikelsystem als Baumdiagramm . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
3
Der typische Kontrollfluss eines Partikelsystem . . . . . . . . . . . . . . . . . . . . . .
5
4
Eine Bewegungsgleichung eines zustandslosen Partikelsystems . . . . . . . . . . . . . .
7
5
Der Datenfluss eines zustandslosen Partikelsystems . . . . . . . . . . . . . . . . . . . .
7
6
Eine Bewegungsgleichung eines zustandserhaltenden Partikelsystems . . . . . . . . . .
8
7
Der Datenfluss eines zustandserhaltenden Partikelsystems . . . . . . . . . . . . . . . . .
8
8
Die moderne Grafikpipeline von OpenGL 2.0 . . . . . . . . . . . . . . . . . . . . . . .
10
9
Ein ausgerichtetes Billboard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
10
Das gesamte Framework in der Übersicht . . . . . . . . . . . . . . . . . . . . . . . . .
17
11
Das Partikelsystem als Klassendiagramm . . . . . . . . . . . . . . . . . . . . . . . . .
18
12
Die Klasse Particle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
13
Die Klasse ParticleSet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
14
Die Bewegungsgleichung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
15
Die Klasse ParticleEmitter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
16
Die Klasse CPUParticleEmitter . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
17
Die Klasse GPUParticleEmitter . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
18
Die Klasse ParticleSystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
19
Eine Partikeltextur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
20
Ein Screenshot der Benchmarkszene . . . . . . . . . . . . . . . . . . . . . . . . . . . .
40
21
Die Leistungen der CPU-Variante gegenüber der der GPU-Variante . . . . . . . . . . . .
42
22
Ein Leistungsvergleich des immediate mode gegen die VBOs in OpenGL . . . . . . . .
43
23
Ein Leistungsvergleich beider Varianten bezüglich der Füllrate . . . . . . . . . . . . . .
45
24
Die Testumgebung ParticleSystemViewer . . . . . . . . . . . . . . . . . . . . . . . . .
45
25
Eine 1D-Textur für die Partikel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
iii
Tabellenverzeichnis
1
Der Aufbau eines Datenelements im Pufferspeicher des Speicherobjekts (CPU-Variante)
30
2
Der Aufbau eines Datenelements im Pufferspeicher des Speicherobjekts (GPU-Variante)
35
3
Die Leistungen der CPU-Variante gegenüber der GPU-Variante . . . . . . . . . . . . . .
41
4
Ein Leistungsvergleich des immediate mode gegen die VBOs in OpenGL . . . . . . . .
43
5
Ein Leistungsvergleich beider Varianten bezüglich der Füllrate . . . . . . . . . . . . . .
44
Listings
1
Eine XML-Ressource zur Beschreibung des Partikelsystems . . . . . . . . . . . . . . .
24
2
Die Methode create() der Klasse CPUParticleEmitter . . . . . . . . . . . . . . . . . .
30
3
Die Methode update() der Klasse CPUParticleEmitter als Pseudocode . . . . . . . . .
31
4
Die Methode update() der Klasse CPUParticleEmitter . . . . . . . . . . . . . . . . . .
31
5
Die Methode render() der Klasse CPUParticleEmitter als Pseudocode . . . . . . . . .
32
6
Die Methode render() der Klasse CPUParticleEmitter . . . . . . . . . . . . . . . . . .
32
7
Der Vertexshader der CPU-Variante . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
8
Der Fragmentshader der CPU-Variante . . . . . . . . . . . . . . . . . . . . . . . . . .
34
9
Der Methode create() der Klasse GPUParticleEmitter als Pseudocode . . . . . . . . .
35
10
Die Methode create() der Klasse GPUParticleEmitter . . . . . . . . . . . . . . . . .
35
11
Die Methode update() der Klasse GPUParticleEmitter . . . . . . . . . . . . . . . . .
35
12
Der Methode render() der Klasse GPUParticleEmitter als Pseudocode . . . . . . . . .
36
13
Die Methode render() der Klasse GPUParticleEmitter . . . . . . . . . . . . . . . . .
36
14
Der Vertexshader der GPU-Variante . . . . . . . . . . . . . . . . . . . . . . . . . . . .
37
1
1
1.1
Einleitung
Motivation
Natürliche Phänomene, wie Rauch, Nebel, Feuer und Wasser lassen sich sehr gut durch Partikelsysteme
visualisieren. Dabei werden möglichst viele kleine Teilchen, so genannte Partikel, animiert und durch geeignete Techniken visualisiert. Es entsteht der Eindruck von größeren, zusammenhängenden Einheiten,
die auch mit ihrer Umwelt interagieren können. Die Berechnungen zur Simulation und Visualisierung
können teilweise recht aufwändig werden. Eine Auslagerung der notwendigen Berechnungen auf den
Grafikprozessor (GPU) der Grafikhardware würde sich anbieten. Durch die flexible Grafikpipeline heutiger Grafikhardware ist eine direkte Programmierung der Berechnungen durch Shader möglich. Der
Hauptprozessor (CPU) des Computersystems könnte dadurch deutlich entlastet werden und parallel zur
GPU andere relevante Berechnungen durchführen. Darüber hinaus ist es möglich, durch die Nutzung von
Shadern sowie neuen Features moderner 3D-APIs komplexe Techniken zur Visualisierung der Partikel
einzusetzen.
1.2
Ziel
Ziel der Arbeit ist es, ein Partikelsystem unter Verwendung programmierbarer Grafikhardware zu realisieren. Partikelanimation und einfache Interaktion mit der Umwelt, zum Beispiel durch Kraftfelder,
werden dabei durch Vertexshader auf der Grafikhardware berechnet. Daneben wird ein visuell gleichwertiges Modell des Partikelsystems realisiert, wobei hier Partikelanimation und Interaktion mit der Umwelt
durch die CPU berechnet werden. Die Grafikhardware dient dabei hauptsächlich der Darstellung. Beide
Modelle werden abschließend hinsichtlich ihrer Performanz verglichen. Zur Visualisierung der Partikel
finden zudem relevante Features aktueller 3D-Hard- und Software Anwendung.
2
Abbildung 1: Eine Collage der „Genesis“-Szene aus „Star Trek II - The Wrath Of Khan“
2
Partikelsysteme
In der Computergrafik eigenen sich Partikelsysteme besonders gut für die Repräsentation stark dynamischer Objekte. Dazu gehören vor allem die Modellierung und Darstellung natürlicher Phänomene, wie
Feuer, Nebel, Rauch und Wasser. Durch Partikelsysteme lassen sich ebenfalls eine Vielzahl grafischer
Effekte, wie zum Beispiel Explosionen, darstellen. Deshalb werden sie neben dem Einsatz in komplexen
wissenschaftlichen Simulationen vor allem für Spezialeffekte in Filmen und Computerspielen verwendet. Eine erste große kommerzielle Anwendung eines grafischen Partikelsystems war in dem Kinofilm
„Star Trek II - The Wrath Of Khan“ von 1982 zu sehen. In der bekannten „Genesis“-Szene wurde ein
spezielles Projektil auf einen Mond abgefeuert, woraufhin der Mond durch eine explosionsartige Schockwelle in einen bewohnbaren Planeten transformiert wurde. Abbildung 1 zeigt eine Collage der Filmszene.
William T. Reeves, der zur damaligen Zeit bei Lucasfilm Ltd. arbeitete und hauptsächlich für den Spezialeffekt verantwortlich war, beschrieb 1983 das eingesetzte Verfahren. In seinem technischen Report
„Particle Systems - A Technique for Modelling a Class of Fuzzy Objects“ [Ree83] erklärt er detailiert
die eingesetzten Techniken anhand des Filmbeispiels und fasst sie unter dem Namen „Partikelsysteme“
zusammen. Aufgrund der chaotischen Charakteristik und Oberflächenbeschaffenheit vieler natürlicher
3
Partikel System
Emitter
Partikel
Partikel
Partikel
Emitter
Emitter
Partikel
Partikel
Partikel
Partikel
Partikel
Partikel
Abbildung 2: Ein Partikelsystem als Baumdiagramm
Phänomene und Effekte, ist eine Modellierung und Darstellung durch klassische Modellierungsverfahren
und Oberflächenbeschreibungen schwierig [Ree83]. Dies macht neue Techniken zur Modellierung und
Darstellung erforderlich. Ein Partikelsystem simuliert die zeitliche und räumliche Bewegung einer meist
großen Anzahl geometrisch primitiver Objekte. Diese Objekte werden auch Partikel genannt. Weiterhin verwendet das System geeignete Verfahren, um die Partikelwolken in ihrer Umwelt zu visualisieren
und damit den Eindruck von größeren dynamischen Einheiten zu vermitteln. Die Partikel besitzen dabei
eine endliche Lebenszeit und verändern ihre Eigenschaften über diese Zeitspanne hinweg. Nach ihrem
„Tod“ können sie erneut „geboren“ werden und ihr Lebenszyklus beginnt von vorne. Neben den Partikeln
selbst, besteht das Partikelsystem üblicherweise aus mindestens einem Emitter, der für die Geburt und
Aussendung der Partikel zuständig ist. Für die Filmszene nutze Reed ein hierarchisches System. Hierbei
konnten die Partikel wiederum andere Partikel aussenden und so zu Emittern des Systems werden. Abbildung 2 zeigt den hierarchischen Aufbau eines einfachen Partikelsystems in einem Baumdiagramm. Die
Dynamik des Systems wird neben den Partikeleigenschaften durch weitere globale Parameter bestimmt.
Diese sind unter anderem die zeitliche Komponente des Systems sowie verschiedene Kräfte, wie zum
Beispiel Gravitation, Wind und Reibung. Sie können dabei als Beschleunigungskräfte auf die Partikel
wirken. Zudem sind in komplexen Systemen Kräfteeinwirkungen der Partikel untereinander möglich.
Das dynamische Bewegungsverhalten von Stoffen lässt sich beispielsweise gut durch Berechnung der
Federkräfte zwischen mehreren Partikeln (Massepunkten) simulieren [Lan99]. Eine Kollisionsdetektion
und Kollisionsbehandlung der Partikel untereinander und mit Objekten in ihrer Umwelt sind ebenfalls
möglich. Durch die begrenzte Lebensspanne der Partikel und das fortlaufende Generieren neuer Partikel
wird zusätzlich der dynamische Charakter des Systems geformt. Um Variationen zu erzeugen und eine zu künstliche Darstellung zur vermeiden, werden die Eigenschaften neugeborener Partikel meist mit
zufälligen Werten variiert. Abbildung 3 zeigt den Kontrollfluss eines einfachen Partikelsystems.
2.1
Partikel
Die Partikel bilden die Grundlage des gesamten Systems. Die Simulation der Partikelbewegung und die
Interaktion mit der Umwelt geben dem System seinen dynamischen, oftmals auch chaotischen Charakter.
4
Emitter initialisieren
Partikel generieren
System aktualisieren
[System beendet]
[sonst]
[Partikel tot]
[sonst]
Partikel darstellen
Abbildung 3: Der typische Kontrollfluss eines Partikelsystem
Eine geeignete Visualisierung der Partikel ist für den Gesamteindruck ebenfalls von entscheidender Bedeutung. Dabei werden die Partikel zunächst von den Emittern des Systems erzeugt und ausgesandt. Die
Partikel besitzen eine Reihe von Eigenschaften, die ihr Verhalten in der Umwelt bestimmen. Damit eine
endliche Simulation der Partikel möglich ist, besitzt jedes Partikel eine begrenzte Lebenszeit. Zudem
verfügen die Partikel über Attribute zur Berechnung der Bewegungssimulation. Dies sind üblicherweise
eine Positionsangabe im Raum und ein Vektor für die Geschwindigkeit des jeweiligen Partikels[Ree83].
Darüber hinaus ist die Partikelmasse für die Berechnung von Kräfteeinwirkungen, wie die der Beschleunigungskräfte, als zusätzliche Eigenschaft notwendig. Im Bereich der grafischen Simulation von Partikelsystemen werden zudem weitere Eigenschaften zur Visualisierung der Partikel benötigt. Dies können
unter anderem Eigenschaften für die Farbe und Form des jeweiligen Partikels sein. Je nach Einsatzzweck
des Systems sind andere Eigenschaften sowie Kombinationen denkbar.
2.2
Emitter
Der Emitter ist praktisch die Geburtsstätte der Partikel. Hier werden die Partikel erzeugt und in ihren
Eigenschaften festgelegt. Der Emitter verwaltet dazu eine Anzahl von Partikeln. Er berechnet aus ihren
Eigenschaften sowie den globalen Parametern des Systems die aktuelle Position und Darstellung der
Partikel. Außerdem registriert er, wenn ein Partikel seine Lebenszeit überschritten hat und generiert gegebenenfalls neue Partikel. Der Emitter kann innerhalb eines Partikelsystems frei im Raum positioniert
5
1
~pnew = ~pinit +~vinit t + ~at 2
2
~pnew
–
neue Position des Partikels
~pinit
–
initiale Position des Partikels
~vinit
–
initialer Geschwindigkeitsvektor des Partikels
~a
–
aktueller Vektor für die Beschleunigungskraft auf den Partikel
t
–
globale Zeit
(1)
Abbildung 4: Eine Bewegungsgleichung eines zustandslosen Partikelsystems [Ros04a]
werden und kann ebenfalls über einen Geschwindigkeitsvektor verfügen. Neben Position und Geschwindigkeit des Emitters wird die Art der Partikelemission meist durch die Form des Emitters bestimmt. Häufig werden kugelförmige oder rechteckige Emitter eingesetzt[Ree83]. Bei Geburt eines Partikels werden
dazu seine Attribute für die initiale Position und den Geschwindigkeitsvektor einer kugelförmigen beziehungsweise rechteckigen Emissionsverteilung angepasst. Die Art der Emission ist dabei aber nicht nur
auf einfache geometrische Formen beschränkt, sondern lässt sich auch auf komplexe polygonale Objekte
oder mathematische Oberflächenbeschreibungen erweitern.
2.3
Zustandslose und zustandserhaltende Systeme
Besonders im Hinblick auf eine spätere Realisierung eines Partikelsystems auf der Grafikhardware ist eine Unterscheidung zwischen einem zustandslosen und einem zustandserhaltenden Partikelsystem sinnvoll.
Ein zustandsloses Partikelsystem speichert nicht die aktualisierten Attribute der Partikel [Lat04]. Jeder
Berechnungsschritt der Simulation basiert auf den initialen Eigenschaften der Partikel. Die neu gewonnenen Daten werden nach der Visualisierung der Partikel verworfen und nicht in die Partikelattribute
zurückgeschrieben. Die initialen Attribute der Partikel werden einmalig festgelegt und sind dann als
konstanter Ausgangspunkt der Simulation anzusehen. Anhand von abgeschlossenen Funktionen versucht
man, die Bewegung und Veränderung der Partikel über eine Zeitspanne hinweg zu beschreiben. Dabei
berechnet eine Funktion die neue Position der Partikel. Die Berechnung basiert auf einigen initialen Attributen der Partikel, wie initiale Position und Geschwindigkeit und auf einigen globalen Parametern,
wie Gravitation und Zeit des Partikelsystems. Abbildung 4 zeigt, wie eine einfache Formel zur Berechnung der aktuellen Positionen aussehen kann [Ros04a]. Als logische Konsequenz der abgeschlossenen
Bewegungsgleichung können zustandslose Partikelsysteme nur sehr eingeschränkt auf lokale Kräfte und
Kollisionen reagieren [Lat04]. Abbildung 5 zeigt den Datenfluss eines zustandslosen Systems. Auffällig
ist der unidirektionale Datenfluss zwischen dem Datenspeicher der Partikelattribute und dem Prozess der
Simulationsberechnung.
Ein zustandserhaltendes Partikelsystem hingegen speichert für jeden Berechnungsschritt die aktualisierten Attribute der Partikel [Lat04]. Jede neue Berechnung für die Partikelsimulation verwendet also stets
6
Partikelattribute
Berechnung der
Simulation
Anzeige
Abbildung 5: Der Datenfluss eines zustandslosen Partikelsystems
1
~pnew = ~pold +~vdt + ~adt 2
2
(2)
~pnew
–
neue Position des Partikels für das aktuelle Zeitintervall
~pold
–
alte Position des Partikels im vorherigen Berechnungsschritt
~v
–
aktueller Geschwindigkeitsvektor des Partikels
~a
–
aktueller Vektor für die Beschleunigungskraft auf den Partikel
dt
–
Zeitintervall für die Simulation
Abbildung 6: Eine Bewegungsgleichung eines zustandserhaltenden Partikelsystems
die Ergebnisse des vorherigen Berechnungsschrittes. Neu gewonnene Daten werden nach der Visualisierung der Partikel nicht etwa verworfen, sondern als neue Partikelattribute zurückgeschrieben. Die
initialen Attribute der Partikel werden bei Geburt vergeben und dann als Ausgangspunkt der Simulation
verwendet. Bewegung und Veränderung der Partikel werden dabei für ein gegebenes Zeitintervall berechnet. Abbildung 6 zeigt wie eine einfache Formel zur Berechnung der aktuellen Positionen aussehen kann.
Zustandserhaltende Partikelsysteme können auf lokale Kräfte und Kollisionen reagieren und sind im Gegensatz zu zustandslosen Systemen gut für solche Umgebungen geeignet [Lat04]. Abbildung 7 zeigt den
Datenfluss eines zustandserhaltenden Systems. Auffällig ist der bidirektionale Datenfluss zwischen dem
Datenspeicher der Partikelattribute und dem Prozess der Simulationsberechnung.
Partikelattribute
Berechnung der
Simulation
Anzeige
Abbildung 7: Der Datenfluss eines zustandserhaltenden Partikelsystems
7
3
Moderne 3D-Grafikhardware
Die Entwicklung moderner 3D-Grafikhardware hat in den letzten Jahren einen großen Anstieg verzeichnen können. Der harte Konkurrenzkampf in diesem Marktbereich zwang viele Hersteller dazu, ständig
neue und innovative Produkte mit mehr Features herauszubringen. Das Preis/Leistungsverhältnis moderner Grafikhardware hat von dieser Entwicklung ebenfalls profitiert. Leistungsfähige Grafikhardware, die
vor einigen Jahren nur professionellen Anwendern mit einem entsprechenden Budget vorbehalten war,
ist heute auch für die breite Masse verfügbar. Computerspiele haben diese Entwicklung ebenfalls stark
beeinflusst. Die grafische Darstellung moderner Computerspiele ist in den letzten Jahren immer realistischer und damit auch aufwändiger geworden. Dabei gilt beispielsweise bei 3D-Spielen eine Bildrate
von mindestens 30 Bildern pro Sekunde als Standard für ein interaktives Spielerlebnis [Ber05]. Durch
die höheren Detailgrade aktueller Spiele ist damit eine performante Hardware zur Darstellung notwendig
geworden. Moderne Grafikhardware bietet heute ein sehr effizientes Rechenwerk. Dabei ist vor allem die
schnelle Verarbeitung von Fließkommaoperationen interessant. Neue Möglichkeiten zur Visualisierung
sorgen ebenfalls für einen höheren Realismus. Dazu ist jedoch das relativ einfache Beleuchtungsmodell
der klassischen Grafikpipeline unzureichend. Aus diesem Grund arbeitet moderne Grafikhardware mit
einer programmierbaren Grafikpipeline, die es erlaubt, einzelne Stufen der Pipeline durch anwendungsspezifische programmierbare Stufen zu ersetzen. Zudem erhöhen neueste Features aktueller Grafikhardware die Flexibilität hinsichtlich der Darstellung komplexer grafischer Szenen. Im Folgenden werden die
wichtigsten Entwicklungen aus dem Bereich der Grafikhardware näher beschrieben. Weiterhin wird auf
einige Features, die im Kontext dieser Arbeit besonders relevant sind, eingegangen.
3.1
Fließkommaleistung
Für die interaktive Darstellung einer 3D-Szene ist unter anderem die schnelle Verarbeitung von Fließkommaoperationen wichtig. Die Transformation und Beleuchtung dreidimensionaler Objekte erfordert
eine Vielzahl dieser Operationen. Seitdem die notwendigen „Transform and Lighting“-Berechnungen
der Grafikpipeline von der CPU des Hostrechners auf die Grafikhardware
1
verlagert wurde, ist dieser
Anspruch nochmals gewachsen. Zudem werden für die Beleuchtung und Schattierung der Fragmente
ebenfalls eine Vielzahl von Fließkommaoperationen benötigt. Aus diesem Grund wurde die Grafikhardware in den letzten Jahren vor allem hinsichtlich ihrer Fließkommaleistung optimiert. Das Rechenwerk
einer modernen Grafikkarte, die Graphics Programming Unit (GPU), kann Fließkommaberechnungen
wesentlich schneller durchführen als ihr Pendant, die Central Processing Unit (CPU) des Hostrechners.
So ergaben spezielle Leistungsuntersuchungen 2 , dass eine nVidia GeForce 5900 Ultra einem fiktivem
Intel Pentium IV Prozessor mit 10-GHz Taktfrequenz entsprechen würde [Meh04]. Wenn man bedenkt,
dass diese Grafikkarte bereits in der zweiten Generation 3 abgelöst wurde, ist dies eine bemerkenswer1 Die nVidia GeForce 256, welche Ende 1999 erschien, konnte als erste Karte Transform and Lighting-Berechnungen durch-
führen
2 Einfache Multiplikation von Fließkommazahlen
3 Aktuelle Generation GeForce 7800 (G70), davor GeForce 6800 (NV40)
8
Texture
Memory
(Geometry)
Vertex
Processor
App.
Memory
Pixel
Unpack
Primitve
Assembly
Clip
Project
Viewport
Cull
(Geometry)
Fragment
Processor
Per
Fragment
Operations
Frame
Buffer
Operations
Rasterize
Pixel
Transfer
(Pixels)
(Pixels)
Frame
Buffer
Read
Control
Pixel
Pack
Pixel Groups
Vertices
Fragments
Textures
= Programmable Processor
Abbildung 8: Die moderne Grafikpipeline von OpenGL 2.0 [Ros04b]
te Leistung. Auf aktuellen Spitzenmodellen werden zudem breite Speicheranbindungen in Kombination
mit einem sehr schnellen dedizierten Speicher verbaut 4 . Dadurch ist die theoretische Speicherbandbreite
einer solchen Grafikkarte erheblich größer als die des Hostrechners.
3.2
Programmierbare Grafikpipeline
Neben der Forderung die Fließkommaleistung ständig zu verbessern, ist auch eine Forderung nach mehr
Flexibilität der Grafikpipeline gewachsen. Umfangreiche grafische Effekte und neue Beleuchtungsmodelle waren mit der klassischen Grafikpipeline kaum mehr zu realisieren. Durch eine Integration von
programmierbaren Bausteinen in die GPU, die so genannten Vertex- und Fragmentprozessoren, konnte
die Grafikpipeline deutlich flexibler gestaltet werden [Ros04a]. Einzelne Stufen der Grafikpipeline, die
zuvor über einen festen Funktionsumfang verfügten, können nun durch programmierbare Stufen ersetzt
werden. Abbildung 8 zeigt eine moderne Grafikpipeline mit ihren programmierbaren Stufen. Die Vertexund Fragmentprozessoren sind auf Fließkommaoperationen optimiert. Zudem findet man auf aktueller
Grafikhardware meist mehrere davon 5 . Berechnungen können auf diese Weise hochgradig parallel ausgeführt werden, was insgesamt eine schnellere Verarbeitung ermöglicht.
3.2.1
Vertexprozessor
Der Vertexprozessor übernimmt Aufgaben, die pro Eckpunkt (Vertex) und seiner zugehörigen Attribute
6
in der zu berechnenden Szene anfallen [Ros04a]. Üblicherweise werden folgende Aufgaben vom
4 nVidia
7800 GTX mit 256-Bit Speicherbusbreite und GDDR-RAM3 [WP05]
Radeon X850 XT PE integriert sechs Vertex Prozessoren [Wei04]
6 Eckpunktnormale, Texturkoordinate, Farbwert etc.
5 ATI
9
Vertexprozessor durchgeführt:
• Transformation von Eckpunkten und Normalen
• Generierung von Texturkoordinaten
• Beleuchtungsberechnungen pro Eckpunkt
Dieses Aufgabenfeld fasst man unter dem Namen Transform and Lighting zusammen. Natürlich kann
der Vertexprozessor auch andere Berechnungen durchführen. Damit ein paralleles Arbeiten der Vertexprozessoren möglich ist, ist pro Vertexprozessor immer nur der Zugriff auf einen Eckpunkt und die
damit verknüpften Eigenschaften möglich. Weiterhin können im Vertexprozessor nur bestehende Daten
modifiziert werden. Dabei kann er keine Geometrie hinzufügen oder entfernen. Außerdem bleiben einige
Aufgaben der „festen Grafikpipeline“ vorbehalten und sind nicht über die Vertexprozessoren programmierbar. Unter anderem sind dies
• Perspektivische Projektion,
• Clipping und
• Backface Culling (Entfernung abgewandter Polygone).
3.2.2
Fragmentprozessor
Der Fragmentprozessor übernimmt Aufgaben, die pro eingehendes Fragment nach der Rasterisierung
der Primitive anfallen [Ros04a]. Diese Aufgaben umfassen üblicherweise
• Texturierung pro Fragment und
• Beleuchtung pro Fragment
Dabei kann ein Fragment aus einer ganzen Reihe von Daten bestehen:
• x,y Positionen nach der Rasterisierung
• Interpolierte Farbwerte
• Interpolierte Normale
• Interpolierte Texturkoordinate(n)
• Einträge im Tiefenpuffer (Depth Buffer)
Aufgrund der Architektur müssen auch hier sämtliche Berechnungen parallelisierbar bleiben. Pro Fragmentprozessor ist jeweils nur der Zugriff auf eines der Fragmente gleichzeitig möglich. Neue Fragmente
können nicht generiert werden. Zudem lassen sich einige Attribute der Fragmente, wie zum Beispiel die
Positionsdaten, nicht mehr verändern.
10
3.2.3
Shadersprachen
Die Programmierung der Vertex- und Fragmentprozessoren wird mit speziellen Programmiersprachen,
den Shadersprachen vorgenommen. Hier haben sich vor allem Hochsprachen wie Cg von nVidia, HLSL
von Microsoft oder auch die OpenGL Shading Language etabliert. Dabei wird zwischen Programmen,
die auf dem Vertexprozessor laufen, den Vertexshadern, und Programmen, die auf dem Fragmentprozessor laufen, den Fragmentshadern, unterschieden [Ros04a]. Die Programmierung in den meisten Shaderhochsprachen ist sehr stark an die prozedurale Programmiersprache C angelehnt. Vor den Hochsprachen
zur Programmierung von Shadern wurde eine Programmierung oft in assemblerähnlichen Sprachen vorgenommen. Aufgrund der niedrigen Abstraktionsebene, der schlechten Portabilität und der schlechten
Wartbarkeit wurde die direkte Programmierung durch diese maschinennahen Sprachen aber mehr und
mehr in den Hintergrund gedrängt.
3.3
Speicherobjekte
Moderne Grafikkarten bieten heute einen großen Speicherausbau und können damit unter anderem eine
bestimmte Anzahl von Texturen resident halten. Dadurch kann die leistungsmäßig teure Busübertragung
der Texturen verringert werden. Analog dazu kann auch ein Transfer der Vertexdaten vom Arbeitsspeicher des PC-Systems zur Grafikhardware zu Leistungseinbrüchen führen. Im so genannten immediate
mode der 3D-APIs werden unter anderem die Vertexdaten zur weiteren Verarbeitung sofort über den
Bus geschickt [WN98]. Dies würde sich besonders bei einem hohen geometrischen Aufkommen schnell
negativ auf die Leistung auswirken. Besonders wenn einige statische Teile der 3D-Szene ständig neu
gezeichnet werden müssten, würde dies eine Vielzahl an redundanten Bustransfers bedeuten. Auch hierbei wäre eine direkte Speicherung oder Pufferung der Vertexdaten im Grafikspeicher wünschenswert.
Moderne 3D-APIs, wie OpenGL, bieten dazu entsprechende Mechanismen an. Dabei ist eine Speicherung häufig genutzter Operationen auf der Grafikhardware in OpenGL schon länger möglich. So können
mehrere OpenGL-Kommandos in einem für die Grafikhardware optimierten Format, den so genannten
Display Lists, im Grafikspeicher abgelegt werden [WN98]. Die Ausführungsgeschwindigkeit einer solchen Display List ist dabei meist deutlich höher, als die Kommandos im immidiate mode einzeln hintereinander aufzurufen. So lassen sich besonders oft benötigte Kommandofolgen effizient speichern. Leider
haben die Display Lists den entscheidenden Nachteil, dass sie sehr statisch sind und wenig Flexibilität
erlauben. Sie werden einmal generiert, kompiliert und anschließend über den Bus zur Grafikhardware
übertragen. Eine nachträgliche Modifikation der Daten ist ohne eine erneute Kompilierung unmöglich.
Da das Kompilieren recht teuer werden kann, sind die Display Lists für stark dynamische Szenen weniger geeignet [WN98].
Seit der Version 1.5 von OpenGL gibt es Erweiterungen für spezielle Speicherobjekte zur Datenpufferung. Diese werden unter dem Namen Vertex Buffer Objects (VBOs) geführt [WE05]. Anders als die
Display Lists sind die VBOs auschließlich als Pufferspeicher, zum Beispiel für Eckpunktdaten, vorgesehen. OpenGL-Kommandos können nicht gespeichert werden. Mit den VBOs ist es möglich, Bereiche
im Grafikspeicher oder Arbeitsspeicher des Hostrechners zu reservieren und als Pufferspeicher für Eck11
punktdaten zu nutzen. OpenGL verwaltet die Speicherverwaltung und den Auslagerungsvorgang. Dabei
entscheidet die 3D-API selbst über eine Pufferung der Daten im Grafikspeicher oder eine Auslagerung
im Arbeitsspeicher des Hostrechners. Dieses Verhalten lässt sich zusätzlich über spezielle Flags für den
primären Verwendungszweck der zu puffernden Daten beeinflussen. So ist es zum Beispiel möglich, in
stark dynamischen Szenen mit häufig wechselnder Geometrie auf eine Auslagerung im Grafikspeicher
zu verzichten. Dies macht Sinn, da die ständigen Buszugriffe zur Modifikation der Vertexdaten auf der
Grafikhardware die Leistung negativ beeinflussen würden. Auf der anderen Seite können die geometrischen Daten statischer Szenen, analog zu den Display Lists, im schnellen Speicher der Grafikhardware
gepuffert werden. In der aktuellen Version 2.0 von OpenGL sind zudem Speicherobjekte vorhanden,
die nicht nur zur Pufferung von Vertexdaten dienen. Eine recht interessante Entwicklung hat sich dabei mit den Pixel Buffer Objects (PBOs) und Frame Buffer Objects (FBOs) aufgetan. Durch Nutzung
dieser speziellen Speicherobjekte hat man die Möglichkeit, Daten der berechneten Fragmente in einen
Pufferspeicher zur weiteren Verarbeitung umzuleiten. Die Daten der Fragmente könnten dann als Quelle
für Vertexdaten dienen. Diese Mechanismen werden im Allgemeinen als „Render To Vertex Array“ und
„Render To Texture“ bezeichnet und können unter anderem dazu genutzt werden, ein zustandserhaltendes Partikelsystem zu realisieren. Insgesamt werden durch die Nutzung von Speicherobjekten redundante
Bustransfers weitgehend vermieden und die Gesamtleistung gesteigert.
3.4
Pointsprites
In speziellen 3D-Anwendungen, wie zum Beispiel bei der Darstellung von Partikelsystemen, ist es notwendig, eine große Anzahl von Objekten zu visualisieren. Dabei ist eine detaillierte geometrische Repräsentation der einzelnen Partikel oftmals kaum möglich. Bei einer zu großen Partikelanzahl würde
das geometrische Aufkommen selbst aktuelle Grafikhardware überfordern. Der damit verbundene hohe
Speicherbedarf könnte ebenfalls zu einem Problem werden. In vielen Fällen kann man die Partikel jedoch durch einfache Primitive mit einer entsprechenden Texturierung annähern. In der Praxis werden dazu häufig Billboards genutzt [Meh03]. Hierbei ist meist ein einfaches texturiertes Quadrat gemeint, dass
stets zum Betrachter beziehungsweise zur Kamera hin ausgerichtet ist. Abbildung 9 zeigt ein solches
Billboard in einer 3D-Szene. Der Normalenvektor des Billboards ist dabei stets parallel zur Richtung der
Sichtlinie der Kamera ausgerichtet. Um auch unregelmäßige Formen darstellen zu können, werden die
Billboards mit geeignete Texturen versehen. Zusätzlich zur Texturierung wird Alphablending eingesetzt
um entsprechende Bereiche der Textur transparent zu gestalten. Damit lassen sich auch andere Formen
darstellen, ohne das zusätzliche Geometrie benötigt wird. Zudem ist mit Alphablending eine visuelle
Überlagerung mehrerer Billboards möglich. In modernen 3D-APIs ist eine Benutzung von Billboards
direkt möglich 7 . In 3D-APIs werden Billboards auch häufig als Point Sprites bezeichnet [Meh03]. Sie
werden ähnlich wie die Punktprimitiven genutzt. Man ist in der Lage, durch Angabe von nur einem
Eckpunkt ein komplett texturiertes Quadrat zu zeichnen, wobei der angegebene Punkt dem Mittelpunkt
entspricht. Eine entsprechende Rasterisierung des Punktes als quadratische Primitive wird dann von der
7 Die
dazu notwendigen Erweiterungen sind seit Version 1.4 Teil von OpenGL
12
Abbildung 9: Ein ausgerichtetes Billboard
3D-API und der Grafikhardware vorgenommen. Desweiteren werden vor dem Rasterisierungschritt die
notwendigen Berechnungen zur Ausrichtung der Billboards automatisch durchgeführt. Die Berechnung
von Texturkoordinaten kann ebenfalls direkt durch die 3D-API und die Grafikhardware erfolgen. Zudem können die Billboards analog zu den Punktprimitiven je nach Betrachterabstand automatisch mit
verschiedenen Distanzfunktionen skaliert werden [Meh03]. Die Verwendung der Point Sprites ist also
denkbar einfach und besonders auf die Visualisierung von Partikelsystemen zugeschnitten. Dennoch hat
dieses Feature einige Nachteile, auf die in einem späteren Kapitel noch eingegangen wird. Ein großer
Vorteil bleibt jedoch im Vergleich zu anderen Ansätzen bestehen: durch die Verwendung der Point Sprites
können Billboards auf Basis eines minimalen Datenaufkommens repräsentiert werden. Es wird deutlich
weniger Speicherplatz benötigt, was besonders im Zusammenhang mit Speicherobjekten interessant sein
kann. Eine Auslagerung der Eckpunktdaten für die Billboards auf den Grafikspeicher würde den Grafikbus weiter entlasten. Da der Speicher der Grafikhardware jedoch begrenzt ist, sollten die auszulagernden
Daten und damit der Pufferspeicher der Speicherobjekte minimal gehalten werden. Durch Nutzung der
Point Sprites wird dies optimal unterstützt und die Gesamtleistung verbessert.
13
4
Entwurf und Design
Im Rahmen der Aufgabenstellung werden zunächst einige Anforderungen an das zu realisierende Partikelsystem gestellt. Anschließend wird in einer Übersicht der objektorientierte Entwurf näher erläutert.
Dabei werden besonders relevante Bausteine und Klassen im Detail vorgestellt. Zudem werden in diesem
Kapitel einige Designentscheidungen näher erläutert.
4.1
Anforderungen
Für eine spätere Realisierung des Partikelsystems ist es sinnvoll, zunächst einige grundlegende Anforderungen zu formulieren. Diese orientieren sich dabei weitgehend an der Aufgabenstellung, wobei eine
Konkretisierung einzelner Teilaufgaben vorgenommen wurde.
Das Partikelsystem wird auf Basis des zustandslosen Modells umgesetzt, so dass eine Partikelsimulation
im Vertex Processor der Grafikhardware möglich wird. Das Partikelsystem verwaltet dazu eine Reihe von
Emittern. Die Eigenschaften der Emitter, wie Position und Größe, lassen sich jederzeit anpassen. Dabei
sind die Emitter für die Aussendung der Partikel zuständig, wobei die Anzahl und die Lebenszeit der
Partikel begrenzt sind. Das Emissionsverhalten wird durch die Attribute des Emitters sowie die initialen
Eigenschaften der Partikel bestimmt. Eine Belegung der Partikeleigenschaften ist zur Initialisierung des
Systems vorgesehen und sollte aus Effizienzgründen nicht während der laufenden Simulation stattfinden.
Weiterhin können die Partikel durch Gravitations- und Windkräfte beeinflusst werden. Die Bewegungsgleichung der Partikel soll auf der Formel aus Abbildung 4 aufbauen. Ein einfaches Kollisionsmodell
ist ebenfalls vorgesehen. Dazu wird eine einfache Kollisionbehandlung der Partikel mit einer gegebenen XZ-Ebene durchgeführt. Die Berechnungen der Partikelsimulation und Interaktion mit der Umwelt
lassen sich auf dem Hauptprozessor (CPU) des PC-Systems oder auf dem Grafikprozessor (GPU) der
Grafikkarte durchführen. Die Berechnung auf der GPU erfolgt dabei durch einen externen Vertexshader. Beide Umsetzungen nutzen jedoch dieselben Berechnungen und sind auch visuell gleichwertig. Die
Partikeldaten werden zum schnellen Zugriff in dafür geeigneten Speicherobjekten (VBOs) abgelegt. Weiterhin werden die Partikel durch Point Sprites visualisiert. Eine visuelle Änderung der Partikel während
ihrer Lebenszeit ist ebenfalls vorgesehen.
4.2
Das Framework
Für die Darstellung und Berechnung des Partikelsystems in einer 3D-Szene wird ein komplettes Framework realisiert. Dabei ist eine Aufteilung des Frameworks in verschiedene Namensräume vorgesehen. Im
obersten Namensraum framework sollen alle weiteren Namensräume integriert werden. Zudem werden
im obersten Namensraum die Klassen Clock, Logger und Camera abgelegt. Während die Klasse Clock
eine einfache Uhr repräsentiert, soll die Klasse Logger ein Aufzeichnen benutzerdefinierter Textmeldungen ermöglichen. Dies ist vor allem in der Entwicklungsphase als zusätzliches Debugwerkzeug hilfreich.
Beide Klassen sollen zur Laufzeit jeweils nur eine Instanz besitzen und global referenzierbar sein. Aus
diesem Grund bietet es sich an, die Klassen als Singletons zu realisieren. Im obersten Namensraum wird
14
zusätzlich die Klasse Camera implementiert. Sie repräsentiert ein einfaches Kameramodell zur Navigation in der 3D-Szene. Im Namensraum math sind zunächst die Klassen Vector2<T>, Vector3<T>
für Vektorartihmetik und Matrix4x4<T> für Matrizenalgebra vorgesehen. Um einen Einfluss auf die
Genauigkeit der Fließkommaberechnungen zu haben, werden diese Klassen als Templateklassen realisiert. Zusätzlich wird die Klasse Quaternion zur Berechnung von Quaternionenalgebra im gleichen
Namesraum abgelegt. Diese Klasse wird von der Kameraklasse genutzt, um die Orientierung der Kamera innerhalb der 3D-Szene zu berechnen. Im Namensraum graphics ist unter anderem die Klasse Image
vorgesehen. Diese Klasse bietet eine einfach Funktionalität zum Einlesen verschiedener Bildformate, die
beispielsweise als Texturen genutzt werden können. Für die Verwaltung und Generierung der OpenGLTexturobjekte ist die die Klasse TextureManager zuständig. Sie verwendet die Klasse Image und wird
ebenfalls als Singleton realisiert. Zur Repräsentation von RGB- beziehungsweise RGBA-Farbtupel werden die beiden Klassen Color3<T> und Color4<T> erstellt. Da die Farbtupeln auf unterschiedlichen
Datentypen basieren können, werden die beiden Klassen als Templateklassen implementiert. Sie werden
ebenfalls im Namensraum graphics abgelegt. In dem eingebetteten Namensraum shader werden einige Klassen zur Manipulation und Verwaltung von Shadern der OpenGL Shading Language realisiert.
Dazu werden die beiden Klassen VertexShader und FragmentShader für die Verwaltung von Vertexbeziehungsweise Fragmentshadern genutzt. Sie werden von der Klasse Shader abgeleitet, die einen generischen Shader der OpenGL Shading Language repräsentiert. Die Klasse ShaderObject bietet unter
anderem den Zugriff auf die Parameter des jeweiligen Shaders und dient so als Schnittstelle. Damit die
Vertex- und Fragmentshader möglichst einfach geladen, kompiliert und verlinkt werden können, wird zudem die Singletonklasse ShaderManager erstellt. Sie verwaltet eine Liste aller Shader. Im Namensraum
effects wird das Partikelsystem realisiert. Die darin enthaltenen Klassen sind im Rahmen dieser Arbeit
besonders interessant und werden im nächsten Abschnitt ausführlicher betrachtet. Abbildung 10 zeigt
eine Übersicht des Frameworks und der einzelnen Namensräume in einem UML-Klassendiagramm.
4.3
Der Namespace effects
In diesem Namensraum wird das eigentliche Partikelsystem durch eine Reihe von Klassen realisiert. In
Abbildung 11 ist die Klassenstruktur des Partikelsystems in einem UML-Klassendiagramm dargestellt.
Im Folgenden werden die einzelnen Klassen ausführlicher betrachtet.
4.3.1
Die Klasse Particle
Die Partikel bilden als kleinste Einheit die Grundlage des gesamten Systems. Die Berechnungen zur Simulation des Partikelsystems basieren hauptsächlich auf den initialen Eigenschaften oder Attributen der
Partikel. Der Aufbau der Partikelstruktur und die Wahl der Eigenschaften sollte vom Einsatzzweck und
den späteren Berechnungen zur Simulation abhängig sein. Die Anzahl der Eigenschaften pro Partikel
sollte dabei minimal gehalten werden. Besonders bei größeren Partikelsystemen mit einer hohen Partikeldichte kann der Speicherbedarf eine nicht unerhebliche Rolle spielen. Ebenso ist die Anzahl der in
einem Vertexshader verwendbaren Eckpunktattribute (Vertex Attributes) typischerweise begrenzt. Dies
15
framework
math
graphics
Vector2<T>
shader
verwaltet
ShaderManager
1
Vector3<T>
*
verwaltet
Shaderprogram
<< use >>
ShaderObject
1
*
Quaternion
FragmentShader
VertexShader
<< use >>
Matrix4x4
effects
generiert
Particle
*
*
Camera
1
emmitiert
ParticleSystem
verwaltet
ParticleEmitter
1
1
verwaltet
1
*
Clock
<< use >>
ParticleSet
1
*
1
verwaltet
1
erzeugt
Logger
1
GPUParticleEmitter
CPUParticleEmitter
XMLParticleSystemParser
Image
<< use >>
TextureManager
Color4<T>
Color3<T>
Abbildung 10: Das gesamte Framework in der Übersicht
16
generiert
Particle
ParticleSet
1
*
*
*
1
verwaltet
1
emmitiert
ParticleSystem
1
1
verwaltet
*
1
verwaltet
ParticleEmitter
1
erzeugt
1
CPUParticleEmitter
GPUParticleEmitter
XMLParticleSystemParser
Abbildung 11: Das Partikelsystem als Klassendiagramm
17
Particle
+pos:Vector3f
+vel:Vector3f
+birth_col:Color4f
+death_col:Color4f
+birth:float
+size:float
+inverted_mass:float
Abbildung 12: Die Klasse Particle
ist besonders für eine Partikelsimulation auf der GPU ein wichtiger Aspekt.
Zunächst wird jeder Partikel als Ausgangspunkt der Berechnungen eine initiale Position haben. Für eine
gerichtete Emission ist darüber hinaus ein Geschwindigkeitsvektor pro Partikel vorgesehen. Die Möglichkeit, die Partikel zu individuellen Zeitpunkten emittieren zu können, soll ebenfalls realisiert werden.
Dazu ist der Zeitpunkt der Geburt des Partikels als Eigenschaft notwendig. Für die Berechnung der Beschleunigungskräfte die auf ein Partikel wirken, sollte zudem die Masse des Partikels mit einbezogen
werden. Die Partikelmasse wird dabei als inverse Masse gespeichert, um spätere Berechnungen zu vereinfachen. Zur Darstellung der Partikel kann unter anderem eine Farbinterpolation über ihre Lebensspanne verwendet werden. Dazu sind zwei RGBA-Farbangaben zum Zeitpunkt der Geburt und zum Zeitpunkt
des Todes als zusätzliche Eigenschaften der Partikel notwendig. Weiterhin ist eine Größenangabe des jeweiligen Partikels als Eigenschaft vorgesehen.
Die Eigenschaften der Partikel werden als Attribute in der Klasse Particle gekapselt. Abbildung 12
zeigt die Klasse in einem UML-Klassendiagramm. Die Instanzen der Klasse Particle sind als Datencontainer für die Eigenschaften der Partikel gedacht. Der Zugriff auf die Partikeleigenschaften, der für
die Berechnungen zur Simulation notwendig ist, sollte möglichst schnell erfolgen können. Um weiteren
overhead durch den Aufruf von Zugriffsmethoden zu vermeiden, werden sämtliche Attribute der Klasse
mit einem öffentlichem Zugriff (public) versehen.
4.3.2
Die Klasse ParticleSet
Die Klasse ParticleSet generiert eine Partikelgruppe die einem Emitter zugewiesen werden kann. In
dieser Klasse werden dazu eine Reihe von Partikel erzeugt und in ihren initialen Eigenschaften festgelegt. Sämtliche Berechnungen für eine spätere Simulation und Darstellung der Partikel werden auf
Basis dieser Eigenschaften durchgeführt. Um das Emissionsverhalten der Partikel zu steuern und so verschiedene Effekte erzielen zu können, müssen die Eigenschaften entsprechend vergeben werden. Die
Klasse ParticleSet kapselt dazu eine Reihe von Parametern, über die sich die Vergabe der initialen Eigenschaften für ganze Partikelgruppen steuern lässt. So ist es möglich, Partikelgruppen mit einer
punktförmigen oder flächigen Verteilung ihrer Startpositionen zu erzeugen. Zudem lässt sich die grobe
18
ParticleSet
+particle_vector:vector<Particle*>
+distribution_type:DISTRIBUTION_TYPE
+particle_vel:Vector3f
+particle_vel_var:float
+particle_life:float
+constant_flow:bool
+particle_min_birth:float
+particle_max_birth:float
+particle_min_size:float
+particle_max_size:float
+particle_birth_color:Color4f
+particle_death_color:Color4f
+particle_birth_color_var:float
+particle_death_color_var:float
+particle_birth_color_var_alpha:bool
+particle_death_color_var_alpha:bool
+build(particle_count:UINT32):void
+release():void
Abbildung 13: Die Klasse ParticleSet
Emissionsrichtung für eine Gruppe von Partikeln durch die Angabe eines Geschwindigkeitsvektors festlegen. Damit verschiedene Effekte, wie zum Beispiel Explosionen, simuliert werden können, sollen auch
kugelförmige Emissionen möglich sein. Dazu kann der Geschwindigkeitsvektor jedes Partikels durch
einen entsprechenden Parameter mit einem Zufallsvektor skaliert werden. Eine Anpassung der Geburtszeitpunkte durch eine entsprechende Parametervergabe ist ebenfalls möglich. Damit lässt sich der Effekt
eines explosionsartigen Ausstoßes weiter unterstützen, wobei auch ein kontinuierliches Emissionsverhalten ermöglicht wird. Über weitere Parameter kann eine entsprechende Verteilung der Partikelmasse
für eine Gruppe gesteuert werden. Um eine flexible Darstellung der Partikel zu ermöglichen, lassen sich
neben den Partikelgrößen auch die Farbeigenschaften über Parameter justieren. Sind die gewünschten
Parameter vergeben, kann die Klasse ParticleSet eine Partikelgruppe generieren und in einem Vektor
ablegen. Die Klasse ist in Abbildung 13 als UML-Klassendiagramm dargestellt.
4.3.3
Die Klasse ParticleEmitter
Ein Partikelsystem enthält üblicherweise mindestens einen oder mehrere Emitter. In dem hier vorgestellten zustandslosem Partikelsystem ist der Emitter primär für die Emission der Partikel, also ihre Aussendung zuständig. Die Festlegung der initialen Eigenschaften findet nicht während der Geburt des Partikels
durch den Emitter statt, sondern bei der Generierung innerhalb des Klasse ParticleSet. In dem hier
vorgestellten Partikelsystem soll pro Emitter genau eine Gruppe von Partikeln, also ein ParticleSet,
19
~ e +W
~ s +W
~Fsum = G
~ e +G
~s
1 ~
Fsum dt 2
~pnew =~e pos + (esize~pinit ) + (~evel +~vinit )dt +
pmass
~Fsum
~e
G
–
Summe aller Kräfte
–
Gravitationskraft im Emitterumfeld
~e
W
~s
G
–
Windkraft im Emitterumfeld
–
Systemweite Gravitationskraft
~s
W
–
Systemweite Windkraft
~pnew
–
neue Position des Partikels
~pinit
–
initiale Position des Partikels
pmass
–
Masse des Partikels
~vinit
–
initialer Geschwindigkeitsvektor des Partikels
esize
–
Größe des Emitters
~e pos
–
Position des Emitters
~evel
–
Geschwindigkeitsvektor des Emitters
dt
–
Zeitintervall der Simulation
(3)
(4)
Abbildung 14: Die Bewegungsgleichung der Partikelsimulation
verwaltet werden. Die zur Berechnung der Bewegungsgleichungen notwendigen Variablen sind ebenfalls
in der Klasse ParticleEmitter gekapselt. Lokale Kräfteinwirkungen, die auf die jeweiligen Partikel
des einzelnen Emitters beschränkt sind, sollen ebenfalls möglich sein. Die Formel in Abbildung 14 berechnet dabei die aktuelle Position der Partikel. Weiterhin ist eine einfache Kollisionsdetektion sowie
Kollisionsbehandlung mit einem virtuellen Boden (XZ-Ebene) vorgesehen. Die Höhe und andere Eigenschaften des Bodens lassen sich ebenfalls pro Emitter festlegen lassen. Zur Darstellung der Partikel wird
Texturemapping verwendet. Daher ist eine Referenz auf ein OpenGL Texturobjekt Teil der Klasse. Eine
Nutzung von Alphablending ist ebenfalls vorgesehen. Die dafür notwendigen OpenGL Aufzählungstypen für die Blendingfaktoren sind als Attribute in der Klasse abgelegt. Dadurch lassen sich pro Emitter
eine Textur und das Blendingverhalten für alle zu emittierenden Partikel festlegen. Eine Nutzung von
individuellen Texturen und Blendingverhalten pro Partikel ist nicht vorgesehen. Für einen Texturwechsel
8
ist aufgrund der Architektur von OpenGL ein Zustandswechsel (state change) notwendig. Dies
kann mitunter zu Leistungseinbußen führen. Eine Minimierung der Zustandswechsel sollte demnach angestrebt werden. Bei der Nutzung eines Texturobjekts pro Partikel wäre die Anzahl der Zustandswechsel
im ungünstigstem Fall direkt an die Partikelanzahl gebunden. Dies würde in fast allen Fällen zu starken Leistungseinbrüchen führen und die Bildwiederholrate reduzieren. Für eine effiziente Verarbeitung
größerer Partikelgruppen werden OpenGL Speicherobjekte zur Pufferung der Eckpunktdaten verwendet.
8 Über
die Funktion glBindTexture2D(...)
20
Auch in diesem Zusammenhang wäre ein Texturwechsel oder ein Wechsel des Blendingverhaltens auf
Partikelebene problematisch.
CPU- und GPU-Variante des Partikelsystems nutzen beide Shader, wobei nur die GPU-Variante die Partikelsimulation direkt auf dem Vertexprozessor der Grafikhardware berechnet. Beide Varianten werden
durch Ableitung der Klasse ParticleEmitter realisiert. Dadurch kann möglichst viel Code der Basisklasse wieder verwendet werden. Zudem sollen auch ein flexibles System mit einer Mischung aus
CPU- und GPU-basierenden Emittern ermöglicht werden. Ein späterer Vergleich beider Varianten wird
dadurch ebenfalls vereinfacht. Um die Funktionsweise des Emitters für die CPU- und GPU-Variante
entsprechend zu abstrahieren, werden die drei Methoden init(), update() und render() in der Basisklasse
ParticleEmitter als abstrakt deklariert. Die Methoden können dann in der abgeleiteten Klasse entsprechend überschrieben werden. Die Methode init() dient dabei der Initialisierung des jeweiligen
Emitters. Die Methode update() dient der Aktualisierung der Simulation und die Methode render()
ist für das Zeichnen der Partikel verantwortlich. In Abbildung 15 ist die Klasse ParticleEmitter in
einem vereinfachtem 9 UML-Klassendiagramm dargestellt.
4.3.4
Die Klasse CPUParticleEmitter
In dieser Klasse wird die CPU-Variante des Partikelsystems realisiert. Dazu werden die abstrakten Methoden create(), update() und render() der Basisklasse ParticleEmitter überschrieben. Abbildung 16
zeigt die Klasse CPUParticleEmitter als UML-Klassendiagramm.
4.3.5
Die Klasse GPUParticleEmitter
In dieser Klasse wird die GPU-Variante des Partikelsystems realisiert, wobei die gesamte Berechnung zur
Simulation der Partikel in einem externen Vertexshader durchgeführt wird. Dazu werden ebenfalls die
abstrakten Methoden create(), update() und render() der Basisklasse ParticleEmitter überschrieben.
Für die interne Verwaltung der OpenGL Vertex Arrays sind zudem einige weitere Attribute notwendig.
Abbildung 17 zeigt die Klasse GPUParticleEmitter als UML-Klassendiagramm.
4.3.6
Die Klasse ParticleSystem
Die Klasse ParticleSystem verwaltet das Partikelsystem. Dazu erhält die Klasse den Zugriff auf sämtliche Partikelgruppen und Emitter des Systems. Der Zugriff wird durch Klassenattribute in Form von
zwei Vektoren mit Zeigern auf die jeweiligen Klasseninstanzen ParticleEmitter und ParticleSet
realisiert. Über verschiedene Zugriffsmethoden lassen sich zudem Zeiger auf Instanzen der Klassen
ParticleEmitter und ParticleSet hinzufügen oder wieder entfernen. Eine Zuweisung der Partikelgruppen zu den jeweiligen Emittern wird ebenfalls in der Klasse ParticleSystem vorgenommen.
Durch weitere Attribute und Zugriffsmethoden lassen sich auch globale Wind- und Gravitationskräfte
festlegen, die für alle Emitter des Partikelsystems gelten. Letztlich werden über diese Klasse alle Emitter
9 Die
Zugriffsmethoden für die Attribute wurden nicht aufgelistet
21
ParticleEmitter
#size:float
#pos:Vector3f
#vel:Vector3f
#gravity:Vector3f
#wind:Vector3f
#timestep:float
#cur_time:float
#particle_set:ParticleSet*
#particle_max_life:float
#blending:bool
#ground_collision:bool
#texture_id:GLenum
#src_blending_factor:GLenum
#dst_blending_factor:GLenum
#shader_obj:ShaderObject
#vertex_buffer:GLuint
#ground_height:float
#ground_friction:float
#ground_bounce_factor:float
#manage_memory:bool
+create():void
+update():void
+render():void
Abbildung 15: Die Klasse ParticleEmitter
CPUParticleEmitter
active_particles: GLuint
+create():void
+update():void
+render():void
Abbildung 16: Die Klasse CPUParticleEmitter
22
GPUParticleEmitter
-POS_ARRAY:GLuint=0
-VEL_ARRAY:GLuint=1
-BIRTH_ARRAY:GLuint=2
-BIRTH_COLOR_ARRAY:GLuint=3
-DEATH_COLOR_ARRAY:GLuint=4
-MASS_ARRAY:GLuint=5
-SIZE_ARRAY:GLuint=6
+create():void
+update():void
+render():void
Abbildung 17: Die Klasse GPUParticleEmitter
aktualisiert und dargestellt. In Abbildung 18 ist die Klasse ParticleSystem als UML-Klassendiagramm
dargestellt.
4.3.7
Die Klasse XMLParticleSystemParser
In dieser Klasse ist die Funktionalität zum Laden und Generieren eines Partikelsystem aus einer XMLDatei gekapselt. Die Klasse ist recht einfach aufgebaut und besitzt nur eine statische Methode. Der
Methode wird als Parameter der Dateiname der XML-Ressource übergeben und man erhält als Rückgabe
einen Zeiger auf eine gültige Instanz der Klasse ParticleSystem. Aufgrund des einfachen Aufbaus der
Klasse wird auf eine Abbildung verzichtet.
4.4
Ein Datenformat zur Beschreibung des Partikelsystems
Für die Erstellung und Steuerung des Partikelsystems ist eine recht beträchtliche Anzahl an Parametern
notwendig. Außerdem sind komplexe Szenen mit mehreren Emittern und Partikelgruppen denkbar. Es
wäre hilfreich, wenn man die Daten die dem jeweiligen Partikelsystem zugrunde liegen in eine externe
Ressource auslagern könnte. Dazu muss zunächst ein geeignetes Dateiformat zur Speicherung der Daten
gefunden werden. Da hier weniger eine effiziente Speicherung mit kleinen Dateigrößen im Vordergrund
steht, wird auf eine Speicherung in einem binären Format verzichtet. Stattdessen wird ein textbasierendes Format genutzt, das vom Syntax her stark mit der eXtensible Markup Language (XML) verwandt
ist. Dabei werden die in XML typischen Tags und Attributnamen so gewählt, dass sie möglichst selbsterklärend sind. Dadurch bleibt die Lesbarkeit einer solchen XML-Ressource weitgehend erhalten und eine
manuelle Bearbeitung wird ebenfalls vereinfacht. Zudem begünstigt der hierarchische Aufbau von XML
eine Repräsentation des Partikelsystems in diesem Format. Listing 1 zeigt ein Partikelsystem in einem
XML-ähnlichem Syntax.
<ParticleSystem >
23
ParticleSystem
-emitter_list:vector<ParticleEmitter*>
-particle_set_list:vector<ParticleSet*>
-manage_memory:bool
-global_gravity:Vector3f
-global_wind:Vector3f
+release():void
+getEmitterCount():UINT32
+getParticleSetCount():UINT32
+getParticleCount():UINT32
+getGlobalGravity():Vector3f
+getGlobalWind():Vector3f
+setGlobalGravity(global_gravity:Vector3f):void
+setGlobalWind(global_wind:Vector3f):void
+update():void
+render():void
+addEmitter(emitter:ParticleEmitter*):void
+addParticleSet(particle_set:ParticleSet*):void
+removeEmitter(index:UINT32):void
+removeParticleSet(index:UINT32):void
+assign(emitter_index:UINT32,particle_set_index:UINT32):void
Abbildung 18: Die Klasse ParticleSystem
24
< G r a v i t y > 0 . 0 −9.81 0 . 0 < / G r a v i t y > <!−− g l o b a l g r a v i t y f o r c e −−>
<Wind > 0 . 0 0 . 0 0 . 0 < / Wind> <!−− g l o b a l wind f o r c e −−>
<!−− t e x t u r e l i s t −−>
< T e x t u r e c o m p r e s s e d = " y e s " mipmapped= " y e s " > . / t e x t u r e s / p a r t i c l e S . png < / T e x t u r e >
<!−− s h a d e r −−>
<Shader v e r t e x s h a d e r =" . / cpu_shader / cpu_shader . vs " f r a g m e n t s h a d e r =" . / cpu_shader / cpu_shader . f s ">
</ S h a d e r >
<!−− P a r t i c l e S e t #1 − d e f i n e s t h e i n i t i a l v a l u e s o f t h e p a r t i c l e s −−>
<ParticleSet >
<Count >300 </ Count > <!−− p a r t i c l e c o u n t −−>
< D i s t r i b u t i o n > p l a n e y z < / D i s t r i b u t i o n > <!−− p a r t i c l e i n i t i a l d i s t r i b u t i o n −−>
< L i f e > 1 3 . 0 < / L i f e > <!−− p a r t i c l e l i f e s p a n −>
< B i r t h c o n s t a n t F l o w = " y e s " min= " 0 . 0 " max= " 0 . 0 " / > <!−− p a r t i c l e b i r t h t i m e −−>
< B i r t h C o l o r v a r = " 0 . 0 " a f f e c t s A l p h a = " no " >0.8 0 . 4 0 . 2 1 . 0 < / B i r t h C o l o r > <!−− p a r t i c l e b i r t h c o l o r −−>
< D e a t h C o l o r v a r = " 0 . 0 " a f f e c t s A l p h a = " no " >0.8 0 . 8 0 . 0 1 0 . 0 < / D e a t h C o l o r ><!−− p a r t i c l e d e a t h c o l o r −−>
< S i z e min= " 5 0 . 0 " max= " 6 0 . 0 " / > <!−− p o i n t s i z e , c a n be s e t g r e a t e r t h a n t h e h a r d w a r e l i m i t ! −−>
<Mass min= " 0 . 5 " max= " 0 . 5 " / > <!−− p a r t i c l e i n i t i a l minimum and maximum s i z e −−>
< V e l o c i t y v a r = " 0 . 0 " >15.0 8 . 0 0 . 0 < / V e l o c i t y > <!−− p a r t i c l e i n i t i a l v e l o c i t y v e c t o r −−>
</ P a r t i c l e S e t >
<!−− P a r t i c l e S e t #2 −−>
<ParticleSet >
<Count >300 </ Count >
< D i s t r i b u t i o n >planeyz </ D i s t r i b u t i o n >
<Life >13.0 </ Life >
< B i r t h c o n s t a n t F l o w = " y e s " min= " 0 . 0 " max= " 0 . 0 " / >
< B i r t h C o l o r v a r = " 0 . 0 " a f f e c t s A l p h a = " no " >0.0 0 . 3 0 . 9 1 . 0 < / B i r t h C o l o r >
< D e a t h C o l o r v a r = " 0 . 0 " a f f e c t s A l p h a = " no " >0.1 0 . 1 0 . 0 1 0 . 0 < / D e a t h C o l o r >
< S i z e min= " 5 0 . 0 " max= " 6 0 . 0 " / >
<Mass min= " 0 . 5 " max= " 0 . 5 " / >
< V e l o c i t y v a r = " 0 . 0 " > −15.0 8 . 0 0 . 0 < / V e l o c i t y >
</ P a r t i c l e S e t >
<!−− P a r t i c l e E m i t t e r #1 − d e f i n e s t h e i n i t i a l e m i t t e r p r o p e r t i e s −−>
< P a r t i c l e E m i t t e r t y p e = " cpu " a s s i g n = " 0 " s h a d e r I D = " 0 " t e x t u r e I D = " 0 " > <!−− e m i t t e r t y p e and a s s i g n m e n t −−>
< P o s i t i o n > 0 . 0 5 7 . 0 0 . 0 < / P o s i t i o n > <!−− e m i t t e r p o s i t i o n −−>
< V e l o c i t y > 0 . 0 0 . 0 0 . 0 < / V e l o c i t y > <!−− e m i t t e r v e l o c i t y v e c t o r −−>
< G r a v i t y > 0 . 0 0 . 0 0 . 0 < / G r a v i t y > <!−− e m i t t e r l o c a l g r a v i t y f o r c e −−>
<Wind > −9.0 0 . 0 0 . 0 < / Wind> <!−− e m i t t e r l o c a l wind f o r c e −−>
< S i z e > 1 . 0 < / S i z e > <!−− e m i t t e r s i z e −−>
< T i m e s t e p > 0 . 0 0 3 < / T i m e s t e p > <!−− e m i t t e r t i m e s t e p p e r u p d a t e −−>
< P a r t i c l e L i f e > 1 5 . 0 < / P a r t i c l e L i f e > <!−− maximum l i f e o f a p a r t i c l e −−>
< B l e n d i n g s r c = "GL_SRC_ALPHA" d s t = "GL_ONE" e n a b l e d = " y e s " / > <!−− b l e n d i n g f u n c t i o n −−>
<!−− l o c a l g r o u n d p r o p e r t i e s f o r s i m p l e c o l l i s i o n d e t e c t i o n and r e s p o n s e −−>
< G r o u n d C o l l i s i o n GroundHeight=" 0.0 " G r o u n d F r i c t i o n =" 1.0 " GroundBounceFactor=" 0.5 " / >
</ P a r t i c l e E m i t t e r >
<!−− P a r t i c l e E m i t t e r #2 −−>
< P a r t i c l e E m i t t e r t y p e = " cpu " a s s i g n = " 1 " s h a d e r I D = " 0 " t e x t u r e I D = " 0 " >
< Positio n >0.0 57.0 0.0 </ Position >
<Velocity >0.0 0.0 0.0 </ Velocity >
<Gravity >0.0 0.0 0.0 </ Gravity >
<Wind > 9 . 0 0 . 0 0 . 0 < / Wind>
<Size >1.0 </ Size >
<Timestep >0.003 </ Timestep >
< P a r t i c l e L i f e >15.0 </ P a r t i c l e L i f e >
< B l e n d i n g s r c = "GL_SRC_ALPHA" d s t = "GL_ONE" e n a b l e d = " y e s " / >
< G r o u n d C o l l i s i o n GroundHeight=" 0.0 " G r o u n d F r i c t i o n =" 1.0 " GroundBounceFactor=" 0.5 " / >
</ P a r t i c l e E m i t t e r >
</ P a r t i c l e S y s t e m >
Listing 1: Eine XML-Ressource zur Beschreibung des Partikelsystems
4.5
Die Darstellung der Partikel
Die Partikel werden durch texturierte Billboards visualisiert. Damit die einzelnen Partikel entsprechend
überlagert werden können, wird additives Blending genutzt. Beim Zeichenvorgang werden keine Daten
in den Tiefenpuffer geschrieben. Dies hat den Vorteil, dass die Billboards in einer beliebigen Reihenfolge
gezeichnet werden können und nicht vorher ihren Tiefenangaben nach sortiert werden müssen [Meh04].
Um mehr Flexibilität für den visuellen Effekt der Überlagerung zu erhalten, werden für die Billboards
25
Texturen im RGBA-Format mit einer Farbtiefe von 32-Bit genutzt. Dadurch kann die Transparenz einzelner Bildpunkte in der Textur (Texel) entsprechend gesteuert werden. So lassen sich durch entsprechende
Billboardtexturen auch unregelmäßige Formen darstellen. Abbildung 19 zeigt eine typische Partikeltex-
(a) Farbanteil
(b) Alphakanal
Abbildung 19: Eine Partikeltextur
tur, wie sie zum Beispiel für die Darstellung von Rauch genutzt werden kann. Zusätzlich zur Texturierung
werden die Partikel durch eine lineare Interpolation zweier RGBA-Farbwerte eingefärbt. In der Klasse
Particle sind dazu die zwei Attribute birth_col und death_col vorhanden. Während der Partikelsimulation wird mit einem Skalierungsfaktor über die Lebenszeit des jeweiligen Partikels zwischen diesen beiden RGBA-Farben linear interpoliert. So ist es bei einer entsprechenden Farbwahl möglich, die Partikel
langsam ausblenden zu lassen. Um diesen Effekt zusätzlich zu verstärken, kann die Billboardgröße über
die Lebenspanne der Partikel hinweg verringert werden. Für die eigentliche Berechnung und Darstellung
der Billboards wird auf die Point Sprites von OpenGL zurückgegriffen. Vor der eigentlichen Realisierung
des Gesamtsystems wurden dazu einige technische Durchstiche gemacht und alternative Techniken evaluiert. Dabei sind auch einige Nachteile im Umgang mit den Point Sprites aufgefallen, die im Folgenden
kurz erläutert werden. Die Point Sprites sind unter OpenGL leider sehr herstellerspezifisch umgesetzt.
Dies hat, je nach verwendeter Grafikhardware, ein unterschiedliches Verhalten in ihrer Darstellung und
Benutzung zur Folge. So werden die Point Sprites auf ATI-Hardware10 während des Clippingvorgangs
wie Punktprimitive behandelt. Sie werden an den Clippingebenen nicht wie quadratische Polygone beschnitten, sondern einfach verworfen. Besonders bei größeren Partikeln fällt das abrupte Verschwinden
der Billboards an den Bildschirmrändern störend auf. Zudem sind die maximalen Punktgrößen, je nach
Hersteller, recht unterschiedlich ausgefallen. Während nVidia insgesamt nur recht kleine Punktgrößen
erlaubt, orientieren sich die maximalen Punktgrößen auf ATI-Grafikhardware eher an den Texturauflö10 Getestet
auf einer ATI Radeon 9800 PRO mit Catalyst 5.6 Treiber
26
sungen und können damit sehr groß werden. In einem kurzem Test wurde außerdem festgestellt, dass die
Mechanismen zur automatischen Berechnung der Punktgrößen unzuverlässig arbeiten. Über ein manuelles Setzen der Punktgrößen im Vertexshader konnten diese Probleme jedoch umgangen werden. Zudem
wurde das störende Clippingverhalten nicht auf nVidia-Hardware beobachtet.
Ist eine Visualisierung der Partikel durch Point Sprites aufgrund dieser Nachteile oder einer fehlenden Hardwareunterstützung nicht realisierbar, bleibt noch die Möglichkeit die Billboards manuell zu
berechnen. Dies wird auch dann notwendig, wenn man sich für Partikeldarstellung nicht auf die stets
quadratische Form der Point Sprites beschränken will. Manuelle Billboards werden üblicherweise durch
texturierte, meist rechteckige Primitive dargestellt. Dabei können die Berechnungen zur korrekten Ausrichtung der Rechtecke in einem Vertexshader realisiert werden. Leider ist der benötigte Speicherbedarf
pro Billboard im Vergleich zu den Point Sprites immer noch erheblich höher. Da eine Pufferung der
Eckpunktdaten im Grafikspeicher auch bei großen Partikelsystemen ermöglicht werden sollte, fiel die
Designentscheidung zugunsten einer Nutzung der Point Sprites.
27
5
Implementierung
Dieses Kapitel der Ausarbeitung beschäftigt sich mit der Implementierung des in Kapitel 4 vorgestellten Partikelsystems. Dabei werden nachfolgend ausschließlich die relevantesten Teile präsentiert. Ein
Schwerpunkt dieser Arbeit ist der Vergleich der CPU- beziehungsweise der GPU-Variante eines Partikelsystems. Die Funktionalität dieser beiden Varianten ist aufgrund des Designs in den beiden Klassen CPUParticleEmitter und GPUParticleEmitter implementiert, die einen CPU-basierenden beziehungsweise GPU-basierenden Emitter des Partikelsystems repräsentieren. Weiterhin sind noch die
Vertex- und Fragmentshader beider Varianten interessant, wobei dies insbesondere für die GPU-Variante
gilt. Aus diesem Grund werden neben den Shadern ausschließlich Teile der Implementation aus den beiden Klassen CPUParticleEmitter und GPUParticleEmitter betrachtet. Auf implementationsspezifische Details der Testumgebung „ParticleSystemViewer“, die auch im Rahmen dieser Arbeit entstanden
ist, wird verzichtet. Es folgt zunächst eine knappe Übersicht der für die Implementation verwendeten
Bibliotheken, APIs und Werkzeuge.
5.1
Verwendete Bibliotheken, APIs und Werkzeuge
Während der Implementationsphase kamen die im Folgenden aufgeführten Bibliotheken, APIs und Werkzeuge zum Einsatz. Als Programmiersprache wurde C++ genutzt. Dabei wurde die Entwicklungsumgebung „Microsoft Visual Studio 2003“ unter Windows XP verwendet. Als API zur Darstellung der 3DSzenen kam OpenGL zum Einsatz. Dabei wurde aufgrund der neueren Features Version 1.5 und in einer
späteren Entwicklungsphase sogar Version 2.0 verwendet. Die Shader wurden allesamt in der OpenGL
Shading Language (GLSL) implementiert. Das im Rahmen dieser Arbeit umgesetzte Testprogramm setzt
daher Grafikkartentreiber mit einer OpenGL 2.0 Unterstützung voraus. Da ebenfalls einige OpenGL Erweiterungen genutzt wurden, kam die plattformunabhängige GLEW-Bibliothek 11 zum Einsatz. Zudem
wurde die GLUT-Bibliothek
12
genutzt. Die grafische Benutzeroberfläche (GUI) des Testprogramms
„ParticleSystemViewer“ wurde hingegen mit Hilfe der Microsoft Foundation Classes (MFC) umgesetzt.
Da zum Zeitpunkt der Implementation eine Entwicklung unter Linux aufgrund mangelnder Funktionalität der ATI-Grafiktreiber nicht in Frage kam, konnte ohne Bedenken auf die MFC-Bibliotheken unter
Windows zurückgegriffen werden. Weiterhin erlaubte die gute Integration der MFC-Komponenten in
die Entwicklungsumgebung eine zügige Entwicklung der GUI des Testprogramms. Die Klassen des im
Rahmen dieser Arbeit erstellten Frameworks sind jedoch weitgehend plattformunabhängig konzipiert 13
und sollten nach geringen Modifikationen auch unter anderen Architekturen lauffähig sein. Die Kommentierung des Quellcodes wurde durch das Werkzeug doxygen 14 unterstützt. Zum Einlesen und Parsen
der eigenen XML-Dateien wurde eine Bibliothek für einen einfachen XML-Parser 15 von Frank Vanden
11 http://glew.sourceforge.net/
12 http://www.xmission.com/˜nate/glut.html
13 Die
Klasse Clock nutzt Routinen zur Zeitmessung aus einer spezifische Windows Bibliothek
14 http://www.doxygen.org
15 http://iridia.ulb.ac.be/
fvandenb/tools/xmlParser.html
28
Berghen verwendet. Das Einlesen verschiedener Bilddaten für Texturen wurde zudem durch die DevILBibliothek 16 unterstützt.
5.2
Die CPU-Variante
Die Funktionalität für die CPU-basierte Berechnung der Partikelsimulation ist in der Klasse CPUParticleEmitter
gekapselt. Die Klasse repräsentiert einen Partikelemitter, der sämtliche Berechnungen zur Simulation der
Partikel direkt auf dem Hauptprozessor (CPU) des PC-Systems durchführt. Für die Implementation wurden die von der Basisklasse ParticleEmitter geerbten abstrakten Methoden create(), update() und
render() überschrieben. Diese drei Methoden umfassen die Funktionalität des Partikelemitters und werden im Folgenden näher betrachtet. Abschließend wird kurz auf den Vertex- und Fragmentshader dieser
Variante eingegangen.
5.2.1
Die Methode create()
Diese Methode ist für die Initialisierung des CPU-basierten Emitters zuständig. Dabei wird zunächst
einmalig ein OpenGL Speicherobjekt zur Pufferung der Partikeleigenschaften generiert. Dabei wird der
Pufferspeicher später in jedem Simulationschritt mit den aktuellen Partikelattributen gefüllt, die zum
Zeichen der Partikel notwendig sind. Pro Partikel wird ein Datenelement mit insgesamt fünf Fließkommazahlen
17
im Pufferspeicher benötigt. Der Aufbau eines solchen Datenelements ist in Tabelle 1 zu
sehen. Listing 2 zeigt den Quellcode der Methode create().
Position
Farbe
x
r
y
z
g
Größe
b
a
size
Tabelle 1: Der Aufbau eines Datenelements im Pufferspeicher des Speicherobjekts (CPU-Variante)
void CPUParticleEmitter : : c r e a t e ( ) {
/ / vertex buffer object creation
i f ( ! v e r t e x _ b u f f e r ) glGenBuffersARB ( 1 , &v e r t e x _ b u f f e r ) ;
}
Listing 2: Die Methode create() der Klasse CPUParticleEmitter
5.2.2
Die Methode update()
Diese Methode aktualisiert den Emitter und führt die notwendigen Berechnungen der Partikelsimulation für den aktuellen Zeitstempel durch. Die Daten, die zum Zeichnen der Partikel notwendig sind,
werden anschließend in den Pufferspeicher des OpenGL Speicherobjektes kopiert. Dazu wird das Speicherobjekt zunächst für eine Kopieraktion der neuen Datenelemente vorbereitet. Der Pufferspeicher des
16 http://openil.sourceforge.net/
17 32-Bit
float Datentyp
29
Speicherobjekts wird bei jedem Aufruf von update() komplett neu gefüllt. Eine Auslagerung der Partikeldaten in den Grafikspeicher wäre aufgrund der vielen Schreibzugriffe ineffizient. Daher wird dem
OpenGL Treiber der Grafikhardware durch ein entsprechendes Flag mitgeteilt, ob eine Einlagerung des
Speicherobjekts im Arbeitsspeicher des Hostrechners gewünscht wird. Über einen weiteren Parameter
wird zudem der gesamte Pufferspeicher des aktuellen Speicherobjekts für ungültig erklärt. So kann die
Grafikhardware parallel zur Verarbeitung der alten Daten eine Neuorganisation des Pufferspeichers vorbereiten. Gerade in Situationen, in denen große Bereiche des Pufferspeichers neu geschrieben werden
müssen, führt dies nach [nW03] zu einer Leistungssteigerung. Dieses Verhalten konnte tatsächlich beobachtet werden. Anschließend wird in einer Schleife auf die Partikeleigenschaften zugegriffen. Innerhalb der Schleife wird die aktuelle Position, Größe und Farbe jedes einzelnen Partikels berechnet und
anschließend in den Pufferspeicher des Speicherobjekts kopiert. Abschließend wird der aktuelle Zeitstempel der Simulation und die Emitterposition aktualisiert. Eine detaillierte funktionale Sicht auf die
Methode update() ist in Listing 3 als Pseudocode dargestellt. Der eigentliche Quellcode der Methode
ist zusätzlich in Listing 4 aufgeführt.
FUNKTION u p d a t e ( )
initialisiereVertexBufferObject ;
SCHLEIFE ü b e r A l l e P a r t i k e l
FALLS P a r t i k e l N i c h t G e b o r e n FERTIG
SONST
berechneZeitIntervall ;
berechnePartikelPosition ;
berechnePartikelFarbe ;
berechnePartikelGröße ;
FALLS K o l l i s i o n M i t B o d e n
berechnePartikelPositionNeu ;
ENDE
fülleVertexBufferObject ;
ENDE
ENDE
aktualisiereEmitterPosition ;
aktualisiereEmitterZeit ;
ENDE
Listing 3: Die Methode update() der Klasse CPUParticleEmitter als Pseudocode
void CPUParticleEmitter : : update ( ) {
i f ( p a r t i c l e _ s e t ==NULL) r e t u r n ;
/ / b i n d v e r t e x b u f f e r o b j e c t and p r e p a r e t o copy d a t a t o i t
s t d : : v e c t o r < P a r t i c l e ∗> p a r t i c l e _ v e c t o r = p a r t i c l e _ s e t −> p a r t i c l e _ v e c t o r ;
g l B i n d B u f f e r A R B (GL_ARRAY_BUFFER_ARB , v e r t e x _ b u f f e r ) ;
/ / s e t u s a g e f l a g and t e l l t h e d r i v e r t o i n v a l i d a t e t h e e n t i r e b u f f e r
g l B u f f e r D a t a A R B (GL_ARRAY_BUFFER_ARB , 8 ∗ ( UINT32 ) p a r t i c l e _ v e c t o r . s i z e ( ) ∗ s i z e o f ( f l o a t ) , NULL, GL_STREAM_DRAW_ARB ) ;
/ / g e t a p o i n t e r t o t h e v e r t e x b u f f e r ( l o c k ) and t e l l t h e d r i v e r we want w r i t e a c c e s s o n l y
f l o a t ∗ p = ( f l o a t ∗ ) glMapBufferARB (GL_ARRAY_BUFFER_ARB , GL_WRITE_ONLY_ARB ) ;
/ / now l o o p o v e r a l l p a r t i c l e s
UINT32 o f f = 0 ;
f l o a t oneOverParticleMaxLife =1.0 f / p a r t i c l e _ m a x _ l i f e ; / / p r e c a l c u l a t e d to avoid i n n e r l o o p d i v i s i o n s
a c t i v e _ p a r t i c l e s =0;
f o r ( UINT32 i = 0 ; i < ( UINT32 ) p a r t i c l e _ v e c t o r . s i z e ( ) ; i ++) {
/ / s e e i f t h e p a r t i c l e h a s y e t t o be b o r n
f l o a t t = c u r _ t i m e −p a r t i c l e _ v e c t o r [ i ]−> b i r t h ;
/ / n o t y e t b o r n we c a n d i s c a r d i t
i f ( t <=0.0 f ) break ;
/ / c a l c u l a t e t h e o n g o i n g t i m e i n t e r v a l o f t h e p a r t i c l e by u s i n g a f l o a t modulo f u n c t i o n
f l o a t p a r t i c l e _ d t =fmod ( t , p a r t i c l e _ m a x _ l i f e ) ;
/ / a d j u s t t h e p a r t i c l e p o s i t i o n t o e m i t t e r p o s i t i o n and s i z e
V e c t o r 3 f p a r t i c l e _ p o s = p o s + ( p a r t i c l e _ v e c t o r [ i ]−> p o s ∗ s i z e ) ;
/ / compute t h e new p a r t i c l e p o s i t i o n b a s e d on p a r t i c l e v e l o c i t y and t i m e i n t e r v a l
p a r t i c l e _ p o s +=( p a r t i c l e _ v e c t o r [ i ]−> v e l + v e l ) ∗ p a r t i c l e _ d t ;
/ / c a l c u l a t e t o t a l a c c e l a r a t i o n f o r c e on p a r t i c l e
V e c t o r 3 f t o t a l _ a c c e l = g r a v i t y +wind ;
30
/ / s c a l e a c c e l e r a t i o n w i t h t h e i n v e r t e d p a r t i c l e mass
t o t a l _ a c c e l ∗= p a r t i c l e _ v e c t o r [ i ]−> i n v e r t e d _ m a s s ;
/ / calculate the r e s u l t i n g p a r t i c l e position
p a r t i c l e _ p o s += t o t a l _ a c c e l ∗ 0 . 5 f ∗ p a r t i c l e _ d t ∗ p a r t i c l e _ d t ;
/ / c a l c u l a t e t h e o n g o i n g l i f e s p a n o f t h e p a r t i c l e u s e d f o r c o l o r and s i z e i n t e r p o l a t i o n
f l o a t p a r t i c l e _ l i f e s p a n = p a r t i c l e _ d t ∗ oneOverParticleMaxLife ;
/ / c a l c u l a t e t h e o n g o i n g c o l o r b a s e d on a l i n e a r i n t e r p o l a t i o n
C o l o r 4 f p a r t i c l e _ c o l = ( ( p a r t i c l e _ v e c t o r [ i ]−> d e a t h _ c o l −p a r t i c l e _ v e c t o r [ i ]−> b i r t h _ c o l ) ∗ p a r t i c l e _ l i f e s p a n ) ;
p a r t i c l e _ c o l += p a r t i c l e _ v e c t o r [ i ]−> b i r t h _ c o l ;
/ / c a l c u l a t e the ongoing p a r t i c l e s i z e ( d e c r e a s e the p a r t i c l e s i z e over time )
f l o a t p a r t i c l e _ s i z e = ( 1 . 0 f− p a r t i c l e _ l i f e s p a n ) ∗ p a r t i c l e _ v e c t o r [ i ]−> s i z e ;
/ / check f o r a c o l l i s i o n with t h e ground
f l o a t d i s t = p a r t i c l e _ p o s . y−g r o u n d _ h e i g h t ;
/ / do a s i m p l e c o l l i s i o n r e s p o n s e
i f ( dist <0.0) {
f l o a t bounce= g r o u n d _ b o u n c e _ f a c t o r ∗ abs ( s i n ( d i s t ) ) ;
f l o a t damping =pow ( 2 . 0 , d i s t / g r o u n d _ f r i c t i o n ) ;
p a r t i c l e _ p o s i t i o n . y= g r o u n d _ h e i g h t + b o u n c e ∗ damping ;
}
/ / copy t h e new p r o p e r t i e s o f t h e o n g o i n g p a r t i c l e t o t h e v e r t e x b u f f e r o b j e c t
p [ o f f ] = p a r t i c l e _ p o s . xyz ;
/ / copy p a r t i c l e p o s i t i o n
p [ o f f +3]= p a r t i c l e _ c o l . r g b a ;
/ / copy p a r t i c l e r g b a c o l o r
p [ o f f +7]= p a r t i c l e _ s i z e ;
/ / copy p a r t i c l e s i z e
/ / i n c r e a s e the ongoing p a r t i c l e count in the v e r t e x b u f f e r o b j e c t
a c t i v e _ p a r t i c l e s ++;
/ / i n c r e a s e the ongoing o f f s e t
o f f +=8;
}
/ / unmap t h e v e r t e x b u f f e r o b j e c t ( u n l o c k )
glUnmapBufferARB (GL_ARRAY_BUFFER_ARB ) ;
/ / u p d a t e t h e e m i t t e r t i m e and p o s i t i o n
c u r _ t i m e += t i m e s t e p ;
p o s += v e l ;
}
Listing 4: Die Methode update() der Klasse CPUParticleEmitter
5.2.3
Die Methode render()
In dieser Methode werden die nach dem Aufruf von update() aktualisierten Partikel zum Zeichnen
durch die Grafikhardware freigegeben. Dabei ist die Methode recht einfach aufgebaut. Zunächst werden die benötigten OpenGL States aktiviert. Dann werden die aktuellen Daten im Pufferspeicher des
Speicherobjekts zum Zeichnen an die Grafikhardware weitergereicht. Abschließend werden die entsprechenden States wieder deaktiviert. Listing 5 zeigt die Methode update() als Pseudocode. Der Quellcode
ist in Listing 6 zu sehen.
FUNKTION r e n d e r ( )
aktiviereVertexBufferObject ;
aktiviereTexturierung ;
aktiviereBlending ;
aktivierePointSprites ;
aktiviereShader ;
zeichnePartikel ;
deaktiviereShader ;
deaktivierePointSprites ;
deaktiviereVertexBufferObject ;
deaktiviereTexturierung ;
deaktiviereBlending ;
ENDE
Listing 5: Die Methode render() der Klasse CPUParticleEmitter als Pseudocode
void CPUParticleEmitter : : render ( ) {
31
i f ( p a r t i c l e _ s e t ==NULL) r e t u r n ;
/ / vertex buffer object setup
g l E n a b l e C l i e n t S t a t e (GL_VERTEX_ARRAY ) ;
g l E n a b l e C l i e n t S t a t e (GL_COLOR_ARRAY ) ;
g l E n a b l e C l i e n t S t a t e (GL_FOG_COORD_ARRAY ) ; / / a b u s e d f o r t h e s i z e a t t r i b u t e . . .
g l B i n d B u f f e r A R B (GL_ARRAY_BUFFER_ARB , v e r t e x _ b u f f e r ) ;
G L s i z e i s t r i d e =8∗ s i z e o f ( f l o a t ) ; / / s t r i d e f o r t h e i n t e r l e a v e d a r r a y
g l V e r t e x P o i n t e r ( 3 , GL_FLOAT , s t r i d e , ( c h a r ∗ )NULL ) ;
g l C o l o r P o i n t e r ( 4 , GL_FLOAT , s t r i d e , ( ( c h a r ∗ )NULL) + 3 ∗ s i z e o f ( f l o a t ) ) ;
g l F o g C o o r d P o i n t e r ( GL_FLOAT , s t r i d e , ( ( c h a r ∗ )NULL) + 7 ∗ s i z e o f ( f l o a t ) ) ;
/ / texturing setup
g l E n a b l e ( GL_TEXTURE_2D ) ;
g l B i n d T e x t u r e ( GL_TEXTURE_2D , t e x t u r e _ i d ) ;
/ / blending setup
glBlendFunc ( s r c _ b l e n d i n g _ f a c t o r , d s t _ b l e n d i n g _ f a c t o r ) ;
g l E n a b l e (GL_BLEND ) ;
/ / point s p r i t e setup
g l E n a b l e ( GL_POINT_SPRITE_ARB ) ;
g l T e x E n v f ( GL_POINT_SPRITE_ARB , GL_COORD_REPLACE_ARB, GL_TRUE ) ;
g l E n a b l e ( GL_VERTEX_PROGRAM_POINT_SIZE ) ;
/ / shader setup
i f ( s h a d e r _ o b j ! =NULL) {
s h a d e r _ o b j −>b e g i n ( ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 1 i ( " t e x t u r e " , 0 ) ;
}
/ / render a l l a c t i v e p o i n t s as point s p r i t e s
g l D r a w A r r a y s ( GL_POINTS , 0 , a c t i v e _ p a r t i c l e s ) ;
// disable states
i f ( s h a d e r _ o b j ! =NULL) {
g l D i s a b l e ( GL_VERTEX_PROGRAM_POINT_SIZE ) ;
s h a d e r _ o b j −>end ( ) ;
}
g l D i s a b l e ( GL_POINT_SPRITE_ARB ) ;
g l D i s a b l e C l i e n t S t a t e (GL_VERTEX_ARRAY ) ;
g l D i s a b l e C l i e n t S t a t e (GL_COLOR_ARRAY ) ;
g l D i s a b l e C l i e n t S t a t e (GL_FOG_COORD_ARRAY ) ;
g l D i s a b l e ( GL_TEXTURE_2D ) ;
g l D i s a b l e (GL_BLEND ) ;
}
Listing 6: Die Methode render() der Klasse CPUParticleEmitter
5.2.4
Der Vertexshader
Im Vertexshader der CPU-Variante werden die schon berechneten Daten einfach an die nächste Stufe
der Grafikpipeline weitergereicht. Dazu werden sie in die entsprechenden Shadervariablen geschrieben.
Zusätzlich werden die Partikelpositionen transformiert. Listing 7 zeigt den Quellcode des Vertexshaders.
/ / color output for the fragment shader
v a r y i n g vec4 p a r t i c l e _ c o l o r ; / / i n t e r p o l a t e d p a r t i c l e c o l o r
v o i d main ( v o i d ) {
/ / get p a r t i c l e color
p a r t i c l e _ c o l o r =gl_Color ;
/ / s e t p o i n t s i z e ( coded i n gl_FogCoord )
g l _ P o i n t S i z e =gl_FogCoord ;
/ / update g l _ p o s i t i o n
gl_Position =gl_ModelViewProjectionMatrix ∗ gl_Vertex ;
}
Listing 7: Der Vertexshader der CPU-Variante
32
5.2.5
Der Fragmentshader
Der Fragmentshader in Listing 8 ist recht einfach aufgebaut. Das gerasterte Fragment des jeweiligen Billboards wird texturiert und durch eine Multiplikation mit der entsprechenden Partikelfarbe eingefärbt.
/ / t e x t u r e map f o r t h e p a r t i c l e s ( p e r e m i t t e r )
uniform sampler2D e m i t t e r _ t e x t u r e ;
/ / i n p u t from t h e v e r t e x s h a d e r
v a r y i n g vec4 p a r t i c l e _ c o l o r ;
/ / p a r t i c l e color calculated in the vertex shader
v o i d main ( v o i d ) {
/ / c a l c u l a t e f i n a l c o l o r by m u l t i p l y i n g w i t h t h e c u r r e n t t e x e l c o l o r
v e c 4 f i n a l _ c o l o r = p a r t i c l e _ c o l o r ∗ t e x t u r e 2 D ( e m i t t e r _ t e x t u r e , g l _ T e x C o o r d [ 0 ] . xy ) ;
/ / s e t r e s u l t i n g fragment color
gl_FragColor= f i n a l _ c o l o r ;
}
Listing 8: Der Fragmentshader der CPU-Variante
5.3
Die GPU-Variante
Die Funktionalität für die GPU-basierte Berechnung der Partikelsimulation ist in der Klasse GPUParticleEmitter
gekapselt. Die Klasse repräsentiert einen Partikelemitter, der sämtliche Berechnungen zur Simulation der
Partikel auf der GPU der Grafikhardware durchführt. Dazu sind die Berechnungen der Partikelsimulation
in einem externen Vertexshader realisiert, der von der Klasse genutzt wird. Für die Implementation wurden die von der Basisklasse ParticleEmitter geerbten abstrakten Methoden create(), update() und
render() überschrieben. Zunächst werden diese drei Methoden näher betrachtet. Abschließend wird auf
den Vertex- und Fragmentshader dieser Variante eingegangen.
5.3.1
Die Methode create()
Diese Methode ist für die Initialisierung des GPU-basierten Emitters zuständig. Dabei wird zunächst
einmalig ein OpenGL Speicherobjekt generiert. Anders als bei der CPU-Variante kann der Pufferspeicher des Speicherobjekts jedoch sofort mit den initialen Partikeleigenschaften gefüllt werden. Zunächst
wird das Speicherobjekt für die Kopieraktion vorbereitet. Da der Pufferspeicher des Speicherobjekts
nur einmal erzeugt und mit den initialen Partikeleigenschaften gefüllt wird, ist eine Auslagerung in den
schnellen Grafikspeicher sinnvoll. Ein ständiger Schreibzugriff, wie bei der CPU-Variante, ist nicht notwendig. Das Setzen eines entsprechenden Flags veranlasst den OpenGL Treiber der Grafikhardware
dazu, den Pufferspeicher entsprechend anzulegen. Anschließend wird in einer Schleife auf die Partikel
zugegriffen, um ihre Eigenschaften in den Pufferspeicher des Speicherobjekts zu übertragen. Dabei benötigt jedes Partikel ein Datenelement bestehend aus siebzehn Fließkommazahlen. Anders als bei der
CPU-Variante, werden sämtliche zur Berechnung notwendigen Partikeleigenschaften in den Vertexprozessoren der Grafikhardware benötigt und müssen demnach im Pufferspeicher zur Verfügung gestellt
werden. Der Aufbau eines solchen Datenelements ist in Tabelle 2 zu sehen. Nach dem Aufruf der Methode create() sollten sämtliche Eigenschaften der Partikel direkt im schnellen Grafikspeicher gepuffert
sein
18 .
18 Über
Eine detaillierte funktionale Sicht auf die Methode create() ist in Listing 9 als Pseudocode
die Flags kann man eine Auslagerung in den gewünschten Speicher nicht erzwingen, sondern nur vorschlagen
33
dargestellt. Der eigentliche Quellcode der Methode ist zusätzlich in Listing 10 aufgeführt.
Position
Geschwindigkeit
Zeit der Geburt
Farbe zur Geburt
Farbe beim Tod
Masse
Größe
x
vx
birth
r
r
mass
size
y
z
vy
vz
g
b
a
g
b
a
Tabelle 2: Der Aufbau eines Datenelements im Pufferspeicher des Speicherobjekts (GPU-Variante)
FUNKTION c r e a t e ( )
initialisiereVertexBufferObject ;
SCHLEIFE ü b e r A l l e P a r t i k e l
übertrageInitialePartikelPostion ;
übertrageInitialePartikelGeschwindigkeit ;
übertrageInitialePartikelZeitDerGeburt ;
übertrageInitialePartikelFarbeZurGeburt ;
übertrageInitialePartikelFarbeBeimTod ;
übertrageInitialePartikelMasse ;
übertrageInitialePartikelGröße ;
ENDE
ENDE
Listing 9: Der Methode create() der Klasse GPUParticleEmitter als Pseudocode
void GPUParticleEmitter : : c r e a t e ( ) {
i f ( p a r t i c l e _ s e t ==NULL) r e t u r n ;
/ / generate a vertex object buffer
i f ( ! v e r t e x _ b u f f e r ) glGenBuffersARB ( 1 , &v e r t e x _ b u f f e r ) ;
g l B i n d B u f f e r A R B (GL_ARRAY_BUFFER_ARB , v e r t e x _ b u f f e r ) ;
UINT32 s i z e = 1 7 ∗ ( UINT32 ) p a r t i c l e _ s e t −> p a r t i c l e _ v e c t o r . s i z e ( ) ∗ s i z e o f ( f l o a t ) ;
g l B u f f e r D a t a A R B (GL_ARRAY_BUFFER_ARB , s i z e , NULL, GL_STATIC_DRAW_ARB ) ;
/ / obtain a pointer to the vertex buffer object ( lock )
f l o a t ∗ p = ( f l o a t ∗ ) glMapBufferARB (GL_ARRAY_BUFFER_ARB , GL_WRITE_ONLY_ARB ) ;
/ / copy a t t r i b u t e s t o t h e v e r t e x b u f f e r
s t d : : v e c t o r < P a r t i c l e ∗> p a r t i c l e _ v e c t o r = p a r t i c l e _ s e t −> p a r t i c l e _ v e c t o r ;
UINT32 o f f = 0 ;
f o r ( UINT32 i = 0 ; i < ( UINT32 ) p a r t i c l e _ v e c t o r . s i z e ( ) ; i ++) {
p [ o f f ] = p a r t i c l e _ v e c t o r [ i ]−> p o s . xyz ;
//
xyz c o m p o n e n t s o f t h e p a r t i c l e i n i t i a l p o s i t i o n
p [ o f f +3]= p a r t i c l e _ v e c t o r [ i ]−> v e l . xyz ;
/ / xyz c o m p o n e n t s o f t h e p a r t i c l e i n i t i a l v e l o c i t y v e c t o r
p [ o f f +6]= p a r t i c l e _ v e c t o r [ i ]−> b i r t h ;
/ / p a r t i c l e b i r t h time
p [ o f f +7]= p a r t i c l e _ v e c t o r [ i ]−> b i r t h _ c o l . r g b a ;
/ / p a r t i c l e rgba color at b i r t h
p [ o f f +11]= p a r t i c l e _ v e c t o r [ i ]−> d e a t h _ c o l . r g b a ; / / p a r t i c l e r g b a c o l o r a t d e a t h
p [ o f f +15]= p a r t i c l e _ v e c t o r [ i ]−> i n v e r t e d _ m a s s ; / / p a r t i c l e i n v e r t e d mass
p [ o f f +16]= p a r t i c l e _ v e c t o r [ i ]−> s i z e ;
// particle size
/ / i n c r e a s e o f f s e t by e l e m e n t number
o f f +=17;
}
/ / unmap t h e v e r t e x b u f f e r o b j e c t ( u n l o c k )
glUnmapBufferARB (GL_ARRAY_BUFFER_ARB ) ;
}
Listing 10: Die Methode create() der Klasse GPUParticleEmitter
5.3.2
Die Methode update()
In dieser Methoden werden nur die Position des Emitters und der Zeitstempel der Simulation aktualisiert. Anders als bei der CPU-Variante wird die Berechnung der Partikelsimulation in einem externen
Vertexshader realisiert. Listing 11 zeigt den Quellcode der Methode update().
void GPUParticleEmitter : : update ( ) {
i f ( p a r t i c l e _ s e t ==NULL) r e t u r n ;
/ / update e m i t t e r a t t r i b u t e s only
c u r _ t i m e += t i m e s t e p ;
p o s += v e l ;
34
}
Listing 11: Die Methode update() der Klasse GPUParticleEmitter
5.3.3
Die Methode render()
In dieser Methode werden alle erforderlichen Daten zur Berechnung der Partikelsimulation an die Grafikhardware weitergegeben. Ähnlich wie bei der CPU-Variante ist dabei zunächst die Aktivierung einiger
OpenGL States notwendig. Neben den Partikeleigenschaften werden für die Berechnungen der Partikelsimulation auch einige Eigenschaften des Emitters benötigt. Da sämtliche Berechnungen der Simulation
auf den Vertexprozessoren der Grafikhardware durchgeführt werden, muss der Zugriff auf die Emittereigenschaften ebenfalls gewährleistet sein. Dabei gelten die Emittereigenschaften für alle Partikel des
Emitters und sind deshalb nur einmal pro Simulationsschritt zu übertragen. Dann kann eine Freigabe der
Daten im Pufferspeicher des Speicherobjektes für eine Weiterverarbeitung durch die Grafikhardware erfolgen. Abschließend werden die entsprechenden States wieder deaktiviert. Listing 12 zeigt die Methode
update() als Pseudocode. Der Quellcode ist in Listing 13 zu sehen.
FUNKTION r e n d e r ( )
aktiviereVertexBufferObject ;
aktiviereTexturierung ;
aktiviereBlending ;
aktivierePointSprites ;
aktiviereShader ;
übertrageEmitterParameterAnShader ;
zeichnePartikel ;
deaktiviereShader ;
deaktivierePointSprites ;
deaktiviereBlending ;
deaktiviereTexturierung ;
deaktiviereVertexBufferObject ;
ENDE
Listing 12: Der Methode render() der Klasse GPUParticleEmitter als Pseudocode
void GPUParticleEmitter : : render ( ) {
i f ( p a r t i c l e _ s e t ==NULL) r e t u r n ;
/ / vertex buffer object setup
g l B i n d B u f f e r A R B (GL_ARRAY_BUFFER_ARB , v e r t e x _ b u f f e r ) ;
G L s i z e i s t r i d e =17∗ s i z e o f ( f l o a t ) ;
g l V e r t e x A t t r i b P o i n t e r A R B (POS_ARRAY, 3 , GL_FLOAT , GL_FALSE , s t r i d e ,
g l V e r t e x A t t r i b P o i n t e r A R B (VEL_ARRAY, 3 , GL_FLOAT , GL_FALSE , s t r i d e ,
g l V e r t e x A t t r i b P o i n t e r A R B ( BIRTH_ARRAY , 1 , GL_FLOAT , GL_FALSE , s t r i d e
g l V e r t e x A t t r i b P o i n t e r A R B (BIRTH_COLOR_ARRAY , 4 , GL_FLOAT , GL_FALSE ,
g l V e r t e x A t t r i b P o i n t e r A R B (DEATH_COLOR_ARRAY, 4 , GL_FLOAT , GL_FALSE ,
g l V e r t e x A t t r i b P o i n t e r A R B (MASS_ARRAY, 1 , GL_FLOAT , GL_FALSE , s t r i d e
g l V e r t e x A t t r i b P o i n t e r A R B ( SIZE_ARRAY , 1 , GL_FLOAT , GL_FALSE , s t r i d e ,
g l E n a b l e V e r t e x A t t r i b A r r a y A R B (POS_ARRAY ) ;
g l E n a b l e V e r t e x A t t r i b A r r a y A R B (VEL_ARRAY ) ;
g l E n a b l e V e r t e x A t t r i b A r r a y A R B ( BIRTH_ARRAY ) ;
g l E n a b l e V e r t e x A t t r i b A r r a y A R B (BIRTH_COLOR_ARRAY ) ;
g l E n a b l e V e r t e x A t t r i b A r r a y A R B (DEATH_COLOR_ARRAY ) ;
g l E n a b l e V e r t e x A t t r i b A r r a y A R B (MASS_ARRAY ) ;
g l E n a b l e V e r t e x A t t r i b A r r a y A R B ( SIZE_ARRAY ) ;
/ / texturing setup
g l B i n d T e x t u r e ( GL_TEXTURE_2D , t e x t u r e _ i d ) ;
g l E n a b l e ( GL_TEXTURE_2D ) ;
/ / blending setup
glBlendFunc ( s r c _ b l e n d i n g _ f a c t o r , d s t _ b l e n d i n g _ f a c t o r ) ;
g l E n a b l e (GL_BLEND ) ;
/ / point s p r i t e s setup
35
( c h a r ∗ )NULL ) ;
( ( c h a r ∗ )NULL) + 3 ∗ s i z e o f ( f l o a t ) ) ;
, ( ( c h a r ∗ )NULL) + 6 ∗ s i z e o f ( f l o a t ) ) ;
s t r i d e , ( ( c h a r ∗ )NULL) + 7 ∗ s i z e o f ( f l o a t ) ) ;
s t r i d e , ( ( c h a r ∗ )NULL) + 1 1 ∗ s i z e o f ( f l o a t ) ) ;
, ( ( c h a r ∗ )NULL) + 1 5 ∗ s i z e o f ( f l o a t ) ) ;
( ( c h a r ∗ )NULL) + 1 6 ∗ s i z e o f ( f l o a t ) ) ;
g l E n a b l e ( GL_POINT_SPRITE_ARB ) ;
g l T e x E n v f ( GL_POINT_SPRITE_ARB , GL_COORD_REPLACE_ARB, GL_TRUE ) ;
g l E n a b l e ( GL_VERTEX_PROGRAM_POINT_SIZE ) ;
/ / update shader uniforms
i f ( s h a d e r _ o b j ! =NULL) {
s h a d e r _ o b j −>b e g i n ( ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 1 f ( " e m i t t e r _ c u r r e n t _ t i m e " , c u r _ t i m e ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 3 f ( " e m i t t e r _ g r a v i t y " , g r a v i t y . x , g r a v i t y . y , g r a v i t y . z ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 3 f ( " e m i t t e r _ p o s i t i o n " , p o s . x , p o s . y , p o s . z ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 3 f ( " e m i t t e r _ w i n d " , wind . x , wind . y , wind . z ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 3 f ( " e m i t t e r _ v e l o c i t y " , v e l . x , v e l . y , v e l . z ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 1 f ( " e m i t t e r _ s i z e " , s i z e ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 1 f ( " e m i t t e r _ p a r t i c l e _ m a x _ l i f e " , p a r t i c l e _ m a x _ l i f e ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 1 f ( " o n e _ o v e r _ e m i t t e r _ p a r t i c l e _ m a x _ l i f e " , 1 . 0 f / p a r t i c l e _ m a x _ l i f e ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 1 f ( " e m i t t e r _ g r o u n d _ h e i g h t " , g r o u n d _ h e i g h t ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 1 f ( " e m i t t e r _ g r o u n d _ b o u n c e _ f a c t o r " , g r o u n d _ b o u n c e _ f a c t o r ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 1 f ( " e m i t t e r _ g r o u n d _ f r i c t i o n " , g r o u n d _ f r i c t i o n ) ;
s h a d e r _ o b j −>s e n d U n i f o r m 1 i ( " e m i t t e r _ t e x t u r e " , 0 ) ;
}
/ / r e n d e r geometry a p o i n t s p r i t e s
g l D r a w A r r a y s ( GL_POINTS , 0 , ( UINT32 ) p a r t i c l e _ s e t −> p a r t i c l e _ v e c t o r . s i z e ( ) ) ;
/ / d i s a b l e opengl s t a t e s
i f ( s h a d e r _ o b j ! =NULL) s h a d e r _ o b j −>end ( ) ;
g l D i s a b l e ( GL_POINT_SPRITE_ARB ) ;
g l D i s a b l e ( GL_TEXTURE_2D ) ;
g l D i s a b l e (GL_BLEND ) ;
g l D i s a b l e V e r t e x A t t r i b A r r a y A R B (POS_ARRAY ) ;
g l D i s a b l e V e r t e x A t t r i b A r r a y A R B (VEL_ARRAY ) ;
g l D i s a b l e V e r t e x A t t r i b A r r a y A R B ( BIRTH_ARRAY ) ;
g l D i s a b l e V e r t e x A t t r i b A r r a y A R B (BIRTH_COLOR_ARRAY ) ;
g l D i s a b l e V e r t e x A t t r i b A r r a y A R B (DEATH_COLOR_ARRAY ) ;
g l D i s a b l e V e r t e x A t t r i b A r r a y A R B (MASS_ARRAY ) ;
g l D i s a b l e V e r t e x A t t r i b A r r a y A R B ( SIZE_ARRAY ) ;
}
Listing 13: Die Methode render() der Klasse GPUParticleEmitter
5.3.4
Der Vertexshader
Die gesamte Simulation der Partikel wird durch einen Vertexshader auf der Grafikhardware berechnet.
Der Vertexshader in Listing 14 führt diese Berechnungen analog zur Methode update() der CPUVariante in Abschnitt 5.2.2 durch. Dabei wird jeweils die aktuelle Position, Größe und Farbe des einzelnen Partikels bestimmt. Diese Daten werden anschließend in die dafür vorgesehenen Variablen geschrieben und zur nächsten Stufe der Grafikpipeline weitergereicht. Listing 14 zeigt den Quellcode des
Vertexshaders.
/ / e m i t t e r p r o p e r t i e s as uniforms
uniform vec3 e m i t t e r _ p o s i t i o n ;
// emitter
uniform vec3 e m i t t e r _ v e l o c i t y ;
// emitter
uniform vec3 e m i t t e r _ g r a v i t y ;
// emitter
uniform vec3 e m i t t e r _ w i n d ;
//
uniform f l o a t e m i t t e r _ c u r r e n t _ t i m e ;
// emitter
uniform f l o a t e m i t t e r _ s i z e ;
//
uniform f l o a t e m i t t e r _ p a r t i c l e _ m a x _ l i f e ;
//
uniform f l o a t o n e _ o v e r _ e m i t t e r _ p a r t i c l e _ m a x _ l i f e ; / /
uniform f l o a t emitter_ground_height ;
//
uniform f l o a t emitter_ground_bounce_factor ;
//
uniform f l o a t e m i t t e r _ g r o u n d _ f r i c t i o n ;
//
/ / p a r t i c l e p r o p e r t i e s as vertex a t t r i b u t e s
a t t r i b u t e vec3 p a r t i c l e _ i n i t i a l _ p o s i t i o n ;
//
a t t r i b u t e vec3 p a r t i c l e _ i n i t i a l _ v e l o c i t y ;
//
attribute float particle_birth ;
//
a t t r i b u t e vec4 p a r t i c l e _ b i r t h _ c o l o r ;
//
a t t r i b u t e vec4 p a r t i c l e _ d e a t h _ c o l o r ;
// particle
attribute float particle_inverted_mass ;
// inverted
attribute float particle_size ;
//
position
velocity
gravity
e m i t t e r wind
c u r r e n t time
emitter size
maximum l i f e o f t h e p a r t i c l e s
1.0 / particle_max_life
h e i g h t o f t h e g r o u n d xz−p l a n e
c o e f f i c i e n t f o r t h e p a r t i c l e bounce a m p l i t u d e
ground f r i c t i o n parameter
particle i n i t i a l position
particle velocity
p a r t i c l e b i r t h time
particle birth color r , g , b , a
birth color r , g , b , a
p a r t i c l e mass
particle size
36
/ / output to the fragment shader
v a r y i n g vec4 p a r t i c l e _ c o l o r ;
/ / interpolated particle color
v o i d main ( v o i d ) {
/ / get c u r r e n t time
f l o a t t = e m i t t e r _ c u r r e n t _ t i m e −p a r t i c l e _ b i r t h ;
/ / c h e c k i f p a r t i c l e h a s b e e n b o r n ; i f i t h a s n o t y e t b e e n b o r n s e t t o i n i t a l p o s i t i o n and make i t i n v i s i b l e
i f ( t <=0.0 f ) {
g l _ P o s i t i o n =vec4 ( p a r t i c l e _ i n i t i a l _ p o s i t i o n , 1 . 0 ) ;
p a r t i c l e _ c o l o r =vec4 ( 0 . 0 , 0 . 0 , 0 . 0 , 0 . 0 ) ;
} else { / / otherwise update p a r t i c l e p o s i t i o n
/ / c a l c u l a t e c u r r e n t timestamp for p a r t i c l e
f l o a t p a r t i c l e _ d t =mod ( t , e m i t t e r _ p a r t i c l e _ m a x _ l i f e ) ;
/ / r e c a l c u l a t e p a r t i c l e p o s i t i o n b a s e d on t h e e m i t t e r p r o p e r t i e s
vec3 n e w _ p a r t i c l e _ p o s i t i o n = e m i t t e r _ p o s i t i o n + p a r t i c l e _ i n i t i a l _ p o s i t i o n ∗ e m i t t e r _ s i z e ;
/ / a d j u s t p a r t i c l e p o s i t i o n b a s e d on v e l o c i t y and t i m e i n t e r v a l
n e w _ p a r t i c l e _ p o s i t i o n +=( p a r t i c l e _ i n i t i a l _ v e l o c i t y + e m i t t e r _ v e l o c i t y ) ∗ p a r t i c l e _ d t ;
/ / c a l c u l a t e t o t a l a c c e l a r a t i o n f o r c e on p a r t i c l e ; sum o f g r a v i t y and wind s c a l e d by t h e p a r t i c l e mass
vec3 t o t a l _ a c c e l e r a t i o n =( e m i t t e r _ g r a v i t y + e m i t t e r _ w i n d )∗ p a r t i c l e _ i n v e r t e d _ m a s s ;
// calculate resulting position
n e w _ p a r t i c l e _ p o s i t i o n +=0.5∗ t o t a l _ a c c e l e r a t i o n ∗ p a r t i c l e _ d t ∗ p a r t i c l e _ d t ;
/ / c a l c u l a t e c u r r e n t e n e r g y amount u s e d f o r a l p h a b l e n d i n g , c o l o r i n t e r p o l a t i o n and s i z e c a l c u l a t i o n
float particle_lifespan=particle_dt ∗ one_over_emitter_particle_max_life ;
/ / c a l c u l a t e p a r t i c l e c o l o r b a s e d on a l i n e a r i n t e r p o l a t i o n
p a r t i c l e _ c o l o r = p a r t i c l e _ b i r t h _ c o l o r + ( ( p a r t i c l e _ d e a t h _ c o l o r −p a r t i c l e _ b i r t h _ c o l o r )∗ p a r t i c l e _ l i f e s p a n ) ;
/ / c h e c k f o r any c o l l i s i o n w i t h t h e g r o u n d
f l o a t d i s t a n c e _ t o _ g r o u n d = n e w _ p a r t i c l e _ p o s i t i o n . y−e m i t t e r _ g r o u n d _ h e i g h t ;
/ / do some s i m p l e c o l l i s i o n r e s p o n s e w i t h t h e g r o u n d
if ( distance_to_ground < 0.0) {
f l o a t bounce= e m i t t e r _ g r o u n d _ b o u n c e _ f a c t o r ∗ abs ( s i n ( d i s t a n c e _ t o _ g r o u n d ) ) ;
f l o a t damping =pow ( 2 . 0 , d i s t a n c e _ t o _ g r o u n d / e m i t t e r _ g r o u n d _ f r i c t i o n ) ;
n e w _ p a r t i c l e _ p o s i t i o n . y= e m i t t e r _ g r o u n d _ h e i g h t + b o u n c e ∗ damping ;
}
/ / c a l c u l a t e p a r t i c l e s i z e b a s e d on p a r t i c l e l i f e s p a n
g l _ P o i n t S i z e =(1.0 − p a r t i c l e _ e n e r g y ) ∗ p a r t i c l e _ l i f e s p a n ;
/ / update g l _ p o s i t i o n
g l _ P o s i t i o n = g l _ M o d e l V i e w P r o j e c t i o n M a t r i x ∗ vec4 ( n e w _ p a r t i c l e _ p o s i t i o n , 1 . 0 ) ;
}
}
Listing 14: Der Vertexshader der GPU-Variante
5.3.5
Der Fragmentshader
Die Anforderungen besagen, dass die CPU- und GPU-Variante des Partikelsystem visuell gleichwertig sein sollen. Daher wird für beide Varianten derselbe Fragmentshader aus Listing 7 verwendet. Ein
späterer Vergleich hinsichtlich der Leistung beider Varianten wird dadurch ebenfalls vereinfacht.
37
6
Ergebnisse
Im Rahmen dieser Arbeit wurde ein Partikelsystem implementiert, dass die vorangegangenen Anforderungen erfüllt. Um den Funktionsumfang der Implementation präsentieren zu können, wurde zudem
eine Testumgebung für das Partikelsystem realisiert, welche zu einem späteren Zeitpunkt kurz vorgestellt
wird. Ein weiterer Schwerpunkt der Aufgabenstellung ist der Vergleich der CPU- und GPU-Variante des
Partikelsystems hinsichtlich der Leistung. Dazu wurden umfangreiche Leistungsuntersuchungen durchgeführt und die Ergebnisse tabellarisch und grafisch aufgetragen.
6.1
Leistungsuntersuchungen
Die Leistungsuntersuchungen wurden mit einem Standard PC-System durchgeführt. Unter anderem kamen dabei folgende Komponenten zum Einsatz:
• AMD Athlon XP 3000+ 19
• 1024 MB Arbeitsspeicher, PC3200 Dual Channel
• GeForce 6800 Ultra 20 mit 256 MB Grafikspeicher
Für jeden der nachfolgenden Leistungsvergleiche wurden spezielle Testszenen geschaffen. Eine Leistungsmessung innerhalb des bestehenden Testprogramms „ParticleSystemViewer“ war nicht vorgesehen,
da der zusätzliche overhead der MFC-Klassenbibliotheken das Ergebnis hätte verfälschen können. Stattdessen wurde ein einfaches OpenGL Programm mit Hilfe der GLUT-Bibliothek geschrieben, dass eine
akkurate Leistungsmessung erlaubte. Dazu wurde eine Testszene mit einem Partikelsystem, das aus einer
XML-Datei geladen werden konnte, erstellt. Anschließend wurden eine Reihe von Leistungsmessungen
durchführt. Zu jedem Messvorgang wurde dabei zunächst die Zeit bestimmt, die für eine Aktualisierung
und Darstellung des Partikelsystems notwendig war. Dieser Wert wurde umgerechnet, so dass eine Angabe in Bildern pro Sekunde ermöglicht wurde. Um ein genaueres Ergebnis zu erhalten und Ausreißer in
den Messwerten besser kompensieren zu können, wurde das arithmetische Mittel mehrerer Messungen
genommen. Zudem konnte für sämtliche Zeitmessungen ein hochauflösender Zeitgeber des Betriebsystems genutzt werden [Lib05]. Abschließend wurde das Ergebnis gespeichert und die Partikelanzahl für
den nächsten Messvorgang erhöht. Durch diese Vorgehensweise konnten schnell und flexibel verschiedenste Leistungsvergleiche der beiden Varianten durchgeführt werden. Im Nachfolgenden werden einige
dieser Vergleiche im Detail betrachtet.
6.1.1
Leistungsvergleich der beiden Varianten
Die reine Berechnungsgeschwindigkeit der Partikelsimulation beider Varianten ist für einen Vergleich
besonders interessant. Dazu wurde eine spezielle Testszene für ein Partikelsystem mit fünf Emittern verwendet. Diese Aufteilung wurde gewählt, da implementationsbedingt pro Emitter ein Speicherobjekt zur
19 2100
20 400
Mhz realer Takt, Barton Kern mit 400 Mhz Frontside Bus
Mhz GPU Taktfrequenz und 1100 Mhz Speichertakt
38
Abbildung 20: Ein Screenshot der Benchmarkszene
Pufferung der Partikeleigenschaften erzeugt wird. Bei Speicherobjekten mit einem sehr großen Pufferspeicher, besonders bei der GPU-Variante, war ein starker Leistungseinbruch zu verzeichnen. Für diesen
Leistungsvergleich war jedoch eine Simulation von bis zu 2.000.000 Partikel vorgesehen. In In diesem
Fall würden bei einer Pufferung in nur einem Speicherobjekt für die GPU-Variante knapp 130 MB
21
Speicher benötigt. Unter Umständen ist im Grafikkartenspeicher kein Platz für einen zusammenhängenden Block dieser Größe. In einer solchen Situation würde der Grafiktreiber auf eine Auslagerung in den
schnellen Grafikspeicher verzichten und eine Pufferung im Arbeitsspeicher des Hostrechners vornehmen. Dieses Verhalten konnte ab einer bestimmten Partikelanzahl tatsächlich beobachtet werden. Um
dennoch eine gute Leistung der GPU-Variante bei einer hohen Partikelanzahl zu gewährleisten, wurden
die Partikeldaten auf mehrere Speicherobjekte verteilt.
Ein entscheidender Faktor bei Leistungsanalysen im Zusammenhang mit Grafikhardware ist die Füllrate. Dieser Wert gibt an, wie viele Fragmente die Grafikhardware in einer bestimmten Zeit berechnen
kann. Dabei spielt neben der physikalischen Pixelauflösung und Farbtiefe, die für die Darstellung benötigt wird, auch der Aufwand bei der Berechnung der Fragmente eine Rolle. Durch Fragmentoperationen,
wie Texturierung und Blending, kann die Füllrate stark reduziert und zum limitierenden Faktor werden. Der Einfluss der Füllrate auf die Gesamtleistung wird in einem späteren Leistungsvergleich noch
ausführlicher untersucht. Für diesen Vergleich sollte die Füllrate als limitierender Faktor jedoch weitgehend ausgeschlossen werden. Deshalb wurde die Bildauflösung auf 320x240 Pixel mit einer Farbtiefe
von 16-Bit reduziert. Zudem wurden alle Partikel der Testszene als untexturierte Punktprimitive dargestellt. Für beide Varianten musste dazu ein spezieller Fragmentshader eingesetzt werden. Darüber hinaus
wurden feste Punktgrößen von 1.0 genutzt und alle Blendingoperationen deaktiviert. So konnten die Anzahl und der Aufwand der Fragmentoperationen minimiert werden. Abbildung 20 zeigt einen Screenshot
der Testszene. Wie die numerischen Ergebnisse des Leistungsvergleich aus Tabelle 3 belegen, sind die
Berechnungen zur Partikelsimulation der GPU-Variante meistens deutlich schneller als die der CPU21 2.000.000*17*sizeof(float)≈
129.7 MB
39
# Partikel
CPU-Variante
GPU-Variante
Prozentuale Leistung GPU zu CPU
10
8950
8239
90,82%
100
7759
8234
104,74%
1000
3292
8124
242,15%
5000
888
6054
668,95%
10000
463
3086
663,66%
50000
84
897
1067,86%
100000
42
465
1107,14%
500000
9
96
1066,67%
1000000
5
49
980%
2000000
3
25
833,33%
Tabelle 3: Die Leistungen der CPU-Variante gegenüber der GPU-Variante
Variante. Nur bei einer kleinen Partikelanzahl mit weniger als 100 Partikeln ist die CPU-Variante etwas
schneller. Bei der GPU-Variante müssen pro Berechnungsschritt eine beträchtliche Anzahl an Parametern an den Vertexprozessor übergeben werden. Dadurch wird zusätzlicher overhead erzeugt, der aber
bei einer größeren Partikeldichte aufgrund der schnelleren Berechnungsgeschwindigkeit zu vernachlässigen ist. In der rechten Spalte der Tabelle ist die prozentuale Leistung der GPU-Variante in Relation
zur CPU-Variante aufgetragen. Wie man erkennen kann, ist die GPU-Variante bei einer Partikelsimulation mit 100000 Partikeln rund elfmal so schnell wie ihr CPU-Pendant. Bei großen Partikelsystemen
mit einer Millionen Partikel und mehr, werden von der GPU-Variante noch interaktive Bildraten produziert. Hier ist die CPU-Variante bereits weit abgeschlagen. Neben der verbesserten Leistung hat die
GPU-Variante einen weiteren erheblichen Vorteil: bei der Verarbeitung der Partikelsimulation durch die
Grafikhardware wird der Hauptprozessor des Hostrechners kaum augelastet. Parallel zur Simulation auf
der GPU sind also andere Berechnungen auf der CPU möglich. Die Ergebnisse des Leistungsvergleich
sind in Diagramm aus Abbildung 21 nochmals grafisch aufbereitet. Dabei wurde zur besseren Übersicht
eine logarithmische Skalierung verwendet.
6.1.2
Leistungsvergleich: immediate mode und Vertex Buffer Objects
In diesem Leistungsvergleich sollte der tatsächliche Leistungszuwachs, der durch eine Verwendung der
OpenGL Speicherobjekte ermöglicht wurde, gemessen werden. Um auch hier weitgehend unabhängig
von der Füllrate zu bleiben, wurde die Testszene des vorherigen Leistungsvergleichs genutzt. Dabei wurde ausschließlich die GPU-Variante betrachtet. Die Messwerte aus dem vorherigen Leistungsvergleich
konnten übernommen werden. Um Vergleichswerte für den immediate mode zu erhalten, musste der
Quellcode der Klasse GPUEmitter modifiziert werden. Der Funktionsumfang zur Nutzung der Speicherobjekte wurde komplett entfernt. Die Partikeleigenschaften sollten gemäß des immediate mode mit
den klassischen OpenGL Funktionsaufrufen, wie glVertex4f(...) und glColor4f(...), über den
40
Bilder pro Sekunde
CPU-Variante
GPU-Variante
Anzahl der Partikel
Abbildung 21: Die Leistungen der CPU-Variante gegenüber der der GPU-Variante
Bus zur Grafikhardware übertragen werden. Um die siebzehn float Einträge pro Partikel übertragen zu
können, mussten jeweils sieben Funktionsaufrufe getätigt werden. Neben der hohen Belastung für den
Grafikbus, wird dadurch ein erheblicher overhead aufgrund der vielen Funktionsaufrufe provoziert. Außerdem muss die Grafikhardware jedes mal eine vollständige Übertragung der Partikeldaten abwarten,
bis sie mit den eigentlichen Berechnungen zur Partikelsimulation anfangen kann. Erwartungsgemäß sollte die Leistung bei einer Pufferung der Partikeldaten im Grafikspeicher deutlich höher sein. Dies konnte,
wie in Tabelle 4 zu sehen ist, eindrucksvoll bestätigt werden. Das Gesamtsystem arbeitet am effizientesten, wenn Daten und Berechnung direkt auf der Grafikhardware gepuffert beziehungsweise durchgeführt werden. Eine ständige Übertragung der Partikeldaten über den Grafikbus „verschenkt“ sehr viel der
enormen Rechenleistung der Grafikhardware. In Abbildung 22 sind die Werte des Leistungsvergleichs
nochmals grafisch aufbereitet. Auch hier wurde eine logarithmische Skalierung verwendet.
6.1.3
Leistungsvergleich beider Varianten bezüglich der Füllrate
Aufgrund anderer Leistungsmessungen hat sich herausgestellt, dass die GPU-Variante die reinen Berechnungen zur Partikelsimulation deutlich schneller als die CPU-Variante ausführen kann. Da die Partikel
aber auch als texturierte Billboards dargestellt werden sollten, musste die Füllrate in diesem Zusammenhang genauer untersucht werden. In einer Testszene wurde ein Partikelsystem mit wahlweise einem
CPU- oder GPU-basierenden Emitter bestückt. Dabei wurden die Partikel selbst als Point Sprites mit
einer festen Punktgröße von 100.0 und einer 64x64 großen RGBA-Textur visualisiert. Additives Blending wurde ebenfalls genutzt. Sämtliche Leistungsmessungen der Szene sind in einer Bildauflösung von
1024x768 Pixel und einer Farbtiefe von 32-Bit vorgenommen worden. Tabelle 5 zeigt die numerischen
Daten des Leistungsvergleichs beider Verfahren. Die Ergebnisse der CPU- und GPU-Variante liegen
in diesem Leistungsvergleich dicht beieinander, wobei die GPU-Variante insgesamt etwas schneller ist.
41
# Partikel
Funktionsaufrufe
Vertex Buffer Objects
Prozentualer Leistungszuwachs
10
7370
8239
111,79%
100
5192
8234
158,59%
1000
1436
8124
565,74%
5000
339
6054
1785,84%
10000
174
3086
1773,56%
50000
35
897
2562,86%
100000
18
465
2583,33%
500000
4
96
2400%
1000000
2
49
2450%
2000000
1
25
2500%
Bilder pro Sekunde
Tabelle 4: Ein Leistungsvergleich des immediate mode gegenüber den Vertex Buffer Objects in OpenGL
5
5
5
2
Anzahl der Partikel
Abbildung 22: Ein Leistungsvergleich des immediate mode gegenüber den Vertex Buffer Objects in
OpenGL
42
# Partikel
CPU-Variante
GPU-Variante
Prozentuale Leistung GPU zu CPU
10
5880
6230
105,95%
100
2091
2481
118,65%
500
532
597
112,22%
1000
274
300
109,49%
2500
111
124
111,71%
5000
56
62
110,71%
7500
37
42
113,51%
10000
28
32
114,29%
25000
12
13
108,33%
50000
6
7
116,67%
Tabelle 5: Ein Leistungsvergleich beider Varianten bezüglich der Füllrate
Man sieht jedoch deutlich, dass das Partikelsystem in diesem Leistungsvergleich stark füllratenlimitiert
ist. Für die Texturierung und Überlagerung der Billboards sind eine erhebliche Anzahl an Fragmentoperationen notwendig, wobei diese linear mit der Partikelanzahl steigen. Der Geschwindigkeitsvorteil der
GPU-Variante, wird durch die gestiegenen Anforderungen an die Füllrate weitgehend aufgehoben. In
Abbildung 23 sind die Werte des Leistungsvergleichs in einem Diagramm dargestellt, wiederum logarithmisch skaliert.
6.2
Die Testumgebung ParticleSystemViewer
Um den Funktionsumfang des im Rahmen dieser Arbeit entstandenen Partikelsystems präsentieren zu
können, wurde eigens dafür eine Testumgebung implementiert. Mit dem Programm „ParticleSystemViewer“ lassen sich verschiedene Partikelsysteme aus XML-Dateien einladen und visualisieren. Dazu
werden die Partikel in einem Hauptfenster grafisch dargestellt. Um durch die 3D-Szene zu navigieren,
kann man über verschiedene Schaltflächen oder Tastaturkürzel eine virtuelle Kamera bewegen. Während der Partikelsimulation lassen sich zudem einige Emitterparameter über zahlreiche Kontrollfelder im
linken Teil der Anwendung modifizieren. Weiterhin können einige Statistiken zur aktuellen Szene angezeigt werden. Insgesamt ist die Bedienung der Anwendung recht einfach gehalten und sollte weitgehend
selbsterklärend sein. Dazu sind die meisten Schaltflächen mit einer Beschriftung versehen oder werden
beim Fokusieren mit der Maus durch Einblendung textueller Beschreibungen in der Statusleiste erklärt.
In Abbildung 24 ist ein Screenshot der Anwendung „ParticleSystemViewer“ zu sehen.
43
Bilder pro Sekunde
CPU-Variante
GPU-Variante
$
Anzahl der Partikel
Abbildung 23: Ein Leistungsvergleich beider Varianten bezüglich der Füllrate
Abbildung 24: Die Testumgebung ParticleSystem Viewer
44
7
Ausblick
Das im Rahmen dieser Arbeit entstandene Partikelsystem auf Basis des zustandlosen Modells ist zur
Darstellung kleinerer Effekte, wie zum Beispiel in Computerspielen, sicherlich geeignet. Dabei kann
die Berechnung der Partikelsimulation sehr effizient auf dem Vertexprozessor der Grafikhardware durchgeführt werden. Es hat sich gezeigt, dass die Grafikhardware bei der Berechnungsgeschwindigkeit der
Partikelsimulation sehr viel schneller arbeitet als der Hauptprozessor des Hostrechners. Dies liegt nicht
zuletzt an der schnellen Fließkommaberechnung, insbesondere im Zusammenhang mit Vektorarithmetik,
die von aktueller Grafikhardware geleistet wird. Ein weiterer Vorteil ist in der weitgehenden Entlastung
des Hauptprozessors im Hostrechner zu sehen. So kann bei der GPU-Variante die CPU andere Berechnungen parallel zur Grafikhardware durchführen. Damit ist man in der Lage die gesamte Rechenlast so
zu verteilen, dass eine optimale Auslastung beider Hardwarekomponenten ermöglicht wird.
Aus diesen Vorteilen heraus entsteht die Motivation in Zukunft ein zustandserhaltendes Partikelsystem ebenfalls auf der Grafikhardware zu realisieren. Besonders für Partikelsimulationen mit komplexen
lokalen Effekten ist das zustandslose Modell weniger geeignet. Für ein zustandserhaltendes Partikelsystem ist das Speichern von Zwischenergebnissen während der Partikelsimulation notwendig. Bei der
hier vorgestellten CPU-Variante des Partikelsystems ist dies durch einige Modifikationen in der Klasse
CPUParticleEmitter einfach zu realisieren. Für die GPU-Variante wäre dies schwieriger. Die Berechnungen zur Partikelsimulation müssten größtenteils vom Vertexprozessor auf den Fragmentprozessor
verlagert werden. Die Zwischenergebnisse der Simulation könnten dann in einem Puffer gespeichert und
in einem zweiten Verarbeitungsschritt zum Zeichnen der Szene genutzt werden. Hierbei ergeben sich
durch jüngste Entwicklungen moderner 3D-APIs neue Möglichkeiten für eine effiziente Simulation der
Partikel auf der Grafikhardware. In diesem Zusammenhang sind die „render to texture“ und „render to
vertex array“ Mechanismen, wie sie zum Beispiel OpenGL seit der Version 2.0 bietet, besonders interessant.
Es hat sich herausgestellt, dass die Füllrate schnell ein limitierender Faktor sein kann. Dies trifft besonders dann zu, wenn zur Darstellung der Partikel Texturzugriffe und andere Fragmentoperationen
notwendig sind. Der Geschwindigkeitsvorteil der GPU-Variante wird dadurch schnell erschlagen. Für
eine zukünftige Entwicklung eines zustandserhaltenden Partikelsystems auf der Grafikhardware ist dies
von besonderer Bedeutung. Hierbei muss die Berechnung zur Partikelsimulation größtenteils auf dem
Fragmentprozessor durchgeführt werden. Da jede zusätzliche Operation pro Fragment eine zusätzliche
Reduktion der Füllrate bedeutet, sind längere Shader besonders problematisch. Jedoch können die Berechnungen für komplexe Partikelsysteme eine Vielzahl von Rechenoperationen erfordern. Daher wäre
ein sinnvoller Schritt für zukünftige Entwicklungen, mehrere Strategien im Hinblick auf die Optimierung
der Füllrate zu entwickeln.
Auch im Bereich der Visualisierung sind für zukünftige Systeme mehrere Verbesserungen denkbar. In
der hier vorgestellten Implementation wurde zur Darstellung der Partikel unter anderem eine einfache
Farbinterpolation verwendet. Hierfür waren pro Partikel zwei RGBA-Farbtupel zu je vier Fließkommazahlen notwendig. Allerdings könnte dies für bestimmte Einsatzzwecke und bei einer kritischen Betrach45
0
1.0 - particle_life
1
Abbildung 25: Eine eindimensionale Textur zur Einfärbung der Partikel
tungsweise als ineffizient angesehen werden. Um Speicherplatz einzusparen, bestünde die Möglichkeit
die Daten des Farbverlaufs zur Einfärbung der Partikel vor der eigentlichen Simulation zu berechnen.
Die Daten könnten dann in einer eindimensionalen Textur für einen späteren Zugriff im Fragmentshader
abgelegt werden. In Abbildung 25 ist eine solche 1D-Textur dargestellt. Während der Partikelsimulation ist zusätzlich im Vertexshader eine entsprechende Texturkoordinate zu berechnen. Dabei kann man
die aktuelle Lebenszeit des jeweiligen Partikels, die in der vorliegenden Implementation bereits im Vertexshader als Skalierungsfaktor für die Farbinterpolation berechnet wird, nutzen. Die Texturkoordinate
kann anschließend an den Fragmentshader für den eigentlichen Texturzugriff weitergegeben werden.
Zur Einfärbung einer kompletten Partikelgruppe wird dann neben der eigentlichen Billboardtextur nur
die eindimensionale Textur benötigt. Dadurch könnte man im Vergleich zum bestehenden Ansatz den
Speicherbedarf, besonders bei einer hohen Partikelanzahl, deutlich reduzieren. Ein weiterer Vorteil dieser Technik ist zudem die Möglichkeit, auch nichtlineare Farbverläufe für die Darstellung der Partikel zu
nutzen.
46
Literatur
[Ber05]
Laurenz Bertuch, Manfred und Weiner. Massiv parallel. ct, 15, 2005.
[Lan99]
Jeff Lander. Devil in the Blue Faceted Dress: Real-time Cloth Animation. Game Developer,
5, May 1999.
[Lat04]
Lutz Latta. Building a Million-Particle System. Website, 2004.
http://www.gamasutra.com/features/20040728/latta_01.shtml.
[Lib05]
MSDN Library. QueryPerformanceCounter Function. Website, 2005.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/
winui/winui/windowsuserinterface/windowing/timers/timerreference/
timerfunctions/queryperformancecounter.asp.
[Meh03] Mehrere. ARB_point_sprite. Website, 2003.
http://oss.sgi.com/projects/ogl-sample/registry/ARB/point_sprite.txt.
[Meh04] Mehrere. GPUGems. Addison-Wesley, 2004.
[nW03]
nVidia Whitepaper. Using Vertex Buffer Objects. Website, 2003.
http://developer.nvidia.com/attach/6427.
[Ree83]
William T. Reeves. Particle Systems - A Technique for Modeling a Class of Fuzzy Objects,
1983.
[Ros04a] Randi J. Rost. OpenGL Shading Language. Addison-Wesley, 2004. with Contributions by
John M. Kessenich and Barthold Lichtenbelt.
[Ros04b] Randi J. Rost. OpenGL Shading Language Master Class. GLSL Masterclass, 2004.
[vdB00] John van der Burg. Building an Advanced Particle System. Website, 2000.
http://www.gamasutra.com/features/20000623/vanderburg_01.htm.
[WE05]
Ian Williams and Hart Evan. Efficient rendering of geometric data using OpenGL VBOs in
SPECviewperf, 2005. White paper.
[Wei04] Lars Weinand. Herbst-Update: ATI Radeon X850 XT PE mit R480-Kern. Website, 2004.
http://www.de.tomshardware.com/graphic/20041215/ati-x850-r480-02.html#
radeon_x850_xt_pe_r480.
[WN98] Mason Woo and Tom Neider, Jackie und Davis. OpenGL Programming Guide Second Edition.
Addison-Wesley, 1998.
[WP05]
Lars Weinand and Darren E. Polkowski. Optimierter Leistungsträger: GeForce 7800 GTX
(G70). Website, 2005.
http://www.de.tomshardware.com/graphic/20050622/index.html.
47