– Diplomarbeit –
Transcrição
– Diplomarbeit –
GPU- BASIERTE E CHTZEIT-S IMULATION UND V ISUALISIERUNG VON S HALLOW WATER E QUATIONS – Diplomarbeit – dem Fachbereich Informatik der Universität Dortmund vorgelegt von Hendrik Becker Universität Dortmund Hiermit versichere ich, daß ich die vorliegende Arbeit selbständig und nur unter Verwendung der angegebenen Quellen und Hilfsmittel verfaßt habe. Dortmund, den 8. Mai 2006 iv Inhaltsverzeichnis 1 Einleitung 1.1 Einführung und Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Mathematische Grundlagen der Arbeit 2.1 Physikalisch basierte Modellierung . . . . . . . . . . . . . . . . 2.1.1 Differentialgleichungen . . . . . . . . . . . . . . . . . 2.1.2 Erhaltungsgleichungen . . . . . . . . . . . . . . . . . . 2.1.3 Navier-Stokes-Gleichungen . . . . . . . . . . . . . . . 2.2 Shallow Water Equations . . . . . . . . . . . . . . . . . . . . . 2.3 Numerische Verfahren zur Lösung der Shallow Water Equations 2.3.1 Räumliche Diskretisierung . . . . . . . . . . . . . . . . 2.3.2 Die Methode der Finiten Differenzen . . . . . . . . . . 2.3.3 Zeitliche Diskretisierung . . . . . . . . . . . . . . . . . 2.3.4 Die semi-lagrange’sche Integrationsmethode . . . . . . 2.3.5 Die diskretisierten Shallow Water Equations . . . . . . . 2.3.6 Das Jacobi-Verfahren . . . . . . . . . . . . . . . . . . . 2.4 Volumenbestimmung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 4 7 7 9 10 12 13 15 17 18 20 Grafikkarten-Programmierung 3.1 Die programmierbare Grafik-Pipeline . . . . . . . . . . . 3.2 General-Purpose Computing on Graphics Processing Units 3.3 Optimierung von GPU Programmen . . . . . . . . . . . . 3.3.1 Bustransfer . . . . . . . . . . . . . . . . . . . . . 3.3.2 Speicher-Zugriff . . . . . . . . . . . . . . . . . . 3.3.3 Arithmetische Intensität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 23 25 27 27 28 29 Implementierung 4.1 Werkzeuge und Bibliotheken . . . . . . . 4.2 Das Rahmenprogramm . . . . . . . . . . 4.2.1 Initialisierung . . . . . . . . . . . 4.2.2 Textur-Datenstrukturen . . . . . . 4.2.3 Das Framebuffer-Objekt . . . . . 4.3 Fragment-Shader-Programme . . . . . . . 4.3.1 Berechnung der Departure Points 4.3.2 Jacobi-Iteration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 31 32 33 33 33 36 38 38 3 4 v . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 vi INHALTSVERZEICHNIS 4.4 4.5 5 4.3.3 Aktualisierung des Geschwindigkeitsfeldes Visualisierung . . . . . . . . . . . . . . . . . . . . 4.4.1 VBOs/PBOs . . . . . . . . . . . . . . . . 4.4.2 Vertex Texture Fetch . . . . . . . . . . . . 4.4.3 Reflektion . . . . . . . . . . . . . . . . . . 4.4.4 Refraktion . . . . . . . . . . . . . . . . . Bedienung . . . . . . . . . . . . . . . . . . . . . . Ergebnisse 5.1 Visuelles Ergebnis . . . . . . . . 5.2 Geschwindigkeit . . . . . . . . 5.3 Genauigkeit . . . . . . . . . . . 5.4 Zusammenfassung und Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 40 41 43 43 45 47 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 49 49 58 64 A Code-Auszüge A.1 Shader-Programme . . . . . . . . . . . . . . . A.1.1 Berechnung der Departure Points . . . A.1.2 Jacobi . . . . . . . . . . . . . . . . . . A.1.3 Aktualisierung der Geschwindigkeiten . A.1.4 Koordinaten-Erzeugung . . . . . . . . A.1.5 Normalen-Erzeugung . . . . . . . . . . A.1.6 Reflektion, Brechung und Fresnel-Term A.1.7 Environment-Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 71 71 73 75 76 77 78 80 Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 Kapitel 1 Einleitung 1.1 Einführung und Motivation Schon von Kindheit an ist das Spielen eine der liebsten Beschäftigungen des Menschen. Wen wundert es also, dass auch - und in den letzten Jahren gerade! - Computer zum Spielen verwendet werden. Der Softwaremarkt wird klar dominiert vom Verkauf von PC- und KonsolenSpielen, während klassische Rechneranwendungen wie Textverarbeitung und Tabellenkalkulation zumindest im privaten Bereich nur noch ein Nischendasein fristen. Die Spieleentwicklung begann vor einigen Jahren mit wenigen, bewegten ASCII- Zeichen auf den zu Monitoren umfunktionierten Fernseh-Bildschirmen, und lebte mehr von den Spiel-Ideen als von der mageren grafischen Darstellung. Leistungsfähigere Rechner ließen diese Anfangszeiten jedoch rasch vergessen, und schon bald tummelten sich animierte Sprites in zweidimensionalen Pixelwelten. Die Grafik-Erzeugung bekam eine eigene Hardware-Industrie, und mit den Ergebnissen der neuen informatischen Disziplin der Computergrafik schafften die Spiele auch den Sprung ins Dreidimensionale. Diese neuen Spiele glänzten durch immer spektakulärere 3D-Effekte, die die vorhandenen Graffikkarten bis an die Grenzen ihrer Leistungsfähigkeit ausreizten. Beflügelt von den hohen Verkaufszahlen legten die Grafikkarten-Hersteller nach, und stellten immer stärkere Grafikchips zur Verfügung, die von den Spieleentwicklern genutzt werden konnten. Heute hat die Komplexität der Schaltkreise in den Grafik-Chips die der CPUs schon lange überholt. In einigen Jahren werden die erzeugten virtuellen Welten ein Ausmaß an Realismus in der grafischen Darstellung erreicht haben, in dem sie von der Wirklichkeit kaum noch zu unterscheiden sind. Neben der realistischen Visualisierung der dargestellten Objekte tritt in vor allem auch ihr naturgetreues Verhalten in den Vordergrund. Auf der Basis von physikalischen Gesetzen werden dafür Modelle entworfen, die vielleicht nicht in allen Bereichen ganz exakt sind, aber dennoch dem Betrachter das Gefühl einer realistischen Interaktion mit der Szenerie vermitteln. Die realitätsnahe Darstellung von bewegtem Wasser ist dabei schon immer eine besondere Herausforderung gewesen. Was vor einigen Jahren im Raytracing-Bereich begann, gewinnt nun auch für virtuelle Welten und moderne Computerspiele an Bedeutung. Interaktive Raten in der 1 2 KAPITEL 1. EINLEITUNG Berechnung der Bewegung und deren ansprechende Darstellung sind dafür jedoch zwingend notwendig. An diesem Punkt setzt diese Arbeit an. Die großen Parallelvearbeitungs-Kapazitäten moderner Grafikkarten mit hohen Speicherbandbreiten und Taktraten, und vor allem der freien Programmierbarkeit bestimmter Teile der Hardware ermöglichen es, die GPU nicht nur für die Darstellung, sondern auch für die Simulation physikalisch korrekten Wasserverhaltens einzusetzen, mit dem Ziel, die für eine Berechnung und Visualisierung in Echtzeit notwendige Rechenleistung zu erbringen. 1.2 Übersicht Die vorliegende Arbeit ist wie folgt gegliedert: • Kapitel 2 befasst sich mit den für die Umsetzung notwendigen mathematischen Grundlagen. Ausgehend von den für physikalisch basierte Modellierung ganz allgemein benötigten Begriffen wird das für die Implementierung verwendete Modell der sogenannten Shallow Water Equations hergeleitet. Ein für die spätere Untersuchung der erzielten Ergebnisse verwendetes Verfahren zur Volumenbestimmung wird vorgestellt. • Kapitel 3 geht auf den Aufbau der eingesetzen Grafikkarten ein, und hält wichtige Aspekte für deren effiziente Programmierung fest, die auch für die Implementierung in dieser Arbeit von Bedeutung sind. • Kapitel 4 beschreibt im Detail das Vorgehen bei der Umsetzung des vorgestellten Verfahrens in eine GPU-Implementierung. Dabei spielen neben dem Verfahren zur Lösung des zugrundeliegenden mathematischen Problems auch die für die Visualisierung eingesetzten Techniken eine Rolle. • Kapitel 5 dient der Vorstellung der erzielten Ergebnisse und deren Analyse in Bezug auf Geschwindigkeit und Genauigkeit der Simulation. Diese Reihenfolge beschreibt auch den chronologischen Ablauf in der Entstehung dieser Arbeit. Nach dem Verständnis der grundlegenden mathematischen Verfahren und der Besonderheiten der modernen Grafikkarten-Programmierung wurde die Implementierung vorgenommen, und abschließend analysiert. Teile des Programmcodes von zentraler Bedeutung für die Implementierung sind auszugsweise in Anhang A zu finden. Der Code liegt der Arbeit auch in Gänze auf einer CD bei. Kapitel 2 Mathematische Grundlagen der Arbeit 2.1 Physikalisch basierte Modellierung Der Ursprung dieser Arbeit findet sich im Feld der physikalisch basierten Modellierung, oder kurz PBM. Das Ziel dieser Form der Modellierung ist es, natürliche Phänomene - wie in diesem Fall das Verhalten von Wasser - auf eine Computeranwendung abzubilden, ohne die Veränderungen der beobachteten Objekte schrittweise von Hand zu animieren [28]. Anwendungen für PBM finden sich in vielen Bereichen: die Zeitersparnis bei der Animation kommt der Trickfilmindustrie zu Gute, der höhere Realismus beflügelt die Spieleindustrie z.B. bei der Entwicklung von Simulatoren, etc., aber auch im wissenschaftlichen Bereich wird PBM eingesetzt, um das qualitative Verhalten eines Systems zu bestimmen [20]. Beispiele für Einsatzgebiete der physikalisch basierten Modellierung sind Masse-Feder-Systeme, wie sie zur Simulation von verformbaren Materialien wie Stoff oder in der Animation von Muskeln und Gewebe verwendet werden [33, 40], oder Partikel-Systeme zur Modellierung von natürlichen Phänomenen wie Feuer, Regen, Schnee, Rauch, oder dem Schwarmverhalten von Tieren etc. [34]. Die physikalischen Gesetze, die ein solches System bestimmen, werden dabei meist in partiellen Differentialgleichungen ausgedrückt. Dabei wird versucht, die Eigenschaften des betrachteten Systems möglichst vollständig zu erfassen, um auf dieser Basis den zeitlichen Ablauf vorherzusagen [42]. Kapitel 2.1 fasst sehr kurz benötigte Verfahren der Modellierung zusammen, während in Abschnitt 2.2 ausführlich das verwendete Lösungschema für die Shallow Water Equations hergeleitet wird. 2.1.1 Differentialgleichungen Gewöhnliche Differentialgleichungen sind Gleichungen, in denen eine Funktion und ihre Ableitungen nach einer Variablen auftreten [48]. Wird die Funktion dabei nach mehr als einer Variable abgeleitet, so spricht man von partiellen Differentialgleichungen. 3 4 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT Als einfaches Beispiel betrachte man eine Funktion u : R × R → R, die z.B. von der Ortsvariablen x und der Zeit t abhängt. Die partielle Ableitung ∂u(x, t) ∂t beschreibt dann die Änderung der Funktion mit der Zeit, und analog ∂u(x, t) ∂x die Änderung der Funktionswerte mit der Ortsvariablen. Sind die Änderungen gleich ergibt sich folgende partielle Differentialgleichung ∂u ∂u = , ∂t ∂x wobei der einfacheren Lesbarkeit halber ∂u(x, t) durch ∂u ersetzt wurde. Durch erneutes Ableiten nach den Variablen t und u erhält man die sogenannte Wellengleichung ∂2u ∂2u = , ∂t2 ∂x2 in der ∂ 2 die zweite Ableitung bezeichnet. Ein wichtiges Beispiel für partielle Differentialgleichungen sind die sogenannten Erhaltungsgleichungen, die die Basis der Kontinuumsmechanik bilden. Das Grundprinzip der Erhaltung besagt, dass sich die Integrale der in Dichtefunktionen dargestellten Zustandsgrößen wie Massedichte, Temperatur, Impuls oder innere Energie in einem bewegten Volumen nicht ändern, solange sie frei von äusseren Einflüssen bleiben. Sie spielen eine wichtige Rolle im in dieser Arbeit vorstellten Modell, und werden daher im folgenden kurz erläutert. Für weitere Informationen wird auf Lehrbücher zum Thema verwiesen (z.B. [23, 43] oder Vorlesungsskripte wie [41]). 2.1.2 Erhaltungsgleichungen Die im Folgenden vorgestelle allgemeine Erhaltungsgleichung lässt sich später auf die Spezialfälle für die drei für diese Arbeit wichtigen physikalischen Größen Masse, Impuls und Energie anwenden. Man kann sich ein gas- oder flüssigkeitsgefülltes Volumen als Kontinuum (aus dem Lateinischen, continuum:„Das Zusammenhängende“) vorstellen, also als ein Objekt welches keine Risse, Brüche, Löcher, Hohlräume oder ähnliches innerhalb seiner Grenzen besitzt. Diese Vorstellung ist wegen der großen Anzahl der in diesem Volumen vorhandenen Moleküle gerechtfertigt, und erlaubt die Untersuchung der beschreibenden Funktionen auf kleinsten Intervallen. Man betrachtet nun einen beliebig geformten, stückweise stetig differenzierbaren dreidimensionalen Bereich Ω mit Volumen V , dessen Rand ∂Ω mit der Oberfläche S, und eine Funktion F (t). Diese gibt zu jedem Zeitpunkt t eine physikalische Größe für den gesamten Bereich Ω an (z.B. die Gesamtmasse, den Gesamtimpuls, etc.). 2.1. PHYSIKALISCH BASIERTE MODELLIERUNG 5 Diese Funktion kann mit Hilfe der ihr zugehörigen Dichten f (r, t) beschreiben werden, indem über den Bereich Ω integriert wird: Z F (t) = f (r, t)dV Ω r bezeichnet dabei einen Ortsvektor. Die Änderung der Größe F mit der Zeit t wird beschrieben durch Z ∂F ∂f (r, t) = dV ∂t ∂t Ω und kann folgende Ursachen haben: 1. Die betrachtete Größe F gelangt durch die Oberfläche in das Volumen hinein oder aus ihm heraus. 2. Im Inneren des Gebietes Ω liegt eine Quelle (bzw. Senke) von F . 3. Fernwirkungen wie z.B. Gravitation oder Wärmestrahlung beeinflussen das Innere von Ω von ausserhalb. Der mit φf bezeichnete, und Stromdichte genannte Vektor gibt die ein- oder ausfließende Größe pro Zeit- und Oberflächeneinheit an. Da nur der senkrecht zur Oberfläche fließende Anteil relevant ist, wird das Skalarprodukt n · φf gebildet, welches negativ wird, wenn φf auf die Oberfläche gerichtet ist. Der Term qf beschreibt die pro Zeit t und Volumeneinheit gebildete Menge an F im Inneren von , und sf die pro Zeit- und Volumeneinheit gebildete Menge an F steht. Die gesamte Änderung von F lässt sich also durch Summation der einzelnen ÄnderungsIntegrale ausdrücken: Z Z Z Z ∂F ∂f (r, t) = dV = − φf · ndS + qf dV + sf dV ∂t ∂t Ω ∂Ω Ω Ω Drückt man nun den Fluss von φf über dem Rand ∂Ω durch das Volumenintegral der Divergenz der Dichtefunktion div(φf ) über dem gesamten Gebiet Ω aus, und berücksichtigt, dass die Gleichung nur möglich ist, wenn bereits die Integranden gleich sind, erhält man die allgemeine Erhaltungsgleichung: ∂f + div(φf ) = qf + sf . ∂t Erhaltung der Gesamtmasse Wählt man für F nun die Gesamtmasse des Systems m, so entspricht die Dichte fm der Massendichte ρ, und die Massenstromdichte φm ergibt sich aus dem Produkt der Geschwindigkeit 6 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT v und der Dichte ρ. Die Terme qm und sm sind beide 0. Durch Einsetzen in die allgemeine Erhaltungsgleichung erhält man die Massenerhaltungsgleichung: ∂ρ + div(vρ) = 0. ∂t Erhaltung des Impulses Die Größe F ist in diesem Fall durch den Gesamtimpuls des Systems gegeben, wodurch fmv der Impulsdichte pv entspricht, und die Impulsstromdichte φmv sich zu φmv = vp · v + p̃ ergibt. p̃ bezeichnet dabei die Impulsänderung durch Druck- und Reibungskräfte. qmv ist 0, da im Inneren keine Kräfte entstehen, die eine Impulsänderung nach sich ziehen. Jedoch wird der Impuls durch eine Fernwirkung smv beeinflusst, nämlich der Gravitationskraft pro Volumen, gegeben durch pg . Für eine genaue Herleitung verweise ich auf [27]. Einsetzen in die allgemeine Erhaltungsgleichung führt zur Impulserhaltungsgleichung: ∂pv + div(vp · v) + div(p̃) = pg . ∂t Erhaltung der Energie Betrachtet man für die Größe F die Gesamtenergie des Systems, also die Summe der potentiellen Energie (pG , das Maß an Energie, das nötig ist, um einen Körper ausgehend von einem Bezugsniveau in seine aktuelle Lage zu versetzen), der kinetischen Energie (pv , die Schwerpunktsbewegung der Moleküle) und der inneren Energie (pu , die Schwingung und Rotation der einzelnen Moleküle), so ist die entsprechende Dichte fe die sogenannte Energiedichte pe . Diese setzt sich wie folgt zusammen: 1 pe = pu + p2v + pG . 2 Im folgenden wird mit p̃v die Reibungsarbeit bezeichnet, die aufgrund der Viskosität (die ”Zähigkeit” einer Flüssigkeit oder eines Gases, die umso höher wird, je dickflüssiger das Medium ist) verrichtet wird. Die Energiestromdichte φe (ein Vektor) ergibt sich zu φe = pev + p̃v +iq , mit der Wärmestromdichte iq = −γgradT , wobei γ den Wärmeleitkoeffizienten ausdrückt, T für die Temperatur und gradT für den Vektor (den sogenannten Gradienten) steht, der in die Richtung zeigt, in der die Temperatur am stärksten ansteigt. Der Quellterm qe ist 0, da im Inneren keine Energie entsteht oder vernichtet wird, wohl aber durch eine Fernwirkung se , mit Ursache in der Wärmestrahlung, gegeben durch qr . Auf eine detaillierte Ausarbeitung verweise ich an dieser Stelle wieder auf [27]. Erneutes Einsetzen in die allgemeine Erhaltungsgleichung führt zur Energieerhaltungsgleichung: ∂pe + div(pev + p̃v − γgradT ) = qr ∂t 2.2. SHALLOW WATER EQUATIONS 2.1.3 7 Navier-Stokes-Gleichungen Mit den im vorigen Abschnitt vorgestellten Erhaltungsgleichungen lässt sich zusammenfassend ein Gleichungssystem aufstellen, das wie folgt aussieht: p pv 0 ∂ = pg pv + div p ⊗ v + p̃ ∂t pe pev + p̃ − γgradT qr Dieses Gleichungssystem ist benannt nach dem Franzosen Claude-Louis Navier und dem Briten George Gabriel Stokes, die unabhängig von einander in der Mitte des 19. Jahrhunderts den Impulssatz für Newtonsche Fluide in differentieller Form gefunden, also die Abhängigkeit von Druck und Geschwindigkeit als Funktion von Ort und Zeit. Eine Variation dieser Gleichungen bezieht sich auf inkompressible Fluide, also Medien, in denen sich die Dichte ρ im Verlauf der Zeit und zwischen den betrachteten Punkten nicht ändert. Es gilt also ∂ρ ∂ρ ∂ρ ∂ρ = = = = 0, ∂t ∂x ∂y ∂z wobei t für die Zeit und x, y und z für die drei räumlichen Richtungen stehen. Eine derartige Inkompressibilität vorausgesetzt, lässt sich die Massenerhaltungsgleichung vereinfachen( [29]) zu ∂u ∂v ∂w + + = 0, ∂x ∂y ∂z und die Navier-Stokes-Gleichungen lassen sich unter dieser Nebenbedingung darstellen als ∂u ∂u ∂u ∂u 1 ∂p ∂2u ∂2u ∂2u +u +v +w = gx − + v( 2 + 2 + 2 ), ∂t ∂x ∂y ∂z ρ ∂x ∂x ∂y ∂z ∂v ∂v ∂v ∂v 1 ∂p ∂2v ∂2v ∂2v +u +v +w = gy − + v( 2 + 2 + 2 ), ∂t ∂x ∂y ∂z ρ ∂y ∂x ∂y ∂z ∂w ∂w ∂w ∂w 1 ∂p ∂2w ∂2w ∂2w +u +v +w = gz − + v( 2 + + ). ∂t ∂x ∂z ∂z ρ ∂z ∂x ∂y 2 ∂z 2 Dabei bezeichnen u, v und w die Geschwindigkeiten in x-, y- und z-Richtung, p den Druck, v die Viskosität der Flüssigkeit, gx , gy und gz die Gravitations-Konstanten für die drei räumlichen Richtungen und t für die Zeit. 2.2 Shallow Water Equations Auf Navier Stokes Gleichungen basierende Simulationen erreichen einen hohen Grad an Realismus. Für das in dieser Arbeit angestrebte Ziel sind sie allerdings zu komplex. Es wird also eine Vereinfachung dieses Systems benötigt, die sich effizient lösen lässt, und trotzdem ein ausreichendes Maß an Genauigkeit liefert, um als Grundlage für ein physikalisch basiertes Wasser-Modell zu dienen. 8 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT Ein erster Schritt besteht darin, den Koeffizienten für die Viskosität der Flüssigkeit v auf Null zu setzen: ∂u ∂u ∂u ∂u 1 ∂p +u +v +w = gx − , ∂t ∂x ∂y ∂z p ∂x ∂v ∂v ∂v ∂v 1 ∂p +u +v +w = gy − , ∂t ∂x ∂y ∂z p ∂y ∂w ∂w ∂w 1 ∂p ∂w +u +v +w = gz − . ∂t ∂x ∂y ∂z p ∂y Das entstandene Modell entspricht nun physikalisch betrachtet nicht exakt dem von Wasser, das z.B. abhängig vom seiner Temperatur oder den in ihm gelösten Stoffen eine Viskosität aufweist, die erheblich von Null verschieden ist. Gleiches gilt aber bereits für die Annahme der Inkompressibilität, wie die in Tabelle 2.1 empirisch ermittelten Werte für die Eigenschaften von Wasser zeigen [17]: Temperatur T (◦C) 0 5 10 15 20 25 30 50 100 Dynamische Viskosität ν (10−3 Psa ) 1,78 1,52 1,31 1,40 1,00 0,89 0,80 0,55 0,28 Kompressibilität κ (10−9 hP a−1 ) 51,0 49,6 45,9 44,2 44,5 46,1 48,9 44,0 47,7 Tabelle 2.1: Viskosität und Kompressibilität von Wasser Reduziert man das entstandene Gleichungssystem noch weiter und betrachtet nur noch die Geschwindigkeiten u und v in x- resp. y-Richtung, erhält man ein noch einfacheres Gleichungssystem, die sog. Shallow Water Equations (SWEs): ∂u ∂u ∂u ∂h +u +v +g =0 ∂t ∂x ∂y ∂x ∂v ∂v ∂h ∂v +u +v +g =0 ∂t ∂x ∂y ∂y ∂h ∂[u(h − b)] ∂[v(h − b)] + + =0 ∂x ∂x ∂y Wie in Abbildung 2.2 gezeigt, stehen u und v weiterhin für die Geschwindigkeiten in x-und y-Richtung, g für die Gravitationskonstante (nur z-Richtung) und t für die Zeit. h bezeichnet 2.3. NUMERISCHE VERFAHREN ZUR LÖSUNG DER SHALLOW WATER EQUATIONS9 Abbildung 2.1: Die im Modell verwendeten Bezeichnungen das Höhenfeld des Wassers und b das Bodenprofil. Allgemein ausgedrückt beschreiben SWEs also den Fluss von dünnen Flüssigkeitsschichten über einer beliebigen Bodentopographie. Sie werden gelegentlich auch als Saint-Venant-Gleichungen bezeichnet, benannt nach dem französischen Ingenieur Adhémar Jean Claude Barré de Saint-Venant (1797-1886), der während seiner Zeit im „Corps des Ponts-et-Chaussées“ die Grundlagen dafür legte. Praktische Anwendungsgebiete der SWEs finden sich in Dammbruch- und Tsunami-Simulationen und in der Küstenmodellierung, und wird sowohl in wissenschaftlichen als auch kommerziellen Programmen eingesetzt (siehe z.B. [12]). Die oben dargestellte Form ist nur eine Spielart der SWEs. Weite Verbreitung finden sie in ozeanographischen oder atmosphärischen Anwendungen, wo zusätzlich die Corioliskraft mit erfasst wird, die die Ablenkung bewegter Körper in einem rotierenden Bezugssystem aus Sicht eines sich mitbewegenden Beobachters wiedergibt. Da wir in dieser Arbeit relativ kleine Gebiete betrachten werden, ist für die realistische Darstellung die Corioliskraft aber vernachlässigbar. 2.3 Numerische Verfahren zur Lösung der Shallow Water Equations Eine Aufgabe der numerischen Mathematik besteht in der Entwicklung von Methoden, mit denen mathematische Probleme effektiv und meißt unter Angabe des dabei gemachten Fehlers (der Abweichung der gefundenen, approximativen Lösung von der exakten) gelöst werden können. Das Hauptanwendungsgebiet liegt demnach in der Simulation von komplexen Vorgängen - wie sie z.B. in der Natur vorkommen - auf schnellen Rechenanlagen, um teure Experimente durch beliebig oft wiederholbare Modellrechnungen zu ersetzen. Um die SWEs und Differentialgleichungen im allgemeinen zu lösen, muss eine Funktion gefunden werden, die mit ihren Ableitungen der Gleichung genügt. Eine analytische Lösung - das „Finden“ einer Funktion mit den gesuchten Eigenschaften für den ganzen betrachteten Bereich - ist aufgrund der Komplexität der Gleichungen nicht möglich. Daher bedient man sich einer geeigneten Diskretisierung, was bedeutet, dass man aus der vorliegenden, kontinuierlichen In- 10 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT formation endlich viele (diskrete) Werte gewinnt - im konkreten Fall ein Netz oder Gitter von Stützstellen - um sie in endlicher Zeit und mit endlichem Speicherplatz bearbeiten zu können. Die diskretisierten Differentialgleichungen enthalten dann keine kontinuierlichen Ableitungen mehr, sondern nur noch diskrete, also rein algebraische Ausdrücke. Da die SWEs sowohl von zeitlichen als auch von räumlichen Variablen abhängen, muss auch eine zeitliche und räumliche Diskretisierung erfolgen. Oft ist die gewählte Diskretisierung bereits eng mit dem eigentlichen Lösungsverfahren (s.u.) verknüpft. 2.3.1 Räumliche Diskretisierung Eine Hauptunterscheidung bei der räumlichen Diskretisierung bildet dabei die Unterteilung in strukturierte und unstrukturierte Netze oder Gitter. In Abbildung 2.2 ist ein Beispiel für ein unstrukturiertes Gitter zu sehen. Unstrukturierte Netze weisen die höchste Flexibilität auf. Man könnte sie zum Beispiel ver- Abbildung 2.2: Ein unstrukturiertes Gitter wenden, um einen realistischen Küstenstreifen mit vorgelagerten Inseln, Felsen oder Wellenbrechern zu modellieren. Sie sind allerdings auch am schwersten zu generieren. Ein Knoten (Gitterpunkt) oder eine Zelle (das Innere eines durch zusammenhängende Kanten (Knotenverbindungen) beschränkten Bereiches) kann nicht einfach anhand zweier bzw. dreier (2D bzw. 3D) Koordinaten identifiziert werden. Obschon in der Informatik effiziente Datenstrukturen für unstrukturierte Gitter bekannt sind (zum Beispiel die sog. Winged Edge-Datenstruktur, [16]), lassen sich diese nur sehr schlecht für eine GPU-Implementierung benutzen. Wie Kapitel 2.3 genauer erläutert, erreicht eine Anwendung auf der GPU für größere Probleme nur dann einen Geschwindigkeitsvorteil gegenüber der CPU, wenn auf den Grafikkarten Speicher sequentiell zugegriffen wird. Oben genannte Datenstruktur lebt aber von der Verwendung von Zeigern auf die Zellen, Kanten und Knoten des Gitters, deren Verfolgung letztendlich zu einem zufälligen Speicherzugriffsmuster führt. Wie im Implementierungs-Teil dieser Arbeit noch genauer beschrieben, liegt daher die Ver- 2.3. NUMERISCHE VERFAHREN ZUR LÖSUNG DER SHALLOW WATER EQUATIONS11 wendung eines regulären Gitters näher. Abbildung 2.3 zeigt einige Spielarten strukturierter Netze [26]. Abbildung 2.3: Verschieden Arten strukturierter Netze: Reguläre, orthogonale Gitter (A und B), krummliniges Gitter (C) und orthogonal, krummliniges Gitter (D). Strukturierte Gitter zeichnen sich dadurch aus, dass die Anzahl der Gitterpunkte in einer Raumdimension konstant ist. Ein Gitter besitzt also eine feste Anzahl nx Knoten in x-Richtung und ny in y-Richtung (im zweidimensionalen Fall, analog besitzt ein strukturiertes 3D-Gitter zusätzlich nz Punkte in z- Richtung). Werden die Punkte der Reihe nach in der jeweiligen Dimension durchnummeriert, kann ein Punkt eindeutig anhand des Zwei- bzw. Drei-Tupels aus x-, y- und z-Index identifiziert werden. Sind die Zellenlängen eines strukturierten Netzes in den einzelnen Raumdimensionen konstant, spricht man von einem regulären Gitter. Stehen die Kanten senkrecht aufeinander, bezeichnet man das Gitter als orthogonal. Generell gilt dabei: Je feiner die Unterteilung des Berechnungsgebietes ist, desto geringer fallen die Näherungen auf den Untergebieten ins Gewicht. Dies gilt sowohl für die zeitliche, wie auch für die räumliche Diskretisierung. Der Nachteil einer sehr feinen Unterteilung liegt dabei auf der Hand: Je mehr Stützstellen ausgewertet werden müssen, um so höher liegt auch der benötigte Rechenaufwand und Speicherplatzbedarf. Ein in der Praxis häufig verwendeter Ansatz zur Minimierung des Fehlers, bei dem gleichzeitigen Versuch, den Gesamt-Rechenaufwand so gering wie möglich zu halten, liegt darin, die Zellen des Gitters weiter zu verfeinern, in denen der Fehler groß wird. Man spricht dann von einem adaptiven Gitter (siehe Abbildung 2.4). . Um die Verfeinerung des Gittes vorzunehmen, müssen also die stark fehlerbehafteten Zellen identifiziert, und anschließend verfeinert werden. Für das angestrebte Ergebnis ist diese Verfeinerung aber nicht nötig, wie in Kapitel 5 gezeigt wird. 12 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT Abbildung 2.4: Ein adaptives Gitter Die Implementierung dieser Arbeit basiert auf der räumlichen Aufteilung des Rechengebietes in ein nicht-adaptives, strukturiertes, reguläres, orthogonales Gitter. Da die Funktionswerte an den Stützstellen wie später ausführlich erklärt in einer Textur gespeichert werden, können die einzelnen Gitterpunkte intuitiv anhand ihrer Textur-Koordinaten identifiziert werden. Ausserdem eignet sich ein derart strukturiertes Gitter gut für die sog. Methode der Finiten Differenzen. 2.3.2 Die Methode der Finiten Differenzen Ausgangspunkt der Finiten Differenzen (FD) Methode ist eine Gleichung in Differentialform. Zur Veranschaulichung betrachte man eine Funktion u : R × R → R, deren Ableitung nach der x-Koordinate man als ∂u ∂x schreibt. Um auf einem regulären Gitter mit den Punkten (xi , yj ), 1 ≤ i ≤ nx und 1 ≤ j ≤ ny einen Differenzenquotienten für die erste Ableitung in x-Richtung zu erhalten, entwickeln wir die Funktion u am Punkt (i + 1, j) in eine Taylorreihe (siehe dazu z.B. [21, 42]). ui+1,j = ui,j + (xi+1 − xi ) ∂ui,j ∂ui,j 2 1 + (xi+1 − xi )2 + ... ∂x 2 ∂x2 Für den Entwicklungsknoten (i − 1, j) erhalten wir ui−1,j = ui,j − (xi − xi−1 ) ∂ui,j ∂ui,j 2 1 − (xi − xi−1 )2 − ... ∂x 2 ∂x2 2.3. NUMERISCHE VERFAHREN ZUR LÖSUNG DER SHALLOW WATER EQUATIONS13 Löst man nun die erste Taylorreihe nach ent als Approximation der Ableitung: ∂ui,j ∂x auf, so ergibt sich der vordere Differenzenquoti- ∂ui,j ui+1,j − ui,j ≈ ∂x 4x Eine Fehlerabschätzung für Taylorreihen besagt, dass die Terme höherer Ableitungen mit O(4x) abgeschätzt werden können (siehe z.B. [42] , [21]). Für 4x → 0 gehen auch sie gegen Null. ∂u Aus der zweiten Taylorreihe erhalten wir nach Auflösen nach ∂xi,j den sog. rückwärtige Differenzenquotienten: ∂ui,j ui,j − ui−1,j ≈ ∂x 4x Durch Subtraktion der zweiten Reihe von der ersten ergibt sich der zentrale Differenzenquotient: ∂ui,j ui+1,j − ui−1,j ≈ ∂x 24x Die Differenzenformeln für die zweiten Ableitungen lassen sich analog herleiten, und man erhält ∂ 2 ui,j ui−1,j − 2ui,j + ui+1,j ≈ . 2 ∂x ∆x2 Ersetzt man nun in der ursprünglichen Gleichung alle partiellen Ableitungen durch die so gebildeten Differenzenquotienten, tauchen in der Gleichung nur noch arithmetische Operationen auf. Das hier vorgestellte finite Differenzenverfahren wird in Abschnitt 2.3.1 zur räumlichen Diskretisierung der SWEs verwendet. Da die Taylorreihen nur bis zu den quadratischen Termen entwickelt wurden, spricht man von einem Fehler in der Ortsdiskretisierung von der Grössenordnung O(4x2 ), bzw. von Genauigkeit zweiter Ordnung für die zentralen Differenzenquotienten. 2.3.3 Zeitliche Diskretisierung Da in den SWEs zeitliche Ableitungen enthalten sind, beschreiben sie also wie sich die entsprechenden Größen mit der Zeit entwickeln. Um dieses Geschehen numerisch zu behandeln, wird ein Anfangszeitpunkt t0 festgelegt, und durch das fortwährende Hinzufügen von Zeitschritten 4t eine Folge von Zeitpunkten „erzeugt“: tn = t0 + n 4 t Dies nennt man zeitliche Diskretisierung. Unter der Bedingung, dass alle betrachteten physikalischen Größen für den Zeitpunkt t0 als Startwerte vorliegen, werden nun Rechenvorschriften gesucht, anhand derer die Lösungswerte für den Zeitpunkt t1 , und dann sukzessive für alle weiteren Zeitpunkte t2 , t3 , ... bis zum Abbruch der Simulation ermittelt werden können. Im allgemeinen unterscheidet man zwei Ansätze zur Diskretisierung partieller Differentialgleichungen in der Zeit: Den expliziten und den impliziten Ansatz. Explizit bedeutet dabei, dass auf einer Seite der Gleichung nur bekannte Größen vorhanden sind, und man daher explizit nach den gesuchten Unbekannten auflösen kann. Auf konkreten 14 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT Fall der SWEs zum Beispiel bezogen bedeutet das, dass die Lösung für den nächsten Zeitschritt ausschließlich anhand geeigneter Koeffizienten aus der Lösung des letzten Zeitschritts gewonnen werden kann. Explizite Verfahren haben also den Vorteil, dass sie mit vergleichbar wenig Rechenaufwand zwischen pro Zeitschritt auskommen. Es ist allerdings bekannt, dass explizite Verfahren Probleme mit der Stabilität haben, wenn die Zeitschritte zu groß gewählt werden. Unter Stabilität eines numerischen Verfahrens versteht man die Robustheit des Algorithmus gegenüber Störungen in den Eingabedaten und Rundungsfehlern, also die Tatsache, dass sich diese Einflüsse während der Berechnung nicht aufsummieren und zur Divergenz des Verfahrens oder einer grob falschen Lösungen führen. Sei f (x) ein mathematisches Problem mit Eingabe x und f˜(x̃) der numerische Algorithmus für dieses Problem mit gestörter Eingabe x̃, so bezeichnet kf (x) − f˜(x̃)k den Fehler der numerischen Lösung. Die Stabilität wird ausgedrückt durch kf (x̃) − f˜(x̃)k. Eine Quantifizierung hängt vom konkreten Problem und der verwendeten Norm ab, und soll hier nicht weiter erörtert werden. Damit ein explizites Verfahren stabil bleibt, muss die Zeitschritt-Größe geeignet klein gewählt werden. Ausschlaggebend für die Wahl des Zeitschritts ist dabei die Maschenweite des Gitters 4x. Als Maß für die Anzahl der Zellen, die sich ein Partikel pro Zeitschritt maximal bewegt, dient dient sogenannte Courant-Friedrichs-Lewy-Zahl c (auch CFL- oder Courant-Zahl genannt): u · 4t c= . 4x Bewegt sich ein Partikel des Fluids in einem Zeitschritt um mehr als eine Zellen-Länge 4x (c > 1) wird das Verfahren instabil. Die Zeitschritt-Größe muss also für jeden Zeitschritt überprüft, und abhängig von den maximal auftretenden Geschwindigkeiten u angepasst werden. Diese sogenannte Zeitschritt-Kontrolle lässt sich allein auf der GPU nicht effizient realisieren. Ein weiterer Nachteil liegt darin, dass die zulässige Zeitschritt-Größe für ”höhere” Geschwindigkeiten so gering wird, dass sehr viele - für eine Echtzeitsimulation zu viele - Zeitschritte nötig werden um eine stabile Lösung zu gewährleisten. Implizite Verfahren ziehen zur Berechnung der Lösung eines Zeitschritts für einen Gitterpunkt zwar ebenfalls die Lösung des letzten Zeitschritts (evtl. auch mehrerer vorheriger Zeitschritte) heran, sind aber zusätzlich an die benachbarten Gitterpunkte gekoppelt. Anders ausgedrückt bedeutet das, dass sich die Lösung nicht mehr einfach aus der des letzten Zeitschritts berechnen lässt, sondern für jeden Zeitschritt ein neues Gleichungssystem, das diese Kopplung beschreibt, aufgestellt und gelöst werden muss. Der Vorteil impliziter Verfahren liegt in deren größerer Robustheit gegenüber größeren Zeitschritten, allerdings sind sie rechenintensiver in den Zeitschritten. Für die Implementierung dieser Arbeit wurde ein impliziter Ansatz verwendet, mit der Absicht, dass auch Zeitschrittgrößen gewählt werden können, die von der CFL-Zahl nicht so stark abhängig sind, dass die Echzeitberechnung der Ergebnisse unmöglich wird. 2.3. NUMERISCHE VERFAHREN ZUR LÖSUNG DER SHALLOW WATER EQUATIONS15 2.3.4 Die semi-lagrange’sche Integrationsmethode In Strömungssimulationen gebräuchlich sind zwei Methoden zur Integration in der Zeit: Das Euler’sche Strömungsschema, bei dem der Beobachter sich an einem fixen Punkt befindet, und der Lagrange’sche Ansatz, bei dem der Beobachter sich mit einem Partikel bewegt. Der erste Ansatz bewahrt die Regelmäßigkeit des Gitters, benötigt aber kleine Zeitschritte um stabil zu bleiben. Der zweite Ansatz erlaubt auch größere Zeitschritte, allerdings werden durch die Bewegung die Partikel ungleichmäßig verteilt. Der in der Arbeit verwendete semi lagrange’sche Ansatz versucht die Vorteile beider Verfahren zu verbinden, in dem entlang der Partikel-Pfade integriert wird und die Zielfunktionen an den Gitterpunkten ausgewertet werden. Betrachtet man zur Verdeutlichung die eindimensionalen Shallow Water Equations ∂u ∂u ∂h +u +g = 0, ∂t ∂x ∂x ∂h ∂[u(h − b)] ∂h + +g = 0, ∂t ∂x ∂x so ergibt sich die lagrange’sche Form als du ∂g +g = 0, dt ∂x dh ∂b ∂u −u +d = 0, dt ∂x ∂x (2.1) (2.2) mit d = h − b und der lagrange’schen Ableitung d ∂ ∂ = +u dt ∂t ∂x in der dx dt = u(x, t) ist. Nach der semi lagrange’schen Methode werden die Ableitungen entlang von Partikel-Pfaden, sogenannten Trajektorien approximiert. Für einen Partikel, der sich zum Zeitpunkt tn+1 an Position xi befindet, betrachtet man auch seine Position einen Zeitschritt früher, zum Zeitpunkt tn . Diese wird als Ausgangspunkt (departure point) x̃ni bezeichnet, da sie bei einer Rückverfolgung entlang der Trajektorie des Partikels den Punkt bezeichnet, an dem sich der Partikel im vorherigen Zeitrschritt befunden hat (siehe Abbildung 2.5. Bezeichnet αin = xi − x̃ni die Verschiebung eines Partikels vom Zeitschritt tn nach tn+1 , so ergeben sich die Funktionswerte für die Ausgangspunkte als ũni = un (xi − αin ) h̃ni = hn (xi − αin ) Die Verschiebung αin wird dabei durch 4tun (xi ) approximiert. Da die x̃ni in der Regel nicht genau auf einen Gitterpunkt fallen, und ein direktes Auswerten der Funktionen u und h damit nicht möglich ist, werden die Werte durch lineare Interpolation 16 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT Abbildung 2.5: Berechnung der Ausgangspunkte gewonnen: Sei xm = m 4 x der m-te Gitterpunkt, und xm−1 < xi − αi < xm , dann gilt ũni ≈ un (xi − αin ) ≈ (xi − αin − xm−1 )un (xm ) + (xm − xi + αin )un (xm−1 ) 4x (2.3) h̃ni ≈ hn (xi − αin ), ≈ (xi − αin − xm−1 )hn (xm ) + (xm − xi + αin )hn (xm−1 ) . 4x (2.4) Die lagrange’schen Ableitungen lassen sich also approximieren durch du un+1 − ũn = dt 4t hn+1 − h̃n dh = dt 4t Übernimmt man diese Approximationen für die Gleichungen 2.1 und 2.2, und nimmt weiterhin die Wasserhöhe d im Intervall (tn , tn+1 ) als konstant an, ergibt sich folgende zeitliche Diskretisierung: un+1 − ũn ∂hn+1 +g =0 4t ∂x (2.5) hn+1 − h̃n ∂b ∂un+1 − un+1 + dn = 0. 4t ∂x ∂x (2.6) 2.3. NUMERISCHE VERFAHREN ZUR LÖSUNG DER SHALLOW WATER EQUATIONS17 2.3.5 Die diskretisierten Shallow Water Equations Nimmt man partielle Ableitung von Gleichung 2.3.5 nach x, können mittels der resultierenden n+1 Gleichung und die ∂u∂x und un+1 -Terme aus eliminiert werden, und man erhält eine sog. Helmholtz-Gleichung in hn+1 : hn+1 + 4t2 g n ∂b ∂hn+1 ∂ 2 hn+1 n n ∂b n ∂ ũ − 4t2 gdn = h̃ + 4tũ − 4td . ∂x ∂x ∂x2 ∂x ∂x (2.7) Über die zuvor beschriebenen zentrale Differenzenquotienten gelangt man zu folgender Diskretisierung für den eindimensionalen Fall, deren Lösung das neue Höhenfeld hn+1 ist: n+1 n+1 − bi−1 hi+1 − hi−1 24x 24x hn+1 − 2hn+1 + hn+1 i−1 i i+1 − 4 t2 gdni 24x b i+1 − bi−1 = h̃ni + 4tũni + 24x ũn − ũn i+1 i−1 − 4 tdni . 24x hn+1 + 4t2 g i b i+1 (2.8) Vollkommen analog zum eindimensionalen Fall verläuft die Herleitung für den zweidimensionalen Fall, und man erhält: hn+1 i,j 2 + 4t g b n+1 n+1 − bi−1,j hi+1,j − hi−1,j + 24x 24x n+1 n+1 bi,j+1 − bi,j−1 hi,j+1 − hi,j−1 i+1,j − 4 t2 gdni,j 24y 24y hn+1 − 2hn+1 + hn+1 i−1,j i,j 4x2 i+1,j + n+1 n+1 hn+1 i,j−1 − 2hi,j + hi,j+1 = h̃ni,j 4y 2 bi+1,j − bi−1,j n bi,j+1 − bi,j−1 + 4t ũni,j + ṽi,j 24x 24y n n n ũn − ũ ṽ i+1,j i−1,j i,j+1 − ṽi,j−1 n − 4 tdi,j + 24x 24y (2.9) Da das Bodenprofil b konstant bleibt, kann auf einen zeitlichen Index verzichtet werden. Der Ablauf zur Lösung der diskreten Shallow Water Equations lässt sich nun wie folgt zusammenfassen: 1. Initialisierung von u0 ,v 0 ,h0 und b. 2. Für jeden Zeitschritt tn+1 : • Berechnung der Ausgangs-Punkte hn , un und v n (Gleichungen 2.3,2.4). • Lösung von Gleichung 2.9 zur Berechnung von hn+1 . 18 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT • Aktualisierung von un+1 und v n+1 (2.3.5 für un+1 , v n+1 analog). Im Rahmen dieser Arbeit reicht das approximative Lösen von Gleichung 2.49 durch wenige Iterationen des sogenannten 2.9 aus. 2.3.6 Das Jacobi-Verfahren Das Jacobi-Verfahren ist ein sogenanntes Iterationsverfahren zur näherungsweisen Lösung für lineare Gleichungssysteme der Form Ax = b, A ∈ Rn×n und x, b ∈ Rn , die z.B. wie folgt aufgebaut sind: a1,1 x1 + a2,1 x2 + . . . +n,1 xn = b1 a1,2 x1 + a2,2 x2 + . . . +n,2 xn = b2 .. . a1,n x1 + a2,n x2 + . . . +n,n xn = bn Zur Lösung wird die ite Gleichung nach xi aufgelöst: xi = X 1 bi − ai,j xj ai,i j6=i Diese Substitution wird ausgehend von einem gegebenen Wert x0 für alle xi wiederholt. In Matrixschreibweise wird die Matrix A des linearen Gleichungssystems Ax = b vorbereitend in eine Diagonalmatrix D, eine untere Dreiecksmatrix L und eine obere Dreiecksmatrix U zerlegt, so dass gilt: A = L + D + U. Die komponentenweise Iterationsvorschrift lässt sich dann darstellen als xn+1 = xn + D−1 (b − Ax), wobei xn+1 die Lösung des nten Iterationsschritt bezeichnet [30]. Das Inverse der Diagonalen D wird also als Approximation von A−1 verwendet, die Teilmatritzen U und L werden ignoriert. Damit die Koeffizienten deutlich ersichtlich werden, werden die diskreten SWEs aus Gleichung 2.3. NUMERISCHE VERFAHREN ZUR LÖSUNG DER SHALLOW WATER EQUATIONS19 2.9 weiter umgeformt 2 4 t2 gdni,j 2 4 t2 gdni,j n+1 + )hi,j 4x2 4y 2 4t2 g(bi−1,j − bi+1,j ) 4t2 gdni,j n+1 − )hi−1,j +( 4 4 x2 4x2 4t2 g(bi+1,j − bi−1,j ) 4t2 gdni,j n+1 +( − )hi+1,j 4 4 x2 4x2 4t2 g(bi,j−1 − bi,j+1 ) 4t2 gdni,j n+1 +( − )hi,j−1 4 4 y2 4y 2 4t2 g(bi,j+1 − bi,j−1 ) 4t2 gdni,j n+1 − )hi,j+1 +( 4 4 y2 4y 2 bi+1,j − bi−1,j n bi,j+1 − bi,j−1 + ṽi,j + 4t ũni,j 24x 24y n n n ũn − ũ ṽ i+1,j i−1,j i,j+1 − ṽi,j−1 n − 4 tdi,j + 24x 24y (1 + = h̃ni,j (2.10) Notwendige Bedingung für die Konvergenz des Verfahrens (sprich die einzelnen Unbekannten nähern sich einem Grenzwert an) sind von Null verschiedene Diagonalen-Elemente (ai,i ). Diese Bedingung ist wie aus obiger Gleichung ersichtlich erfüllt, da neben von Null verschiedene Zeitschrittgrößen 4t und Schwerkraftkonstante g auch eine Wasserhöhe d > 0 vorausgesetzt wird. Hinreichende ist die strikte Diagonaldominanz der Matrix A. Nach [30] ist eine Matrix dann strikt diagonaldominant, wenn sie das starke Zeilensummenkriterium erfüllt: q∞ := max i=1...n n X ai,k < 1, ai,i i=1,k6=i bzw. |ai,i | > X |ai,j |. j6=i Betrachtet man die konkreten Koeffizienten aus Gleichung 2.10, so muss gelten (1 + ( 2 4 t2 gdni,j 2 4 t2 gdni,j + )> 4x2 4y 2 4t2 g(bi−1,j − bi+1,j ) 4t2 gdni,j − ) 4 4 x2 4x2 +( 4t2 g(bi+1,j − bi−1,j ) 4t2 gdni,j − ) 4 4 x2 4x2 +( 4t2 g(bi,j−1 − bi,j+1 ) 4t2 gdni,j − ) 4 4 y2 4y 2 +( 4t2 g(bi,j+1 − bi,j−1 ) 4t2 gdni,j − ). 4 4 y2 4y 2 20 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT Durch Ausrechnen der Summe der rechten Seite gelangt man zu 2 4 t2 gdni,j 2 4 t2 gdni,j 2 4 t2 gdni,j 2 4 t2 gdni,j (1 + + )>− − , 4x2 4y 2 4x2 4y 2 woraus die Gültigkeit der Ungleichung direkt ersichtlich wird und von einer Konvergenz des Jacobi-Verfahrens ausgegangen werden kann. Das beschriebene Verfahren zur Diskretisierung der Shallow Water Equations wird in [29] als nicht energieerhaltend bezeichnet. Obwohl in den zugrundeliegenden Gleichungen keine Dämpfung enthalten ist, werden die Wellen nach und nach flacher, bishin zur absoluten beruhigung der Wasseroberfläche. Dies lässt sich durch die durch räumliche Diskretisierung und die implizite Integration eingeführte numerische Dämpfung erklären, und stellt in sofern kein Problem da, als dass dieses „Aussterben“der Wellen genau das ist, was wir in der Natur erwarten würden. Als Maß für die Genauigkeit der Lösung kann also nur die Betrachtung des Volumens herangezogen werden. In [29] wird von einem Volumenverlust von fünf Prozent gesprochen, ein Wert der optisch nicht festzustellen ist, und damit im Rahmen des Akzeptablen liegt. Da eine geometrische Analyse des Volumens - z.B. durch Zerlegung in Tetraeder - zu ungenau ist, bediene ich mich einer sogenannten Gauß Quadratur. 2.4 Volumenbestimmung Die Gaußsche Quadraturformel (2n + 2)ter Ordnung für eine beliebige polynomielle Funktion f über einem Interval [a, b] ist für n = 1 definiert als I (1) (f ) = p p b−a f (c − 1/3h) + f (c + 1/3h), 2 b−a mit c = b+a 2 und h = 2 . Die zugehörige summierte Gaußsche Quadraturformel hat die Gestalt 1 (1) Ih (f ) = hX f (xj + h0 ) + f (xj+1 − h0 ) 2 j=0 1 mit xj = a + jh, h = (b − a)/n und h0 = ( 12 − 2√ )h, und ist eine exakte Repräsentation 3 Rb von f (x), so lange der Grad des Polynoms f kleiner als drei ist. Auf einen Beweis der Kora rektheit dieser Aussage will ich an dieser Stelle verzichten, verweise den interessierten Leser stattdessen auf [41]. Betrachtet man das Höhenfeld für die Wasseroberfläche als zweidimensionale Funktion h(x, y) = z, und auch das das Bodenprofil beschreibende Höhenfeld als solche, also b(x, y) = z 0 , so lässt sich das zwischen Boden und Wasseroberfläche eingeschlossene Volumen V 0 bis auf Run- 2.4. VOLUMENBESTIMMUNG 21 dungsfehler genau bestimmen durch V = n−1 X n−1 X k=0 l=0 − n−1 X n−1 X k=0 l=0 1 1 i=0 j=0 1 1 i=0 j=0 hXhX h(xk,i + h0 , yl,j + h0 ) + h(xk,i+1 − h0 , yl,j+1 − h0 ) (2.6) 2 2 hXhX b(xk,i + h0 , yl,j + h0 ) + b(xk,i+1 − h0 , yl,j+1 − h0 ). 2 2 für xi,k = k 4 x + i 4 x und yj,l = l 4 y + j 4 y. Für Polynome bis zum Grad fünf werden drei Stützstellen benötigt: 2 (2) Ih (f ) h X 1 = 5f (xj + h0 ) + 8f (xj + h) + 5f (xj+1 − h0 ). 18 2 j=0 Das Volumen ergibt sich analog zu 2.7 zu n−1 X n−1 X 2 n−1 X n−1 X 2 2 h X h X 5h(xk,i + h0 , yl,j + h0 ) 18 18 i=0 j=0 k=0 l=0 h h +8h(xk,i + , yl,j + ) + 5h(xk,i+1 − h0 , yl,j+1 − h0 ) 2 2 V = 2 h X h X 5b(xk,i + h0 , yl,j + h0 ) 18 18 i=0 j=0 k=0 l=0 h h +8b(xk,i + , yl,j + ) + 5b(xk,i+1 − h0 , yl,j+1 − h0 ) . 2 2 − (2.7) 22 KAPITEL 2. MATHEMATISCHE GRUNDLAGEN DER ARBEIT Kapitel 3 Grafikkarten-Programmierung 3.1 Die programmierbare Grafik-Pipeline Eine wesentliche Neuerung im Grafikkartensektor der letzten Jahre ist die Programmierbarkeit der sogenannten Shader-Units. Diese Teile der Grafik-Pipeline berechnen die Farbwerte der Szenerie an den Knoten der Geometrie und für die Pixel der auf den Bildschirm projizierten Ausgabe, in Abhängigkeit von Parametern wie z.B. Beleuchtung und Texturierung. Man spricht von Vertex-Shadern für die Knoten-Verarbeitung und von Fragment- oder Pixel-Shadern für die Bearbeitung der Pixel. Die programmierbaren Shader-Units lösen die sogenannte Fixed Function Pipeline ab, in der Transformation, Beleuchtung und die Verarbeitung der Texturen in der Grafik-Pipeline nur durch wenige Parameter beeinflussbar waren. Diese Funktionalität wird nun durch vom Benutzer programmierte Vertex- und FragmentProgramme festgelegt (vgl. Abbildungen 3.1 und 3.2, [31]). Der Funktionsumfang, der in den Shadern zur Verfügung steht, wird dabei in sogenannten Profilen beschrieben, da er (im Gegensatz zu normalen CPUs) zwischen den einzelnen GrafikkartenGenerationen sowohl für den Vertex-Shader als auch für den Fragment-Shader stark variiert. Das Vertex-Programm wird für jeden Knoten der Szenengeometrie einmal ausgeführt. Mehrere parallele Vertex-Pipelines auf der Hardware ermöglichen das gleichzeitige Abarbeiten der Knotenprogramme für verschiedene Vertices. Die Anzahl der zur Verfügung stehenden Pipelines ist abhängig von der eingesetzten Grafik-Hardware. So stehen z.B. auf einer mit Nvidia GForce 6800 GT Chipsatz ausgestatteten Grafikkarte sechs Vertex-Pipelines zur Verfügung, während es auf dem neueren GForce 7800GTX Chipsatz bereits acht sind. 23 24 KAPITEL 3. GRAFIKKARTEN-PROGRAMMIERUNG Abbildung 3.1: Programmierbarkeit bei der Knotenverarbeitung Abbildung 3.2: Programmierbarkeit bei der Fragmentverarbeitung Das Fragment-Programm wird für jeden gerenderten Pixel ausgeführt. Die große Anzahl an nötigen Programmdurchläufen wird dabei analog zum Vertex-Shader auf mehrere Pixel-Pipelines verteilt. Auch hier ist die Anzahl der zur Verfügung stehenden Pipelines abhängig von der Hardware. Den 16 Pipelines auf der NVidia GForce 6800GT stehen bereits 24 Pipelines auf der neueren 7800GTX gegenüber. Die programmierbaren Fragment-Shader sind in dieser Arbeit in besonderem Maße für die Lösung der Gleichungssysteme relevant. Neben der Verwendung der arithmetischen Rechenwerke wird vor allem von der Möglichkeit Gebrauch gemacht, über sogenannte Texture Image 3.2. GENERAL-PURPOSE COMPUTING ON GRAPHICS PROCESSING UNITS 25 Units aus den Shader-Programmen heraus mit 32bit Genauigkeit Fließkommazahlen aus dem Texturspeicher zu lesen, und die (Zwischen-)Ergebnisse der Berechnungen auch wieder im Texturspeicher abzulegen. Zur Programmierung der Vertex- und Fragmentshader stehen mehrere Sprachen zur Verfügung. Diese reichen von Assembler-ähnlichen, hardwarenahen Sprachen bis hin zu C-ähnlichen High-Level-Sprachen wie GLSL (GL Shading Language) oder der von NVidia entwickelten Sprache Cg (C for Graphics), die via Compiler in hardwarenahen Code übersetzt werden. Die Shader-Programme der Arbeit sind in Cg verfasst, könnten aber auch mit verhältnismäßig geringem Aufwand in eine andere Sprache übertragen werden. Vertex-Shader werden in dieser Arbeit nur zur Visualisierung verwendet, für das Lösen der Gleichungssysteme werden sie nicht benötigt. Eine Besonderheit der oben genannten NVidiaGPUs liegt in ihrer Fähigkeit, auch im Vertex-Shader auf den Textur-Speicher zuzugreifen, was auf GPUs anderer Hersteller bislang nur im Fragment-Shader möglich ist. Diesen Vorgang nennt man Vertex-Texture-Fetch, oder kurz VTF. Im Implementierungsteil dieser Arbeit wird sich dieser Besonderheit für eine in Kapitel 4.4 vorgestellte Variante in der Visualisierung zu nutze gemacht. 3.2 General-Purpose Computing on Graphics Processing Units GPGPU steht für General-Purpose Computing on Graphics Processing Units, und beschreibt zusammenfassend die Bemühungen einer wachsenden Gemeinschaft, Grafikkarten für die Lösung von Problemen zu nutzen, die auf den ersten Blick nichts mit deren „traditioneller“ Aufgabe - der Bilderzeugung - zu tun haben, um die Leistung der CPU für die Lösung des gleichen Problems zu übertreffen. Vergleicht man die theorethische Spitzenleistung von 63 GFLOPS einer GeForce 6800 Ultra (GFLOPS steht für ”giga floating point operations per second”, also eine Milliarde Gleitkommaoperationen in der Sekunde) mit den 14.8 GFLOPs eines Intel Pentium 4 SSE, und hält sich vor Augen, dass sich die Performance zur Zeit jährlich in etwa vervierfacht, werden die Gründe dafür schnell klar. Das vielzitierte Mooresche Gesetz, das die Beobachtung beschreibt, dass sich die Komplexität und Leistungsfähigkeit von integrierten Schaltkreisen alle 18 Monate in etwa verdoppelt, wird also von der Grafikkartenentwicklung noch weit übertroffen [38]. Vor allem die steigende Flexibilität der Hardware, die in den letzten Jahren vom Ändern einzelner Bilderzeugugs-Parameter zu voller Programierbarkeit - also dem Ausführen von Mikrocode auf der Hardware - erweitert wurde, erlauben eine abstrakte Sicht auf die GPU, in der sie als paralleler Data-Stream-Processor erscheint. Im Gegensatz zum bei der CPU-Programmierung gebräuchlichen Instruction-Stream-Processing (Abbildung 3.3), bei dem Daten und Anweisungen im gleichen Speicher residieren und zur Ausführung abwechselnd in den Cache (einen schnellen, meist in die CPU integrierten PufferSpeicher) geladen werden und dem das klassische von Neumann-Modell zugrunde liegt, wird beim Data-Stream-Processing (Abbildung 3.4) zunächst der Prozessor mit den auszuführenden 26 KAPITEL 3. GRAFIKKARTEN-PROGRAMMIERUNG Instruktionen konfiguriert, die sich für die zu verarbeitenden Daten-Blöcke nicht ändern. Beide Ansätze haben ihre Vor- und Nachteile: Beim Instruction-Stream-Processing wird ein hoher Grad an Flexibilität erreicht. Dadurch dass die Daten-Sequenz aber vollständig von der Anweisungs-Sequenz bestimmt wird, geht Rechenleistung verloren, wenn die Anweisungen sich für einen großen Datenblock nicht ändern. Abbildung 3.3: Instruction-Stream-Processing Das Data-Stream-Prozessing ist gerade dann von Vorteil. Genügend Speicherbandbreite vorausgesetzt, kann die Abarbeitung der Instruktionen auf verschiedene parallele Pipelines verteilt werden ( [47]). Abbildung 3.4: Data-Stream-Processing Die Daten der Eingabe entsprechen dabei auf der GPU den in Texturen gespeicherten Farbwerten, die in verschiedenen Formaten vorliegen können. Welche Texturformate unterstützt werden, hängt mit dem verwendeten Grafikkarten-Modell zusammen. Für diese Arbeit werden 128 Bit-tiefe RGBA-Texturen verwendet, die also 32 Bit Fließkomma-Genauigkeit in allen vier Kanälen erreichen. 3.3. OPTIMIERUNG VON GPU PROGRAMMEN 3.3 27 Optimierung von GPU Programmen Damit eine GPU-Implementierung den gewünschten Geschwindigkeitsgewinn gegenüber ihrem CPU-Gegenstück erzielt, müssen allerdings einige Besonderheiten beachtet werden, wie sie ausführlich z.B in [19], [49], [14] oder [24] beschrieben werden. Im folgenden werde ich einige dieser Merkmale beschreiben, und auf ihre Berücksichtigung in der Implementierung dieser Arbeit eingehen. 3.3.1 Bustransfer Bevor Daten überhaupt auf der Grafikkarte verarbeitet werden können, müssen sie je nach Grafikkarten-Typ über den AGP- oder PCI-Express-Bus zur GPU übertragen werden. Abbildung 3.5 zeigt die verschiedenen Speicherbereiche und deren Zusammenhang [14]. Abbildung 3.5: Die verschiedenen Speicherbereiche Die Übertragung von der CPU zur GPU stellt einen nicht zu unterschätzenden Flaschenhals dar. So könnte z.B. die Addition zweier großer Vektoren auf der CPU bereits durchgeführt sein, bevor die Daten überhaupt zur GPU übertragen worden sind. Gleiches gilt für den Rücktransport der Daten von der GPU zum Hauptspeicher. Diese Kosten amortisieren sich erst, wenn sehr oft über die Daten iteriert wird, bevor sie zum Hauptspeicher zurücktransferiert werden. Die Implementierung dieser Arbeit kommt ganz ohne Rücktransfer von der GPU zur CPU aus. Die Ergebnisse der näherungsweisen Lösung bleiben im Speicher der Grafikkarte, und werden „vor Ort“ mit den in Kapitel 4 beschriebenen Techniken visualisiert. Abgesehen von der Übertragung der intialen Geschwindigkeits- und Höhenwerte werden auch keine weiteren Daten von der CPU zur GPU übertragen. 28 KAPITEL 3. GRAFIKKARTEN-PROGRAMMIERUNG 3.3.2 Speicher-Zugriff Generell können drei Arten von Speicherzugriffs-Mustern unterschieden werden: • cached: Der gleiche Speicherbereich wird wieder und wieder gelesen. Nach dem ersten Zugriff liegen die Daten im sog. Textur-Cache, wo sie besonders schnell abrufbar sind. • sequentiell: Die Zugriffe erfolgen auf im Video-Speicher hintereinander liegende Bereiche, bzw. auf lokale Nachbarschaften wie im Texturspeicher benachbarte Texel. • zufällig: Die zu lesenden Daten liegen überall über den Video-Speicher verteilt. Sowohl für die CPU- als auch für die GPU-Progammierung gilt: Gute Performanz hängt eng mit der Lokalität der Speicher-Verweise zusammen. Zufällige Speicherzugriffe führen zu einer dramatischen Verschlechterung der erzielten Ergebnisse in Bezug auf die Geschwindigkeit. Während der CPU Cache mit weit höheren Taktraten agiert als der der GPU, zudem sowohl lesende wie auch schreibende Zugriffe unterstützt und weitaus größer ist - der Cache der GPU beschränkt sich typischerweise auf wenige Texel - hat die GPU - deren Hauptzweck das Füllen von zusammenhängenden Speicherregionen mit Bildinformationen ist - gegenüber der CPU große Vorteile im sequentiellen Bereich, wie Abbildung 3.6 zeigt. Abbildung 3.6: Vergleich der Speicher-Zugriffsgeschwindigkeit Eine Anwendung kann also den größten Gewinn durch eine Portierung auf die GPU erwarten, wenn sie um einen sequentiellen Speicherzugriff aufgebaut ist. Der in der Implementierung verwendete Algorithmus trägt dieser Beobachtung Rechenschaft. Die Berechnung der Höhen- und Geschwindigkeits-Werte an den Gittererpunkten erfolgt streng sequentiell, und alle zur Bestimmung der zentralen Differenzenquotienten nötigen lesenden Speicherzugriffe beziehen sich auf unmittelbar benachbarte Texel. 3.3. OPTIMIERUNG VON GPU PROGRAMMEN 3.3.3 29 Arithmetische Intensität Speicherzugriffs-Muster sind nicht der einzige bestimmende Faktor für eine im Vergleich zur CPU schnelle GPU Implementierung. Der Hauptvorteil der GPU liegt in der immensen Rechenleistung. So lange die Anwendung von arithmetischen Operationen dominiert wird, und die Anzahl der Speicherzugriffe (Textur-Lookups) verhältnismäßig gering ist, wird sie auf der GPU schneller laufen als auf einer CPU. Um die Latenz einer Texture-Fetch-Operation z.B. in einem Fragment-Shader-Programm zu verdecken, wird der GPU-interne Scheduler, der für die Verwaltung der Resourcen zuständig ist, zunächst die Bearbeitung eines benachbarten Fragments aufnehmen. Sobald die TexturEinheit die angeforderten Daten vorliegen hat, wird die Arbeit am nächsten Fragment ausgesetzt (man spricht von Suspending), und im ersten Fragment wieder aufgenommen, um dort mit den arithmetischen Operationen fortzufahren (Resuming). Daher ist es auch für gute Programm-Leistung förderlich, wenn nicht alle Textur-Lookups zu Beginn des Shaders ausgeführt werden, sondern sich möglichst gleichmäßig auf den gesamten Code verteilen. Das Verhältnis von arithmetischen zu Speicher-Operationen bezeichnet man als arithmetische Intensität einer Anwendung ( [19]). Die Anzahl der Operationen die eine Anwendung pro aus dem Textur-Speicher gelesenen Wert ausführen sollte, um die Latenz zu verstecken, liegt derzeit zwischen 7 und 10. Der in der Implementierung der Arbeit verwendete Shader, der die näherungsweise Lösung des Gleichungssystems vermittels eines Jacobi-Verfahrens vornimmt, und den Großteil der Rechenzeit des Algorithmus in Anspruch nimmt, besteht aus etwas mehr als 82 AssemblerInstruktionen, von denen acht Textur-Lookups sind. Rechnete man auf diesen Zahlen basierend die arithmetische Intensität der Anwendung aus, erhielte man einen Wert von 7.2. Ein verbreiteter Test (wie er z.B. in [39] verwendet wird), mit dem man einfach überprüfen kann, ob eine GPU-Anwendung compute bound (in der Leistung von der der arithmetischen Einheiten in der Pipeline begrenzt) oder memory bound (durch die Speicher-Bandbreite begrenzt) ist, sieht vor, die Texur-Bandbreite zu halbieren. Im konkreten Fall dieser Arbeit bedeutet das mit 64-Bit-tiefen Texturen zu rechnen, anstatt mit 128-Bit. Die Ungenauigkeit der Lösung spielt für diesen reinen Leistungs-Test keine Rolle. Ändert sich die beobachtete Geschwindigkeit dadurch nicht, ist der Speicherzugriff nicht der Engpass, und die Anwendung ist compute bound. Dieser Test führt bei der hier vorliegenden Implementierung (bei ausgeschalteter Visualisierung) dazu, dass sich die erreichten Frames pro Sekunde mehr als verdreifachen (ein Frame enstpricht einem näherungweise gelösten Zeitschritt). Ganz offensichtlich wird der Code intern noch derart optimiert, dass die arithmetische Intensität wesentlich niedriger liegt, als aus der manuellen Auswertung des Codes ersichtlich. Tatsächlich ergibt eine Analyse der Shaderprogramme mit dem von NVidia bereitgestellten Tool NVShaderPerf [9], das zur Abarbeitung aller Operationen einer Jacobi-Iteration 33 Zyklen benötigt werden, unter der Annahme, dass ein Textur-Lookup mit einem Takt zu Buche schlägt. Da die Anzahl der Textur-Zugriffe nach wie vor bei 10 liegt, ergibt sich eine arithmetische Intensität von nur 2.3. Die Anwendung ist also in der Tat memory bound, was für numerische Simulationen im all- 30 KAPITEL 3. GRAFIKKARTEN-PROGRAMMIERUNG gemeinen aber keine Besonderheit darstellt, sondern die Regel ist, wie z.B. in den Veröffentlichungen von [15, 18] festgestellt. Kapitel 4 Implementierung 4.1 Werkzeuge und Bibliotheken Im folgenden soll nun die Implementierung der Arbeit beschrieben werden. Vorab sei erwähnt, dass die Implementierung in C++ ausschließlich auf Windows Basis erfolgte. Der Support für Grafikkarten-Treiber ist auf dieser Plattform deutlich besser als z.B. unter Linux, was sich primär durch den größeren Markt für Spiele auf diesem Betriebssystem erklären lässt. Dennoch wurden die Entwicklungswerkzeuge und Bibliotheken so gewählt, dass eine Portierung auf Linux und andere Plattformen ohne große Komplikationen möglich ist, da alle entweder als open source Pakete verfügbar oder zumindest frei erhältlich sind. Im einzelnen wurden verwendet: • Eclipse (Version 3.1.0, [5]) • CDT (Version 2.1.1, [4]) • MinGW Minimalis GNU for Windows (Version 3.4.2, [32]) • NVidia SDK (Version 9.5, [37]) • NVidia CG Toolkit (Version 1.3, [8]) • GLEW OpenGL Extension Wrangler Library (Version 1.3.3, [13]) • GLUT for Mingw32 (Version 3.7.3, [6]) • Framebufferobject Class (Version 1.0 [7]) Eclipse ist ein open source Projekt, das sich die Bereitstellung einer flexiblen Entwicklungsumgebung zum Ziel gesetzt hat. Ursprünglich für Java-Anwendungen gedacht, steht mit CDT auch ein Plugin zur Programmierung von C/C++ Applikationen zur Verfügung. Als Compiler wird der GNU Compiler für Windows MinGW verwendet, der eine spezielle Version von GLUT, dem OpenGL Utility Toolkit benötigt. Für die Shader-Programmierung 31 32 KAPITEL 4. IMPLEMENTIERUNG werden das NVidia SDK in Verbindung mit dem NVidia CG Toolkit eingesetzt. Die GLEW-Bibliothek vereinfacht dabei den Umgang mit den verwendeten OpenGL-Extensions, die zum Teil direkt, zum Teil aber auch von der Framebufferobject Klasse benutzt werden. Des weiteren wurde zur Analyse der Shader-Programme das von NVidia bereitgestellte Programm NVShaderPerf [9], und zur optischen Auswertung der erzielten Ergebnisse das VisualisierungsProgramm GMV [1] verwendet. 4.2 Das Rahmenprogramm Wie bereits im Grundlagen-Kapitel dieser Arbeit beschrieben, wird in jedem Zeitschritt ein Gleichungssystem aufgestellt. Dafür müssen zunächst die Funktionen für die Geschwindigkeiten ũ und ṽ sowie für die Höhenwerte h̃ an den Ausgangspunkten auf Basis der gegebenen u,v,h und b-Werte berechnet werden (Schritt 1). Sobald diese vorliegen, kann mit dem iterativen Lösen des Systems begonnen werden (Schritt 2). Mit den so gewonnenen neuen hWerten und den zuvor berechneten Funktionen für die Ausgangspunkte ist es dann möglich, die Geschwindigkeits-Funktionen u und v für den nächsten Zeitschritt zu aktualisieren (Schritt 3). Die für die Visualisierung notwendigen neuen h-Werte liegen bereits nach Schritt 2 vor. Jedoch wird, um unnötig häufiges Umstellen der OpenGL-Projektions-Matrix zu vermeiden (s.u.), die Visualisierung erst nach Schritt 3 vorgenommen. Der Ablauf des Algorithmus lässt sich also wie in Abbildung 4.1 gezeigt darstellen. Abbildung 4.1: Skizze des Programmablaufs 4.2. DAS RAHMENPROGRAMM 4.2.1 33 Initialisierung Der erste Schritt für den eigentlichen Progammablauf besteht im Anlegen und Füllen geeignet dimensionierter Arrays mit den initialen Werten für u, v, h und b. Dabei entspricht der Wert des Arrays an Position j × n + i dem Wert für den Gitterpunkt (i, j), wobei n die Anzahl der Punkte in x-Richtung angibt. Die Werte werden in der beiliegenden Implementierung (Methoden allocateValueArrays, addWater, allocateTextures) prozedural gesetzt, ein Import der initialen Werte aus einer Bitmap oder einem beliebigen Gittergenerator wäre als Erweiterung denkbar. Um das beschriebene System auf der GPU lösen zu können, müssen diese Werte anschließend in geeignete Texturen überführt werden. 4.2.2 Textur-Datenstrukturen Betrachtet man ein Gitter mit n × n Punkten, so muss für jeden dieser Punkte ein Texel für die entsprechenden u, v, h, b, ũ, ṽ, h̃ Werte reserviert werden. Das Anlegen der 128 Bit tiefen RGBA Texturen (32 Bit pro Kanal) erfolgt via OpenGL (Methode allocateTextures). Die Textur wird an dieser Stelle noch nicht mit Farbwerten gefüllt, sondern an ein FramebufferObjekt gebunden. 4.2.3 Das Framebuffer-Objekt Ein Buffer ist in diesem Zusammenhang ein zweidimensionales Array, das Daten pro Pixel speichert. Ein 1024×768 Pixel großer Bildschirm wird dabei auch durch ein Array mit 1024×768 Elementen repräsentiert. Die Bezeichnung Framebuffer ist eine Zusammenfassung vierer verschiedener Sorten von Buffern: • Der Color Buffer, in dem jedem Pixel eine Farbe zugeordnet wird. Er stellt die sichtbare Ausgabe eines (OpenGL-)Programms dar. • Der Depth Buffer, in dem die Tiefeninfomation für jedes durch ein Pixel repräsentiertes Objekt gehalten wird. Er wird z.B. für die Sichtbarkeits- Berechnung von Objekten verwendet. • Der Stencil Buffer, mit dem nicht sichtbare Teile der Ausgabe maskiert werden. • Der Accumulation Buffer, der zur Komposition der Ausgabe-Pixel aus mehreren Pixeln dient. Er wird für z.B. für Antialiasing-, Unschärfe- und Schatten- Effekte verwendet. Genauere Infomationen finden sich z.B. unter [10] und in vielen OpenGL- Tutorials. Framebuffer-Objekte (FBOs) sind eine OpenGL Erweiterung, die es ermöglicht, off-screen in andere als den vom verwendeten windowing system (X, Microsoft Windows, etc...) bereitgestellten Framebuffer (dem sichtbaren on-screen Fenster) zu rendern. Sie ist als Ablösung für die sogenannte PBuffer Extension konzipiert, die dies zwar bereits ermöglichte, aber einige gravierende Nachteile hatte, von denen hier nur die für die Implementierung relevanten aufgezählt werden sollen ( [22]): • Plattformabhängigen Implementierung, die eine protable Entwicklung von Applikationen erschwert, 34 KAPITEL 4. IMPLEMENTIERUNG • jeder PBuffer ist an seinen eigenen OpenGL Kontext gebunden, woraus ”teure” Context Switches beim Wechsel des Rendering-Zieles resultieren, • das Texturformat ist durch das Pixel-Format des PBuffers bestimmt, und • jeder PBuffer hat seine eigenen Depth-, Aux- und Stencil- Buffer. Die wesentlichen Neuerungen des Frambuffer-Objekt-Konzepts liegen neben der plattformübergreifend einheitlich definierten Schnittstelle und dem Entfallen eines Context-Switches beim Wechsel des Rendering-Zieles vor allem in der Art und Weise, wie die verschiedenen Framebuffer attachable images an ein Framebuffer-Objekt „angekoppelt“werden können. Dies wird in Abbildung 4.2 verdeutlicht. Es wird hier unteschieden zwischen Texture- und Renderbuffer-Images, von denen erstere im Anschluss an eine Rendering-Operation in das Framebuffer-Objekt als Textur zur Verfügung stehen. Man spricht bei diesem Vorgang auch von „Render to Texture“ (siehe z.B. [2]). Die Original-Spezifikation kann unter [3] eingesehen werden. Konzeptionell ist ein FBO also nur eine Struktur von Zeigern, nur denen beispielsweise eine Textur als das Ziel einer Rendering-Operation festgelegt wird. Abbildung 4.2: Die Architektur des Framebuffer-Objekts Die Implementierung macht nur von der Möglichkeiten Gebrauch, die das Andocken von Texture-Images mit sich bringen. Der Depth-Buffer wird nicht benötigt. Obwohl eine Verwendung des Stencil-Buffers zum Einsparen von Berechnungen - auf den Teilen des Gebiets, in denen kein Wasser vorhanden ist - denkbar ist, wird dieser Teil der Spezifikation zum Zeitpunkt der Implementierung von den verfügbaren OpenGL-Treibern nicht unterstützt. Wie eingangs erwähnt wird eine Bibliothek für den Umgang mit den Framebuffer-Objekten eingesetzt, die FramebufferObject Class. Diese kapselt dabei folgenden Aufrufe: • glGenFramebuffersEXT Generiert eine Framebuffer ID, an der das Objekt zur späteren Verwendung eindeutig identifiziert werden kann. 4.2. DAS RAHMENPROGRAMM 35 • glBindFramebufferEXT Legt das an seiner ID identifizierte Objekt als aktiven Framebuffer fest. Alle Rendering-Operationen zeichnen nun in diesen Framebuffer. • glFramebufferTexture2DEXT Ordnet einem ColorAttachment eines FramebufferObjektes eine Textur zu. Alle in diesen Framebuffer gerenderten Pixel stehen wie beschrieben danach als Texel der Textur zur Verfügung, und können in späteren RenderingDurchläufen entsprechend verwendet werden. Da auf eine Textur nicht gleichzeitig schreibend und lesend zugegriffen werden kann, und der Jacobi-Löser auf Grundlage der Lösung der letzten Iteration und der zuvor berechneten Departure Points die neue berechnet, werden insgesamt drei Framebuffer-Objekte mit den entsprechenden Texturen benötigt: Zwei Texturen beinhalten die u, v, h und b Werte, die nach dem „Ping Pong“ Prinzip abwechselnd als Quelle für bzw. als Ziel des Rendering-Durchlaufs dienen. Aus der ersten werden jeweils die Werte des letzten Iterationschrittes gelesen, und in den zuvor aktivierten Framebuffer - an den die zweite Textur gebunden ist - komponentenweise die errechneten Werte des aktuellen Iterationsschritts geschrieben. Die dritte Textur dient für die Werte der Departure Points ũ, ṽ, h̃. Der „freie“ Kanal dieser im folgenden als Ausgangs-Punkte-Textur bezeichneten RGBA-Textur wird mit den Höhenwerten h des letzten berechneten Zeitschritts (nicht Iterationsschritts) belegt, da diese für den JacobiLöser zur Verfügung stehen müssen. Nachdem die Framebuffer-Objekte entsprechend initialisiert wurden (Methode allocateTextures), wird OpenGL auf einen an die Größe der Texturen angepassten Viewport (glViewport(0, 0, n, n)) eingestellt. Via glDrawPixels werden die Werte dann in sukzessive in die Texturen „gezeichnet“. Abbildung 4.3 zeigt die Zuordnung der initialen Werte zu den entsprechenden Textur-Kanälen. Von nun an stehen die benötigten Werte also als Texturen, und damit zur Berechnung in den progammierbaren Shadern zur Verfügung. Abbildung 4.3: Aufteilung der Unbekannten auf die einzelnen Kanäle der Texturen 36 KAPITEL 4. IMPLEMENTIERUNG 4.3 Fragment-Shader-Programme Die Angabe eines Profils (siehe Kapitel 3.1) ist neben der Nennung der „Haupt-Methode“ - dem Einstiegspunkt - zum Kompilieren der Cg-Programme zu auf der GPU ausführbarem Assembler-Code notwendig. Für eine vollständige Übersicht der zur Zeit existierenden, von Cg unterstützten Profile verweise ich auf [8]. Die Auswahl des Profils und das Laden und Kompilieren der Programme geschieht in der beiliegenden Implementierung in der Methode initShader. Das in dieser Arbeit verwendete Profil für den Fragment-Shader ist CG_PROFILE_FP40. Obwohl der von CG_PROFILE_FP30 unterstützte Befehlsumfang vollkommen ausreichend wäre, hat ein früher Test auf einer GForce 6800GT einen (wenn auch sehr geringen) Geschwindigkeitsgewinn durch Verwenden des CG_PROFILE_FP40 ergeben. Tabelle 4.1 zeigt die erreichten Frames pro Sekunde (ein Frame entspricht dabei der Berechnung der Ausgangspunkte, 15 Jacobi-Iterationen und anschließende Aktualisierung der Geschwindigkeiten) bei ausgeschalteter Visualisierung, jeweils gemittelt über 60 Sekunden Simulationslaufzeit. Gittergröße 128×128 256×256 512×512 CG_PROFILE_FP30 67,89 23,94 6,19 CG_PROFILE_FP40 67,73 24,22 6,64 Tabelle 4.1: Frames pro Sekunde in Abhängigkeit vom verwendeten Profil Dieser Schluss ist wohlgemerkt nicht zwangsläufigerweise für alle Grafikkarten und Treiberversionen zulässig. Die Ergebnisse zeigen aber, dass auch die allgemein vertretene Annahme, das „niedrigste“ mögliche Profil berge die größten Vorteile, ihre Ausnahmen hat. Generell sollten Applikationen also die verfügbaren Profile auf ihre Leistungsfähigkeit hin testen, um das für den konkreten Fall optimale zu ermitteln. Ein Schritt der Berechnung - sei es nun die Berechnung der Ausgangspunkte, eine JacobiIteration oder das Geschwindigkeits-Update - läuft nun im wesentlichen immer nach dem gleichen Muster ab: • Der OpenGL-Viewport wird auf eine an die Größe des Gitters angepasste Dimension und orthogonalen Projektionsmodus eingestellt. • Das ausgewählte Cg Profil wird aktiviert. • Das entsprechende, zuvor geladene Shader-Programm wird aktiviert. • Die benötigten Texturen und andere Parametern werden an das Shader-Programm gebunden. Letztere variieren von Fall zu Fall, und werden in den folgenden Abschnitten beschrieben. • Das Ziel-Framebuffer-Objekt wird aktiviert. 4.3. FRAGMENT-SHADER-PROGRAMME 37 • Der komplette Viewport wird ausgefüllt. Dafür gibt es zwei Möglichkeiten: 1. Die Ränder des Viewports werden mit einzelnen Linien gezeichnet. Der verbleibende innere Teil wird mit einem Quad überdeckt. 2. Es wird ein einzelnes, „viewport-großes“Quad gezeichnet. • Deaktivieren der Textur- und anderen Parameter und des Shader-Programms. Beim Ausfüllen des Viewports ist darauf zu achten, dass zuvor der Polygon- Modus in OpenGL auf GL_FILL gesetzt wurde. Das separate Zeichnen der Ränder ermöglicht die Verwendung spezieller Shader-Programme für die Berechnung der Randwerte. Verzichtet man darauf, so muss in den Shaderprogrammen für das ganze Gebiet via if-Klauseln der Rand „nachbearbeitet“ werden. Erstere Art der Randbehandlung wird vielfach empfohlen (siehe z.B. [49], [24]). Die Fragment-Shader-Units der GPU arbeiten parallel auf mehrere Pixeln gleichzeitig, für die jeweils nur ein Branch des Codes ausgeführt werden kann. Ist dies nicht möglich, fällt die GPU zurück auf die sogenannte branch predication Strategie, in der beide Branches ausgeführt werden, aber nur einer wirklich eine Änderung im Ergebnis verursacht. Während der Implementierung wurden beide Ansätze getestet, es konnte aber kein signifikanter Geschwindigkeits-Gewinn durch die Verwendung separater Shader-Programme für die Ränder festgestellt werden. Dies lässt sich dadurch erklären, dass die Anwendung ohnehin memory bound ist (siehe Kapitel 3.1.2, 3.1.3), und genügend Kapazität für die relativ wenigen konditionalen zusätzlichen Instruktionen (von einer im Programm für die Jacobi-Iterationen (das von allen am häufigsten ausgeführt wird), bis zu maximal drei im Programm für das Geschwindigkeits-Update) zur Verfügung steht. Die separate Randbehandlung ist in der beiliegenden Implementierung standardmäßig deaktiviert. Sie lässt sich aber per KommandozeilenParameter einschalten (siehe Kapitel 4.5). Ein wichtiger Parameter, der automatisch im Fragment-Shader im Register WPOS zur Verfügung steht und von dort in den Programmen ausgelesen werden kann, ist die Position des Fragments (Pixels) im Rendering-Ziel. Aufgrund der Wahl des Viewports als an die TexturGröße angepasstes Quadrat, und dem orthogonalen Projektionsmodus gibt dieses Zweitupel aus float-Werten (Datentyp float2) stets den Mittelpunkt des Fragments an, beginnend bei (0.5, 0.5) für den linken oberen, bis zu (n − 0.5, n − 0.5) für den rechten unteren Pixel. Der Index des dem Fragment logisch zugeordneten Gitterpunktes lässt sich also errechnen, in dem man jeweils 0.5 von den Richtungskomponenten subtrahiert. Um mittels Textur-Lookup die dem Gitterpunkt zugeordneten Werte zu gelangen, wird die Textur-Koordinate in der jeweiligen Dimension als Quotient aus Gitter-Index und der Anzahl der Gitterpunkte (n−1), die in allen Programmen als Parameter übergeben werden muss, gebildet. Dadurch werden die Textur-Koordinaten auf das Interval von (0, 0) für den linken oberen Texel bis (1, 1) für den rechten unteren Texel begrenzt. Ein tex2D-Aufruf an diesen Koordinaten (siehe [8]) liefert die in den Texturen gespeicherten Funktionswerte als Viertupel aus float-Werten (Datentyp float4). Weitere notwendige Parameter, die in allem Shader-Programmen benötigt werden, sind: • Die Zeitschritt-Größe ∆t, • die horizontale Gitter-Weite ∆x und 38 KAPITEL 4. IMPLEMENTIERUNG • die vertikale Gitter-Weiter ∆y. Diese werden von OpenGL via cgGetNamedParameter namentlich identifiziert und über cgSetParameter1f als float-Werte an die Programme übergeben (siehe [8]). 4.3.1 Berechnung der Departure Points Das Shader-Programm zur Berechnung der Ausgangs-Punkte finden sich in der beiliegenden Implementierung in der Dateien frag_departurecalc.cg, sowie im Anhang in Abschnitt A.1.1. Zur Berechnung werden die Werte der Lösung des letzten Zeitschritts benötigt. Diese liegen wie zuvor beschrieben als Textur vor. Nach Berechnung der Textur-Koordinaten und Auslesen der u-, v- und h-Werte für den Gitterpunkt wird die Verschiebung des an dieser Position (i, j) befindlichen Partikels seit dem letzten Zeitschritt durch ∆t · uni,j · ∆x approximiert. Durch Subtraktion selbiger von der aktuellen Position im Gitter erhält man eine Approximation für den Ausgangspunkt. Durch Verwendung von floor- und ceil-Funktionen können die nächst-kleineren bzw. nächst-größeren ganzzahligen Gitterindizes ermittelt, und die zugehörigen Texturkoordinaten bestimmt werden. Die Bestimmung der ũ-, ṽ- und h̃-Werte erfolgt dann analog zum in Kapitel 2 beschriebenen Verfahren über bilineare Interpolation aus den dort ausgelesenen Werten. Die von Cg bereitgestellte Funktion lerp zur eindimensionalen linearen Interpolation wird dafür zunächst für die oberen und unteren Funktionswerte ausgeführt, und anschließend erneut für die Ergebnisse dieser beiden Interpolationen. Die ũ-, ṽ- und h̃-Werte werden zusammen mit dem h-Wert aus der Eingabe-Textur wie in Abbildung 4.3 gezeigt in die Ausgabe geschrieben. Randbehandlung Betrachtet man einen Partikel des Fluids, der zum Zeitpunkt dieser Berechnung auf dem Rand liegt, so muss er sich, da er in dem zugrundeliegenden Modell nicht von aussen „eingedrungen“ sein kann, zuvor an einer der beiden folgenden Positionen befunden haben: 1. Er hat im vorherigen Zeitschritt bereits auf dem Rand gelegen (die orthogonal zum Rand verlaufende Geschwindigkeitskomponente ist gleich Null). 2. Er hat im Inneren des Gebietes gelegen. Im ersten Fall ist der nächstkleinere ganzzahlige Gitterindex gleich dem nächstgrößeren, nämlich genau der des Randpunktes. Bei der linearen Interpolation wird also in dieser Richtung im Endeffekt gar nicht interpoliert, sondern direkt der Randwert gewählt. Dies ist für die ũ-, ṽund h̃-Werte korrekt. Im zweiten Fall sind die kleineren und größeren ganzzahligen Indizes verschieden, liegen aber alle im Inneren des Gebietes, wodurch die Interpolation das gewünschte Ergebnis liefert. Eine gesonderte Randbehandlung kann in diesem Schritt also entfallen. 4.3.2 Jacobi-Iteration Die Shader-Programme zur Berechnung der Lösung eines Schrittes des Jacobi-Verfahrens finden sich in der beiliegenden Implementierung in den Dateien frag_jacobi.cg, bzw. 4.3. FRAGMENT-SHADER-PROGRAMME 39 frag_jacobi_left.cg, frag_jacobi_right.cg, frag_jacobi_low.cg und frag_jacobi_up.cg. Das Programm zur Berechnung der Werte für die inneren Gitterpunkte findet sich zusätzlich im Anhang im Abschnitt A.1.2. Zusätzlich zu ∆t, ∆x und ∆y wird der Parameter g für die Gravitations-Konstante benötigt. Da die Ableitungen wie in Gleichung 2.54 durch zentrale Differenzenquotienten ersetzt wurden, werden zunächst ausgehend von den durch die Fenster-Position gegebenen Textur-Koordinaten 1 des aktuellen Fragments durch Subtraktion bzw. Addition von n−1 auf die x- bzw. y-Komponente die Koordinaten für die direkt benachbarten Texel bestimmt. tex2D-Lookups für diese Koordinaten in die Textur für die Ausgangs-Punkte, sowie die Textur, die die Ergebnisse der letzten Iteration enthält, liefern alle Funktionswerte, die für die Berechnung des h-Wertes der neuen Iteration notwendig sind. Dieser Wert ist auch der einzige, der von diesem Shader-Programm verändert wird. Die u- und v-Werte werden erst im Anschluss an alle Iterationen aktualisiert, und der b-Wert ist ohnehin zeitlich konstant. Im Anschluss an eine Jacobi-Iteration wird im Rahmenprogramm ein Wechsel der Eingabeund Ziel-Framebuffer-Objekte vorgenommen. Somit stehen die h-Werte einer Iteration in der darauffolgenden als Eingabe zur Verfügung. Randbehandlung Vor der eigentlichen Berechnung werden in diesem Schritt die senkrechten Komponenten der Geschwindigkeiten an den Randknoten auf Null gesetzt (ui,j = 0 für i = 0, n − 1, vi,j = 0 für j = 0, n − 1). In den konkaven Ecken des Beckens sowohl u als auch v. Diese Art der Randbehandlung wird als nicht reflektiv bezeichnet, und modelliert das Verhalten der Wellen wie z.B. in einem festen Becken ( [29]). Obwohl diese Randbehandlung sich intuitiv für die Aktualisierung der Geschwindigkeiten im nächsten Schritt anbietet, muss sie doch im Shader-Programm für die Jacobi-Iterationen vorgenommen werden. Andernfalls würde bei der Berechnung der Ausgangspunkte für die Randwerte im darauffolgenden Zeitschritt fälschlicherweise nicht mehr aus den Werten einer inneren Zelle interpoliert, sondern direkt die Randwerte gewählt werden, was das Ergebnis in der Visualisierung deutlich verfälscht. Ferner verfügen die Gitterpunkte auf dem Rand des Gebiets naturgemäß über keinen Nachbarn, der direkt ausserhalb neben dem Rand liegt. Zur Bestimmung der zentralen Differenzenquotienten muss ein linker und rechter, bzw. ein oberer und unterer Nachbar gegeben sein. Sie müssen also durch einseitige Differenzenquotienten ersetzt werden. Betrachtet man den linken Rand, so wird z.B. der Term für die erste Ableitung der Funktion u, u −u u −u also 1,j2∆x−1,j , in dem u−1,j nicht definiert ist, ersetzt durch 1,j∆x 0,j . Um die zweite Ableitung für die Funktion hn+1 am linken Rand, also hn+1 2,j n+1 n+1 hn+1 −1,j −2h0,j +h1,j 2 ∆x n+1 n+1 hn+1 0,j −2h1,j +h2,j . ∆x2 zu ersetzen, wird zusätzlich noch der Wert benötigt: Gleichermaßen verfährt man für die Ableitungen der anderen Funktionen. Dies ist in den speziellen Shader-Programmen für den Rand (frag_jacobi_left.cg, frag_jacobi_up.cg, etc.) entsprechend implementiert, führt aber zu einer unruhigen, „gekräuselten“ Visualisierung nahe der Ränder, sowie einem nur sehr schwach bewegten Rand. Dies liegt an der nicht ausreichenden Genauigkeit erster Ordnung dieser einseitigen Ableitungen. Ein optisch ansprechenderes Ergebnis lässt sich erzielen, wenn man das „Ersetzen“ der Grafik- 40 KAPITEL 4. IMPLEMENTIERUNG karte überlässt: Erlaubte Textur-Koordinaten liegen im Bereich [0.0, 1.0]2 . Da die Texturen bei der Erzeugung auf Clamp-Modus eingestellt wurden, wird ein Wert kleiner Null zu Null, und ein Wert größer Eins zu Eins. Also liefert ein Textur-Lookup für einen Gitterpunkt neben dem Rand stets die Werte für den Randpunkt zurück. u −u Damit ergibt sich analog zum obigen Beispiel gewissermaßen automatisch 1,j2∆x0,j für die er−u +u1,j ste, und 0,j für die zweite Ableitung der Funktion u. ∆x2 Wenn auch vom numerischen Standpunkt aus betrachtet sehr ungenau, und auch nur mit einer Genauigkeit von erster Ordnung abschätzbar, so ist das Ergebnis doch hinreichend gut für eine Echtzeit-Visualisierung, wie sie hier angestrebt wird. 4.3.3 Aktualisierung des Geschwindigkeitsfeldes Das Shader-Programm zur Berechnung der Geschwindigkeitskomponenten u und v aus der Lösung des aktuellen Zeitschrittes finden sich in der beiliegenden Implementierung in der Dateien frag_velocityupdate.cg. In Abschnitt A.1.3 des Anhangs ist er ausführlich erklärt. Basierend auf den Werten der letzten Jacobi-Iteration (und damit der näherungsweisen Lösung des Systems), sowie den in Schritt 1 berechneten Werten für die Ausgangspunkte werden hier die Geschwindigkeiten u und v in der Werte-Textur durch die neu zu bestimmenden ersetzt (Gleichung 2.45). Analog zum Vorgehen in den Jacobi-Schritten werden dazu die Koordinaten der links, rechts, oben und unten angrenzenden Texel ermittelt, und die Werte aus den Texturen ausgelesen, und anhand von Gleichung 2.45 die neuen u- und v-Werte ermittelt. Randbehandlung Eine Randbehandlung wird in diesem Schritt genau wie im Jacobi-Schritt dadurch notwendig, dass die Randpunkte keinen direkten Nachbarn ausserhalb des Gebietes aufweisen. Entsprechend werden auch hier die einseitigen Differenzenquotienten entweder durch durch Clampen am Textur-Rand implizit umgesetzt, wobei darauf geachtet wird, dass die Terme 2∆x und 2∆y durch ∆x bzw. ∆y ersetzt werden. 4.4 Visualisierung Während bei einer vergleichbaren CPU-Implementierung das berechnete Höhenfeld im Hauptspeicher liegt, und von dort mit den üblichen OpenGL-Befehlen dargestellt werden kann, liegen die Daten nach dem Lösen durch die GPU im Texturspeicher der Grafikkarte. Wie in Kapitel 3.2.1 erläutert, ist ein Rücktransfer vom Grafikkarten-Speicher zum Hauptspeicher zwar machbar, aber da er nach jedem Zeitschritt - also mehrmals pro Sekunde - ausgeführt werden muss, zu teuer. Es musste eine Möglichkeit gefunden werden, die zur Visualisierung benötigten Knoten an ihren neuen Positionen zu zeichnen, ohne deren z-Komponente beim Aufruf der glVertex3f- Befehle zu kennen. Zum jetzigen Zeitpunkt stehen dafür zwei Techniken zur Auswahl, nämlich VBOs/PBOs, und der Zugriff auf Texturen im Vertex-Shader durch sogenannte Vertex Texture Fetches. 4.4. VISUALISIERUNG 4.4.1 41 VBOs/PBOs Die Abkürzungen VBOs und PBOs stehen für Vertex Buffer Objects bzw. Pixel Buffer Objects, eine OpenGL Erweiterung, die im Februar 2003 vom ARB (OpenGL Architecture Review Board, [11]) freigegeben wurde ( [45, 46]). Sie schließt eine Lücke in der Rendering-Pipeline, in dem sie es erstmalig ermöglicht, durch Fragment-Shader erzeugte Daten im Vertex-Shader wiederzuverwenden, ohne dass die GPU-CPU-Schwelle überwunden werden muss. VBOs sind als Erweiterung zu den schon seit OpenGL Version 1.1 bekannten Vertex Arrays zu verstehen, die vor allem auf die Reduktion von Funktionsaufrufen und der Überrepräsentation von Objekten bei der Verwendung „normaler“ OpenGL-Zeichenfunktionen abzielten. Betrachtet man einen Würfel mit sechs Seiten und acht (gemeinsamen) Punkten, so wird beim Zeichnen dieses Würfels über eine Folge von sechs Quads jeder Punkt drei mal verarbeitet (über einen glVertex-Aufruf). Es werden also insgesamt 24 Koordinaten-Tupel vom Client (dem PC, mit Hauptspeicher, AGP-/PCI-Bus und CPU) zum Server (die GPU mit Video-Speicher) gesendet. Bei der Verwendung zusätzlicher Vertex- Attribute wie TexturKoordinaten, Normalen oder Farb-Werten wird dieser Transfer noch weiter erhöht. Bei Vertex Arrays hingegen wird zunächst ein Bereich im Server-Speicher mit den Attributen (Koordinaten, Farb-Werte, Normalen, Textur-Koordinaten) für die im zu zeichnenden Objekt gemeinsam verwendeten Knoten gefüllt. Der Server wird dann durch glEnableClientStateAufrufe auf die Verwendung dieser Daten vorbereitet, die ihm im Anschluss z.B. durch Zeiger auf die entsprechenden Speicher-Bereiche über die Funktionen glVertexPointer, glColorPointer, etc. zugänglich gemacht werden. Die eigentliche Zeichenarbeit erledigt im Anschluss ein Aufruf von glDrawElements, der neben Parametern wie z.B. für die Art der zu zeichnenden Primitive (GL_POINTS, GL_LINES, GL_QUADS, etc.) ein Zeiger auf einen mit Indices gefüllten Speicherbereich übergeben wird, welche die Reihenfolge der Verwendung der zuvor spezifizierten Knoten festlegen. Eine MehrfachÜbertragung der Attribute für den selben Knoten entfällt also. Der Performance-Gewinn dieser Technik lässt sich mit dem durch die Verwendung von DisplayListen vergleichen. Deren großer Nachteil - die fehlende Möglichkeit, den Inhalt einer einmal erzeugten Display-Liste zu ändern - entfällt dabei. Nichts desto trotz müssen die Vertex-Attribute auch bei der Verwendung von Vertex-Arrays für jedes Rendern des darzustellenden Objektes erneut vom Client zum Server übertragen werden, sofern sie sich geändert haben. An diesem Punkt setzt die VBO/PBO-Architektur an. Der Inhalt eines Framebuffers (im speziellen auch eines Framebuffer-Objektes) kann via glReadPixels in ein Pixelbuffer-Objekt übertragen werden. Wohin die Daten bei disem „Kopiervorgang“ gelangen, ist Sache des Treibers, lässt sich aber durch die Verwendung des usage flag-Parameters bei der Puffer-Initialisierung beeinflussen. Dieser Parameter ist aus mehreren Teilen zusammengesetzt, Tabelle 4.2 beschreibt die Teile des Parameters. Die Verwendung des STREAM_DRAW_ARB-Parameters bietet sich für die Implementierung an. Die Koordinaten und Normalen der Vertices ändern sich vor jedem Visualisierungs-Schritt, und werden auch nur einmal zur Anzeige gebracht. Der glReadPixel-Aufruf kopiert die Daten also aus dem Speicher der Grafikkarte in den Speicher der Grafikkarte, wodurch der teure Transfer zum Client-Speicher entfällt. Es ist denkbar, dass kein Kopieren im eigentlichen Sinne vorgenommen wird, sondern der Treiber intern diesen Vorgang durch das Setzen von Zeigern auf den betroffenen Bereich optimiert, dies ist aber nicht Teil der Spezifikation. 42 KAPITEL 4. IMPLEMENTIERUNG Name des Parameters STATIC_... DYNAMIC_... STREAM_... ..._READ_ARB ..._DRAW_ARB ..._COPY_ARB Definition „1 zu n“ Beziehung zwischen Erzeugung und Verwendung. „n zu n“ Beziehung zwischen Erzeugung und Verwendung. „1 zu 1“ Beziehung zwischen Erzeugung und Verwendung. Der Client verwendet die Daten. AGP oder System-Speicher bevorzugt. Der Server verwendet die Daten. Schneller Video-Speicher bevorzugt. Beide Seiten verwenden die Daten. Tabelle 4.2: glBufferDataARB-Parameter Um die im PBO gehaltenen Daten im Anschluss verwenden zu können, wird durch einen erneuten Aufruf von glBindBufferARB - diesmal in Verbindung mit dem GL_ARRAY_BUFFER_ARBParameter - der Speicherbereich mit den Vertex-Koordinaten neu „gebunden“, und über einen glVertexPointer-Aufruf dem Server zur Visualisierung übergeben. Die Koordinaten jedes Knotens des Gitters müssen also in einem separaten Fragment-ShaderDurchlauf für jeden Visualisierungsschritt neu berechnet werden, indem die RGB-Werte einer eigens dafür erzeugten, an ein Framebuffer-Object gebundenen Textur mit den x, y und z Koordinaten gefüllt werden. Per glReadPixels werden die Koordinaten wie beschrieben in ein PBO kopiert, und im Anschluss (als VBO neu gebunden) dem Server zur Anzeige übergeben. Das Shader-Programm für diesen Schritt findet sich in der beiliegenden Implementierung in der Datei frag_coordinates.cg, und wird im Anhang in Abschnitt A.1.4 genauer erläutert. Um eine realistische Darstellung der Wasseroberfläche zu ermöglichen, werden neben den Vertex-Koordinaten auch die Normalen der Oberfläche benötigt, anhand derer später eine LichtReflektion bzw. Brechnung berechnet werden kann. Dazu wird analog zur Berechnung der Koordinaten in einem weiteren zusätzlichen Fragment- Shader-Durchlauf eine Vertex-Normale für alle Knoten berechnet. Abbildung 4.4: Berechnung der Vertex-Normalen 4.4. VISUALISIERUNG 43 Aus den in Abbildung 4.4 rot dargestellten Kanten-Vektoren wird durch Bildung des Kreuzproduktes die in Grün gezeichnete Normale des entsprechenden Flächenstücks gebildet. Durch Mittelwert-Bildung erhält man die Normale für den Knoten (blau). Implementiert ist dieses Vorgehen in der beigelegten Datei frag_normals.cg, eine genaue Erklärung der verwendeten Befehle findet sich in Abschnitt A.1.5 im Anhang. 4.4.2 Vertex Texture Fetch Mit der vorletzen Generation von Grafikkarten (GForce 6-Serie) wurde den Entwicklern die Möglichkeit gegeben, nicht nur im Fragment-Shader auf Texturen zuzugreifen, sondern auch im Vertex-Shader. Diese als Vertex Texture Fetch bezeichnete Technik lässt sich in der Implementierung dieser Arbeit hervorragend für ein dynamisches Displacement-Mapping der für die Visualisierung der Wasseroberfläche verwendeten Knoten ausnutzen. Die Wasseroberfläche wird dafür zunächst als flaches Gitter mit beliebiger, konstanter Höhe gerendert. Den Knoten werden dabei Textur-Koordinaten zugeordnet, anhand derer die nach Abschluss der Berechnung als Textur im Grafikkarten-Speicher vorliegen Simulationsergebnisse - insbesondere die neuen Höhenwerte - identifiziert werden können. Ein Vertex-ShaderProgramm kann nun auf die den Knoten zugeordneten Textur-Werte zugreifen. Gesonderte Fragment-Shader-Durchläufe für die Berechnung der Koordinaten bzw. der Vertex-Normalen können also entfallen, da dies im Vertex-Shader implementiert ist. 4.4.3 Reflektion Ist man nur an einer Visualisierung der Höhenwerte interessiert, so könnte diese bereits nach dem Lösen des Systems erfolgen (Abb. 4.1), da das neue Geschwindigkeits-Feld dafür keine Rolle spielt. Liegen diese Werte zum Zeitpunkt der Visualisierung bereits vor, wird z.B. ein Einfärben der gerenderten Wasserfläche abhängig von den vorherrschenden Geschwidigkeiten möglich. Im folgenden wird jedoch erklärt, wie sich der Farbwert eines Pixels der Wasseroberfläche auf eine möglichst naturgetreue Art und Weise bestimmen lässt. Dazu muss zunächst der Einfluss des an der Oberfläche reflektierten Lichts der Umgebung berücksichtigt werden. Nach dem normalen Reflexionsgesetz gilt: „Einfallswinkel gleich Ausfallswinkel“. Fällt ein Strahl I im Winkel ΘI auf eine Oberfläche, deren Ausrichtung durch eine Normale N bestimmt ist, so muss der Winkel zwischen dem reflektierten Strahl R und der Normalen ΘR gleich ΘI sein. Abbildung 4.5 veranschaulicht diesen Zusammenhang. Zur Bestimmung von I im Vertex-Shader wird neben dem Ortsvektor des Oberflächenpunktes P (der aktuelle Vertex) auch der Ortsvektor V des Augenpunktes benötigt. Dieser wird als uniformer Parameter an das Programm übergeben. I ergibt sich damit als Differenz aus P und V. Die Berechnung des Reflektionsvektors für I und der zuvor ermittelten normierten VertexNormale N erledigt die Cg-eigene Funktion reflect, die als Parameter eben diese beiden Vektoren erhält. Der Reflektionsvektor für jeden Vertex wird in die Ausgabe des VertexShaders geschrieben, und vom Rasterisierer für jeden Pixel zwischen den Knoten interpoliert. Dort wird dieser Vektor zum Lookup in eine sogenannte Environment Map verwendet, die die Farbinformationen der Umgebung enthält. 44 KAPITEL 4. IMPLEMENTIERUNG Abbildung 4.5: Lichtreflektion an einer Oberfläche Cube Maps Alle gängigen neuen Grafikkarten unterstützen sogenannte Cube Map-Texturen, eine OpenGLExtension die bereits im Jahr 1999 verabschiedet wurde [44]. Eine Cube Map besteht aus sechs quadratischen Bildern, die wie die Oberfläche eines Würfels zu einander passen. Zusammengenommen ergibt sich dadurch ein omnidirektionales Bild, das zur Repräsentation der weit entfernten Umgebung einer Szene oder eines Objektes, der Environment Map, benutzt wird. Abbildung 4.6 zeigt ein Beispiel einer solchen Cube Map. Abbildung 4.6: Eine Cube Map 4.4. VISUALISIERUNG 45 Beim Erzeugen der Cube Map werden die einzelnen Seiten des Würfels anhand ihrer Lage im Raum identifiziert: GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT bezeichnet die rechte Seite des Würfels, GL_TEXTURE_CUBE_MAP_POSITIVE_Y_EXT die hintere, etc. Die einzelnen Bilder werden dabei wie ganz normale 2D-Texturen geladen. Weiterführende Erklärungen zu Environment Maps, Cubemaps und der Verwendung dieser in CG finden sich in vielen Tutorials im Internet (siehe z.B. [25]). 4.4.4 Refraktion Der zweite wichtige Einfluss auf die Farbe eines Pixels der Wasseroberfläche ist der durch Refraktion oder Lichtbrechung beigetragene Anteil der Farbe des Untergrundes, über dem sich die Wasserfläche befindet. Das Snelliussches Brechungsgesetz (benannt nach dem niederländischen Mathematiker Willebrord van Roijen Snell, 1580-1626) beschreibt die Brechung eines Lichtstrahls beliebiger Wellenlänge beim Übergang von einem transparenten Medium in ein anderes mit anderem Brechungsindex. Der Brechnungsindex einiger Elemente ist in Tabelle 4.3 aufgeführt. Das SnelliMaterial Luft Wasser Diamant Glas Brechungsindex 1.0003 1.3333 2.4 1.5 Tabelle 4.3: Brechungsindizes einiger Elemente ussches Gesetz muss nicht eigens implementiert werden, sondern ist im Cg Funktionsumfang in der Funktion refract enthalten. Als Parameter werden analog zur Reflektion der Vektor des einfallenden Strahls I, die Oberflächennormale N sowie das Verhältnis η1 /η2 der Brechungsindizes der beiden Medien. Beim Übergang des Strahls von Luft zu Wasser liegt dieser also bei 1.0003 1.3333 . Abbildung 4.7 veranschaulicht den Zusammenhang. Der berechnete Vektor des gebrochenen Lichtstrahls wird im Anschluss wieder in die Ausgabe des Vertex-Shaders geschrieben, und für den Fragment-Shader zwischen den benachbarten Knoten interpoliert. Dort wird er zum Lookup des durch Brechung beigetragenen Farbwertes in die zuvor beschriebene Environment Map verwendet. Nachdem im Fragment-Shader für jeden Pixel die Farbwerte für das gebrochene und reflektierte Licht vorliegen, muss zwischen diesen geeignet interpoliert werden, um ein optisch ansprechendes Ergebnis zu erzielen. Betrachtet man in der Natur eine Wasseroberfläche, so kann man den Boden nur dann sehen (also den durch Brechung, beigetragenen Teil des Lichts) wenn man fast senkrecht von oben darauf blickt. Bei flacherem Winkel hingegen wird sehr viel mehr Licht an der Oberfläche reflektiert. Dieses Phänomen wird als Fresnel-Effekt bezeichnet, der an dieser Stelle aufgrund der hohen Komplexität nicht im Detail beschrieben werden kann. Im folgenden sei jedoch eine schnelle, häufig in graphischen Anwendungen verwendete empirische Annäherung beschrieben. 46 KAPITEL 4. IMPLEMENTIERUNG Abbildung 4.7: Lichtbrechung an einer Oberfläche Fresnel-Term Der reflektierte Anteil am Farbwert eines Oberflächenpunktes cref lässt sich anhand von Gleichung 4.1 bestimmen: cref = max(0, min(1, bias + scale × (1 + I · N )power )). (4.1) Dabei bezeichnet I den einfallenden Strahl und N die Oberflächennormale auf Auftreffpunkt. Falls I annähernd mit N übereinstimmt, ergibt sich für den Reflexionskoeffizient ein Wert nahe an null. Weichen nun I und N zunehmend voneinander ab, steigt der Koeffizient schnell und stetig an, bis zu einem Punkt an dem Totalreflexion (cref = 1) erreicht ist. Die Werte, die den Parametern bias, scale und power gegeben werden, sind letztendlich geschmackssache, und können im beiliegenden Shader-Quelltext geändert werden. Der endgültige Farbwert des Pixels lässt sich anhand von Gleichung 4.2 ermitteln. color = cref × ref lectedColor + (1 − cref ) × ref ractedColor (4.2) Dieser Wert wird in der Implementierung für jeden Vertex bestimmt, und in die Ausgabe des Vertex-Shaders geschrieben. Der Rasterisierer der GPU übernimmt eine Interpolation für die zwischen benachbarten Vertices liegenden Pixel. Nach diesen im Vertex-Shader verrichteten Vorarbeiten wird im für die WasseroberflächenVisualisierung gebundenen Fragment-Shader-Programm nur noch der Environment-Map-Lookup entlang der berechneten Reflexions- und Refraktions-Vektoren vorgenommen, und das Ergebnis anhand des Fresnel-Koeffizienten gemischt. Zur Darstellung von mitschwimmenden Objekte (Bojen, etc.) müssen die u- und v-Werte für 4.5. BEDIENUNG 47 deren letzte Position ausgelesen und interpoliert, und die Verschiebung für den neuen Zeitschritt berechnet werden. Prototypisch realisiert ist dies in den Methoden drawParticles und updateParticlePositions. Die Objekte haben dabei keinen Einfluss auf Bewegung des Wassers. Die für das Rendern des Wassers eingesetzten Shader-Programme sind in der Implementierung in den Dateien vert_reflect_refract.cg und frag_surface.cg (VBO/PBO-Modus), bzw. in vert_displacement.cg (VTF-Modus) enthalten. Eine genauere Erklärung findet sich im Anhang (Abschnitte A.1.6, A.1.7). 4.5 Bedienung Die Parametrisierung des Programms erfolgt beim Aufruf über die Kommandozeile. Die folgende Liste beschreibt die frei wählbaren Parameter und deren Verwendung. Alle Parameter sind optional. Fehlen sie, wird auf eine Default-Einstellung zurückgegriffen. • -n # # steht für die Anzahl der Gitterpunkte, mit denen das Gebiet in x- und y-Richtung diskretisiert wird (Default: 256). • -iterations # # gibt die Anzal der Jacobi-Iterationen pro Zeitschritt an (Default: 10). • -timestep # # steht für die Zeitschrittweite ∆t (Default: 0.05). • -sidelength # # bezeichnet die Seitenlänge des Beckens (Defalut: 10). Aus dieser Angabe wird in Verbindung mit der Anzahl der Knoten ∆x und ∆y bestimmt. • -width # # ist gleich der Breite des Ausgabefensters in Pixel (Default: 800). • -height # # steht für die Fensterhöhe in Pixel (Default: 600). • -maxtime # # gibt die maximale Simulationslaufzeit in Sekunden an (Default: unendlich). • -vtf Ist diese Option angegeben, wird die VTF-Methode der Visualisierung verwendet (Default ist Visualisierung mittels VBO/PBO). • -volumeCalcTime # # gibt den Zeitpunkt an (bis auf fünf Nachkommastellen genau), an dem eine Volumenberechnung erfolgen soll. Die Ausgabe des Ergebnisses erfolgt in die Konsole. Dieser Parameter kann mehrmals angegeben werden. • -exportTime # # gibt den Zeitpunkt an (bis auf fünf Nachkommastellen genau),an dem das Ergebnis der Berechnungen in eine GMV-Datei exportiert werden soll. Ausgabename der Datei ist #.gmv, wobei # für den gewählten Zeitpunkt steht. Dieser Parameter kann mehrmals verwendet werden. • -border Durch setzen dieses Flags wird die Berechnung der Randwerte in separaten Shader-Programmen aktiviert (Default: aus). • -waterlevel # # gibt die Höhe der Wasseroberfläche h über dem gesamten Bereich an (Default: 2.00). 48 KAPITEL 4. IMPLEMENTIERUNG • -bottomlevel # # gibt die Höhe Höhe des Bodens b über dem gesamten Bereich an (Default: 1.0). • -block x,y,w,h,z fügt einen „Block“ im Becken an der angegebenen Stelle ein. ’x’ bezeichnet den Index des Gitters in x-Richtung, ’y’ den Index in y-Richtung, ’w’ steht für die Ausdehnung in Gitterpunkten in x-Richtung, ’h’ für die Ausdehnung in yRichtung, ’z’ gibt die Höhe an. Dieser Parameter kann mehrmals verwendet werden, eine „sinnvolle“ und zulässige Platzierung wird nicht überprüft. • -peak x,y,w,h,z Fügt eine „Wassersäule“ im Becken an der angegebenen Stelle ein. ’x’ bezeichnet den Index des Gitters in x-Richtung, ’y’ den Index in y-Richtung, ’w’ steht für die Ausdehnung in Gitterpunkten in x-Richtung, ’h’ für die Ausdehnung in y-Richtung, ’z’ gibt die Höhe an. Dieser Parameter kann mehrmals verwendet werden, bei Fehlen des Parameters wird eine einzelne Wassersäule in der Beckenmitte plaziert (analog zu der in Kapitel 5.3 beschriebenen. Eine Validierung der gewählten Werte findet nicht statt. Neben diesen Parametern für den Programmstart stehen zur Laufzeit die folgenden Tasten zur Verfügung: • ’r’,’R’ schaltet die Rotation der Kamera ein und aus. • ’g’,’G’ erzeugt eine Ausgabe des aktuellen Simulationszustands im GMV-Format. Ausgabename der Datei ist #.gmv, wobei # wieder für die Anzahl der gerechneten Zeitschritte steht. Kapitel 5 Ergebnisse 5.1 Visuelles Ergebnis Abbildungen 5.1 bis 5.4 zeigen eine Reihe von Bildschirmfotos der Visualisierung eines flachen Beckens. Die Simulation, auf der diese Bilder basieren, löst die Shallow Water Equations für ein Becken mit jeweils zehn Metern Seitenlänge, das durch ein Gitter mit 256×256 Punkten räumlich diskretisiert wurde, bei einer Zeitschrittgröße von 0,05 Sekunden. In der Initialisierung wurden mehrere „Wassersäulen“ willkürlich auf der Wasseroberfläche verteilt. Die Visualisierung beinhaltet wie in Kapitel 4.4 beschrieben die Berechnung von reflektierten Lichteinflüssen aus einer Umgebungskarte. Die an der Oberfläche gebrochenen „Strahlen“ werden ebenfalls berechnet, und lassen den an einen gekachelten Schwimmbecken-Boden angelehnten Beckenboden erkennen. Beide Einflüsse auf den Farbwert werden gemäß des bestimmten Fresnel-Koeffizienten mit einander vermischt. Die Bilder entstanden jeweils im Abstand von einigen Sekunden Programmlaufzeit. Abbildung 5.7 zeigt das Ergebnis einer Simulation mit gleichen Parametern, mit zufällig eingebauten rechteckigen Hindernissen. Die Bilder entstanden wieder innerhalb der ersten Sekunden der Programmlaufzeit. Es sei an dieser Stelle angemerkt, dass die FPS (Frames pro Sekunde), die im Fenstertitel angegeben sind, nicht repräsentativ sind. Sie wurden durch das Stoppen der Simulation zur Screenshot-Erzeugung verfälscht. 5.2 Geschwindigkeit Als Maß für die erzielte Geschwindigkeit der Implementierung gelten die erreichten Bilder (Frames) pro Sekunde. Da die Änderungen in den Laufzeiten abhängig von einer ein- oder ausgeschalteten separaten Randbehandlung kaum messbar sind, wurde exemplarisch nur der 49 50 KAPITEL 5. ERGEBNISSE Abbildung 5.1: Visualisierungsergebnis, flaches Becken Fall ohne diese betrachtet. Die Verwendung von Display-Listen für die Darstellung des Becken-Bodens ist bei den durchgeführten Testläufen obligatorisch, da immediate mode rendering unnötigt (das Bodenprofil ändert sich nicht) und einfach zu kostspielig ist. Bei einer Gittergröße von 256 Knoten in jeder Dimension zum Beispiel sinkt die Framerate durch Verzicht auf Display-Listen im VertexTexture-Fetch-Modus von 26.6 auf 15.5 Bilder pro Sekunde bei 10 Jacobi-Iterationen auf einer NVidia 6800GT Grafikkarte. Der Verlust nimmt mit steigender Zahl von Gitterpunkten weiter zu. Die angegebenen FPS-Werte wurden jeweils über 30 Sekunden Programm-Laufzeit gemittelt, wobei die erste Sekunde, in der die komplette Initialisierung der Simulation mit Datentransfer zur GPU, dem Laden der Shader-Programme, der Erzeugung der Bodengeometrie etc. nicht mit eingerechnet wurde. Damit ein Vergleich zwischen Grafikkarten der letzen und vorletzen Generation, und damit auch ein Ausblick auf zu erwartende Ergebnisse mit Grafikkarten der nächsten Generation möglich ist, wurden die Tests mit identischer Konfiguration sowohl auf einer NVidia 6800GT, wie auch auf einer NVidia 7800GTX Grafikkarte gerechnet, deren relevante technische Spezifikationen in Tabellen 5.2 und 5.1 aufgeführt sind (siehe [35], [36]). 5.2. GESCHWINDIGKEIT 51 Abbildung 5.2: Visualisierungsergebnis, flaches Becken Bus-Technologie Speicher Speicher Schnittstelle Speicher Bandbreite (GB/Sek) Vertex-Pipelines Pixel-Pipelines PCI Express 256MB 256-bit 54.4 8 24 Tabelle 5.1: Technische Spezifikation der verwendeten NVIDIA 7800GTX GPU Bus-Technologie Speicher Speicher Schnittstelle Speicher Bandbreite (GB/Sek) Vertex-Pipelines Pixel-Pipelines AGP 128MB 256-bit 35.2 6 16 Tabelle 5.2: Technische Spezifikation der verwendeten NVidia 6800GT GPU Für die Geschwindigkeitsanalyse ist neben der der Dimension des Gitters lediglich die Anzahl 52 KAPITEL 5. ERGEBNISSE Abbildung 5.3: Visualisierungsergebnis, flaches Becken der Jacobi-Iterationen pro Zeitschritt, und die Größe des Fensters der Ausgabe interessant. Parameter wie der Abstand der Gitterpunkte oder die Zeitschritt-Weite beeinflussen zwar das Ergebnis der Simulation, jedoch nicht die Geschwindigkeit der Darstellung derselben. Die Tests wurden exemplarisch mit fünf, zehn und fünfzehn Jacobi-Iterationen durchgeführt. Je größer das Fenster für die Darstellung ist, desto mehr Farbwerte müssen für die Pixel der Wasseroberfläche durch Cubemap-Lookups für Reflexion und Brechung berechnet werden. Der Unterschied in den erreichten Frame-Raten ist jedoch verhältnismäßig gering. Betrachtet man ein Gitter mit 256 mal 256 Punkten, und führt die Simulation mit 10 Jacobi-Iterationen durch, so sinkt die Frame-Rate von 19.8 FPS bei einer Fenstergröße von 400 mal 300 Pixeln, die aufgrund der Wahl des Augenpunktes vollständig mit Wasseroberfläche „bedeckt“sind, nur auf 19.5 FPS bei einer Fenstergröße von 1200 mal 900 Pixeln. Die nachfolgend angegebenen Werte beziehen sich daher alle auf eine ”mittlere” Fenstergröße von 800 mal 600 Pixel. Da sich ein Unterschied in der Geschwindigkeit der Visualisierung abhängig von der verwendeten Technik (Vertex-Texture-Fetch bzw. VBO/PBO) auf den eingesetzten GPUs aber durchaus feststellen lässt, wurden alle Tests für beide Techniken vollzogen. Es sei an dieser Stelle angemerkt, dass die Synchronisation zwischen der Bildwiederholrate des Monitors und der Grafikkarte für diese Tests im Treiber deaktiviert werden muss. Die gemessenen Frames pro Sekunde sind sonst durch die Monitor-Frequenz nach oben beschränkt. Tabellen 5.3 und 5.4 zeigen die erzielten Frameraten in Abhängigkeit von der Gittergröße und Anzahl der Iterationen. 5.2. GESCHWINDIGKEIT 53 Abbildung 5.4: Visualisierungsergebnis, flaches Becken Gittergröße 64×64 128×128 256×256 512×512 FPS, 5 Iter. 377,62 110,34 26,39 6,71 FPS, 10 Iter. 295,11 78,84 19,18 4,88 FPS, 15 Iter. 243,03 61,69 15,02 3,84 Tabelle 5.3: NVidia 6800GT, Vertex Texture Fetch Gittergröße 64×64 128×128 256×256 512×512 FPS, 5 Iter. 344,32 100,5 26,17 6,89 FPS, 10 Iter. 278,89 74,84 18,87 4,92 FPS, 15 Iter. 231,54 58,69 14,84 3,86 Tabelle 5.4: NVidia 6800GT, VBO/PBO Eine logarithmische Skalierung der Y-Achse in den in Abbildungen 5.5 und 5.6 (GeForce 6800GT) bzw. 5.8 und 5.9 (GeForce 7000GTX) dargestellten Diagrammen macht deutlich, dass die Framerate linear mit der Problemgröße abnimmt. Die via Vertex-Texture-Fetch visua- 54 KAPITEL 5. ERGEBNISSE lisierte Wasseroberfläche erlaubt generell leicht höhere Frame-Raten als eine Visualisierung mittels VBOs/PBOs. Der Geschwindigkeitsgewinn liegt bei 9,6% bei der kleinsten Problemgröße mit nur fünf Jacobi-Iterationen, und nimmt kontinuierlich mit steigender Problemgröße und höherer Iterationsanzahl ab, auf bis zu weniger als 0.5% für das feinste Gitter mit höchster Iterationszahl. Abbildung 5.5: NVidia 6800GT, Vertex Texture Fetch Abbildung 5.6: NVidia 6800GT, VBO/PBO 5.2. GESCHWINDIGKEIT 55 Die Ursachen dafür liegen zum einen darin, dass der Berechnungsaufwand für die Simulation in diesen Fällen zum Teil weit über dem für die Visualisierung liegt, und die Vorteile der Vertex-Texture-Fetch-Implementierung - nämlich eine geringere Anzahl an Texturzugriffen für die Visualisierung - sich nicht mehr so deutlich im Ergebnis wiederspiegeln. Zum anderen werden die für die Visualisierung benötigten arithmetischen Operationen, sprich das Bestimmen von Vertex-Koordinaten und -Normalen, im VTF-Modus komplett in die VertexShader verlegt. Deren im Vergleich mit den Pixel-Pipelines recht kleine Anzahl muss mit steigender Knotenzahl mehr und mehr Arbeit verrichten, wodurch diese nicht mehr so effizient parallelisiert werden kann wie im VBO/PBO- Modus. Diese Tendenz wird durch die in Tabellen 5.5 und 5.6 abgebildeten Ergebnisse der Performance-Tests auf der NVidia 7800GTX GPU noch deutlicher sichtbar (siehe auch Abbildungen 5.8 und 5.9). Gittergröße 64×64 128×128 256×256 512×512 FPS, 5 Iter. 692,31 258,23 64,36 16,02 FPS, 10 Iter. 590,82 216,78 54,45 13,65 FPS, 15 Iter. 514,97 186,66 47,24 11,86 Tabelle 5.5: NVidia 7800GTX, Vertex Texture Fetch Gittergröße 64×64 128×128 256×256 512×512 FPS, 5 Iter. 714,79 282,69 78,48 19,44 FPS, 10 Iter. 607,39 230,31 63,12 15,64 FPS, 15 Iter. 525,61 195,02 52,79 13,47 Tabelle 5.6: NVidia 7800GTX, VBO/PBO 56 KAPITEL 5. ERGEBNISSE Abbildung 5.7: Visualisierungsergebnis, Hindernisse 5.2. GESCHWINDIGKEIT 57 Abbildung 5.8: NVidia 7800GTX, Vertex Texture Fetch Abbildung 5.9: NVidia 7800GTX, VBO/PBO Die 7800GTX verfügt über das 1.5fache der parallelen Pixel-Pipelines der 6800GT, aber nur 2 Vertex Shader mehr. Hinzu kommt, dass der deutlich schneller Speichertakt dieser Karte die VBO/PBO-Implementierung zusätzlich begünstigt. Der Geschwindigkeitsgewinn reicht bis zu 21,5% bei großer Gittergröße und niedriger Iterationszahl. Kleinere Gittergrößen dämpfen diesen Gewinn durch eine bessere Auslastung der Vertex-Shader, ebenso wie steigende Iterationszahlen durch eine Verlagerung der Hauptlast der Anwendung auf den Simulationsteil. Dennoch lässt sich auch für die kleinste Gittergröße und höchsten Iterationszahlen noch eine Steigerung von 2% für das VBO/PBO-Verfahren verbuchen. 58 KAPITEL 5. ERGEBNISSE Macht man sich noch einmal bewusst, das eine Gittergröße von 512 mal 512 Knoten für ein Gleichungssystem mit 786.432 Unbekannten steht (u,v und h), welches bei kleiner Iterationszahl auf einer GPU neueren Typs nahezu 20 mal pro Sekunde mit optischer Genauigkeit gelöst und anprechend visualisiert werden kann, ist die erzielbare Geschwindigket in Hinblick auf Echtzeitanwendungen sehr gut. 5.3 Genauigkeit Obwohl das Ziel des hier vorgestellten Verfahrens die optisch korrekte Simulation und Darstellung von bewegtem Wasser war, soll im folgenden auch die numerische Genauigkeit untersucht werden, um seine Relevanz für praktische Anwendungen ausserhalb des reinen GrafikUmfeldes zu klären. Mit dem in Kapitel 2.3 vorgestellten Integrationsverfahren, der Gauss-Quadratur, wird das zwischen Wasseroberfläche und Boden eingeschlossene Volumen über einen längeren Simulationszeitraum für verschiedene Feinheits-Stufen in der räumlichen Diskretisierung und durch Veränderung der Iterationsanzahl erreichte Lösungsgenauigkeit beobachtet. Als Testszenario dient ein „Wasserbecken“ mit 10 mal 10 Metern Seitenlänge, und ebenem Bodenprofil. Der initiale Wasserstand über dem gesamten Becken liegt bei einem Meter. Zusätlich wird in der Mitte des Beckens eine quadratische „Wassersäule“von einem Meter Höhe platziert, die durch die Schwerkraft getrieben in sich zusammenfällt, und für eine kreisförmige Wellenausbreitung in der Beckenmitte sorgt. Die ”Eckpunkte” dieser Wassersäule sind abhängig von der Anzahl der Gitterpunkte, und liegen bei ((n/2)−(n/10), (n/2)−(n/10)), ((n/2)+ (n/10), (n/2) − (n/10)), ((n/2) + (n/10), (n/2) + (n/10)) und ((n/2) − (n/10), (n/2) + (n/10)). Das totale Volumen zum Zeitpunkt t0 kann somit analytisch exakt bestimmt werden, und beträgt 104. Da vor allem der stark bewegte Zeitraum zu Beginn der Simualtion von Interesse ist, wurden das Volumen während der ersten 10 Sekunden sekündlich berechnet. Je weiter sich die Wasseroberfläche ihrem Ruhezustand annähert, um so geringer ist die Veränderungen im Volumen, weshalb eine Auswertung alle fünfzehn Sekunden bis zum Abbruch der Simulation nach drei Minuten ausreicht. Die Zeitschrittgröße 4t wurde auf 0.05 Sekunden festgesetzt. Alle hier durchgeführten Tests verzichten dabei auf eine separate Randbehandlung in eigenen Shadern. Die Randbehandlung wird wie in Kapitel 4.3.2 beschrieben implizit durch Clampen am Texturrand bewerkstelligt. 5.3. GENAUIGKEIT T (Sek.) 1 2 3 4 5 6 7 8 9 10 15 30 45 60 75 90 105 120 135 150 165 180 59 V (5 Iter.) 99,77 97,47 98,54 98,75 98,1 97,59 98,04 98,07 97,54 97,48 97,39 96,6 96,13 95,79 95,51 95,27 95,05 94,85 94,66 94,47 94,28 94,16 V (10 Iter.) 99,28 97,42 98,42 98,37 97,54 97,68 97,95 97,56 97,34 97,57 97,12 96,57 96,19 95,94 95,75 95,61 95,48 95,36 95,25 95,15 95,05 94,95 V (15 Iter.) 99,13 97,38 98,35 98,25 97,44 97,65 97,87 97,44 97,32 97,52 97,06 96,55 96,2 95,96 95,78 95,63 95,5 95,39 95,28 95,17 95,07 94,96 V (20 Iter.) 99,08 97,37 98,32 98,21 97,41 97,64 97,84 97,41 97,3 97,5 97,04 96,54 96,2 95,96 95,79 95,66 95,54 95,43 95,33 95,23 95,13 95,03 Tabelle 5.7: Volumenentwicklung für ein 50×50 Gitter Tabelle 5.7 und Abbildung 5.10 zeigt das errechnete Volumen für das mit 50 mal 50 Punkten räumlich diskretisierte Becken zu verschiedenen Zeitpunkten und in Abhängigkeit von der Anzahl der durchgeführten Jacobi- Iterationen. Bereits nach Verstreichen der ersten Sekunde hat sich das Volumen auf unter 100 reduziert. Die durch die Wassersäule entstandene Welle erreicht ungefähr zum Zeitpunkt t = 2 Sekunden den Rand des Beckens. Von diesem Zeitpunkt an schwankt das betrachtete Volumen bei zehn, fünzehn und zwanzig durchgeführten Jacobi-Iterationen pro Zeitschritt nahezu identisch um einen rasch fallenden Mittelwert. Nach 30 Sekunden hat sich die Oberfläche weitgehend beruhigt, und der errechnete Wert liegt bei ca. 96,55. Dies entspricht einem Verlust von 7,1% vom exakten Startvolumen von 104. Dieser Wert liegt zwar nahe an den in [29] angegebenen 5%, offenbar ist die räumliche Diskretisierung für das betrachtete Gebiet mit nur 50 Punkten in jeder Dimension jedoch nicht fein genug für das Verfahren. Das Volumen fällt auch nach Beruhigung der Wasseroberfläche bei allen angegebenen Iterationszahlen langsam aber stetig ab, eine Tendenz, die sich auch bei den feineren räumlichen Diskretisierungen erkennen lässt. 60 KAPITEL 5. ERGEBNISSE T (Sek.) 1 2 3 4 5 6 7 8 9 10 15 30 45 60 75 90 105 120 135 150 165 180 V (5 Iter.) 102,11 101,6 100,4 101,08 101,32 101,37 101,08 100,42 100,53 100,82 100,46 99,78 99,36 98,98 98,62 98,3 98 97,73 97,47 97,23 97,01 96,79 V (10 Iter.) 101,83 100,62 100,84 101,16 101,1 100,52 100,44 100,74 100,77 100,4 100,13 99,79 99,4 99,09 98,83 98,59 98,37 98,17 97,99 97,83 97,68 97,54 V (15 Iter.) 101,58 100,32 100,82 100,99 100,65 100,24 100,54 100,61 100,27 100,16 100,19 99,67 99,33 99,06 98,82 98,62 98,44 98,27 98,12 98 97,89 97,81 V (20 Iter.) 101,38 100,21 100,74 100,8 100,33 100,22 100,49 100,34 100,04 100,18 100 99,54 99,25 99 98,79 98,6 98,43 98,29 98,16 98,05 97,95 97,86 Tabelle 5.8: Volumenentwicklung für ein 100×100 Gitter Ähnlich wie im zuvor betrachteten 50×50 Gitter, schwankt das Volumen auch in Tabelle 5.8 bzw. Abb. 5.11 gezeigten Fall einer 100×100 Punkte Diskretisierung ab der zweiten Sekunde Simulationszeit recht stark. Bis auf den unzureichend genau gelösten Fall von fünf JacobiIterationen ist der Volumenverlust bei Erreichen des Ruhezustandes bereits deutlich geringer ausgeprägt. Der Verlust von etwa 4.1% zum Zeitpunkt von 30 Sekunden deckt sich damit mit dem der CPU-Implementierung in [29]. 5.3. GENAUIGKEIT T (Sek.) 1 2 3 4 5 6 7 8 9 10 15 30 45 60 75 90 105 120 135 150 165 180 61 V (5 Iter.) 103,13 103,13 103,1 102,56 101,94 102,11 102,43 102,54 102,6 102,64 101,82 102,04 101,42 101,19 100,91 100,55 100,36 100,04 99,83 99,57 99,34 99,12 V (10 Iter.) 103,06 103,04 102,47 102,04 102,42 102,56 102,63 102,64 102,53 102,12 102,37 101,81 101,45 101,24 101,04 100,82 100,6 100,38 100,18 99,99 99,8 99,63 V (15 Iter.) 102,99 102,86 102,09 102,31 102,5 102,58 102,55 102,29 101,97 102,11 101,87 101,81 101,46 101,23 101,03 100,82 100,64 100,46 100,29 100,13 99,98 99,83 V (20 Iter.) 102,92 102,62 102,03 102,37 102,5 102,49 102,28 101,95 102,08 102,24 102,05 101,65 101,38 101,17 100,97 100,8 100,64 100,5 100,36 100,24 100,12 100 Tabelle 5.9: Volumenentwicklung für ein 200×200 Gitter Die in Abbildung 5.12 bzw. Tabelle 5.9 dargestellten Ergebnisse einer 200×200 Punkte Diskretisierung zeigen immer noch die Tendenz des Volumenverlustes. Es ist aber bereits zu erkennen, dass dieser erst einsetzt, nachdem die Welle den Beckenrand erreicht hat. Dies erklärt sich durch die Ungenauigkeit der Lösung am Rand, die wie in Kapitel 4.3.2 beschrieben keine Genauigkeit zweiter Ordnung aufweist, wie im Beckeninneren. Aufgrund der feineren räumlichen Diskretisierung wird im Vergleich zu den gröberen Diskretisierungen überproportional mehr im Inneren gerechnet, wodurch dieser Effekt nicht mehr ganz so stark ins Gewicht fällt. Rein optisch fällt dieser Volumenverlust auch nach mehreren Minuten zwar nicht auf, dürfte aber für eine praktische Anwendung ausserhalb des reinen Computer-Grafik-Umfeldes durchaus von Bedeutung sein. 62 KAPITEL 5. ERGEBNISSE T (Sek.) 1 2 3 4 5 6 7 8 9 10 15 30 45 60 75 90 105 120 135 150 165 180 V (5 Iter.) 103,44 103,44 103,44 103,43 103,36 102,92 102,54 102,43 102,66 102,85 103,04 102,76 102,46 102,1 101,74 101,46 101,22 101,02 100,83 100,64 100,45 100,25 V (10 Iter.) 103,4 103,41 103,4 103,13 102,65 102,61 102,88 102,98 103,03 103,06 102,51 102,66 102,17 102,19 101,82 101,77 101,49 101,38 101,15 101,02 100,82 100,68 V (15 Iter.) 103,37 103,37 103,26 102,74 102,66 102,91 103 103,05 103,07 103,05 102,76 102,36 102,19 102,08 101,92 101,72 101,52 101,34 101,19 101,04 100,89 100,74 V (20 Iter.) 103,35 103,33 103,03 102,61 102,84 102,96 103,02 103,04 103,01 102,81 102,85 102,54 102,26 102,02 101,82 101,65 101,5 101,35 101,2 101,06 100,93 100,79 Tabelle 5.10: Volumenentwicklung für ein 300×300 Gitter Analog zur 200×200 Punkte Diskretisierung verhält sich der in Abbildung 5.13 bzw. Tabelle 5.10 dargestellte Fall mit einer Auflösung von 300×300 Punkten bei einer langen Simulationslaufzeit gleich für alle durchgeführten Variationen in der Iterationszahl des Lösers bis auf die nur sehr unzureichend gelöste Variante mit fünf Iterationen. Diese weist die größte Abweichung vom Startvolumen auf. Hier ist dadurch, dass die Welle später als in den zuvor beschriebenen Fällen den Rand erreicht (ein Effekt, der in diesem Abschnitt noch genauer Untersucht wird), noch deutlicher zu erkennen, dass der Verlust des Volumens erst einsetzt, wenn die Berechnung der Ränder eine Rolle spielt. 5.3. GENAUIGKEIT T (Sek.) 1 2 3 4 5 6 7 8 9 10 15 30 45 60 75 90 105 120 135 150 165 180 63 V (5 Iter.) 103,58 103,58 103,58 103,58 103,58 103,57 103,45 103,12 102,85 102,7 103,14 102,64 102,53 102,66 102,46 102,18 101,86 101,6 101,54 101,44 101,23 100,96 V (10 Iter.) 103,57 103,57 103,56 103,56 103,43 103,06 102,84 102,9 103,09 103,17 103,28 103,03 102,6 102,39 102,32 102,24 102,11 101,93 101,71 101,51 101,35 101,23 V (15 Iter.) 103,55 103,55 103,54 103,45 103,07 102,87 103,03 103,17 103,22 103,26 103,07 102,85 102,8 102,41 102,41 102,17 102,03 101,92 101,71 101,61 101,44 101,3 V (20 Iter.) 103,53 103,53 103,51 103,23 102,91 103,02 103,17 103,22 103,26 103,28 102,83 102,94 102,63 102,43 102,34 102,08 101,98 101,8 101,62 101,49 101,31 101,17 Tabelle 5.11: Volumenentwicklung für ein 400×400 Gitter Abbildung 5.14 und Tabelle 5.11 zeigen die Volumenentwicklung für den am feinsten diskretisierten Testfall bei 400×400 Gitterpunkten. Die Abweichung vom Startvolumen beträgt zum Ende der Simulation nach drei Minuten nur noch 2,8%. Zusammenfassend lässt sich sagen, dass die in [29] vorgestellten Ergebnisse die Volumenerhaltung betreffend in dieser GPUImplementierung durchaus erreicht werden. Rein optisch ist der Verlust nicht festzustellen, auch die Schwankung des Volumens wenn die beschriebene Welle den Rand erreicht, fällt einem Betrachter nicht auf. Berechnet man die Ränder wie in Kapitel 4.3.2 beschrieben separat, kann man den gegenteiligen Effekt beobachten: Das Volumen steigt nach Ende der Simulation langsam, aber kontinuierlich an. Eine auf höhere numerische Genauigkeit ausgelegte Implementierung müsste also bei der Randbehandlung einseitige Ableitungen höherer Ordnung wählen. Auffallend an den konstant mit Zeitschrittweite 4t = 0.05 gerechneten Simulationen ist der Umstand, dass die Welle, obwohl sie immer vom gleichen Punkt in der Mitte des Beckens ausgeht, immer später den Rand erreicht, je feiner das Gebiet räumlich diskretisiert wurde. Wenn das Verfahren in der Zeit voll explizit wäre, würde es einer Zeitschrittgröße von n12 bedürfen, wobei n wieder die Anzahl der Gitterpunkte pro Dimension (in den Testfällen also 100,200,300 und 400) ist. Dadurch, dass das Verfahren halbimplizit in der Zeit ist, schwächen sich diese unteren Schranken auf bis zu n1 ab. Abbildung 5.15 zeigt einen Schnitt durch die Wasseroberflächen bei einer räumlichen Diskretisierung von (von oben nach unten) 400×400, 64 KAPITEL 5. ERGEBNISSE 300×300, 200×200 und 100×100 Punkten, jeweils zu einer Simulationszeit von 0,5 Sekun1 den. Die Zeitschrittweite war bei allen Versuchen auf 0.0025, also 400 festgelegt. Wenn auch auf die Feinheit der räumlichen Diskretisierung zurückzuführende Unterschiede im Ergebnis zu erkennen sind, so wird auch deutlich, dass sich die Welle in allen Abbildungen etwa gleich weit vom Mittelpunkt wegbewegt hat. Diese Zeitschritt-Größe ist also geeignet klein gewählt, um eine zeitliche Stabilität der Simulation zu gewährleisten. Experimente haben ergeben, dass diese Stabilität bis zu einer Zeitschrittgröße von ca. 0,0075 Sekunden bei einem Gitter von 400×400 Punkten gewährleistet ist. Verdoppelt man die Gitterweite, diskretisiert das gleiche Gebiet mit z.B. nur noch 200×200 Punkten, verdoppelt sich auch in etwa die zulässige Zeitschrittweite. Danach verringert sich die Ausdehnung der Welle im gleichen Simulationszeitraum, wie Abbildung 5.16 zeigt. Die Querschnitte veranschaulichen wieder die Ausbreitung nach je 0,5 Sekunden Simulationszeit, bei einer festen räumlichen Diskretisierung von 400 Punkten in jeder Dimension. Der Parameter ∆t wurde dabei (von oben nach unten) ausgehend von 0,0075 um jeweils 0,0025 erhöht, was zu einem sichtbaren Unterschied in der Geschwindigkeit der Ausbreitung der Welle führt. Da man von Echtheitsberechnungen spricht, wenn sich die real verstreichende Zeit mit der Simulationszeit - in diesem Fall das Produkt der Zeitschrittweite 4t und der Anzahl der gerechneten/visualisierten Zeitschritte - deckt, lässt sich der Echtzeit-Anspruch der Implementierung auf heute verfügbaren GPUs nur für kleinere Gittergrößen aufrechterhalten. 5.4 Zusammenfassung und Ausblick Die Entwicklung - vor allem die Implementierung - wurde leider immer wieder behindert durch mangelnde oder falsche Dokumentation, fehlende Werkzeuge zur Fehleranalyse und Behebung des hardwarenahen Codes für die Grafikkarten- Programmierung. Hier bleibt nur zu wünschen, dass dieses Manko in Zukunft behoben werden wird. Das gesetzte Ziel dieser Arbeit - die Simulation und Visualisierung von Wasser in Echtzeit auf Basis der Shallow Water Equations - wurde trotzdem erreicht, jedoch sind die Problemgrößen, für die diese Aussage gilt, stark abhängig von der eingesetzten Grafik-Hardware. Die rasante Entwicklung auf diesem Gebiet lässt jedoch die berechtigte Hoffnung zu, dass in naher Zukunft mit dem hier vorgestellten Verfahren auch wesentlich größere Probleme effizient berechnet und visualisiert werden können. Die erzielten Geschwindigkeiten dieser Implementierung für das Lösen der Shallow Water Equations liegen aber immer noch deutlich über denen einer vergleichbaren CPU-Implementierung. Gerade bei Problemen, die eine große Anzahl von verschiedenen möglichen Eingaben berücksichtigen müssen, ist eine jede Verbesserung in den erreichten Geschwindigkeiten von großem Vorteil. Man stelle sich zum Beispiel einen Küsten-Streifen vor, der durch eine Reihe von zu plazierenden Wellenbrechern vor großen Flutwellen geschützt werden soll. Die optimalen Positionen der Wellenbrecher könnten z.B. von einem genetischen Algorithmus berechnet werden, der aus den Ergebnissen vieler durchgeführter Simulationen lernt. Hier ist ein schnelles Verfahren zur Lösung sehr wichtig. Letztendlich bleibt es dem Benutzer überlassen, ob er an „optischer Genauigkeit“ in Echtzeit oder einem genaueren Simulationsergebnis interessiert ist. 5.4. ZUSAMMENFASSUNG UND AUSBLICK Abbildung 5.10: Volumenentwicklung bei 50×50 Gitterpunkten Abbildung 5.11: Volumenentwicklung bei 100×100 Gitterpunkten 65 66 KAPITEL 5. ERGEBNISSE Abbildung 5.12: Volumenentwicklung bei 200×200 Gitterpunkten Abbildung 5.13: Volumenentwicklung bei 300×300 Gitterpunkten 5.4. ZUSAMMENFASSUNG UND AUSBLICK Abbildung 5.14: Volumenentwicklung bei 400×400 Gitterpunkten 67 68 KAPITEL 5. ERGEBNISSE Abbildung 5.15: Ausbreitung der Welle nach einer halben Sekunde bei (von oben nach unten) 400, 300, 200 und 100 Gitterpunkten in jede Dimension 5.4. ZUSAMMENFASSUNG UND AUSBLICK Abbildung 5.16: Volumenentwicklung bei 400×400 Gitterpunkten bei Zeitschrittweiten von (von oben nach unten) 0.0075, 0.01, 0.0125, 0.015 69 70 KAPITEL 5. ERGEBNISSE Anhang A Code-Auszüge A.1 Shader-Programme A.1.1 Berechnung der Departure Points Der folgende Fragment-Shader-Code wird zur Berechnung der Ausgangspunkte-Textur verwendet, und vor den Jacobi-Iterationen ausgeführt. 1 float4 main (uniform sampler2D valueTexture, 2 uniform float deltaT, 3 uniform float deltaX, 4 uniform float deltaY, 5 uniform float n, 6 float2 pos: WPOS) : COLOR { 7 8 float2 gridindex = float2((pos.x-0.5),(pos.y-0.5)); 9 float2 texcoords = float2((gridindex.x / (n-1)), 10 (gridindex.y / (n-1))); 11 float4 values = tex2D(valueTexture, texcoords ); 12 13 float2 alpha = float2(0,0); 14 alpha.x = deltaT * (values.x * deltaX); 15 alpha.y = deltaT * (values.y * deltaY); 16 17 float x = clamp((gridindex.x - alpha.x),0,(n-1)); 18 float y = clamp((gridindex.y - alpha.y),0,(n-1)); 19 20 float minx = ((floor(x)) / (n-1)); 21 float maxx = ((ceil(x)) / (n-1)); 22 float miny = ((floor(y)) / (n-1)); 71 72 ANHANG A. CODE-AUSZÜGE 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 44 float maxy = ((ceil(y)) / (n-1)); float p =(x - floor(x)); float q =(y - floor(y)); float4 float4 float4 float4 ul ur lr ll = = = = float4 ulur float4 lllr tex2D(valueTexture, tex2D(valueTexture, tex2D(valueTexture, tex2D(valueTexture, float2(minx,miny)); float2(maxx,miny)); float2(maxx,maxy)); float2(minx,maxy)); = lerp(ul,ur,p); = lerp(ll,lr,p); float4 inter = lerp(ulur,lllr,q); inter.a = values.b; if (values.b <= values.a) { inter.r = 0.0; inter.g = 0.0; } return inter; } 1-6 Deklaration der Hauptmethode 8 Bestimmung des Gitterindex aus dem Mittelpunkt der Pixel-Koordinate 9-10 Bestimmung der Texturkoordinaten für den Gitterpunkt 11 Textur-Lookup für u, v, h und b des Gitterpunktes 13-15 Berechnung des Ausgangspunktes 17-23 Berechnung der Texturkoordinaten für die „Eckpunkte“ der den Ausgangspunkt umschließenden „Zelle“ 25-26 Bestimmen der Interpolationsfaktoren 28-31 Textur-Lookups für die Eckpunkte 33-36 Interpolation von ũ, ṽ und h̃ aus den Eckpunkten 37 Übertragung von h in den Alpha-Kanal der Ausgabe 39-42 Null-Setzen der Geschwindigkeiten für den Fall, dass die Bodenhöhe größer als die Wasserhöhe ist 44 Ausgabe der Werte A.1. SHADER-PROGRAMME A.1.2 73 Jacobi Der folgende Fragment-Shader-Code implementiert eine Iteration des Jacobi-Verfahrens. Er wird für die Knoten im Inneren des Gebietes verwendet, kann aber auch wie in Kapitel 4.3.2 beschrieben auf dem Rand verwendet werden. 01 float4 main (uniform sampler2D valueTexture, 02 uniform sampler2D departureTexture, 03 uniform float deltaT, 04 uniform float deltaX, 05 uniform float deltaY, 06 uniform float n, 07 uniform float g, 08 float2 pos: WPOS) : COLOR { 09 10 float2 gridindex = float2((pos.x-0.5),(pos.y-0.5)); 11 float2 texcoords = float2((gridindex.x / (n-1)), 12 (gridindex.y / (n-1))); 13 float2 leftCoords = float2(texcoords.x - (1/(n-1)), texcoords.y); 14 float2 rightCoords = float2(texcoords.x + (1/(n-1)), texcoords.y); 15 float2 upCoords = float2(texcoords.x, texcoords.y - (1/(n-1))); 16 float2 lowCoords = float2(texcoords.x, texcoords.y + (1/(n-1))); 17 18 float4 centerVal = tex2D(departureTexture, texcoords); 19 float4 leftVal = tex2D(departureTexture, leftCoords ); 20 float4 rightVal = tex2D(departureTexture, rightCoords); 21 float4 lowVal = tex2D(departureTexture, lowCoords); 22 float4 upVal = tex2D(departureTexture, upCoords); 23 24 float4 oldValCenter = tex2D(valueTexture, texcoords); 25 float4 oldValLeft = tex2D(valueTexture, leftCoords); 26 float4 oldValRight = tex2D(valueTexture, rightCoords); 27 float4 oldValUp = tex2D(valueTexture, upCoords); 28 float4 oldValLow = tex2D(valueTexture, lowCoords); 29 30 float xSquare = deltaX * deltaX; 31 float ySquare = deltaY * deltaY; 32 float tSquare = deltaT * deltaT; 33 float tSquareG = tSquare * g ; 34 float tSquareGd = tSquareG * d ; 35 float d = (centerVal.a - oldValCenter.a); 36 37 if (gridindex.x<=1.0 ||gridindex.x>=(n-1)) oldValCenter.r=0; 38 if (gridindex.y<=1.0 ||gridindex.y>=(n-1)) oldValCenter.g=0; 39 float b = centerVal.b 40 + deltaT*((centerVal.r*(oldValRight.a-oldValLeft.a)/(2*deltaX)) 74 ANHANG A. CODE-AUSZÜGE 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 } +(centerVal.g*( oldValLow.a - oldValUp.a )/(2*deltaY))) - deltaT*d*(((rightVal.r-leftVal.r)/(2*deltaX)) +(( lowVal.g - upVal.g )/(2*deltaY))); float dd=1+2*tSquareGd*((xSquare+ySquare)/(xSquare*ySquare)); float Ax = oldValCenter.b+ tSquareG * ( ((oldValRight.a - oldValLeft.a) / (2 * deltaX)) *((oldValRight.b - oldValLeft.b) / (2 * deltaX)) +((oldValLow.a - oldValUp.a) / (2 * deltaY)) *((oldValLow.b - oldValUp.b) / (2*deltaY))) - tSquareGd * ( ((oldValLeft.b-(2*oldValCenter.b)+oldValRight.b)/xSquare) +((oldValUp.b-(2*oldValCenter.b)+oldValLow.b)/ySquare)); float4 retValue = oldValCenter; retValue.b = oldValCenter.b + ((1.0/dd) * (b - Ax)); if (retValue.b <= (oldValCenter.a)) { retValue.b = centerVal.a; } return retValue; 1-8 Deklaration der Hauptmethode 10 Bestimmung des Gitterindex aus dem Mittelpunkt der Pixel-Koordinate 11-12 Bestimmung der Texturkoordinaten für den Gitterpunkt 13-16 Bestimmung der Texturkoordinaten der benachbarten Gitterpunkte 18-22 Texturlookups für ũ, ṽ und h̃, sowie den Höhenwert h des letzten Zeitschritts 24-28 Texturlookups für u, v, b sowie den Höhenwert h des letzten Iterationsschritts 31-35 Deklaration von Hilfsvariablen 37-38 Null-Setzen der Geschwindigkeiten auf dem Rand. 39-43 Berechnen der „rechten Seite“ des Gleichungssystems (b) 45 Berechnen des Faktors für das Hauptdiagonal-Element 47-55 Berechnung der „linken Seite“ des Gleichungssystems (Ax) aus der Lösung des letzten Iterationsschritts 58 Korrektur der Lösung des letzten Iterationsschritts A.1. SHADER-PROGRAMME 75 60 Festsetzen des Höhenwertes für den Fall, dass Höhenwert des Bodens über dem der Wasseroberfläche liegt 62 Ausgabe der Ergebnisse des Iterationsschritts A.1.3 Aktualisierung der Geschwindigkeiten Der im folgenden abgebildete Fragment-Shader-Code wird für die Aktualisierung der Geschwindigkeiten u und v im Anschluss an die Jacobi-Iterationen ausgeführt. 01 float4 main (uniform sampler2D valueTexture, 02 uniform sampler2D departureTexture, 03 uniform float deltaT, 04 uniform float deltaX, 05 uniform float deltaY, 06 uniform float n, 07 uniform float g, 08 float2 pos: WPOS) : COLOR { 09 10 float2 gridindex = float2((pos.x-0.5),(pos.y-0.5)); 11 float2 texcoords = float2((gridindex.x/(n-1)), 12 (gridindex.y / (n-1))); 13 14 float2 leftCoords = float2(texcoords.x-(1/(n-1)), texcoords.y); 15 float2 rightCoords = float2(texcoords.x+(1/(n-1)), texcoords.y); 16 float2 upCoords = float2(texcoords.x, texcoords.y-(1/(n-1))); 17 float2 lowCoords = float2(texcoords.x, texcoords.y+(1/(n-1))); 18 19 float4 oldDepartureValues = tex2D(departureTexture, texcoords); 20 float4 newCenter = tex2D(valueTexture, texcoords); 21 22 float4 newLeft = tex2D(valueTexture,leftCoords); 23 float4 newRight = tex2D(valueTexture,rightCoords); 24 float4 newUp = tex2D(valueTexture,upCoords); 25 float4 newLow = tex2D(valueTexture,lowCoords); 26 27 float4 retValue = newCenter; 28 29 retValue.r = oldDepartureValues.r 30 - (deltaT*g*(newRight.b-newLeft.b)/(2*deltaX)); 31 retValue.g = oldDepartureValues.g 32 - (deltaT*g*(newLow.b-newUp.b)/(2*deltaY)); 33 34 if (newCenter.b <= newCenter.a) { 35 retValue.b = oldDepartureValues.a; 36 retValue.r = 0; 37 retValue.g = 0; 76 ANHANG A. CODE-AUSZÜGE 38 39 40 41 42 43 44 45 46 47 48 49 } } if (newLeft.b <= newLeft.a ||newRight.b <=newRight.a) { retValue.r = 0; retValue.b = oldDepartureValues.a; } else if (newUp.b <= newUp.a ||newLow.b <= newLow.a) { retValue.g = 0; retValue.b = oldDepartureValues.a; } return retValue; 1-8 Deklaration der Hauptmethode 10 Bestimmung des Gitterindex aus dem Mittelpunkt der Pixel-Koordinate 11-12 Bestimmung der Texturkoordinaten für den Gitterpunkt 14-17 Bestimmung der Texturkoordinaten der benachbarten Gitterpunkte 19 Textur-Lookup für ũ, ṽ und h̃ 20-25 Textur-Lookups für u,v und h des aktuellen Gitterpunktes und seiner Nachbarn 27-32 Aktualisierung der Geschwindigkeiten 34-46 Null-Setzen der Geschwindigkeiten an den Rändern 48 Ausgabe der aktualisierten Geschwindigkeiten u und v, h und b. A.1.4 Koordinaten-Erzeugung Das folgende Fragment-Shader-Programm wird zur Berechnung der Knotenkoordinaten für die VBO/PBO Visualisierungs-Methode der Wasseroberfläche verwendet, und im Anschluss an die erfolgte Lösung eines Zeitschrittes verwendet. 01 float4 main(uniform sampler2D valueTexture , 02 uniform float deltaX, 03 uniform float deltaY, 04 uniform float xoffset, 05 uniform float yoffset, 06 uniform float n, 07 float2 pos: WPOS) : COLOR { 08 09 float2 gridindex = float2((pos.x-0.5),(pos.y-0.5)); 10 float2 texcoords = float2((gridindex.x / (n-1)), 11 (gridindex.y / (n-1))); A.1. SHADER-PROGRAMME 12 13 14 15 16 17 18 19 20 } float4 values 77 = tex2D(valueTexture, texcoords); float x = xoffset + (gridindex.x * deltaX); float y = yoffset + (gridindex.y * deltaY); float4 retValue = return retValue; float4(x,y,values.b,1); 1-7 Deklaration der Hauptmethode 9 Bestimmung des Gitterindex aus dem Mittelpunkt der Pixel-Koordinate 10-11 Bestimmung der Texturkoordinaten für den Gitterpunkt 12 Texturlookup für den Höhenwert h 14-15 Berechnung der x- und y-Koordinaten des Knotens aus dem übergebenen Offset und dem Gitterindex 17 Zuordnung der x-, y- und h- Werte zum Ausgabewert. Der Alpha-Wert wird auf 1 gesetzt. 18 Ausgabe der den Koordinaten entsprechenden Farbwerte. A.1.5 Normalen-Erzeugung Die Berechnung der Knoten-Normalen durch dieses Fragment-Shader-Programm erfolgt nach der Berechnung der Koordinaten. 01 float3 main(uniform sampler2D valueTexture , 02 uniform float deltaX, 03 uniform float deltaY, 04 uniform float n, 05 float2 pos: WPOS) : COLOR { 06 07 float2 gridindex = float2((pos.x-0.5),(pos.y-0.5)); 08 float2 texcoords = float2((gridindex.x / (n-1)), 09 (gridindex.y / (n-1))); 10 11 float4 displacement = tex2D(valueTexture, texcoords); 12 13 float2 leftCoords = float2(texcoords.x-(1/(n-1)), texcoords.y); 14 float2 rightCoords = float2(texcoords.x+(1/(n-1)), texcoords.y); 15 float2 upCoords = float2(texcoords.x, texcoords.y-(1/(n-1))); 16 float2 lowCoords = float2(texcoords.x, texcoords.y+(1/(n-1))); 17 78 ANHANG A. CODE-AUSZÜGE 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 } = = = = tex2D(valueTexture, tex2D(valueTexture, tex2D(valueTexture, tex2D(valueTexture, float4 float4 float4 float4 left right up low float3 float3 float3 float3 toLeft toRight toUp toLow float3 float3 float3 float3 ulNormal urNormal lrNormal llNormal = = = = = = = = leftCoords); rightCoords); upCoords); lowCoords); normalize(float3(-deltaX,0,left.b-displacement.b)); normalize(float3(deltaX,0,right.b-displacement.b)); normalize(float3(0,deltaY,up.b-displacement.b)); normalize(float3(0,-deltaY,low.b-displacement.b)); cross(toUp,toRight); cross(toRight, toLow); cross(toLow, toLeft); cross(toLeft, toUp); float3 normal = (ulNormal + lrNormal + llNormal + urNormal) / 4; normal = normalize(float3(-normal.x,normal.y,-normal.z)); return normal; 1-5 Deklaration der Hauptmethode 7 Bestimmung des Gitterindex aus dem Mittelpunkt der Pixel-Koordinate 8-9 Bestimmung der Texturkoordinaten für den Gitterpunkt 11 Texturlookup für den Höhenwert h 13-16 Bestimmung der Texturkoordinaten der benachbarten Gitterpunkte 18-21 Textur-Lookups für die h-Werte der benachbarten Gitterpunkte 23-26 Bestimmen der „Kantenvektoren“ 28-31 Bestimmen der Normalen der benachbarten Flächen aus den Kantenvektoren (Kreuzprodukt) 33-34 Interpolation und Normalisierung der Normalen des Knotens aus den Normalen der benachbarten Flächen A.1.6 Reflektion, Brechung und Fresnel-Term Das folgende Vertex-Shader-Programm berechnet die Brechungs- und Reflektions- Vektoren für jeden Knoten der die Wasseroberfläche beschreibenden Geometrie, sowie den Fresnel-Term für die Interpolation der Farbwerte im darauffolgenden Fragment-Shader-Durchgang. 01 struct vertex { A.1. SHADER-PROGRAMME 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 79 float4 color : COLOR; float4 position : POSITION; float3 normal : TEXCOORD0; }; struct fragment { float4 position float fresnelTerm float3 refract float3 reflect }; : : : : POSITION; TEXCOORD1; TEXCOORD2; TEXCOORD3; uniform float4x4 modelViewProj : state.matrix.mvp; fixed fast_fresnel(float3 I, float3 N, float3 fresnelValues) { fixed power = fresnelValues.x; fixed scale = fresnelValues.y; fixed bias = fresnelValues.z; return bias + pow(1.0 + dot(I, N), power) * scale; } fragment main(vertex IN, uniform float3 eyepos) { const float refractFactor = 0.75024375; fragment OUT; OUT.position = mul(modelViewProj,IN.position); float3 I = normalize(IN.position.xyz - eyepos); OUT.reflect = reflect(I,IN.normal); OUT.refract = refract(I,IN.normal,refractFactor); OUT.fresnelTerm = fast_fresnel(I,IN.normal,float3(2.0,1.0,0.2)); return OUT; } 1-5 Deklaration des Eingabe-Structs (Knoten-Attribute) 7-12 Deklaration des Ausgabe-Structs (Fragment-Attribute) 14 Referenzierung der Model-View-Projection-Matrix (MVP) 16-21 Deklaration einer Hilfsmethode zur Berechnung des Fresnel-Terms 23 Deklaration der Hauptmethode 80 ANHANG A. CODE-AUSZÜGE 25 Deklaration des konstanten Licht-Brechungsfaktors beim Übergang von Luft in Wasser 27 Erzeugung einer Instanz des Ausgabe-Structs 28 Berechnung der Pixel-Position durch Multiplikation der Knotenposition mit der MVPMatrix 30 Bestimmen des Vektors vom „Auge“ zum Oberflächenpunkt 31 Berechnung des an der Oberfläche reflektierten Vektors 32 Berechnung des an der Oberfläche gebrochenen Vektors 33 Berechnung des Fresnel-Terms durch die Hilfsmethode 36 Ausgabe der Information A.1.7 Environment-Mapping Der letze der verwendeten Fragment-Shader führt anhand der zuvor berechneten Reflektions und Brechungs-Vektoren die Lookups in die Umgebungskarten durch. 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 struct fragment { float4 position float fresnelTerm float3 refract float3 reflect }; : : : : POSITION; TEXCOORD1; TEXCOORD2; TEXCOORD3; float4 main (fragment IN, uniform samplerCUBE cubeMap) : COLOR { float4 color = lerp(texCUBE(cubeMap,IN.reflect), texCUBE(cubeMap,IN.refract), IN.fresnelTerm); return color; } 1-6 Deklaration des Eingabe-Structs (Pixel-Attribute) 8-9 Deklaration der Haupt-Methode 11-13 Lookups entlang der Brechungs- und Reflektions-Vektoren, und Mischen der Farbwerte in einem durch den Fresnel-Term gegebenen Verhältnis 15 Ausgabe des Farbwertes Literaturverzeichnis [1] The General Mesh GMVHome.html. Viewer. http://www-xdiv.lanl.gov/XCM/gmv/ [2] OpenGL Render-to-Texture, 2002. NVIDIA Corporation Game Developer Conference 2002, http://developer.nvidia.com/object/gdc_oglrtt.html. [3] EXT_framebuffer_object OpenGL Extension Specification, http://www.opengl.org/documentation/extensions/ EXT_framebuffer_object.txt. 2005. [4] C/C++ Development Tools, 2006. http://www.eclipse.org/cdt. [5] Eclipse IDE, 2006. http://www.eclipse.org. [6] GLUT for Mingw32, 2006. glut.htm. http://mywebpage.netscape.com/PtrPck/ [7] GPGPU Programming Resources, 2006. http://sourceforge.net/projects/ gpgpu. [8] NVIDIA CG Toolkit, 2006. cg_toolkit.html. http://developer.nvidia.com/object/ [9] NVShaderPerf , 2006. ://developer.nvidia.com/object/nvshaderperf_home.html. [10] OpenGL - The Industry’s Foundation for High Performance Graphics, 2006. http://www.opengl.org/. [11] The OpenGL Architecture Review Board, 2006. http://www.opengl.org/about/ arb/. [12] Telemac, numerical modelling system for hydraulics developed by EDF-DRD and distributed by SOGREAH, hydroinformatics, 2006. http://www.telemacsystem.com/gb/default.html. [13] The OpenGL Extension http://glew.sourceforge.net. Wrangler Library, 2006. [14] C EBENOYAN , C.: GPU Gems. Addison Wesley, 2005. Chapter 28, Graphics Pipeline Performance. 81 82 LITERATURVERZEICHNIS [15] A LTIERI , M., C. B ECKER und S. T UREK: On the realistic performance of Linear Algebra components in iterative solvers. In: B UNGARTZ , H.-J., F. D URST und C. Z ENGER (Hrsg.): High Performance Scientific and Engineering Computing: Proceedings of the International FORTWIHR Conference on HPSEC, Munich, March 16–18, 1998, Bd. 8 d. Reihe Lecture Notes in Computational Science and Engineering, S. 3–12. Springer, Berlin, 1999. ISBN 3-540-65730-4. [16] BAUMGART, B.: Winged-Edge Polyhedron Representation for Computer Vision, 1975. http://www.baumgart.org/winged-edge/winged-edge.html. [17] BAUMGARTNER , A. und L IEBSCHER , H.-J.: Allgemeine Hydrologie - Quantitative Hydrologie. - In: Lehrbuch der Hydrologie Bd. 1. Gebr. Borntraeger, 1996. 2. Auflage. [18] B ECKER , C., S. K ILIAN und S. T UREK: Hardware–oriented Numerics and concepts for PDE software. In: FUTURE 1095, S. 1–23. Elsevier, 2003. International Conference on Computational Science ICCS2002, Amsterdam. [19] B UCK , I.: GPU Gems 2. Addison Wesley, 2005. Chapter 32, Taking the Plunge into GPU Computing, Ian Buck. [20] C ARPENDALE , S.: CPSC 453: Introduction to Computer Graphics. Techn. Ber., Department of Computer Science, University of Calgary, 2003. [21] D ÖRFLER , W. und P ESCHEK , W.: Einführung in die Mathematik für Informatiker. Carl Hanser Verlag München Wien, 1998. [22] G REEN , S.: The OpenGL Framebuffer Object Extension, 2005. NVIDIA Corporation Game Developer Conference 2005 , http://download.nvidia.com/developer/presentations/2005/GDC/ OpenGL_Day/OpenGL_FrameBuffer_Object.pdf. [23] G ROSSMANN , C. und ROOS , H.-G.: Numerik partieller Differentialgleichungen. Teubner, 1994. 2. Auflage. [24] H ARRIS , M.: GPU Gems 2. Addison Wesley, 2005. Chapter 34, GPU Flow-Control Idioms. [25] K ABEK , C.: Einblick über die Anwendungsmöglichkeiten von Cg, 2004. http://people.fh-landshut.de/ gschied/opengl/ 13-cganwendung-ckabek/. [26] K INZELBACH , W. und RUTSCHMANN , P.: Vorlesungskript Hydraulik 2 - ETH Zürich, 2005. http://water2.uibk.ac.at/de/0/de_k0_s1_4.html. [27] K LOTZ , M.: Herleitung der Navier-Stokes-Gleichungen. http://www.wissenschaft-online.de/spektrum/projekt2/gaes15.htm, 2006. [28] L AWSON , S.: Physically Based Modeling. Techn. Ber., Department of Computing and Informatics at the University of Lincoln, 2005. LITERATURVERZEICHNIS 83 [29] L AYTON , A.T. und VAN DEN PANNE , M.: A numerically efficient and stable algorithm for animating water waves. The Visual Computer, (18):4153, 2002. [30] M EISTER , A.: Numerik linearer Gleichungssysteme. Vieweg, 1999. ISBN 3-528-031352. [31] M ÜLLER , H.: Digitale Bilderzeugung, Jul 2004. Vorlesungsskript, Universität Dortmund, http://ls7-www.cs.uni-dortmund.de/students/lectures/ doc_dbe/ss04/skript.shtml. [32] M UMIT K HAN: MinGW http://www.mingw.org. - Minimalist GNU for Windows, 2006. [33] N EDEL , L und T HALMANN , D.: Anatomic modeling of deformable human bodies. The Visual Computer, (16):306–321, 2000. [34] NVIDIA: Particle System Dynamics. Techn. Ber., School of Computer Science, Carnegie Mellon University, 2001. [35] NVIDIA: NVIDIA 6800 GPUs, 2006. geforce_6800.html. http://www.nvidia.com/page/ [36] NVIDIA: NVidia 7800 GPUs, 2006. geforce_7800.html. http://www.nvidia.com/page/ [37] NVIDIA: NVIDIA SDK, 2006. sdk_home.html. http://developer.nvidia.com/object/ [38] OWENS , J. D., L UEBKE , D., G OVINDARAJU , N., H ARRIS , M., K RÜGER , J., L EFOHN , A. E. und P URCELL , T. J.: A Survey of General-Purpose Computation on Graphics Hardware. Techn. Ber., Eurographics 2005, 2005. State of The Art Report. [39] P HARR , M. und R. F ERNANDO: GPU Gems 2 : Programming Techniques for HighPerformance Graphics and General-Purpose Computation. Addison-Wesley Professional, March 2005. [40] P ROVO , X.: Deformation Constraints in a Mass?Spring Model to Describe Rigid Cloth Behavior. Techn. Ber., Institut National de Recherche en Informatique et Automatique, 2002. http://www.rocq.inria.fr/syntim/research/provot. [41] R ANNACHER , R.: Einführung in die Numerische Mathematik, 2005. Vorlesungsskript, Institut für Angewandte Mathematik Universität Heidelberg. [42] R ANNACHER , R.: Vorlesungskripten Prof. R. Rannacher Numerische Mathematik, 2005. http://numerik.iwr.uni-heidelberg.de/l̃ehre/notes/. [43] S CHWARZ , H.R.: Numerische Mathematik. Teubner, 1984. [44] SGI: ARB_texture_cube_map, 1999. http://oss.sgi.com/projects/ ogl-sample/registry/ARB/texture_cube_map.txt. 84 LITERATURVERZEICHNIS [45] SGI: ARB_pixel_buffer_object, 2003. http://oss.sgi.com/projects/ ogl-sample/registry/ARB/ pixel_buffer_object.txt. [46] SGI: ARB_vertex_buffer_object Extension Specification, http://oss.sgi.com/projects/ ogl-sample/registry/ARB/ vertex_buffer_object.txt. 2003. [47] S TRZODKA , R., D OGGETT, M. und KOLB , A: Scientific Simulations on Programmable Graphics Hardware. Techn. Ber., Caesar Research Center Bonn, ATI Research, Universität Siegen, 2005. [48] W ITKIN , A. und BARAFF , D.: Physically Based Modeling Differential Equation Basics. Techn. Ber., Pixar Animation Studios, 2001. [49] W OOLLEY, C.: GPU Gems 2. Addison Wesley, 2005. Chapter 35, GPU Program Optimization.