Kapitel 2 Graphikkarten Programmierung

Transcription

Kapitel 2 Graphikkarten Programmierung
Kapitel 2
Graphikkarten Programmierung
Moderne Grafikkarten sind dafür ausgelegt, den Prozess des Renderings eines Bildes sehr schnell auszuführen, um in Echtzeit Animationen mit qualitativ möglichst hochwertigen Effekten zu berechnen.
Dabei resultiert der Geschwindigkeitsvorteil aus der einfach parallel zu bearbeitenden Bildberechnung, die auf die einzelnen Prozessoren einer GPU verteilt werden kann. Außerdem werden Vektoroperationen oder Matrix-Vektor-Produkte in einem einzigen Aufruf über sogenannte Packed arrays
berechnet. Diese Single Instruction Multiple Data (SIMD) Berechnungen sind eine Art Rückkehr des
Vektorrechners aus der Ecke der Hochleistungsrechner auf die Ebene der PC-Technologie. Früher
musste der Programmierer zu einer Assembler-Sprache greifen und den Code auf den eingesetzten
Grafikchip direkt anpassen, inzwischen gibt es dafür Hochsprachen (siehe [FK03]). Das macht es
wiederum interessant, auf der GPU auch Berechnungen vorzunehmen, die typischerweise parallelen Code auf der CPU erfordern, wie beispielsweise Filterverfahren der Bildverarbeitung, aber auch
einfache finite Differenzen für partielle Differentialgleichnungen, deren Ortsabhängigkeiten oder Geschwindigkeitsfelder in Texturen gespeichert und über Texturzugriffe neu berechnet werden können.
2.1
Shader Programmierung
Die Idee der Shader stammt aus den großen Studios für Animationsfilme. Ende der 80er Jahre wurde
bei Pixar für ihr Rendering-Interface Renderman eine eigene Shader-Sprache entwickelt. Die Anwendung beschränkte sich jedoch auf das relativ langsame Batch-Rendering einzelner Filmframes. Mit
einem Shader berechnen die Renderer für jeden Geometriepunkt respektive dargestellten Pixel das
Aussehen, statt nur statisch eine einzige Farbe oder Textur zu verwenden. Trotz einfacher Geometrie
erscheinen damit gerenderte Objekte mit komplexer Oberflächenstruktur. Diese Idee geht auf eine
frühe Arbeit von Robert Cook [Coo84] zurück, der den Ablauf des Shading in einer Baumstruktur
organisiert hat. In diese Bäume an unterschiedlichen Stufen eingreifen zu können, genügt ein rein
knotenbasierter Ansatz nicht. Auf der viel tiefer liegenden Ebene der Rasterung dagegen erzielt man
wie der Name schon sagt, das bessere Shading, die mit den Nachbarpunkten des Gitters abgestimm17
18
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
te Abstufung. Moderne Grafikkarten beherrschen diese Technologie in Echtzeit. Der Programmierer
lädt seinen Shader-Code in die GPU. Die Graphikkarte führt diesen Code während des Renderings
sehr schnell für jeden einzelnen Punkt aus. Dabei kann das Bild einfach parallel berechnet werden,
in dem es in Bereiche zerlegt und auf die einzelnen Prozessoren einer GPU verteilt wird. Im Renderprozess sind Vektoroperationen oder Matrix-Vektor-Produkte über Packed arrays auszuführen. Statt
in Assembler und für einen speziellen Grafikchipsatz kann die GPU heute in einer Hochsprache angesprochen werden. Die Besonderheit liegt darin, dass der Compiler den in einer solchen Sprache
abgefassten Shader in Code für die jeweilige GPU während der Ausführung übersetzt. Shader beschreiben keine Geometrien oder Objekte, das ist immer noch die Aufgabe von APIs wie OpenGL
oder Direct3D. Aber sie beeinflussen, wie die Grafikkarte Transformationen, Licht und Farben verarbeitet.
An dieser Stelle wird es nötig, den Begriff Fragment deutlich vom Pixel zu unterscheiden. Während
ein Pixel die kleinste Einheit auf dem Rasterschirm darstellt, umfasst das Fragment wesentlich mehr
Information und ist die abstraktere und von der anzusteuernden Hardware losgelöste Variante einer
kleinsten Rastereinheit. Mit an die Graphikprozessoren übergebenen Knoten (Vertices) und mit diesen
Fragmenten kann nun auf der Graphikkarte operiert werden, ohne dass die CPU in diesen Vorgang
eingreifen muss.
Zur Erinnerung: Buffer beinhalten gleichmäßig für alle Pixel des Graphikfensters (oder des Bildes
bei Offscreen-Rendering) gespeicherte Informationen. Sichtbar ist nur der (Front Left, Front Right)
Colorbuffer. Der Framebuffer ist die Vereinigung sämtlicher Buffer.
Definition 2.1 Ein Fragment ist in der Computergraphik der Begriff für sämtliche Daten, die benötigt
werden, um den Farbwert des Pixels im Colorbuffer zu erzeugen. Das beinhaltet (aber ist nicht beschränkt auf):
• Rasterposition
• z-Tiefe
• Interpolierte Attribute (Farbe, Texturkoordinaten , etc.)
• Einträge im Stencilbuffer
• Alphawerte
• Window ID
Man denke sich das Fragment als die Vereinigung alle Daten, die benötigt werden, um den Farbwert
des Pixels zu bestimmen, zusammen mit allen Daten, mit denen getestet wird, ob der Colorbuffer
überhaupt erreicht wird.
2.2. SHADE TREES
2.2
19
Shade trees
Um den Illuminationsprozess und Schattierungen zu modularisieren, hat man entsprechende Shader
implementiert. Der nächste Entwicklungsschritt betraf Entscheidungsbäume, um diese verschiedenen
Shader und Kombinationen in einem Programm benutzbar und zur Laufzeit entscheidbar einzusetzen.
Zum Beispiel stammt von Whitted (1982) die Idee eines Scanline Algorithmus, bei dem eine verkettete Liste einzelner Spans mit der Information (z-Werte, Normalen) an den jeweiligen Eckpunkten
assoziiert wird. Diese Idee konnte sich allerdings nicht gegen eine stärker objektorientierte Beschreibung durchsetzen, wie sie im Format des Renderman Interface Bytestream (RIB) festgehalten ist.
Definition 2.2 Ein Shade tree besitzt eine Baumstruktur, in deren Knoten Parameter der Kinderknoten eingehen und daraus Parameter für die darüberliegenden Elternknoten produzieren.
Die Parameter sind dabei Werte für einzelne Terme und Begriffe, die man aus Beleuchtungsmodellen kennt, z.B. der Spekularkoeffizient ks oder Oberflächennormalen. In den Knoten werden diese
Parameter aus darunterliegenden Halbbäumen gesammelt und weiter bearbeitet, um schließlich die
Farbgebung des Pixels zu erhalten. So werden z.B. Knoten als Spekularterm, Ambienter Term aber
auch Square root oder Mix-Knoten bezeichnet.
Abbildung 2.1. Shade tree für Kupfer, nach Robert L. Cook [Coo84].
Unterschiedliche Objekte können verschiedene Schattierungsbäume haben. Der Mix-Knoten erlaubt
das Mischen spezieller Shader für besondere Zwecke wie beispielsweise Holzmaserung.
Bemerkung 2.1 Außer Shade trees gibt es zur Modellierung von Licht sogenannte Light trees und
zur Modellierung von atmosphärischen Effekten entprechend Atmosphere trees.
20
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
Abbildung 2.2. Der mix-Knoten in einem Shade tree, nach Robert L. Cook [Coo84].
Lichter und ihre Parameter werden genau wie Objekte behandelt. Lichtberechnung und Streuung in
der Atmosphäre hängt vom Betrachterstandpunkt und der z-Tiefe ab.
Abbildung 2.3. Ein Light tree gibt die Lichtposition zurück, nach Robert L. Cook [Coo84].
Bemerkung 2.2 Häufig interessiert bei einem Highlight NICHT die Position der erzeugenden Lichtquelle sondern nur, WO es erscheint. Also möchte man ein Highlight positionieren und die Lichtrichtung als Ergebnis erhalten. Ein entsprechender Light tree ist in Abb. 2.3 gezeigt.
Beispiel 2.1 Ein benutzerseits definierter Shader kann die Welt aus der Sicht einer Biene (andere
Wahrnehmung der Spektralfarben) wiedergeben.
Beispiel 2.2 Relativitätsaspekte wie die spektrale Verzerrung bei Lichtgeschwindigkeit kann in einem
Shader implementiert werden. Dabei werden Projection trees nötig, die neben den Standardprojektionen wie paralleler und linear perspektivischer Projektion auch den gekrümmten Raum darstellen
können. Durch den Doppler Effekt entsteht eine Farbverschiebung, bei der sehr schnell unser übliches Farbspektrum rekalibriert werden muss, da ansonsten alle Farben, auf die man zufliegt, zu weiß
überstrahlen.
2.2. SHADE TREES
21
Abbildung 2.4. Tübingen, links: relativistisch verzerrt, rechts auch unter Berücksichtigung des Doppler Effekts
bei der Ausbreitung des Lichts. Bilder von Ute Kraus.
2.2.1
Reyes-Pipeline und Renderman Interface
Cook, Carpenter und Catmull gelten als die Urheber der sogenanten Reyes-Pipeline (siehe Abb. 2.5).
Die geographische Nähe der Lukasfilm Studios zu Point Reyes (siehe Abb. 2.6) hat dem Akronym aus
Renders everthing you ever saw sicherlich Vorschub geleistet.
Renderman greift diese Pipeline auf und speichert in sogenannten RIB-Files, dem Renderman Interface Bytestream die Punkte auf der Oberfläche eines Objekts, ihre Orientierung und die Lichtquellen
und übergibt diese einem Surface shader, der daraus Lichtfarbe und Lichtrichtung bestimmt. Wie
aus Abb. 2.5 hervorgeht, stellt das RIB-File die Eingabe für Programme wie beispielsweise 3Delight
dar, die Renderman Formate lesen und in Bilddaten ausgeben können. Renderman ist auf das (Nach-)
Bearbeiten einzelner Frames spezialisiert und beschränkt. Das Programm hat nicht den Anspruch,
Animationen zu erstellen, also zwischen einzelnen Bildern zu vermitteln.
2.2.2
Dicing oder Würfelalgorithmus
Ähnlich zu Catmulls Subdivision Algorithmus für Pixel werden beim Dicing alle Objekte in Mikropolygone zerlegt, deren Kantenlänge Subpixelgröße hat (Beispielsweise 1/2 Pixel).
(1) Dicing geschieht vor der perspektivischen Transformation, d.h. man schätzt die Größe der Mikropolygone aufgrund der anschließenden perspektivischen Transformation.
(2) Schattierung geschieht in Weltkoordinaten. Da alle quadrilateralen Polygone unter Pixelgröße
sind, kann mit einfachem Flatshading gearbeitet werden, das nur einen Farbwert für jedes Polygon
kennt.
(3) Das Bild wird in einzelne Rechtecke unterteilt, um nicht alle Gitter von Mikropolygonen und
Subpixelinformationen für das gesamte Bild sequenziell abarbeiten zu müssen. Auf diese Weise ist
22
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
Abbildung 2.5. Reyes-Pipeline.
der Algorithmus leicht parallelisierbar.
(4) Jedes Objekt wird mit der linken oberen Ecke seiner Bounding Box in das Rechteckgitter einsortiert. Die Bildbereiche werden nun von links nach rechts und von oben nach unten abgearbeitet.
Im Speicher muss nur die Information für einen Bildbereich gehalten werden, mit Ausnahme der
z-Werte, so dass die Speichertiefe des z-Buffers limitierend ist.
2.3
C for graphics
Mit der Programmbibliothek C for graphics (Cg) entstand 2002 ein verlässlicher Standard zur Ansteuerung der programmierbaren Teile eines Graphikprozessors. Bis dahin musste für jede Graphikkarte ein eigenes Interface zum Beispiel in Assembler geschrieben werden, was einerseits eine Hürde
für viele Programmierer darstellte und andererseits das Portieren der Anwenderprogramme extrem
schwierig machte.
2.3. C FOR GRAPHICS
23
Abbildung 2.6. Road to Point Reyes, eine Simulation aus Shade trees von Robert L. Cook [Coo84], und die Landkarte mit der entsprechenden Stelle.
Die Entwicklung von Cg wurde von Bill Mark bei NVIDIA in enger Kooperation mit Microsoft
betrieben, womit die beiden entscheidenden Plattformen für Graphikentwicklung, nämlich OpenGL
und Direct3D abgedeckt wurden. Über das Cg Tutorial von Fernando und Kilgard (siehe [FK03]),
das auf der SIGGRAPH 2003 zum Bestseller wurde, fand die Sprache rasche Verbreitung. Als rufende Programme sind Applikationen in beiden Graphikbibliotheken gleichermaßen möglich, und
Cg-Programme brauchen diesen APIs nicht angepasst werden. Dabei speist sich die Cg-Bibliothek
aus drei wesentlichen Quellen (siehe Abb. 2.7), nämlich der in der Graphikprogrammierung weit verbreiteten Programmiersprache C/C++, der aus der Reyes-Pipeline motivierten Shading Language und
den 3D APIs OpenGL und Direct3D.
Abbildung 2.7. Die Programmbibliothek C for graphics (Cg) speist sich aus drei Quellen.
24
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
Die folgende Skizze (Abb. 2.8) zeigt eine vereinfachte Graphikpipeline, über der man sich jede 3DApplikation oder ein Computerspiel denken kann, das mit OpenGL oder Direct3D Anweisungen auf
der CPU implementiert bleibt. Programmierbare Teile des Graphikprozessors sind der Vertex- (blau
hinterlegt) und der Fragmentprozessor (rot hinterlegt). Moderne Graphikkarten haben heute 16 bis 32
parallele Prozessoren in der Pixelpipeline, die das Rendering entsprechend beschleunigen.
OpenGL Anweisungen
?
Vertex-Verarbeitung
-
Rasterung
Transformationen
Licht- und Farbberechnung
-
Pixel-Verarbeitung
-
Framebuffer
Pixelbezogene
Farbberechnung
Abbildung 2.8. Vereinfachte OpenGL-Grafik-Pipeline. Die Teile, die sich bei neueren Grafikkarten frei programmieren lassen, sind farbig hinterlegt.
2.3.1
Cg - Historische Entwicklung
Die historische Entwicklung in der Zeitachse macht deutlich, wie sich seit den siebziger und speziell
in den achtziger Jahren parallele Stränge abzeichnen, die alle das gleiche Ziel hatten, nämlich ein
am Objekt orientiertes Bild schnell und in guter Qualität auf den Schirm bringen zu wollen (siehe
Abb. 2.9). Es wird ebenfalls deutlich, dass sich Standards nur dann durchsetzen, wenn sich eine kritische Firmenmasse auf diese Standards einlässt. Projekte wie NeXT sind über die Zeit eingestellt
worden.
2.3.2
Programmierbarer Vertex Prozessor
Untransformierte Knoten (Vertices) aus einem GPU-Frontend werden typischerweise als Vertex-IndexStream zu Graphikprimitiven zusammengestellt, um als Polygone, Linien und Punkte gerastert werden zu können. An dieser Stelle können die Knoten für eine optimale Darstellung umsortiert, transformiert und neu geordnet werden. Dadurch ist dieser Teil grundsätzlich programmierbar geworden
und lässt natürlich auch eigene Programmierung zu, die vor allem zur Laufzeit interessant wird, wenn
beispielsweise eine geänderte Transformation eine andere Dreieckszerlegung eines Polyeders erfordert.
2.3. C FOR GRAPHICS
25
Abbildung 2.9. Die historische Entwicklung im Überblick.
2.3.3
Programmierbarer Fragment Prozessor
Die schließlich gerasterten und für Interpolationen vortransformierten Fragmente sind über die Ortsangaben der Pixel (Pixel location stream) in der Pipeline auf dem Weg zum Colorbuffer. Im Fragmentprozessor erhalten sie ihre endgültige Schattierung häufig erst durch Texturen, die im Fall prozeduraler Texturen auch wieder notwendig während der Laufzeit anzupassen sind. Schon einfaches
Mipmapping setzt voraus, dass eine Entscheidung für die eine oder andere Textur von der Größe des
ankommenden Graphikprimitivs abhängt. Gerasterte vortransformierte Fragmente werden weiteren
Transformationen unterzogen: Bumpmapping und generelles Beeinflussen der Lichtmodelle ist auf
dieser Ebene leicht und vor allem schnell möglich.
2.3.4
CgFX Toolkit und Austauschformat
Ein standardisiertes Austauschformat zur Darstellung von Effekten setzt saubere Schnittstellen zu den
verschiedenen Graphikkarten voraus, die derzeit auf dem Markt erhältlich sind. Dann aber garantiert
26
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
Abbildung 2.10. Aufbau des programmierbaren Vertex Prozessors.
es auch die Verbreitung der nötigen Bibliotheken, die diesen Standard unterstützen. Je mehr große
Software-Pakete solche Austauschformate in ihren Code aufnehmen, um so stärker wird sich die
spezielle Implementierung verbreiten. Verlässlichkeit wird auf diese Weise propagiert.
Mit CgFX, einem Produkt von Microsoft und NVIDIA, wurde ein Austauschformat entwickelt, das
textbasiert, also lesbar und editierbar ist. Gebräuchliche Suffix ist *.fx.
CgFX geht in den folgenden Punkten über Cg hinaus:
1. Mechanismus für multiple Renderpfade
2. Beschreibung von nichtprogrammierbarem Renderstatus (Alpha-Test-Modus, Texturfilter)
3. Zusätzliche Annotation für Shaderparameter
Darüber hinaus wurde ein CgFX Toolkit zur Verfügung gestellt, das einen CgFX Compiler benötigt,
um zur Laufzeit ausführbare GPU Anweisungen zu erstellen. Auf dieser Basis sind Plugin-Module für
sogenanntes Digital Content Creation (DCC) möglich. Eine Beispieldatei ist am Ende dieses Kapi-
2.3. C FOR GRAPHICS
27
Abbildung 2.11. Aufbau des programmierbaren Fragment Prozessors.
tels in Abb. 2.14 dargestellt. Alle großen Animationsprogramme (Alias|Wavefront’s Maya, discreet’s
3dStudioMax und Softimage|XSI) unterstützen Cg über CgFX und DCC Applikationen.
2.3.5
Compiler und Bibliotheken
KEINE GPU kann ein Cg-Programm direkt ausführen. Es muss zunächst kompiliert werden. Dazu
wählt man ein 3D Programming Interface entweder in OpenGL (Prefix der Syntax: cgGL) oder
in Direct3D (Prefix der Syntax: cgD3D). Das dynamische Kompilieren (Kompilieren zur Laufzeit!)
wird über Cg-Bibliotheksaufrufe durchgeführt. Dazu besteht die Cg-Bibliothek aus (a) Cg-Runtime
instructions und (b) Cg-Compiler instructions.
Während ein C-Programm Dateien lesen und schreiben, über Standardschnittstellen mit dem Terminal
oder anderen Eingabeformen bedient werden, Graphiken anzeigen und über Netzwerk kommunizieren kann, geht das alles mit Cg nicht. Ein Cg-Programm kann NUR Positionen, Farben, Texturkoordinaten; Punktgrößen und uniforme Variablen entgegennehmen, Berechnungen durchführen und
Zahlenwerte zurückgeben.
Im Application Programming Interface (API) (siehe das OpenGL Beispiel 2.6) wird die nötige Headerdatei geladen.
28
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
Abbildung 2.12. Das CgFX-Austauschformat wird an den Cg-Kompiler übergeben.
#include <Cg/cg.h>
Diese Headerdatei wiederum lädt aus dem Standardpfad /usr/include/Cg die weiteren Header:
#include
#include
#include
#include
#include
<Cg/cg_bindlocations.h>
<Cg/cg_datatypes.h>
<Cg/cg_enums.h>
<Cg/cg_errors.h>
<Cg/cg_profiles.h>
Das Interface zu OpenGL wird mit
#include <Cg/cgGL.h>
geladen, indem bereits der Aufruf für cg.h enthalten ist. Also genügt der letzte Aufruf.
Die entsprechenden Bibliotheken stehen üblicherweise unter /usr/lib/libCg.so respektive
/usr/lib/libCgGL.so. Sie können mit den Kompilerflags -lCg beziehungsweise -lCgGL
geladen werden. Das Kompilieren der Shader zur Laufzeit geschieht über Bibliotheksaufrufe!
Eine Entry function definiert ein Cg-Vertex- oder Cg-Fragmentprogramm und ist ein Analogon zur
main function in C/C++. Da man aber viele solcher Entry functions in einem rufenden API haben
2.3. C FOR GRAPHICS
29
kann, sollte man sie nicht ebenfalls main nennen, um Verwirrungen vorzubeugen. Internal functions
sind Hilfsfunktionen, die von den Entry functions aufgerufen werden können. Das sind beispielsweise
von der Cg-Standardbibliothek zur Verfügung gestellte oder selbstgeschriebene Funktionen. Die Zeile
return OUT; gibt die initialsierte Output Struktur zurück (mit entsprechender Semantik, die den
einzelnen Komponenten zugeordnet ist).
Zum Kompilieren von Cg-Code muss zum einen der Name des Cg-Programms bekannt sein, zum
anderen muss der Profilname jeweils für Vertex- und Fragmentprofil gewählt werden. Da die Profile
abhängig von der Graphikkarte sind, sollte ein Profil gewählt werden, das nach Möglichkeit von allen
Graphikkarten unterstützt wird. Will man aber die Besonderheit eines speziellen Profils oder einfach
ein neueres Profil und seine Vorzüge ausnutzen, sollte eine Abfrage an die GPU geschehen, mit der
man das Vorhandensein entsprechender Möglichkeiten sicherstellt und wahlweise einfachen Cg-Code
für ältere Graphikkarten zur Verfügung stellt.
Cg-Vertexprofile:
arbvp1
vs 1 1
vs 2 x
OpenGL Basic multivendor programmibility ARB-vertex-profile
DirectX8 Vertex shader
DirectX9 Vertex shader
Cg-Fragmentprofile:
arbfp1
ps 1 1
ps 2 x
2.3.6
OpenGL Basic multivendor programmibility ARB-fragment-profile
DirectX8 Pixel shader
DirectX9 Pixel shader
Ähnlichkeit mit C
Cg liest sich einfach, wenn man mit C vertraut ist: Viele Keywords sind gleich oder erschließen sich
einfach aus ihrem Name (hier ein Auszug aus der alphabetischen Liste):
asm*, bool, break, · · · , pixelfragment*, · · · , while
!!!ACHTUNG: Sie sollten Keywords NIE als Identifier verwenden!!!
Auch Strukturen sind in gleicher Weise aufgebaut wie in C. Dem Keyword struct folgt ein Identifier mit dem Namen und in geschweiften Klammern die Liste der Variablen. Handelt es sich dabei
aber um eine IN- oder OUT-Struktur, wird jede Komponente um eine sogenannte Semantik erweitert.
30
2.3.7
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
Besonderheiten
Besonderheiten 1: Semantik Über die Semantik wird der Input oder das Ergebnis eines Cg-Programms an der richtigen Stelle in die Graphikpipeline eingegliedert. Die Semantik wird hinter einem
Doppelpunkt und in Großbuchstaben hinter einem Membernamen angefügt und mit einem Komma
vom nächsten Member getrennt. POSITION, COLOR, TEXCOORD0, PSIZE, NORMAL sind
mögliche Semantiken. Die Semantik POSITION hängt entscheidend davon ab, ob sie über ein Vertexoder ein Fragmentprofil in die Graphikpipeline eingefügt werden soll, denn eine Knotenposition
wird anders interpretiert, als eine Rasterposition. Texturkoordinaten werden mit angehängter Ziffer
einem Texturkoordinatensatz zugeordnet, da man häufig mehrere Texturzugriffe in einem Programm
ermöglichen möchte. Und schließlich will man mit PSIZE die sogenannten Partikelsysteme ebenfalls
hardwarenah steuern können.
Exkurs Partikelsysteme
Partikelsysteme stellen eine Möglichkeit dar, ein sprühendes oder fließendes Objekt und seine Materialeigenschaft über einzelne, nicht verbundene Punkte zu modellieren. Dabei berechnet man Trajektorien dieser Partikel nach (einfachen) physikalischen Gesetzen und modelliert graphisch die Darstellung dieser einzelnen Punkte, in dem man beispielsweise die Punktgröße mit der Zeit variiert. Bessere
Effekte erzielt man mit sogenannten Point sprites, kleinen meist quadratischen Texturen, die automatisch senkrecht zur Blickrichtung ausgerichtet werden und deren Mittelpunkt mit der Punktposition
übereinstimmt. Die üblichen Texturkoordinaten (typischerweise laufen uv-Koordinaten in den Intervallen [0,1]x[0,1]) werden ebenfalls automatisch an das entsprechende Quadrat mit der angegebenen
Punktgröße angepasst.
Mit Partikelsystemen kann man beispielsweise Feuerwerk, Spritzwasser, Springbrunnen, Wasserfälle,
aber auch semitransparente Objekte wie Flammen oder Rauch ansprechend und einfach darstellen.
Beispiel 2.3 Mit der einfachen Gleichung
1
Pfinal = Pinitial + vt + at2
2
wird eine Vorwärtsintegration eines Anfangswertproblems beschrieben. Wählt man für jedes Partikel eine zufällige Anfangsgeschwindigkeit v bei konstanter (Erd-)Beschleunigung a, kann man die
Punktgröße und Farbe mit der Zeit t variieren.
Besonderheiten 2: Vektoren Auf der Graphikhardware werden immer wieder Vektoroperationen
benötigt, die mit Rasterkoordinaten, Farben oder homogenen Raumkoordinaten umgehen und daher
typische Vektorlängen von zwei, drei oder vier haben. Daher liegt es nahe, diese Operationen in der
Hardware abzubilden und die Graphikleistung auf diese Weise zu beschleunigen. Will man diese
Graphikleistung optimal ansteuern, muss auch der Compiler entsprechende Datentypen kennen, was
2.3. C FOR GRAPHICS
31
Abbildung 2.13. Zwei Partikelsysteme mit Point sprites, die eine Flamme und einen Wasserstrahl mit entsprechend
unterschiedlichem Gravitationsverhalten darstellen. Bild von Daniel Jungblut.
in der Hochsprache C/C++ nicht der Fall ist. Cg dagegen kennt die Datentypen float2, float3,
float4 beziehungsweise entsprechende Vektoren, die mit den Standardnamen anderer Datentypen
und den Ziffern 2, 3 und 4 gebildet werden. Sie sind NICHT äquivalent mit einem Array derselben
Länge in C/C++, da die Vektoren als sogenannte Packed arrays gespeichert werden.
float x[4] 6= float4 x
Vektoren sind KEINE Keywords der Programmiersprache, könnten also als Identifier verwendet werden. Man sollte es aber vermeiden, um Verwirrungen vorzubeugen.
Bemerkung 2.3 Wenn zwei Input-Vektoren als packed arrays gespeichert sind, können typische vektorwertige Operationen (skalare Multiplikation, Addition, Negation, Skalarprodukt, Kreuzprodukt,
Vertauschen von Indizes) in einer einzigen Instruktion berechnet werden. Packed arrays helfen dem
Cg-Compiler, die schnellen Vektoroperationen der programmierbaren GPUs auszunutzen. Die GPU
ist ein Vektorrechner.
Außerdem sollte man beachten, dass man auf die einzelnen Einträge eines Vektors sehr effizient mit
der Ziffer des entsprechenden Index zugreift. Dagegen ist ein Zugriff über eine Referenz, die erst
ausgewertet werden muss, ineffizient oder sogar unmöglich.
float4 x = {1.0, 0.0, 1.0, 1.0};
// Initialisieren wie in C
32
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
int index = 3;
float scalar;
scalar = x[3];
// Effizienter Zugriff, scalar = 1.0;
scalar = x[index];
// Ineffizient oder unmoeglich!
Besonderheiten 3: Matrizen Da die GPU natürlich auch Matrix-Vektor-Operationen hardwarenah
unterstützen muss, liegt es nahe, dass es in Cg dafür entsprechende Matrizen gibt. Hier einige Beispiele:
float4x4
half3x2
fixed2x4
double4x4
16 Elemente 32 bit
6 Elemente 16 bit
8 Elemente 32 bit, [-2.0, 2.0 [
16 Elemente 64 bit
Effizienter Datentyp für Fragmentoperationen
Effizient für exp2-Auswertung (Fog)
Die sechs Elemente von half3x2 entsprechen einer Matrix mit drei Reihen und zwei Spalten. Matrizen sind für alle besonderen Datentypen der GPU verwendbar, die im nächsten Paragraphen kurz
vorgestellt werden.
Besonderheiten 4: Datentypen Neu hinzugekommene Datentypen auf der GPU sind half und
fixed. Mit half haben insbesondere alle Fragmentoperationen geringeren Speicherbedarf und laufen schneller ab, ohne dass man beispielsweise bei Farbinterpolationen einen sichtbaren Unterschied
zur vollen Darstellung in float oder gar double ausmachen kann. Der Datentyp fixed dagegen verfolgt als Festkommazahl eine andere Philosophie, nämlich mit dem gleichen Speicherbedarf
eines float eine größere Genauigkeit im Bereich von [-2.0, 2.0 [ zu garantieren, also in einem Intervall, das bei Operationen mit den Einträgen zweier Vektoren der Länge Eins maximal auftreten kann.
Dieser Datentyp ist sehr effizient für exp2-Auswertungen, die beispielsweise für atmosphärische Effekte wie Nebel (Fog) gebraucht werden (plötzliches Erscheinen von Objekten in Abhängigkeit ihrer
z-Tiefe).
Besonderheiten 5: Konstruktoren Man kann alle diese Datentypen samt angehängter Ziffern wie
Funktionen benutzen, also eine beliebige Zahlenfolge in einen Vektor oder eine Matrix packen. Sie
sind damit sogenannte Konstruktoren.
float4(1, 0, 1, 1); // erzeugt einen Vektor (Packed array)
Besonderheiten 6: Qualifier uniform Mit dem Qualifier uniform wird deutlich gemacht, dass
eine Variable aus einem externen Programm, also üblicherweise einem OpenGL oder Direct3D API,
2.3. C FOR GRAPHICS
33
an das Cg-Programm übergeben wird. Anders als in Renderman darf ein als uniform übergebener Parameter durchaus auf der GPU verändert werden. In Cg wird nicht zwischen uniform und
einem nur in Renderman bekannten Qualifier varying unterschieden. Ein mit uniform übergebener Parameter wird als Variable behandelt. Wenn eine Variable nicht initialisiert wurde, kann das in
der Entry function immer noch geschehen und dabei auch mit einer Semantik versehen werden, die
beispielsweise für die Ausgabe dieses Cg-Programms benötigt wird.
Besonderheiten 7: Swizzling Eine syntaktische Besonderheit stellt das Swizzling dar. Damit ist
der Zugriff auf die Komponenten von Vektoren oder Matrizen in beliebiger Reihenfolge möglich.
Zunächst können die Komponenten entsprechender Vektoren float4 position, color; über
die folgende Konvention aufgerufen und zugewiesen werden (wenn kein w angegeben wird, ist implizit w = 1):
float3 P = position.xyz;
float4 Q = position.xyzw;
float4 C = color.rgba;
Beide Suffix Zeichenketten sind gültig, können aber nicht gemischt werden. Sie bezeichnen in natürlicher Weise die erste (r oder x), zweite (g oder y), dritte (b oder z) und vierte (a oder w) Komponente.
Weder C noch C++ unterstützen das Swizzling, da keine der Sprachen Vektorrechnung unterstützt.
Beispiel 2.4 Dieses Beispiel zeigt, wie einfach mit der Syntax einzelne Komponenten eines Vektors
überschrieben werden können.
float4 vec1 = float4(4.0, -2.0, 5.0,
float2 vec2 = vec1.yx;
// vec2 =
float scalar = vec1.w;
// scalar
float3 vec3 = scalar.xxx; // vec3 =
vec1.xw = vec2;
// vec1 =
3.0); // float4 als Konstruktor
(-2.0, 4.0)
= 3.0
(3.0, 3.0, 3.0)
(-2.0,-2.0, 5.0, 4.0)
Beispiel 2.5 Gleiches gilt für Matrizen mit der Notation *. m<row><column>.
float4x4 myMatrix;
float
myScalar;
float4 myVec4;
myScalar = myMatrix._m32;
myVec4 = myMatrix._m00_m11_m22_m33
myVec4 = myMatrix[0]
// myMatrix[3][2]
// Diagonale
// erste Reihe der Matrix
34
2.3.8
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
Fehlerbehandlung
Bei den Cg-Compilerfehlern gibt es einerseits die konventionellen Fehler wie inkorrekte Syntax
(Tippfehler) oder inkorrekte Semantik (falsche Anzahl der Parameter). Derartige Fehler treten bereits beim Vorkompilieren zu Tage, man kennt diese Art Fehler aus C/C++. Es empfiehlt sich, eine
Fehlerfunktion im OpenGL oder Direct3D API zur Verfügung zustellen, wie in Beispiel 2.6 geschehen. Syntaktische Fehler werden mit der entsprechenden Stelle aus dem API sowie dem Kontext des
Cg-Programms an das Terminal ausgegeben.
Eine zweite Art der Fehler ist neu: der profilabhängige Fehler. Das ausgewählte Vertex- oder Fragmentprofil unterstützt die (an sich korrekten) Aufrufe nicht. Hierbei unterscheidet man nun drei verschiedene Arten solcher profilabhängiger ERROR:
(a) Capability. Ein Beispiel: Bisher (2003) wird vom Vertexprofil kein Texturzugriff erlaubt, in Zukunft wird sich das ändern. Cg kann das heute schon kompilieren, aber die Hardware oder das 3D
API kann es nicht umsetzen.
(b) Context. Ein Beispiel: Ein Vertexprogramm muss die Semantik POSITION zurückgeben, sonst
entsteht ein Fehler. Dagegen kann ein Fragmentprofil keine entsprechende Vertexposition zurückgeben, weil das in den Fluss der Graphikpipeline nicht passt.
(c) Capacity. Ein Beispiel: Einige GPUs erlauben nur vier Texturzugriffe in einem Renderpfad,
bei anderen ist der Zugriff unbeschränkt. Diese Art Fehler ist schwierig zu finden, da die Anzahl
der Zugriffe oft nicht klar ersichtlich ist (vergleichbar mit einem Segmentation Fault in der CPUProgrammierung).
Beispiel 2.6 Ein in OpenGL geschriebenes API stellt die auf der CPU zu kompilierenden Programmteile vor. Zum besseren Überblick sind die Teile des Codes blau gefärbt, die grundsätzlich nötig sind
oder sich auf den programmierbaren Vertexprozessor beziehen. Dagegen sind die Teile mit Bezug auf
den programmierbaren Fragmentprozessor in rot hervorgehoben. Die Übergabe von Parametern ist
grün dargestellt.
/*
Open-GL program using Cg for programming a simple vertex-shader
by Daniel Jungblut, IWR Heidelberg, February 2008
based on example code of Cg Tutorial (Addison-Wesley, ISBN 0321194969)
by Randima Fernando and Mark J. Kilgard.
*/
#include <cstdlib>
#include <stdio.h>
#include <GL/glut.h>
#include <Cg/cg.h>
#include <Cg/cgGL.h>
static CGcontext
static CGprofile
static CGprogram
cg_context;
cg_vertex_profile;
cg_vertex_program;
2.3. C FOR GRAPHICS
static CGprofile
static CGprogram
35
cg_fragment_profile;
cg_fragment_program;
static CGparameter cg_parameter_vertex_scale_factor;
static CGparameter cg_parameter_vertex_rotation;
static CGparameter cg_parameter_fragment_color;
// Error checking routine for Cg:
static void checkForCgError(const char *situation) {
CGerror error;
const char *string = cgGetLastErrorString(&error);
if (error != CG_NO_ERROR) {
printf("%s: %s\n", situation, string);
if (error == CG_COMPILER_ERROR) {
printf("%s\n", cgGetLastListing(cg_context));
}
exit(1);
}
}
// keyboard callback:
void keyboard(unsigned char key, int x, int y) {
switch (key) {
case 27: // Escape
case ’q’:
cgDestroyProgram(cg_vertex_program);
cgDestroyProgram(cg_fragment_program);
cgDestroyContext(cg_context);
exit(0);
break;
}
}
// display function:
void display() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
cgGLBindProgram(cg_vertex_program);
checkForCgError("binding vertex program");
cgGLEnableProfile(cg_vertex_profile);
checkForCgError("enabling vertex profile");
// Hier werden die Werte der einheitlichen Parameter "scale_factor", "vertex_rotation" festgesetzt:
cgGLSetParameter1f(cg_parameter_vertex_scale_factor, 0.7);
cgGLSetParameter1f(cg_parameter_vertex_rotation, 1.509);
cgGLBindProgram(cg_fragment_program);
checkForCgError("binding fragment program");
cgGLEnableProfile(cg_fragment_profile);
checkForCgError("enabling fragment profile");
GLfloat color[] = {0.2, 0.7, 0.3};
cgGLSetParameter3fv(cg_parameter_fragment_color, color);
// Rendern eines Dreiecks. Hierfuer wurde keine Farbe ausgewaehlt!
glBegin(GL_TRIANGLES);
glVertex2f(-0.8, 0.8);
glVertex2f(0.8, 0.8);
glVertex2f(0.0, -0.8);
glEnd();
cgGLDisableProfile(cg_vertex_profile);
checkForCgError("disabling vertex profile");
cgGLDisableProfile(cg_fragment_profile);
checkForCgError("disabling fragment profile");
36
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
glutSwapBuffers();
}
int main(int argc, char **argv) {
glutInitWindowSize(400, 400);
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
glutInit(&argc, argv);
glutCreateWindow("Vertex and fragment shaders");
glutDisplayFunc(display);
glutKeyboardFunc(keyboard);
glClearColor(0.1, 0.2, 0.8, 1.0);
cg_context = cgCreateContext();
checkForCgError("creating context");
cg_vertex_profile = cgGLGetLatestProfile(CG_GL_VERTEX);
cgGLSetOptimalOptions(cg_vertex_profile);
checkForCgError("selecting vertex profile");
cg_vertex_program = cgCreateProgramFromFile(cg_context, CG_SOURCE,
"E6_vertex.cg", cg_vertex_profile, "more_complex_vertex_shader", NULL);
checkForCgError("creating vertex program from file");
cgGLLoadProgram(cg_vertex_program);
checkForCgError("loading vertex program");
// Verbinden der Variable "cg_parameter_vertex_scale_factor"
// mit der Variable "scale_factor" aus dem Vertex-Shader:
cg_parameter_vertex_scale_factor = cgGetNamedParameter(cg_vertex_program, "scale_factor");
checkForCgError("getting scale_factor parameter");
cg_parameter_vertex_rotation = cgGetNamedParameter(cg_vertex_program, "rotation");
cg_fragment_profile = cgGLGetLatestProfile(CG_GL_FRAGMENT);
checkForCgError("selecting fragment profile");
cg_fragment_program = cgCreateProgramFromFile(cg_context, CG_SOURCE,
"E6_fragment.cg", cg_fragment_profile, "simple_fragment_shader", NULL);
checkForCgError("creating fragment program from file");
cgGLLoadProgram(cg_fragment_program);
checkForCgError("loading fragment program");
cg_parameter_fragment_color = cgGetNamedParameter(cg_fragment_program, "color");
checkForCgError("getting fragment parameter color");
glutMainLoop();
return 0;
}
Beispiel 2.7 Passend zum vorhergehenden Beispiel ist hier ein Cg-Vertexprogramm aufgeführt.
/*
More complex vertex shader
by Daniel Jungblut, IWR Heidelberg, February 2008.
*/
void more_complex_vertex_shader(float4 position : POSITION,
out float4 out_position : POSITION,
uniform float scale_factor,
uniform float rotation) {
2.3. C FOR GRAPHICS
37
// Erzeugung der 2D-Skalierungsmatrix:
float2x2 scale_matrix = float2x2(scale_factor, 0.0, 0.0, scale_factor);
float sin_rot, cos_rot;
sincos(rotation, sin_rot, cos_rot);
float2x2 rotation_matrix = float2x2(cos_rot, -sin_rot, sin_rot, cos_rot);
// Transfomieren der Vertices mit Hilfe der Skalierungsmatrix:
out_position = float4(mul(scale_matrix, mul(rotation_matrix, position.xy)), 0, 1);
}
Beispiel 2.8 Ebenfalls zum vorhergehenden Beispiel passend ist hier ein ganz einfaches Cg-Fragmentprogramm aufgeführt.
/*
Simple fragment shader
by Daniel Jungblut, IWR Heidelberg, February 2008.
*/
void simple_fragment_shader(out float4 out_color : COLOR, uniform float3 color) {
out_color = float4(color, 1.0);
}
2.3.9
Parameter, Texturen und mathematische Ausdrücke
Mit dem Qualifier uniform werden Parameter eines externen Programms an das Cg-Programm
übergeben. Wenn eine Variable NICHT initialisiert wurde, kann das in der Entry function geschehen.
Hier können auch mit dem Qualifier const nicht veränderbare Konstanten gesetzt werden.
const float pi = 3.14159;
// NICHT veraenderbar!!!
pi = 4.0;
// NICHT erlaubt!
float a = pi++;
// NICHT erlaubt!
Texture sampler werden uniform übergeben, d.h. sie treten als Teil einer Eingabe an den Fragmentprozessor auf.
uniform sampler2D decal
// Teil einer IN-Struktur
Um auf eine Textur zuzugreifen, gibt es Standard Cg Funktionen, die den Namen der uniform übergebenen Textur mit den Texturkoordinaten versieht und als Farbe zurückgibt.
38
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
OUT.color = tex2d(decal, texCoord);
Folgende Texture sampler sind möglich:
Sampler Typ
Textur Typ
Anwendung
sampler1D
sampler2D
sampler3D
samplerCUBE
samplerRECT
Eindimensionale Textur
Zweidimensionale Textur
Dreidimensionale Textur
Cube Map Textur
Non-Power-of-Two
Non-Mipmapped 2D-Textur
1D Funktion
Abziehbilder (Decal), Normalenfelder
Volumendaten, Dämpfungsterme
Environment Maps, Skybox
Videofilme, Fotos
Tabelle 2.1. Mögliche Texturaufrufe
Mathematische Ausdrücke können einerseits Operatoren sein, die auf der Graphikkarte immer auch
für Vektoren gelten. Wenn skalare Operationen mit Vektoroperationen gemischt werden, wird der skalare Wert automatisch so häufig wiederholt, bis er die Länge des Vektors erreicht. Dieses Verschmieren eines Skalars auf einen Vektor wird Smearing genannt und garantiert wieder die Geschwindigkeitsvorteile eines Vektorrechners.
Operator Art
+
*
/
Negation
Addition
Subtraktion
Multiplikation
Division
Auswertung
Links nach rechts
Tabelle 2.2. Ausnahmen der Auswertung (Rechts nach links) bei: ++, +=, sizeof, (type)
Beispiel 2.9 Hier werden einige typische Vektoroperationen vorgestellt.
float3 modulatedColor = color * float3(0.2, 0.4, 0.5);
modulatedColor *= 0.5;
float3 specular = float3(0.1, 0.0, 0.2);
modulatedColor += specular;
negatedColor = -modulatedColor;
float3 direction = positionA - positionB;
Sehr effizient implementiert und daher eigenen Routinen gleicher Funktion vorzuziehen sind die in
der folgenden (überhaupt nicht vollständigen) Tabelle gelisteten Funktionsaufrufe. ACHTUNG: Es
gibt in Cg KEINE IO-Routinen, KEINE String-Manipulationen und KEINE Speicherallokationen.
2.4. ÜBUNGSAUFGABEN
Prototyp
abs(x)
cos(x)
cross(v1, v2)
ddx(a)
ddy(a)
dot(a,b)
reflect(v,n)
normalize(v)
determinant(M)
mul(M,N)
mul(M,v)
mul(v,M)
tex2D(sampler,x)
tex3Dproj(sampler,
texCUBE(sampler,x)
39
Profil
Beschreibung
alle
Vertex, Adv. Fragment
Vertex, Adv. Fragment
Advanced Fragment
Advanced Fragment
alle
Vertex, Adv. Fragment
Vertex, Adv. Fragment
Vertex, Adv. Fragment
Vertex, Adv. Fragment
Vertex, Adv. Fragment
Vertex, Adv. Fragment
Fragment
Fragment
Fragment
Absolutwert von Skalar oder Vektor
Kreuzprodukt zweier Vektoren
Richtungsableitung nach x im Fragment a
Richtungsableitung nach y im Fragment a
Skalarprodukt
Reflexion von Vektor v bei Normale n
Normalisieren des Vektors v
Determinante der Matrix M
Matrizenprodukt
Matrix-Vektorprodukt
Vektor-Matrixprodukt
2D-Texturaufruf
Projektiver 3D Texturaufruf
CubeMap Texturaufruf
Tabelle 2.3. Standardisierte Funktionsaufrufe, sehr effiziente Implementierung
Das Function overloading wird von allen diesen Funktionen ebenfalls sehr effizient unterstützt. Damit
ist gemeint, dass man sich nicht um den speziellen Datentyp kümmern muss, der in eine der Funktionen eingeht: die Cg-Bibliothek sucht für jeden Datentyp die richtige Funktion aus. Das gilt auch für
Vektoren, so dass es beispielsweise für die Funktion abs(x) egal ist, ob x ein Skalar oder ein Vektor
ist.
2.4
Übungsaufgaben
Aufgabe 2.1 Einfacher Vertex Shader
Ändern Sie den in der Übung vorgestellten einfachen Vertex-Shader wie folgt: Verschieben Sie das
Dreieck um einen beliebigen Offset. Verwenden Sie hierzu die in Cg vorgesehenen Vektorvariablen.
Das Dreieck soll anschließend noch voll sichtbar sein. Verändern Sie die Farbe des Dreiecks. Finden
Sie eine Möglichkeit jedem der drei Eckpunkte des Dreiecks eine andere Farbe zu geben?
Wichtig: Ändern Sie hierfür nur den Shader ”E3.cg”, nicht jedoch die C++-Datei ”main.cpp”!
Das in der Übung vorgestellte Beispielprogramm finden Sie auch im Netz unter
www.iwr.uni-heidelberg.de/groups/ngg/CG2008/lecture.php.
Beantworten Sie folgende Fragen durch Kommentare im von Ihnen geänderten Shader: Warum muss
40
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
das Programm nicht neu compiliert werden, wenn man nur den Shader verändert? Worin unterscheidet sich der Übergabeparameter der Shader-Funktion ”simple vertex shader” von den Übergabeparametern, die Sie aus C oder C++ kennen? Warum ist das so? Der hier vorgestellte einfache VertexShader verändert die Koordinaten und die Farbe der einzelnen Vertices. Welche Attribute eines Vertex
kann ein Vertex-Shader noch verändern?
Aufgabe 2.2 Listen aufrufen
Programmieren Sie ein Moebiusband als Trianglestrip. Zeichnen Sie mehrere dieser Bänder, wobei
Sie in Ihrer Display-Routine das Band direkt aufrufen oder ein mit void glNewList(GLuint list,
Glenum mode) vorkompiliertes Band zeichnen lassen. Lassen Sie sich die Framerate beim Drehen
nacheinander für beide Varianten auf dem Bildschirm ausgeben. Das Umschalten sollte über die
Taste l geschehen. Wie verhält sich die Framerate?
Aufgabe 2.3 Fragment-Shader
Der Ausgangscode für diese Aufgabe ist unter
www.iwr.uni-heidelberg.de/groups/ngg/CG2008/lecture.php zu finden. Erweitern Sie den Fragment-Shader, so dass die Farbe der Fragmente durch einen einheitlichen Parameter
über das Hauptprogramm festzulegen ist. Drehen Sie das Dreieck mit Hilfe des Vertex-Shaders um
einen Winkel, der durch das Hauptprogramm gesteuert werden kann. Erklären Sie den Unterschied
zwischen variierenden und einheitlichen Parametern. Erklären Sie den Begriff call-by-result. Warum
werden in Cg genau Vektoren bis zur Dimension 4 unterstützt? Schreiben Sie die Antworten zu den
Fragen als Kommentare in einen der beiden Shader.
Hinweis: Im Gegensatz zu Aufgabe E03 sind zur erfolgreichen Bearbeitung dieser Aufgabe auch
Änderungen im Hauptprogramm nötig.
Aufgabe 2.4 Heat Equation
Berechnen Sie eine numerische Lösung der linearen Wärmeleitungsgleichung
∂ut = ∆u auf R+ × Ω
u(x, 0) = u0 auf Ω̄
u = 0 auf R+ × ∂Ω
im Zweidimensionalen. Ein semiimplizites Diskretisierungsschema für den Zeitschritt führt zu dem
Gleichungssystem
(1 − τ ∆h )uτ = Auτ = u0
2.4. ÜBUNGSAUFGABEN
41
wobei u0 die Wärmeverteilung zu Beginn und uτ die Wärmeverteilung zum Zeitpunkt τ beschreibt.
Diskretisiert man den Laplace-Operator mittels finiter Differenzen, so ergeben sich die Gleichungen
i−1,j
(1 + 4τ )ui,j
+ ui+1,j
+ uτi,j−1 + ui,j+1
) = ui,j
∀ i, j
0
τ − τ (uτ
τ
τ
in denen die Indizes i und j die Raumkoordinaten der einzelnen Gitterpunkte angeben. Das Gleichungssystem soll mit Hilfe des Jacobi-Verfahrens gelöst werden für das sich folgende Iterationsvorschrift ergibt
ui,j
neu =
1
i,j
+ ui,j+1
+ ui,j−1
+ ui+1,j
(τ (ui−1,j
alt ) + u0 ).
alt
alt
alt
(1 + 4τ )
Implementieren Sie dieses numerische Lösungsverfahren, wobei die einzelnen Iterationsschritte des
Jacobi-Verfahrens in einem Fragment-Shader berechnet werden. Gehen Sie dabei wie folgt vor:
(a) Laden Sie von der Vorlesungswebseite die Datei waves.png herunter. Schreiben Sie zunächst
ein OpenGL Programm mit zweidimensionalem Weltkoordinatensystem, so dass jeder Texel dieser
Textur genau auf einen Pixel des Ausgabefensters abgebildet wird. Setzen Sie die Texturfilter auf
GL NEAREST um Interpolationsfehler zu vermeiden. Die vorgegebene Textur ist ein Graustufenbild.
Da später mehr als ein Farbkanal benötigt wird, übertragen Sie die Textur im RGB-Format an die
Graphikkarte, wobei die drei Kanäle R, G und B jeweils mit dem Grauwert des entsprechenden Texels
initialisiert werden.
(b) Erweitern Sie dieses Programm um einen Fragment-Shader, der die Auswertung der Textur übernimmt.
(c) Implementieren Sie eine idle-Funktion, die mit Hilfe des Befehls glCopyTexSubImage2D(...)
den Inhalt des aktuellen Color-Buffer in die Textur kopiert und anschließend das Bild neu zeichnet.
(d) Nach hinreichend vielen Jacobi-Iterationen ist das Gleichungssystem näherungsweise gelöst.
Speichern Sie nach 120τ Aufrufen der idle-Funktion den aktuellen Inhalt des Color-Buffers in eine
Datei und beenden Sie das Programm. Der Color-Buffer kann mit der Funktion glReadPixels(...)
ausgelesen werden.
(e) Implementieren Sie zum Abschluss das Jacobi-Verfahren in Ihrem Fragment-Shader. Verwenden
Sie den Rot-Kanal zur Speicherung des Iterationsfortschritts und den Grün-Kanal zur Speicherung
der rechten Seite u0 des Gleichungssystems. Um die Pixel am Rand des Bildes korrekt zu verarbeiten,
genügt es die Wrapping-Parameter für die Texturkoordinaten auf GL CLAMP zu setzen.
42
KAPITEL 2. GRAPHIKKARTEN PROGRAMMIERUNG
(f) Wenden Sie das Programm für τ = 1, 2, 4, 8 auf das Ausgangsbild an und speichern Sie die
Ergebnisbilder gut erkennbar ab. Beschreiben Sie das Ergebnis dieses Verfahrens als Kommentar
in Ihrem Shader.
2.4. ÜBUNGSAUFGABEN
Abbildung 2.14. Das CgFX-Austauschformat, eine Beispieldatei.
43