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