Animationsunterstützung für die deklarative GUI-Bibliothek Grapefruit Matthias Reisner 30. November 2008 2 Inhaltsverzeichnis 1 Einleitung 5 2 Funktionale reaktive Programmierung 7 3 Konzepte und Implementierung 9 3.1 Grafiksignale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3.1.1 Verkettung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 3.1.2 Zwischenspeicherung . . . . . . . . . . . . . . . . . . . . . . . 10 3.2 Transformationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3.3 Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.4 Primitive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.5 Farben und Beleuchtung . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.5.1 Beleuchtungsattribute . . . . . . . . . . . . . . . . . . . . . . 13 3.5.2 Farben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3.5.3 Materialien . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3.5.4 Beleuchtung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.6 Texturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 3.7 Anbindung an die Benutzeroberfläche . . . . . . . . . . . . . . . . . . 19 3.7.1 Projektion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 3.7.2 Widget GraphicDisplay . . . . . . . . . . . . . . . . . . . . . . 20 4 Zusammenfassung und Ausblick 23 4 Inhaltsverzeichnis 1. Einleitung Seit dessen Einführung im Jahr 1992 hat sich OpenGL zur am weitesten verbreiteten Programmierschnittstelle für 2D- und 3D-Grafikanwendungen entwickelt. Eigenschaften wie Plattformunabhängikeit, Erweiterbarkeit und eine Fülle von Möglichkeiten zur Darstellung von Grafiken führten auch dazu, dass OpenGL in der Insustrie heute die meist genutzte Programmierschnittstelle ist. Die OpenGL-Spezifikation beschreibt eine Fülle von Befehlen für die Darstellung von Grafiken, die überwiegend performance-orientiert sind. Viele davon sind aus diesem Grund Low-Level-Befehle, deren Verhalten nur unter bestimmten Kontextbedingungen definiert ist. Andere Befehle sind nur in Kombination mit bestimmten weiteren Befehlen sinnvoll oder erlauben die Übergabe ungültiger Parameter. Syntaktisch korrekte Befehlssequenzen können also Laufzeitfehler verursachen, die sich möglicherweise noch nicht beim Testen der entwickelten Software, sondern erst beim Einsatz unter realen Bedingungen bemerkbar machen. Die Haskell-Anbindung für OpenGL, HOpenGL, behebt bzw. reduziert einige dieser Probleme. Die Übergabe falscher Parameter an Funktionen wird dort bspw. weitesgehend unterbunden, indem neue Datentypen definiert werden, die nur Datenkonstruktoren enthalten, die zulässige Parameterwerte widerspiegeln. Außerdem werden bspw. zusammengehörige OpenGL-Befehle in neuen Funktionen gekapselt und können so nicht mehr unabhängig voneinander ausgeführt werden. Trotzdem ist es noch möglich fehlerhafte oder undefinierte, bzw. nicht sinnvolle Befehlssequenzen anzugeben. Diese Arbeit soll die oben beschriebenen Probleme im Umganmg mit OpenGL beheben, indem die komplexen OpenGL-Funktionalitäten in typsichere Schnittstellen abgebildet werden. Semantisch falsche oder, der OpenGL-Spezifikation nach, undefinierte Befehlssequenzen sollen bereits zur Compile-Zeit erkannt und durch Typfehler signalisiert werden. Entsprechend sollen typkorrekte Haskell-Ausdrücke zur Laufzeit keine Fehler produzieren, die auf einer fehlerhaften Ausführung von OpenGLBefehlen basieren. Es werden Implementierungsmethoden vorgestellt, die die beschriebenen Probleme beheben bzw. umgehen sollen. Dabei soll nicht der gesamte OpenGL-Funktionsumfang umgesetzt werden, sondern für die wichtigsten Funktionalitäten jeweils eine 6 1. Einleitung oder mehrere Beispielimplementationen angegeben werden, die das prinzipielle Vorgehen beim Beseitigen oben genannter Probleme demonstrieren. Die Implementierung erfolgt in Form einer Grafikbibliothek für die Haskell-Bibliothek Grapefruit, die es erlaubt, grafische Oberflächen deklarativ zu beschreiben. Grundlage dieser Arbeit bildet Grapefruit in der Version 0.0, sowie die OpenGL-Spezifikation in der Version 2.1. 2. Funktionale reaktive Programmierung Ein Großteil der heute verfügbaren GUI- und Grafikbibliotheken verwenden ein imperatives Programmiermodell. Dabei muss durch den Programmierer explizit beschrieben werden, wie eine gewisse Lösung erreicht werden soll. Das bedeutet z.B. das Erstellen von Widgets, das Registrieren von Eventhandlern auf diesen Widgets und das Lesen und Schreiben des Widget-Zustandes innerhalb dieser Eventhandler. Einen anderen Ansatz verfolgt das deklarative Programmiermodell. Hierbei liegt das Hauptaugenmerk auf der Beschreibung dessen was erreicht werden soll bzw. welches Problem gelöst werden soll, während imperative Ansätze eher eine Beschreibung erfordern, wie ein Problem gelöst werden soll. Solch ein deklaratives Programmiermodell für die Beschreibung reaktiver und interaktiver Systeme in funktionalen Programmiersprachen stellt die funktionale reaktive Programmierung, kurz FRP, dar. Das Schlüsselkonzept der FRP sind Signale. Ein Signal beschreibt dabei ein Teilverhalten des Systems über alle Zeit. Typischerweise existieren folgende Arten von Signalen: • Kontinuierliche Signale besitzen zu jedem Zeitpunkt einen Wert, können also als Abbildung der Zeit auf einen bestimmten Wertebereich aufgefasst werden. Technisch bedingt können kontinuierliche Signale allerdings nur zu diskreten Zeitpunkten gelesen werden, was ein Sampling erforderlich macht. Einfachstes Beispiel für diese Art von Signalen ist die Zeit selbst. • Diskrete Signale beschreiben diskrete Eventfolgen, besitzen daher also nicht zu jedem Zeitpunkt einen Wert. Ein typisches diskretes Signal stellt die Folge der eingegebenen Zeichen einer Computertastatur dar. Bei jeder Tasteneingabe wird dabei das aktuelle Zeichen auf einem Ereignisstrom transportiert. • Segmentierte Signale können als kontinuierliche Signale mit diskreten Änderungszeitpunkten aufgefasst werden. Sie besitzen zu jeden Zeitpunkt einen Wert, der sich allerdings nur zu diskreten Zeitpunkten ändert. Ein Beispiel wäre hier der Inhalt eines Textfeldes einer grafischen Oberfläche, dessen Inhalt sich durch, auf der Tastatur eingegebene, Zeichen ändert. 8 2. Funktionale reaktive Programmierung Die Parameter von FRP-Funktionen sind typischerweise solche Signale. Ändern sich Teilzustände des Systems, werden die neuen Zustandswerte über Ereignisströme (engl.: event streams) an die verknüpften Komponeten gesendet und die entsprechenden Funktionen neu ausgewertet. Das explizite Registrieren von Eventhandler entfällt also; Zustandsänderungen bewirken eine implizite Neuberechnung des Systems bzw. von Teilsystemen. Grapefruit Grapefruit ist eine Haskell-Implementierung von FRP mit Fokus auf der Beschreibung von grafischen Benutzeroberflächen und Grafiken. Grafische Oberflächen werden in Grapefruit als System verbundener Komponenten dargestellt, die über Signale miteinander kommunizieren. Das Beschreiben von Komponentensystemen aus Komponenten wird durch sogenannte Arrows erleichtert. Arrows sind eine Erweiterung der Haskellsyntax, die durch Implementierung der Klassen Arrow bzw. ArrowLoop genutzt werden kann. Grapefruit unterstützt kontinuierliche, diskrete und segmentierte Signale, wobei die Verknüpfung und Transformation von Signalen erfolgt rein funktional erfolgt. Vorherige FRP-Implementationen verwenden vorwiegend einen ereignisgetriebenen (engl. pull-based ) Ansatz, d.h. bei einem Ereignis oder einem Zeitschritt wird das gesamte System neu berechnet. Dagegen ist Grapefruit datengetrieben (engl. push-based ) und berechnet Teilkomponenten nur bei tatsächlichen Änderungen an den zu Grunde liegenden Signalen neu. Unnötige Updates werden so vermieden. Derzeitige Basis von Grapfruit ist Gtk2Hs, eine Gtk -Anbindung für Haskell, Grapefruit kann aber leicht an andere Oberflächenbibliotehen (auch plattformübergreifend) angebunden werden. 3. Konzepte und Implementierung 3.1 Grafiksignale Zur Implementierung eines Grafiksignals wäre der erste, intuitive Ansatz, ein segmentiertes Signal über einem Datentyp zu definieren, der den Aufbau einer Grafik widerspiegelt: type GraphicSignal = SSignal Graphic Dieser Ansatz scheint zunächst geeignet, da in OpenGL jede Änderung an einer Grafik zum vollständigen Neuzeichnen der Grafik führt. Bei jeder Änderung am Grafiksignal könnte die entprechende neue Grafik direkt auf der zugehörigen Oberfläche gezeichnet werden. Allerdings könnte so eine wichtige OpenGL-Komponente zur effizienten Darstellung komplexer Grafiken nicht genutzt werden – Display-Listen (siehe 3.1.2). OpenGL bietet die Möglichkeit, Befehle zum Setzen von Punktkoordinaten, sogenannten Vertices, nach deren Auswertung intern in einem optimierten Format zu speichern. Der Abruf dieser gespeicherten Werte ist dabei oftmals erheblich schneller, als die Punktkoordinaten neu zu berechnen und die Befehle zum Setzen der Koordinaten erneut auszuführen. Diese Effizienzsteigerung ist besonders deutlich festzustellen, wenn sehr viele Vertices durch komplizierte Berechnungen ermittelt werden, z.B. durch trigonometrische Funktionen wie bei Kugeloberflächen. Die Implementierung von Grafiksignalen unterscheidet sich daher vom intuitiven Ansatz, und verfolgt einen effizienteren Ansatz: Bei der Konstruktion eines Grafiksignals werden für alle, sich potentiell verändernden Eigenschaften der Grafik interne Referenzzellen angelegt, die Informationen über den aktuellen Zustand dieser Eigenschaft speichern. Analog werden die Referenzzellen auch erst bei der Zerstörung des Grafiksignals aufgelöst. Änderungen an der Grafik werden so nur noch durch Änderungen an den internen Referenzzellen bzw. durch Neuberechnung der DisplayListen dargestellt. Mit diesem Ansatz setzt sich ein Grafiksignals nun also aus drei Komponenten zusammen: • Initialisierungs-Aktion init(G): Diese I/O-Aktion legt die internen Referenzzellen zum Speichern von Daten an, initialisiert sie jeweils mit einem Anfangswert und führt ggf. OpenGL-Befehle, wie das Anlegen von Display-Listen, 10 3. Konzepte und Implementierung aus. Die Auswertung der Initialisierungs-Aktion liefert als Ergebnis ein Paar bestehend aus der Rendering-Aktion und einem Aktualisierungssignal. • Rendering-Aktion renderG : Die Rendering-Aktion beschreibt, wie die Grafik in Abhängigkeit der aktuellen Werte in den Referenzzellen auf der zugehörigen Zeichenfläche dargestellt (gerendert) werden muss. • Aktualisierungssignal Dupdate G : Das Aktualisierungssignal ist ein diskretes Signal über I/O-Aktionen und spiegelt die Änderungen an den Grafikeigenschaften wider. Jeder Wert dieses Signals ist eine I/O-Aktion, die die Änderungen, die an den internen Referenzzellen des Grafiksignals bzw. des OpenGL-Zustands vorgenommen werden müssen, durchführt. Zu beachten ist, dass renderG und Dupdate G erst nach der Auswertung von init(G) feststehen. Es ergibt sich also der folgende Haskelltyp zur Darstellung eines Grafiksignals: newtype GraphicSignal = GraphicSignal (IO (IO (), DSignal (IO ()))) 3.1.1 Verkettung Die Verkettung zweier Grafiksignale entspricht einem Verschmelzen der beiden Grafiken zu einer neuen Grafik. Dazu wurde die Funktion compound definiert, die zwei Grafiksignale G1 und G2 zu einem neuen Signal G verbindet. init(G) ergibt sich dabei aus dem Auswerten von init(G1 ) und init(G2 ), renderG durch renderG1 renderG2 , sowie Dupdate G durch = Dupdate G1 ++ Dupdate G2 . 3.1.2 Zwischenspeicherung Bei der Darstellung von animierten Grafiken kommt es häufig dazu, daß sich bestimmte Teile der Grafik nur selten oder nie ändern. Für diese Teile genügt es die Auswertung der Grafikbeschreibung nur dann durchzuführen, wenn zuvor tatsächlich eine Änderung stattgefunden hat. Die ausgewertete Grafikbeschreibung kann bis zur nächsten Änderung zwischengespeichert und in der Rendering-Aktion abgerufen werden. In den meisten Fällen bringt dies einen erheblichen Performanzgewinn bei der Darstellung der Grafik. OpenGL bietet für diesen Zweck das Prinzip der Display-Listen. Mittels dem Befehl defineNewList bzw. defineList und dem Parameter Compile kann eine Liste von OpenGL-Befehlen zu einer Gruppe zusammengefasst werden, und in einer DisplayListe gespeichert werden. Der callList-Befehl wird beim Rendering genutzt, um die so gespeicherten Befehle bereits ausgewertet abzurufen. Zum Zwischenspeichern von Grafiken innerhalb von Grafiksignalen wird die Funktion cached bereitgestellt. Zu einem Grafiksignal G entsteht ein Grafiksignal G0 = cached G, dessen Rendering-Aktion renderG innerhalb der Funktion defineList ausgeführt wird. Alle durch renderG erzeugten Vertices werden so innerhalb einer Display-Liste zwischengespeichert. Die sich ergebende Rendering-Aktion renderG0 entsteht durch den Aufruf der gespeicherten Display-Liste. Eine I/O-Aktion des Änderungssignals Dupdate G0 ergibt sich durch Ausführen der I/O-Aktion von Dupdate G , erneutem Auswerten der Rendering-Aktion renderG mit den aktuellen Werten und anschließendem Überschreiben der Display-Liste mit renderG . 3.2. Transformationen 3.2 11 Transformationen Bei Transformationsmatrizen handelt es sich in OpenGL immer um 4 × 4-Matrizen1 . Zur Darstellung solcher Matrizen wurde der Datentyp Matrix4x4 eingeführt. Jeder 4-dimensionale Vektor beschreibt dabei eine Spalte der Matrix. data Matrix4x4 t = Matrix4x4 (Vector4D (Vector4D (Vector4D (Vector4D t) t) t) t) Die Funktion transpose berechnet zu einer Matrix M die transponierte Matrix M T . transpose :: Matrix4x4 t -> Matrix4x4 t Des weiteren wurde der Datentyp Transformation definiert, der häufig verwendete Klassen von Transformationsmatrizen zusammenfasst und eine intuitivere Beschreibung der anzuwendenden Transformationen erlaubt. data Transformation = | | | Transformation (Matrix4x4 Scalar) Rotation (Vector3D Scalar) Scalar Translation (Vector3D Scalar) Scaling (Vector3D Scalar) Dabei beschreibt Transformation M eine Transformation, die aus der Multiplikation von M mit der aktuellen Modelview-Matrix hervorgeht; Rotation v α eine Rotation von α Grad um die Rotationsachse mit dem Richtungsvektor v; Translation v sx eine Verschiebung um den Vektor v und Scaling sy eine Skalierung um den sz Faktor sx in x-, sy in y- bzw. sz in z-Richtung. Mit Hilfe der Funktion transformation lassen sich Grafiksignale einer Transformation unterwerfen. Die Funktion erhält ein Signal ST über einer Tranformation T und ein Grafiksignal G, und gibt ein Grafiksignal G0 zurück, auf das die Transformation angewendet wird. transformation :: SSignal Transformation -> GraphicSignal lighting -> GraphicSignal lighting Dabei ergibt sich init(G0 ) aus dem Anlegen einer Referenzzelle für den aktuellen Wert von ST und dem Auswerten von init(G). Die Rendering-Aktion renderG0 entsteht durch Lesen des aktuellen Wertes T aus der Referenzzelle und dem Anwenden der Transformation T . Dazu wird eine Kopie der aktuell gesetzten Modelview-Matrix auf dem OpenGL-Matrixstapel gesichert und die zu T gehörige Matrix mit der aktuellen Modelview-Matrix multipliziert. Beim anschließenden Ausführen von renderG wirken sich alle Änderungen an der Modelview-Matrix auf die Objektkoordinaten aller Vertices aus G aus. Damit sich die Änderungen nicht auch auf später ausgeführte OpenGL-Befehle beziehen, wird nach dem Ausführen von renderG die aktuelle Modelview-Matrix wieder durch die auf dem Matrixstapel gesicherte ersetzt und von dort entfernt. Das Änderungssignal Dupdate G0 ergibt sich schließlich aus Dupdate ST ++ Dupdate G . 1 OpenGL speichert Vertices intern in Form von 4-dimensionalen Koordinaten, wobei die 4. Koordinate, die sogenannte w-Koordinate, für die Normalisierung von Vertices-Koordniaten genutzt wird. 12 3. Konzepte und Implementierung 3.3 Objekte Im Modul Graphics.Rendering.Grapefruit.Objects werden Grafiksignale zur Verfügung gestellt, die einfache geometrische Körper beschreiben. Alle beschriebenen Körper besitzen voll ausgefüllte Oberflächen und das zugehörige Änderungssignal ist leer; Form und Oberfläche der Körper kann also nur durch äußere Transformationen geändert werden. cube beschreibt einen Grafiksignal, das einen Würfel der Kantenlänge l = 1 (Einheitswürfel) zeichnet. Die Funktion cylinder ns nt liefert ein Grafiksignal, das einen Zylinder der Höhe h = 1 und approximierter kreisförmiger Grund- und Deckfläche mit dem Durchmesser d = 1 zeichnet. Der Zylinder besteht aus nt übereinandergelegten Scheiben, mit jeweils ns rechteckigen Seitenflächen. Ähnlich wie cylinder wird durch die Funktion cone ns nt ein Kegel der Höhe h = 1 und approximierter kreisförmiger Grundfläche mit dem Durchmesser d = 1 beschrieben. Der Kegel kann als degenerierter Zylinder aufgefasst werden, wobei die Deckfläche den Durchmesser d = 0 hat. Auch der Kegel besteht aus nt übereinandergeschichteten Scheiben mit jeweils ns trapezförmigen Seitenflächen. Die Funktion sphere ns nt liefert eine approximierte Kugel mit dem Durchmesser d = 1 (Einheitskugel). Die Kugel besteht aus nt übereinandergelegten Scheiben, mit jeweils ns trapezförmigen Seitenflächen. 3.4 Primitive In der Praxis sind die beschriebenen geometrischen Körper für viele Zwecke allerdings zu speziell, um komplexe Grafiken darstellen zu können. Zur Darstellung eines Torus kann bspw. keine der gegebenen Objekte genutzt werden. Komplexere Grafiken werden daher über die Zusammensetzung ihrer Oberfläche aus kleineren Teilflächen beschrieben. Die kleinsten dieser Teilflächen, z.B. Dreiecke oder Vierecke, werden Primitiven genannt. In OpenGL erfolgt die Beschreibung einer Primitiven durch Angabe ihrer Eckpunkte, der Vertices, in Form von 4-dimensionalen Koordinaten. Mit jedem Vertex können zusätzliche Informationen – Attribute – assoziiert sein, die das Erscheinungsbild der zugehörigen Teilfläche beeinflussen2 , z.B. ein Farbwert, Texturkoordinaten, etc. Die Befehle zum Setzen der einzelnen Vertices erfolgt in OpenGL nur zwischen den Befehlen glBegin und glEnd, bzw. in HOpenGL in einer I/O-Aktion innerhalb der Funktion renderPrimitive, die einem entsprechenden Begin/End-Paar entspricht. Werden diese Befehle außerhalb dieser Umgebung aufgerufen, so ist deren Verhalten nicht definiert. Ebenso dürfen innerhalb der Begin/End-Umgebung nur eine vorgegebene Menge von Befehlen aufgerufen werden, nämlich die Befehle, die die entsprechenden Vertex-Attribute setzen. Das Aufrufen von anderen Befehlen innerhalb dieser Umgebung stellt einen Fehlerfall dar und resultiert in undefiniertem Verhalten. Das Definieren einer weiteren Begin/End-Umgebung innerhalb solch einer Umgebung stellt ebenfalls solch einen einen Fehlerfall dar. 2 Ein Vertex kann auch gemeinsamer Eckpunkt mehrerer Primitiven sein, z.B. bei der Primitivenart TriangleStrip. Die assoziierten Attribute werden auch in diesem Fall nur einmal angegeben und wirken sich auf alle zugehörigen Primitiven gleichermaßen aus. 3.5. Farben und Beleuchtung 13 Um diese Art fehlerhafter Grafikbeschreibungen zu verhindern, erfolgt die Beschreibung von Grafiken nun nicht mehr direkt durch OpenGL-ähnliche Befehle, sondern durch Funktionen, die der eigentlichen Problemstellung semantisch näher liegen. Es soll dadurch verhindert werden, dass innerhalb und außerhalb einer Begin/EndUmgebung unzulässige Befehle ausgeführt werden. OpenGL stellt 10 verschiedene Arten von Primitiven bereit. Als welche Primitivenart die jeweiligen Vertices innerhalb einer Begin/End-Umgebung zu behandeln sind, wird über einen Parameter an glBegin bzw. renderPrimitive festgelegt. Im folgenden soll das neue Darstellungsprinzip am Beispiel von nicht zusammenhängenden Vierecken beschrieben werden, das sich aber leicht auch auf die anderen Primitivenarten übertragen lässt. Ein Viereck wird dargestellt durch seine vier Eckpunkte, jeweils in Form von 4dimensionalen Koordinaten. Dazu wird ein neuer Datentyp Quadrilateral eingeführt, der diese vier Punkte als Werte vom Typ Vertex4D, parametrisiert über dem Komponententyp Scalar, in einem Datenkonstruktor kapselt. Des weiteren werden die mit den jeweiligen Vertices verknüpften Attribute ebenfalls bei der Beschreibung des Vierecks mit angegeben. Da die Art der Attribute aus Effizienzgründen abhängig von gewissen Kontextbedingungen sein soll (siehe 3.5.1), wird der Typ der Attribute nicht konkretisiert, sondern der Datentyp Quadrilateral durch einen Typparameter attribute erweitert, der die Art der verknüpften Attribute in jedem Vertex widerspiegelt. data Quadrilateral attribute = Quadrilateral (Vertex4D (Vertex4D (Vertex4D (Vertex4D Scalar Scalar Scalar Scalar attribute) attribute) attribute) attribute) Das Prinzip zur Konstruktion eines Grafiksignals aus einem Signal über einer Liste von Vierecken wird durch folgenden Pseudocode beschrieben3 : quadrilaterals :: AttributeList attributes => ListSignal (Quadrilateral attributes) -> GraphicSignal quadrilaterals quadListSignal = GraphicSignal $ do (getQuads, quadsUpdate) <- newReferencCell quadListSignal return (do quads <- getQuads renderPrimitive Quads $ transformToVertices quads , quadsUpdate) Die Funktion transformToVertices beschreibt dabei eine I/O-Aktion, die die Vertices aller Vierecke sowie die zugehörigen Vertex-Attribute setzt. 3.5 3.5.1 Farben und Beleuchtung Beleuchtungsattribute OpenGL bietet die Möglichkeit Grafiken durch Lichtquellen beleuchten zu lassen. Zur korrekten Darstellung von beleuchteten Grafiken ist es jedoch erforderlich, zusätzliche Attribute, wie bspw. Normalenkoordinaten oder Materialeigenschaften, mit 3 Für segmentierte Signale über Listen stellt Grapefruit einen eigenen Signaltyp ListSignal bereit, der Änderungen an der Liste inkrementell und damit effizienter beschreibt 14 3. Konzepte und Implementierung jedem Vertex zu verknüpfen. Für unbeleuchtete Grafiken sind wiederum andere Attribute als für beleuchtete Grafiken mit den Vertices verknüpft. Um auch hier Ineffizienzen, durch den Aufruf von OpenGL-Befehlen zum Setzen nicht benötigter Attribute, zu vermeiden, wird im folgenden zwischen beleuchteten und unbeleuchteten Grafiksignalen unterschieden. Um diese Unterscheidung im Typsystem widerzuspiegeln, erhält der bisherige Datentyp GraphicSignal eine zusätzliche Typvariable lighting, die den Beleuchtungszustand des Grafiksignals angibt. Erlaubte Beleuchtungszustände sind alle Instanzen der Klasse LightingState, d.h. Lit für beleuchtete und Unlit für unbeleuchtete Grafiksignale. class LightingState state where type LightingStateAttribute state :: * setLightingStateAttribute :: state -> LightingStateAttribute state -> IO () withLightingState :: state -> IO a -> IO a initLightingState :: state -> IO () Für jeden Beleuchtungszustand müssen Funktionen definiert werden, die eine I/OAktion einem bestimmten Beleuchtungszustand unterwerfen (withLightingState), einen Beleuchtungszustand initialisieren (initLighingState) und die entsprechenden Attribute zu einem Vertex setzen (setLightingStateAttributes). Jede Instanz der Klasse LigthingState definiert des weiteren ein assoziieres Typsynonym LightingStateAttribute, welches den Typ eines, mit einem Vertex assoziierten, Attributwertes definiert. Entsprechend ergibt sich nun die abgeänderte Definition für Grafiksignale mit dem Typparameter lighting: newtype GraphicSignal lighting = GraphicSignal (IO (IO (), DSignal (IO ()))) 3.5.2 Farben Zur Beschreibung von Farben wurde der Record-Datentyp RGBAColor eingeführt. Die Beschreibung einer Farbe C als RGBA-Farbwert erfolgt durch Angabe des Rotanteil cR (C) mittels red, des Grünanteils cG (C) mittels green, des Blauanteils cB (C) mittels blue und der Transparenz cA (C) mittels alpha. Die einzelnen Komponenten sind vom Typ Scalar und es gilt cR (C), cG (C), cB (C), cA (C) ∈ [0..1]. Volltranzparente Farben werden durch cA (C) = 0, vollständig opake Farben durch cA (C) = 1 gekennzeichnet. Auf Basis dieser allgemeinen Spezifikationsmöglichkeit werden 16 Standard-Farben im Datentyp PlainColor bereitgestellt, deren zugehöriger RGBA-Farbwert mit der Funktion fromPlainColor ermittelt werden kann. Die Werte der einzelnen RGBAKomponenten sind in Tabelle 3.1 angegeben (es gilt dabei jeweils cA (C) = 1). 3.5.3 Materialien Für beleuchtete Grafiksignale genügt es nicht eine einzelne Farbe für die Oberfläche von Grafikobjekten anzugeben. Stattdessen wird die Beschreibung der Oberfläche von einem einzelnen Farbwert auf die Beschreibung eines Materials erweitert. Somit gilt für die damit versehenen Teile der Grafik nicht mehr ein absoluter Farbwert, 3.5. Farben und Beleuchtung 15 sondern es wird durch die Angabe von Farbkomponeten beschrieben, wie die Oberfläche auf unterschiedliche Lichtquellen reagieren soll. Dies geschieht durch Angabe vier verschiedener Farbwerte, die die Lichtfarbe des von Lichtquellen bestrahlten Materials widerspiegeln: • Emissive Lichtfarbe mE : Die einzige nicht von einer Lichtquelle abhängige Komponente ist die emissive Lichtfarbe. Sie spiegelt die vom Objekt ausgestrahlte Lichtfarbe wider. • Ambiente Lichtfarbe mA : Die ambiente Lichtfarbe stellt die, durch die Umgebung entstandene, indirekte Beleuchtung dar. Das Licht wird durch angenommene Reflexionen in der Umgebung so stark zerstreut, dass sich die Herkunftsrichtung nicht mehr bestimmen lässt. • Diffuse Lichtfarbe mD : Licht, das auf eine matte Oberfläche trifft und gleichmäßig in alle Richtungen verteilt wird, kann durch die diffuse Lichtfarbe beschrieben werden. • Spekulare Lichtfarbe mS : Spekulare Lichtfarbe stellt die Farbe eines Richtungslichtes dar, das vom Material ebenfalls in eine bestimmte Richtung zurückgeworfen wird. Jede einzelne dieser Lichtfarben wird durch einen Farbwert vom Typ RGBAColor beschrieben. Die drei von einer Lichtquelle abhängigen Farben mA , mD und mS wurden in einem Record-Datentyp LightColor gekapselt. Die zugehörenden RecordFunktionen sind ambient für mA , diffuse für mD und specular für mS . data LightColor = LightColor { ambient :: RGBAColor, diffuse :: RGBAColor, specular :: RGBAColor } Eine vollständige Beschreibung eines Materials kann nun über den Record-Datentyp Material geschehen. Die Definition der emissiven Lichtfarbe mE erfolgt durch Angabe eines Farbwertes vom Typ RGBAColor über emissionColor. Alle lichtquellenabhängigen Lichtfarben (mA , mD , mS ) werden durch einen Wert vom Typ LightColor über reflectionColor beschrieben. Diese Lichtfarben spezifizieren die Farbanteile, die vom Material aus dem einstrahlenden Licht reflektiert werden sollen. Zur Beschreibung der Stärke des Oberflächenglanzes msh dient die Funktion shininess mit einem Argument vom Typ Scalar. Gültige Werte für msh sind Werte aus dem Bereich 0, für matte Oberflächen, bis 128, für sehr glänzende Oberflächen. Je höher der Wert für msh , desto konzentrierter wird das Licht reflektiert, d.h. desto kleiner ist die Fläche, auf der das Licht reflektiert wird. Zu beachten ist, daß sich msh nur auf den spekularen Anteil mS des eingeworfenden Lichtes bezieht. data Material = Material { reflectionColor :: LightColor, emissionColor :: RGBAColor, shininess :: Scalar } 16 3. Konzepte und Implementierung Schließlich besteht die Möglichkeit, für eine Oberfläche sowohl die Vorderseite, als auch die Rückseite durch zwei verschiedene Materialien zu beschreiben. Der RecordDatentyp DoubleFaceMaterial stellt die Funktionen frontFace für das Material der Vorderseite und backFace für das Material der Rückseite zur Verfügung. Beide Funktionen erwarten ein Argument vom Typ Material. Die jeweilige Vorderseite bzw. Rückseite wird durch den Normalenvektor der Fläche bestimmt. data DoubleFacedMaterial = DoubleFacedMaterial { frontFace :: Material, backFace :: Material } 3.5.4 Beleuchtung Während die Darstellung von unbeleuchteten Grafiksignalen ohne zusätzliche Lichtquellen vonstatten geht, können für beleuchtete Grafiksignale drei verschiedene Lichtquellentypen definiert und in die Szene eingefügt werden. Dazu wird die Funktion light i SL SA bereitgestellt, die ein beleuchtetes Grafiksignal konstruiert, das eine in die Szene eingefügte Lichtquelle repräsentiert. Dabei ist i vom Typ Int, eine für jede Lichtquelle eindeutige ID, SL ein Signal über den Lichtquellentyp L, und SA ein Signal über den Aktivitätszustand A der Lichtquelle. A wird dargestellt durch einen Bool-Wert, wobei die Werte True für aktiv (d.h. die Lichtquelle wird in die Farbberechnung mit einbezogen) und False für inaktiv stehen. Als Ergebnis wird ein beleuchtetes Grafiksignal G zurückgegeben. Hierbei ergeben sich init(G) aus dem Anlegen von Referenzzellen für L und A, renderG aus dem Setzen der entsprechenden Lichtparameter zur aktellen Lichtquellenart L für die Lichtquelle i und dem Setzen des aktuellen Aktivitätszustandes A, sowie Dupdate G aus Dupdate SL ++ Dupdate SL . Der Datentyp Light beschreibt eine Lichtquelle und besitzt drei verschiedene Datenkonstruktoren, je einen für Direktionallicht, Punktlicht und gerichtetes Punktlicht. Die einzelnen Lichtquellentypen werden im folgenden näher beschrieben. Zur Beschreibung eines 3-dimensionalen Richtungsvektors wurde der Typ Direction definiert, der nur einen Typalias für einen 3-dimensional Vektortyp Vector3D Scalar. type Direction = Vector3D Scalar data Light = DirectionalLight LightColor Direction | PositionalLight LightColor Attenuation | SpotLight LightColor (Direction, Scalar) (Attenuation, Scalar) Direktionallicht Ein Direktionallicht stellt eine unendlich weit entfernte Lichtquelle dar, deren Lichtstrahlen parallel auf die Grafikszene geworfen werden. Sie kann mit dem Konstruktor DirectionalLight beschrieben werden. Als erstes Argument wird die von der Lichtquelle abzustrahlende Lichtfarben-Zusammensetzung als Wert vom Typ LightColor angegeben. Zu beachten ist, daß sich die Strahlungsintensität des ausgesendeten Lichtes an keiner Stelle eines Lichtstrahls ändert, d.h. auch die Lichtfarben konstant 3.5. Farben und Beleuchtung 17 bleiben. Das zweite vom Typ Direction definiert die Position der Licht Argument x quelle. Sei v~d = y der als Argument übergebene Vektor, dann wird die Position z der Lichtquelle am Ende eines Strahls angenommen, der im Koordinatenursprung beginnt und durch den Punkt (x, y, z) geht. Punktlicht Eine Lichtquelle, die ihr Licht gleichmäßig in alle Richtungen abstrahlt, wird Punktlicht genannt. Eine Punktlichtquelle im Koordinatenursprung kann durch den Datenkonstruktor PositionalLight dargestellt werden. Wie bei direktionalem Licht wird als erstes Argument die Farbzusammensetzung des abgestrahlten Lichtes in Form eines LightColor-Wertes erwartet. Im Unterschied zu Direktionallicht nimmt die Intensität des abgestrahlten Lichtes bei Punktlichtquellen mit zunehmender Entfernung von der Lichtquelle ab. Zur Beschreibung der Intensitätsabnahme wurde der Record-Datentyp Attenuation eingeführt, der die Komponenten constant für den konstanten Anteil k0 , linear für den linearen Anteil k1 und quadratic für den quadratischen Anteil k2 der Intensitäsabnahme enthält. Sei d die Entfernung eines Punktes von der Punktlichtquelle und I die Strahlungsintensität aller Komponenten des von der Punktlichtquelle abgestrahlten Lichtes. Dann beträgt die Strahlungsintensität Ip des Lichtes im Punkt p Ip = k2 d2 I . + k1 d + k0 Soll eine, in jedem Punkt gleichmäßige, Strahlungsintentität verwendet werden, kann die Funktion noAttenuation als Attenuation-Wert genutzt werden. Sie spiegelt keine Abnahme der Strahlungsintentität wider, d.h. k0 = 1, k1 = 0 und k2 = 0. Gerichtetes Punktlicht (Spotlight) Ähnlich einer Punktlichtquelle strahlt eine gerichtete Punktlichtquelle ihr Licht nicht gleichmäßig in alle Richtungen, sondern nur in einem kegelförmigen Bereich in eine bestimmte Richtung ab. Zu Beschreibung derartiger Lichtquellen wurde der Konstruktor SpotLight eingeführt. Auch hier ist das erste Argument vom Typ LightColor und beschreibt die Farbzusammensetzung des abgestrahlten Lichtes. Das zweite Argument ist ein Paar (vd , α) vom Typ (Direction, Scalar). Dabei x beschreibt vd = y die Richtung, in die das Licht von der Lichtquelle abgestrahlt z wird. α ∈ [0..90] ist der Winkel zwischen einem Strahl s0 , der im Koordinatenursprung entspringt und durch den Punkt (x, y, z) geht, und jedem Strahl s, der im Koordinatenursprung entspringt und durch einen Punkt auf dem Rand des Lichtkegel geht. Als letztes Argument wird ein Wert (k, c) vom Typ (Attenuation, Scalar) erwartet. Wie bei Punktlichtquellen beschreibt die Abnahme der Lichtintensität mit zunehmender Entfernung von der Lichtquelle. Zusätzlich kann eine Abnahme der Lichtintensität vom Zentrum des Lichtkegels zum Rand hin durch den Exponenten c spezifiziert werden. Seien p ein Punkt im Lichtkegel der gerichteten Punktlichtquelle, d die Entfernung zwischen p und der Lichtquelle, α der Winkel zwischen der 18 3. Konzepte und Implementierung Geraden durch das Lichtkegelzentrum und einer Geraden durch die Lichtquelle und p, sowie I die Strahlungsintensität aller Komponenten des von der Lichtquelle abgestrahlten Lichtes, ki die Komponenten der Lichtintensitätsabnahme k und c der Exponent der Lichtintensitätsabnahme hin zum Rand des Lichtkegels. Dann beträgt die Strahlungsintensität Ip des Lichtes in p Ip = 3.6 cos(α)c I. k2 d2 + k1 d + k0 Texturen Zusätzlich zu Farbwerten bzw. Materialien können in OpenGL Texturpunkte mit den Vertices assoziiert werden. Im Renderingprozess können dann die entsprechenden Koordinaten einer Textur auf die angegebenen Koordinaten an den Vertices abgebildet werden. Durch Texturen lassen sich wesentlich realistischere Grafiken erzeugen, sowie eine Vielzahl von Spezialeffekten umsetzen. Entsprechend bietet OpenGL eine Vielzahl von Möglichkeiten zur Spezifikation von Texturen. Im folgenden soll die Anwendung von Texturen beispielhaft an 2-dimenstionalen Texturen betrachtet werden, so wie sie auch in der aktuellen Implementierung vorliegt. Zur Darstellung von Texturen wurde die Klasse Texture eingeführt. Jede Instanz τ dieser Klasse muss ein assoziieres Typsynonym TextureAttribute definieren und die Funktion applyTextureAttribute implementieren. Der Typ eines Satzes von Texturkoodinaten, der mit jedem Vertex einer Primitive assoziiert werden muss, wird dabei durch TextureAttribute τ angegeben. applyTextureAttribute liefert für eine Textur und einen Satz von Texturkoordinaten eine I/O-Aktion, die am aktuellen GL-Zustand die entsprechenden Änderungen zum Setzen der Texturkoordinaten vornimmt. Um auch im Umgang mit Texturen die fehlerhafte Anwendung von OpenGL-Befehlen zu unterbinden, ist das Auftragen von Texturen einigen Einschränkungen unterworfen. So können auf Primitiven nur Texturen abgebildet werden, die auch vorher spezifiziert wurden. Abgesichert wird dies, indem die Erzeugung einer Textur in einer Funktion gekapselt wurde und nicht nach außen frei gegeben wird. Eine als Parameter übergebene Funktion erhält eine Textur als Argument und erzeugt daraus ein Grafiksignal. Innerhalb dieses Grafiksignals kann diese Textur an den entsprechenden Stellen als Vertex-Attribut frei verwendet werden. Die in der Funktion erzeugte Textur wird schließlich auf die übergebene Funktion angewendet, und erzeugt dadurch das resultierende Grafiksignal. Im folgenden ist die Funktion withTexture als Pseudocode dargestellt, die nach dem beschriebenen Prinzip ein Grafiksignal erzeugt, das eine Textur auf Primitiven abbildet. 3.7. Anbindung an die Benutzeroberfläche 19 withTexture :: Size -> [RGBAColor] -> (Texture2D -> GraphicSignal lighting) -> GraphicSignal lighting withTexture size pixelData graphicsWithTexture = GraphicSignal $ do texName <- generateTextureName initTextureProperties texName applyPixelData texName pixelData let (GraphicSignal initialize) = graphicsWithTexture texName (rendering, update) <- initialize return (do enableTexture texName rendering disableTexture texName , update) 3.7 Anbindung an die Benutzeroberfläche In den vorherigen Abschnitten wurden Funktionen beschrieben, die zur Konstruktion von komplexeren Grafiksignalen dienen. Im folgenden soll nun gezeigt werden, wie Grafiksignale an eine grafische Oberfläche angebunden werden können. 3.7.1 Projektion Um auf einer 2-dimensionalen Oberfläche eine 3-dimensionale Grafik dargestellen zu können, müssen zunächst alle 3-dimensionalen Punkte auf eine Ebene projiziert werden, von der ein rechteckiger Ausschnitt das Sichtfenster bildet. Die Punkte liegen nach den ausgeführten Transformationen als sogenannte eye coordinates vor und müssen im Prozess der OpenGL-Vertex-Transformation in clip coordinates umgeformt werden. Dies geschieht durch linksseitige Multiplikation mit einer 4×4-Matrix, der Projektionsmatrix. Zu Beschreibung solch einer Projektion wurde der Datentyp Projection eingeführt, der einer I/O-Aktion entspricht, die für die darzustellende Grafik eine Projektionsmatrix anwendet. Die aktuelle Implementierung stell zwei Funktionen zur Erzeugung von Projektionen bereit, orthogonalProjection für eine Parallelprojektion und frustumProjection für eine perspektivische Projektion. Beide Funktionen erwarten einen Wert B vom Record-Datentyp ClippingBox, der das Sichtvolumen genauer spezifiziert. B enthält die maximalen Koordinaten der einzelnen Seitenflächen des Sichtvolumens, wobei left den linken Rand l, right den rechten Rand r, bottom den unteren Rand b und top den oberen Rand t darstellen. Weiterhin stellt n die Koordinate der nahen Begrenzungsebene und f den Abstand des Auges zur hinteren Begrenzungsebene dar. Die Position des Auges wird bei beiden Projektionen 0 im Punkt 0 angenommen. 0 Parallelprojektion Bei der Parallelprojektion werden alle Punkte mit gleichen z-Koordinaten auf den gleichen Punkt des Sichtfensters abgebildet. Das Sichtvolumen bildet hier einen Quader; die Größen der vorderen und hinteren Begrenzungsflächen sind also identisch. 20 3. Konzepte und Implementierung Entsprechend erscheinen Objekte, die von der Position des Auges weiter entfernt sind, die der Position des Auges näher sind. Die Punk nicht kleiner, als Objekte r l te b bzw. t werden auf die linke untere bzw. rechte obere Ecke des −n −n Sichtfensters abgebildet. Die Funktion orthogonalProjection gibt einen Fehler zurück, falls l = r ∨ b = t ∨ n = f. orthogonalProjection :: ClippingBox -> Projection Perspektivische Projektion Die perspektivische Projektion stellt eine, in Bezug auf Sichten in der realen Welt, natürlichere Projektionsmethode dar. Weiter entfernte Objekte erscheinen kleiner als Objekte, die sich näher an der Position des Auges befinden. Das Sichtvolumen bildet bei dieser Projektion einen Pyramidenstumpf, dessen Deckfläche der Fläche des Sichtfensters und dessen Grundfläche entfernten Begrenzungsfläche der weiter r l entspricht. Auch hier werden die Punkte b bzw. t auf die linke untere −n −n bzw. rechte obere Ecke des Sichtfensters abgebildet. Die Funktion frustumProjection gibt einen Fehler zurück, falls l = r ∨ b = t ∨ n = f ∨ n ≤ 0 ∨ f ≤ 0. frustumProjection :: ClippingBox -> Projection 3.7.2 Widget GraphicDisplay Der letzte Schritt bei der Darstellung von Grafiken ist nun ein Grafiksignal an eine Gtk-Zeichenfläche anzubinden und die Grafik darauf rendern zu lassen. Folgender Pseudocode stellt die Kernpunkte der Anbindung an eine Zeichenfläche heraus: initDrawingArea :: (LightingState lighting => lighting -> GraphicSignal lighting -> SSignal Projection -> IO () initDrawingArea lighting (GraphicSignal initializeGraphics) projectionSignal = do drawingArea <- createGLDrawingArea initializeDrawingArea drawingArea ~(renderGraphics, updateStream) <- unsafeInterleaveIO $ do (getProjection, projectionUpdate) <- newReferenceCell projectionSignal (rendering, graphicsUpdate) <- initializeGraphics return (do projection <- getProjection applyProjection projection applyLighting lighting rendering , projectionUpdate ‘mappend‘ graphicsUpdate) 3.7. Anbindung an die Benutzeroberfläche 21 initGtkWidget drawingArea $ do initLightingState lighting initGLBuffers DSignal.register updateStream $ do redrawGtkWidget drawingArea $ do clearBuffers renderGraphics swapBuffers Zu beachten ist die verzögerte Ausführung der Initialisierung des Grafiksignals G mittels unsafeInterleaveIO. Da bei der Initialisierung von G bereits OpenGLBefehle ausgeführt werden können, z.B. durch Verwendung von Display-Listen, muss init(G) im Kontext einer GL-Zeichenfläche ausgeführt werden. Die Ausführung von init(G) wird also solange herausgezögert, bis in der Funktion initGtkWidget ein entsprechender GL-Kontext bereitgestellt wird. Tatsächlich erfolgt das Rendering eines Grafiksignals in der Implementierung auf einem neuen Grapefruit-Widget drawingArea. Ein Grafiksignal G und ein segmentiertes Signal SP über einer Projektion werden mittels Graphics := G und ViewportProjection := SP an die Zeichenfläche gebunden und stellen somit die Eingänge des Widgets dar. 22 3. Konzepte und Implementierung Farbname Black Navy Green Teal Maroon Purple Olive Silver Gray Blue Lime Aqua Red Fuchsia Yellow White cR 0.0 0.0 0.0 0.0 0.5 0.5 0.5 0.75 0.5 0.0 0.0 0.0 1.0 1.0 1.0 1.0 cG 0.0 0.0 0.5 0.5 0.0 0.0 0.5 0.75 0.5 0.0 1.0 1.0 0.0 0.0 1.0 1.0 cB 0.0 0.5 0.0 0.5 0.0 0.5 0.0 0.75 0.5 1.0 0.0 1.0 0.0 1.0 0.0 1.0 Tabelle 3.1: RGBA-Komponenten der 16 Standard-Farben 4. Zusammenfassung und Ausblick In dieser Arbeit wurden Methoden vorgestellt, den stark imperativen Charakter der Grafikbibliothek OpenGL in einen problemorientierteren, deklarativen zu ändern. Dies wurde auf Basis funktionaler reaktiver Programmierung in Form einer Grafikbibliothek für die FRP-Implementierung Grapefruit dargestellt. Es wurde gezeigt, wie sich 3-dimensionale Grafiken in Grafiksignale transformieren lassen, ohne dabei auf die Verwendung von Display-Listen, eine wichtige OpenGL-Komponente für die effiziente Darstellung von Grafiken, verzichten zu müssen. Weiter wurden Funktionen vorgestellt, mit denen sich Grafiksignale zu komplexeren Grafiksignalen verknüpfen lassen, darunter die Verkettung von Grafiksignalen, Möglichkeiten zur Zwischenspeicherung von Teilen einer Grafik, sowie einer Reihe von Transfomationen über Grafiksignalen. Es wurde weiter eine Reihe von einfachen Grafikobjekten zu Verfügung gestellt und eine Möglichkeit zur Definition komplexerer Grafiken aus grafischen Primitiven, beispielhaft anhand von nicht zusammenhängenden Vierecken, vorgestellt. Eine Unterscheidung zwischen beleuchteten und unbeleuchteten Grafiksignalen wurde durch Einführung von parametrisierten Grafiksignalen vorgenommen. Dadurch wurde das Setzen nicht benötigter Vertex-Attibute und ein damit einhergehender Performance-Verlust unterbunden. Es wurden weiter verschiedene Beleuchtungsmodelle vorgestellt und die Abbildung von Texturen auf Grafiken am Beispiel von 2-dimensionalen Texturen beschrieben. Schließlich wurde gezeigt, wie sich die entworfenen Grafiksignale auf der Benutzeroberfläche darstellen lassen. Dazu wurde ein neues Grapefruit-Widget in Form einer OpenGL-Zeichenfläche eingeführt, auf dem sich ein Grafiksignal unter einer bestimmten Projektion rendern lässt. Beim Entwurf und der Implementierung wurde Hauptaugenmerk auf das Umgehen von Grafikbeschreibungen gelegt, die einen Fehlerfall darstellen bzw. entsprechend der OpenGL-Spezifikation in undefiniertem Verhalten resultieren. Derartige Grafikbeschreibungen wurden unter Ausnutzung des Typsystems von Haskell unterbunden. Des weiteren wurde beim Entwurf der Datentypen und Funktionen auch besonderer Wert darauf gelegt, die Effizienz beizubehalten, die bereits durch die zugrundeliegende OpenGL-Implementierung geboten wird. 24 4. Zusammenfassung und Ausblick Erweiterungsmöglichkeiten Die vorliegende Arbeit beschreibt Ansätze für die typsichere Umsetzung einer Auswahl der vielen komplexen OpenGL-Möglichkeiten. Anhand von Beispielimplementierungen wurde die prinzipielle Umsetzbarkeit solcher OpenGL-Features demonstriert. Beim Schreiben dieser Arbeit bzw. der Implementierung der Grafikbibliothek stellten sich u.a. im folgenden genannte Punkte als, im Rahmen dieser Arbeit, zu komplex oder zeitintensiv heraus, und bieten einen Ansatzpunkt für Erweiterungen an der Grafikbibliothek: • Implementierung weiterer Primitivenarten: Die aktuelle Implementierung unterstützt nur die Beschreibung nicht zusammenhängender Vierecke als Primitiven. Weitere Primitiventypen wie Dreiecke, Polygone, etc. könnten leicht nach dem Prinzip der Vierecke ergänzt werden. • Umsetztung von Texturfunktionen und -filtern: OpenGL beitet eine Vielzahl von Möglichkeiten Texturen auf Primitiven abzubilden. Durch die Angabe von Texturfunktionen und Texturfiltern können verschiedenste grafische Effekte umgesetzt werden. Weiteren Spielraum für Erweiterungen bilden Multitexturing oder die automatisierte Mipmap-Generierung. • Automatische Berechnung von Lichtquellen-IDs: Die aktuelle Implementierung unterstützt keine automatische Vergabe der IDs von Lichtquellen, der Nutzer muss selbst dafür sorgen muß, dass jede dieser IDs innerhalb eines Grafiksignals nur einmal vergeben wird.