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