Animationsunterstützung für die deklarative GUI

Werbung
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.
Herunterladen