Ba elorarbeit Implementierung einer interaktiven dreidimensionalen

Werbung
Goethe Universität Frankfurt am Main
Institut ür Informatik
Sommersemester 2014
Baelorarbeit
Implementierung einer interaktiven
dreidimensionalen Anzeige von Bildaussnitten
von Grammatik-komprimierten Bildern in der
funktionalen Programmiersprae Haskell
S A
N D
Betreuer:
Abgabedatum:
Prof. Dr. Manfred Schmidt-Schauß
26.06.2014
E
gemäß Bachelor-Ordnung Informatik 2011 §25 Abs. 11
Hiermit bestätigen wir, dass wir die vorliegende Arbeit selbstständig verfasst haben und keine
anderen ellen oder Hilfsmiel als die in dieser Arbeit angegebenen verwendet haben.
Frankfurt, den 26. Juni 2014
Sergej Anikuschkin
Nils Dallmeyer
I
A
gemäß Bachelor-Ordnung Informatik 2011 §25 Abs. 1
Die Abschnie haben wir wiefolgt aufgeteilt:
Sergej Anikuschkin
Nils Dallmeyer
Gemeinsam
1, 2.1, 4.2.4, 4.3.1, 5
2.2, 2.3, 2.4, 4.1, 4.2.1, 4.2.2, 4.2.3, 4.3.2, 6
3, 4.4
Die gerade genannte Aueilung entspricht auch ungeähr der Verteilung der Aufgaben in der
Implementierung.
II
D
Wir möchten uns bei allen bedanken, die uns bei der Anfertigung der Bachelorarbeit unterstützt
haben. Besonders danken möchten wir folgenden Personen:
Herrn Prof. Dr. Manfred Schmidt-Schauß und Herrn Dr. David Sabel ür die Beratung bei der
Wahl des emas und ür die Unterstützung auf dem Weg der Erstellung der Arbeit.
Herrn Rafael Franzke und Herrn Sorin Constantinescu ür die Durchsicht der Arbeit und die
nützlichen Verbesserungsvorschläge.
Herrn Tobias Berner ür seine fortwährende konstruktive Kritik am Seitenlayout und ür seine
ausührliche Korrekturlesung.
III
I
1
Einleitung
2
Grundlagen
2.1
2.2
2.3
2.4
Programmbesreibung
4
Implementierung
4.3
4.4
2
Grammatik zur Matrixkomprimierung . . . . .
2.1.1 Kontextfreie Grammatiken . . . . . . .
2.1.2 S2ASLP-Grammatiken . . . . . . . . . .
Lineare Algebra . . . . . . . . . . . . . . . . . . .
OpenGL . . . . . . . . . . . . . . . . . . . . . . .
2.3.1 Transformation geometrischer Daten .
2.3.2 Immediate Mode und Displaylisten . .
OpenGL in Haskell . . . . . . . . . . . . . . . . .
2.4.1 Monaden . . . . . . . . . . . . . . . . . .
2.4.2 Beispiel zu OpenGL mit GLUT . . . . .
3
4.1
4.2
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 2
. 2
. 3
. 6
. 7
. 7
. 8
. 9
. 9
. 11
14
17
Programmstruktur . . . . . . . . . . . . .
Matrix . . . . . . . . . . . . . . . . . . . .
4.2.1 Dateiformat . . . . . . . . . . . .
4.2.2 Datenstruktur . . . . . . . . . . .
4.2.3 Laderoutine . . . . . . . . . . . .
4.2.4 Ausschnisberechnung . . . . .
Grafik . . . . . . . . . . . . . . . . . . . . .
4.3.1 Kamera und Bewegung . . . . .
4.3.2 Zeichenroutine . . . . . . . . . .
Selektion und GUI-Modus . . . . . . . .
4.4.1 Taschenlampe . . . . . . . . . . .
4.4.2 Mausauswahl . . . . . . . . . . .
4.4.3 Transparentes Auswahlrechteck
4.4.4 GUI-Modus . . . . . . . . . . . .
IV
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
17
20
20
21
21
21
29
29
32
36
37
38
39
41
V
5
Analysen
5.1
5.2
5.3
6
Zusammenfassung und Ausbli
6.1
6.2
43
Generatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Laufzeitanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Programmtests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
49
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Abbildungsverzeinis
50
Literaturverzeinis
51
1
E
Ziel dieser Bachelorarbeit ist es, ein Programm zur dreidimensionalen Anzeige von komprimierten Graustufenbildern beziehungsweise Daten zu implementieren. Bei der Arbeit mit komprimierten Daten kommt der Verarbeitung dieser eine besondere Rolle zu. Eine komplee Dekomprimierung der Daten ist nicht erwünscht, denn der durch die Komprimierung gewonnenen Platz
würde wieder beansprucht werden. Bei einer exponentiellen Kompressionsrate würde zum Beispiel eine Berechnung zwischenzeitlich exponentiellen Platz verbrauchen. In diesem Forschungsgebiet geht es daher um Algorithmen, welche direkt auf den komprimierten Daten effizient arbeiten können. Effizient heißt hierbei klassisch, dass die Algorithmen der Komplexitätsklasse P
angehören. Die Kompressionsart welche dieser Arbeit zu Grunde liegt, ist die Kompression von
Matrizen durch sogenannte S2ASLP-Grammatiken. Diese Art von Grammatiken basiert auf den
kontextfreien Grammatiken und unterliegt dabei einigen Einschränkungen. Diese Einschränkungen bewirken, dass die Grammatiken nur auf Matrizen mit einer Größe von 2n × 2n , mit n ∈ N,
abgeleitet werden kann. Die Matrix wiederum stellt das Bild dar, einzelne Matrixeinträge sind
Graustufenwerte und die Position der Einträge ist die Position im dreidimensionalen Raum. Die
Graustufenwerte, welche bei 0 beginnen und bei 255 enden, können auch als Höhenwerte interpretiert werden.
Die konkrete Aufgabe der Bachelorarbeit besteht nun darin, Ausschnie einer solchen Matrix
als Höhenbild anzuzeigen, wobei das mit der Programmiersprache Haskell erreicht werden soll.
Das Ziel des Programms ist also die Visualisierung der Grammatik und das wiederum soll in
der Forschung weiterhelfen. Es wird dabei auf der komprimierten Darstellung des Bildes, also
auf der zugehörigen S2ASLP-Grammatik, gearbeitet. In der Bachelorarbeit werden zwei Schwerpunkte gesetzt. Ein Schwerpunkt ist die Arbeit mit der Grammatik. Es wird zuerst die theoretische Grundlage in Kapitel 2 ür die Grammatik beschrieben, danach wird in Kapitel 4 die Implementierung des dazugehörigen Datentyps und die Verarbeitung von Ausschnien intensiv erläutert und in Kapitel 5 wird der Laufzeitaskpekt mit passenden Testreihen betrachtet. Der zweite
Schwerpunkt ist der dreidimensionale Raum. Für die Darstellung des Raumes wird im Programm
OpenGL verwendet. Zuerst wird in Kapitel 2 geklärt was OpenGL ist und wie es zusammen mit
Haskell funktioniert, danach wird in Kapitel 4 die Implementierung des Raumes, der Bewegung
im Raum und der Darstellung der Matrix ausgeührt und abschließend werden noch verschiedene Interaktionsarten mit der dreidimensionalen Welt vorgestellt. Zusätzlich beschreibt Kapitel 3
alle Funktionen des Programms.
Insgesamt wird dabei auf die Implementierung des Programms großen Wert gelegt, denn die
Hauptaufgabe war es, dieses Programm von Grund auf zu programmieren. Auf theoretische Aspekte wird nur grundlegend eingegangen, wobei wichtige Punkte wie Kompression und Laufzeit
von Bedeutung sind.
1
2
G
Zunächst behandeln wir Grundlagen, welche ür die Aufgabenstellung eine besondere Rolle spielen. Insbesondere umfasst das den theoretischen Hintergrund der Grammatikkomprimierung,
einige Grundlagen aus der Linearen Algebra sowie Besonderheiten in Haskell bezüglich dieser
Bachelorarbeit.
2.1 Grammatik zur Matrixkomprimierung
Zunächst wollen wir etwas auf die Grundlagen der verwendeten Matrixkomprimierung eingehen. Dieser Abschni basiert weitgehend auf (LSS14).
2.1.1
Kontextfreie Grammatiken
Eine kontextfreie Grammatik beschreibt eine Menge von Regeln die bestimmte Symbole durch
andere ersetzen. Die formale Definition einer kontexfreien Grammatik lautet nach (Sch13):
Definition 1 (KFG) Eine kontextfreie Grammatik wird beschrieben durch G = (Σ, V, S, P ), wobei
die Komponenten folgende Bedeutung haben:
• Σ ist die endliche Menge von Terminalsymbolen beziehungsweise Terminalen.
• V ist die endliche Menge von Nicherminalsymbolen beziehungsweise Variablen oder
Nicherminalen.
• S ∈ V ist das Startsymbol.
• P ist die endliche Menge der Produktionen.
Zudem muss eine kontexfreie Grammatik folgende Bedingungen erüllen:
• Σ und V sind disjunkt, also Σ ∩ V = ∅.
• Sei A die Menge aller Symbole, also A ∶= Σ ∪ V . Für P muss gelten, P ⊆ V × A∗ .
x → y ist die Notation ür eine Produktion oder Regel (x, y) ∈ P .
2
3
2.1. GRAMMATIK ZUR MATRIXKOMPRIMIERUNG
2.1.2
S2ASLP-Grammatiken
Die S2ASLP-Grammatiken sind kontextfreie Grammatiken mit einer Reihe von Einschränkungen. Durch den Auau der Grammatik soll eine komprimierte Darstellung von 2n × 2n großen
Matrizen ermöglicht werden, wobei n ∈ N gilt.
Definition 2 (S2ASLP-Grammatik) Eine S2ASLP-Grammatik G ist azyklisch und kontextfrei. Außer-
dem muss ür jedes Nicherminal von G gelten, dass es genau eine Produktion ür dieses Nichtterminal gibt, sodass die Ersetzung eines Nicherminals eindeutig ist. Jeder Produktion dieser
Matrix kann eine Höhe n ∈ N zugeteilt werden. Seien A und Ai beliebige Nicherminale von G.
Sei a ein Terminal von G. Für ein Nicherminal A gilt, dass es nur vier erlaubte Produktionen
gibt. Diese sind:
(a) A → (
A1 A2
) , dabei gilt ür A1 bis A4 dass ihre Höhe um eins kleiner ist als bei A.
A3 A4
(b) A → A1 + A2 , dabei gilt dass A, A1 und A2 die selbe Höhe haben.
(c) A → A1 ⋅ A2 , dabei gilt dass A, A1 und A2 die selbe Höhe haben.
(d) A → a , dabei gilt dass A Höhe 0 hat.
Die Höhe eines Nicherminals n gibt dabei an, dass die Größe der entsprechenden Matrix 2n ×2n
ist. Dieser Zusammenhang folgt aus dem Auau der Grammatik. Die Matrixgröße verändert
sich nur dann, wenn die Produktion (a) angewendet wird. Beim Anwenden dieser Produktion
verdoppelt sich sowohl die Spaltenanzahl, als auch die Zeilenanzahl. Die Verdopplung würde so
o durchgeührt werden bis ein Terminal erreicht wird. Da n angibt, wie o eine Verdopplung
stafinden würde, bis man ein Terminal erreicht, gilt der besagte Zusammenhang. Für die Produktionen (b), (c) gilt, dass die Matrizen erst addiert bzw. multipliziert werden wenn sie nur noch
Terminale enthalten. Die Größe der Grammatik G wird notiert durch ∣G∣ und ist definiert als
Summe der Größen aller Produktionen die G enthält.
Beispiel 1 (S2ASLP-Grammatik) Die folgende S2ASLP-Grammatik wird im Folgenden nur durch
ihre Produktionen beschrieben, da die Nicherminale und Terminale deutlich erkennbar sind.
Dabei ist das Startsymbol S :
S → A1 + A2
A1 → (
A3 A3
)
A4 A4
A2 → (
A4 A3
)
A3 A4
A3 → (
A5 A5
)
A5 A5
A4 → (
A6 A6
)
A6 A6
A5 → 1
A6 → 2
2.1. GRAMMATIK ZUR MATRIXKOMPRIMIERUNG
4
Die vollständige Matrix würde gemäß der Grammatik folgendermaßen aufgebaut werden:
∗
S → A1 + A2 → (
⎛A5
⎜A5
⎜
⎜A6
⎝A6
A5
A5
A6
A6
A5
A5
A6
A6
A3 A3
A A3 ∗
)+( 4
)→
A4 A4
A3 A4
A5 ⎞ ⎛A6
A5 ⎟ ⎜A6
⎟+⎜
A6 ⎟ ⎜A5
A6 ⎠ ⎝A5
A6
A6
A5
A5
A5
A5
A6
A6
A5 ⎞ ⎛3
A5 ⎟ ∗ ⎜3
⎟→⎜
A6 ⎟ ⎜3
A6 ⎠ ⎝3
3
3
3
3
2
2
4
4
2⎞
2⎟
⎟
4⎟
4⎠
Es folgt im Weiteren ein Pseudocode der zeigt, wie ein Eintrag einer Matrix, die durch eine
S2ASLP-Grammatik beschrieben ist, berechnet werden kann:
1. Sei S ein beliebiges Nicherminal der Grammatik und seien alle Si mit i ∈ N Nicherminale
die von S abgeleitet werden.
2. Sei S nun das Startsymbol.
3. Sei P = (x, y) mit x, y ∈ N>0 der zu selektierende Eintrag der Matrix.
4. Sei halfsize die halbe Seitenlänge der gesamten Matrix.
5. Man betrachte nun die Art der Produktion von S :
• Falls S bereits berechnet wurde, dann gib das Ergebnis zurück.
• Falls die Produktion S → (
S1 S2
) ist, dann folgt:
S3 S4
(a) Wenn x ≤ halfsize und y ≤ halfsize gilt, dann starte wieder in Schri 5 mit
S = S1 , halfsize = halfsize/2 und P = (x, y).
(b) Wenn x > halfsize und y ≤ halfsize gilt, dann starte wieder in Schri 5 mit
S = S2 , halfsize = halfsize/2 und P = (x − halfsize, y).
(c) Wenn x ≤ halfsize und y > halfsize gilt, dann starte wieder in Schri 5 mit
S = S3 , halfsize = halfsize/2 und P = (x, y − halfsize).
(d) Wenn x > halfsize und y > halfsize gilt, dann starte wieder in Schri 5 mit
S = S4 , halfsize = halfsize/2 und P = (x − halfsize, y − halfsize).
• Falls die Produktion S → S1 + S2 ist, dann folgt:
(a) Wenn S1 = S2 gilt, dann starte wieder in Schri 5 mit S = S1 , halfsize = halfsize,
P = (x, y) und addiere das Ergebnis mit sich selbst.
(b) Wenn S1 ≠ S2 gilt, dann starte zweimal in Schri 5, einmal mit S = S1 , halfsize =
halfsize, P = (x, y) und dann mit S = S2 , halfsize = halfsize, P = (x, y) . Addiere
die Ergebnisse der Berechnungen.
• Falls die Produktion S → S1 ⋅ S2 ist, dann folgt:
(a) Wenn S1 = S2 gilt, dann starte wieder in Schri 5 mit S = S1 , halfsize = halfsize,
P = (x, y) und multipliziere das Ergebnis mit sich selbst.
5
2.1. GRAMMATIK ZUR MATRIXKOMPRIMIERUNG
(b) Wenn S1 ≠ S2 gilt, dann starte zweimal in Schri 5, einmal mit S = S1 , halfsize =
halfsize, P = (x, y) und dann mit S = S2 , halfsize = halfsize, P = (x, y) .
Multipliziere die Ergebnisse der Berechnungen.
• Falls die Produktion A → a ist, dann folgt:
(a) Gib a als Ergebnis zurück.
Die Seitenlänge der Matrix kann dadurch bestimmt werden, dass die Höhe des Startsymbols ermielt wird, denn wie bereits beschrieben folgt daraus die Seitenlänge 2n . Die Anzahl der vom
Startsymbol bis zum Terminalfall durchlaufenen Produktionen von der Form Def. 2 (a) ist dabei
die Höhe des Startsymbols. Die Wiederverwendung der bereits berechneten Ergebnisse erfolgt,
indem man mitspeichert zu was ür einem Ergebnis ein Nicherminal geührt hat und dieses
Ergebnis wiederverwendet, falls dieses Nicherminal wieder besucht wird.
Um den Pseudocodealgorithmus besser verstehen zu können, wird mit dem Algorithmus anhand
der Beispielgrammatik 1 der Matrixeintrag an Stelle (3, 2) berechnet:
• Zu Beginn ist S das Startsymbol, P ist (3, 2) und halfsize ist 2. Da S auf A1 + A2 abgeleitet
wird folgt eine Überprüfung auf Gleichheit von A1 und A2 .
• Da A1 und A2 nicht gleich sind wird zuerst das Ergebnis ür das Nicherminal A1 berechnet. A1 wird auf vier Nicherminale abgeleitet, somit wird x = 3 und y = 2 mit halfsize = 2
verglichen.
• Es gilt x > halfsize und y = halfsize, somit wird mit dem zweiten abgeleiteten Nicherminal
A3 weitergerechnet, dabei werden halfsize = 1 und P = (1, 2) mitgegeben.
• A3 wird auch auf vier Nicherminale abgeleitet und mit x = halfsize und y > halfsize folgt,
dass mit A5 dem drien abgeleiteten Nicherminal weitergerechnet wird. Dabei werden
halfsize = 1 und P = (1, 1) mitgegeben.
• A5 leitet auf das Terminal 1 ab und somit wird dieses als Ergebnis zurückgegeben. Das
heißt auch, dass A1 , A3 und A5 mit dem Ergebnis 1 wiederverwendet werden können.
• Jetzt steht noch die Berechnung des Nicherminals A2 mit P = (3, 2) und halfsize = 2 aus.
Wie auch bei der Berechnung von A1 folgt hier, dass mit dem zweiten von A2 abgeleiteten
Nicherminal, also A3 , weitergerechnet wird.
• Da A3 bereits berechnet wurde, kann das Ergebnis 1 wiederverwendet werden. Das Ergebnis von A1 und A2 ist somit 1 und die Addition liefert ür das Startsymbol das Ergebnis
2.
• Die Berechnung ergibt also, dass sich in der Matrix an der Stelle (3,2) eine 2 befindet.
Abschließend wird noch die Kompressionseigenscha der S2ASLP-Grammatik betrachtet. Sei
m ∈ N die Höhe des Startsymbols einer Grammatik. Mit dieser Grammatik wird eine Matrix
mit 4m Einträgen erzeugt. Das Ziel welches mit den Grammatiken verfolgt wird ist, dass diese
Anzahl an Einträgen durch möglichst wenig Nicherminale in der Grammatik dargestellt wird.
Das bedeutet auch, dass Nicherminale mehrfach verwendet werden müssen, um gleiche Muster
die im Bild, also in der Matrix vorhanden sind, nachzubilden.
Bei einer perfekten Kompression, ür die jeder Eintrag in der Matrix gleich sein muss, ist es
möglich im Vergleich zur Matrixgröße eine logarithmische Größe der Grammatik zu erreichen.
2.2. LINEARE ALGEBRA
6
Denn mit h+1 Nicherminalen wird bei perfekter Kompression eine Matrix mit 4h Einträgen beschrieben. Die Grammatik, die solch eine Kompression durchührt, leitet angefangen vom Startsymbol jedes Nicherminal auf vier gleiche Nicherminale ab. Das geht solange weiter, bis ein
Nicherminal auf ein Terminal abgeleitet wird. Falls jedoch keine Kompression vorliegt und jeder Eintrag der Matrix durch ein Nicherminalsymbol repräsentiert wird, dann ist die Anzahl
der Nicherminale sogar größer als die Anzahl der Matrixeinträge, denn schon allein die Anzahl
der Nicherminale die auf Terminale ableiten entspricht der Anzahl an Einträgen in der Matrix.
2.2 Lineare Algebra
Für eine dreidimensionale Anzeige mit OpenGL benötigen wir etwas Lineare Algebra. Da wir
später mit komprimierten Matrizen arbeiten, werden hier allerdings auch schon nötige Definitionen getroffen, die ür die Kompression später relevant sind. Wir haben hierür auf (A+ 11)
zurückgegriffen.
Definition 3 (Matrix)
(a) Eine m × n Matrix ist ein rechteckiges Schema über dem Körper K mit m Zeilen und n
Spalten:
⎛ a11 a12 . . . a1n ⎞
a22 . . . a2n ⎟
⎜a
⎟
A = ⎜ 21
⎜ ⋮
⎟
⎝am1 am2 . . . amn ⎠
(b) A wird quadratisch genannt, falls m = n.
(c) Sei A quadratisch. Dann nennt man die Elemente a11 , a22 , . . . , ann Diagonalelemente und
A ist Diagonalmatrix, falls aij = 0 ür alle Elemente aij mit i ≠ j gilt.
(d) Die Einheitsmatrix E ist eine Diagonalmatrix, bei der aii = 1 ür alle Diagonalelemente aii
gilt.
⎛1 . . . 0⎞
E = ⎜⋮ ⋱ ⋮⎟
⎝0 . . . 1⎠
Definition 4 (Operationen zwisen Matrizen)
(a) Wenn ür zwei Matrizen A und B jeweils die Anzahl der Zeilen und Spalten übereinstim-
men, so ist die Addition von A und B komponentenweise definiert:
⎛ a11 . . . a1n ⎞ ⎛ b11 . . . b1n ⎞ ⎛ a11 + b11 . . . a1n + b1n ⎞
⎟
⋱
⋮ ⎟+⎜ ⋮
⋱
⋮ ⎟=⎜
⋮
⋱
⋮
A+B=⎜ ⋮
⎝am1 . . . amn ⎠ ⎝bm1 . . . bmn ⎠ ⎝am1 + bm1 . . . amn + bmn ⎠
7
2.3. OPENGL
(b) Analog zur komponentenweisen Addition ist das komponentenweise Produkt ○ definiert:
⎛ a11 . . . a1n ⎞ ⎛ b11 . . . b1n ⎞ ⎛ a11 ⋅ b11 . . . a1n ⋅ b1n ⎞
⎟
⋱
⋮ ⎟○⎜ ⋮
⋱
⋮ ⎟=⎜
⋮
⋱
⋮
A○B=⎜ ⋮
⎝am1 . . . amn ⎠ ⎝bm1 . . . bmn ⎠ ⎝am1 ⋅ bm1 . . . amn ⋅ bmn ⎠
(c) Sei A eine m × n-Matrix und B eine n × l-Matrix. Dann sind die Elemente von C ür
A ⋅ B = C wiefolgt definiert:
n
cik = ∑ aij bjk
j=1
Die Definition des Matrix-Vektor-Produkts ist hier implizit enthalten, da ein Vektor eine
Matrix mit Spaltenanzahl 1 ist.
Ein nützliches Werkzeug sind Geradengleichungen, da man mithilfe von Geradengleichungen
leicht Schnipunkte mit Ebenen berechnen kann. Für unsere Zwecke eignet sich die Darstellung
in Parameterform gut.
Definition 5 (Geradengleiung in Parameterform)
Die Gleichung g ∶ x = s + λv nennt man Geradengleichung in Parameterform der Geraden g ,
wobei s ein Ortsvektor, v ein Richtungsvektor und λ ∈ K ist.
Ähnlich dazu lässt sich auch die Ebenengleichung in Parameterform definieren.
Definition 6 (Ebenengleiung in Parameterform)
Eine Ebene E besitzt eine Gleichung in Parameterform E ∶ x = s+λv+µw wobei s ein Ortsvektor
ist, v und w linear unabhängige Richtungsvektoren und λ, µ ∈ K sind.
Es sind noch andere Definitionen gebräuchlich, insbesondere in Normalenform, allerdings benötigen wir diese nicht, weshalb wir sie hier nicht angeben.
Als Körper wählen wir im Folgenden stets die reellen Zahlen, das heißt es gilt K = R.
2.3 OpenGL
OpenGL ist eine Schnistelle zwischen Soware und der Grafikkarte und lässt sich als State
Machine auffassen. Dazu sei allerdings gesagt, dass neuere OpenGL-Versionen dieses Konzept
weitegehend aufgegeben haben, etwas mehr findet sich kompakt in (OGL), allerdings genügt
ür unsere Zwecke das alte Konzept. Dies steht natürlich in einem ziemlichen Widerspruch zum
funktionalen Konzept der Programmiersprache Haskell, allerdings werden wir in Kapitel 2.4 kurz
erläutern, wie man diese Differenz überbrücken kann. Zunächst wollen wir uns aber mit den ür
uns relevanten Teilen von OpenGL beschäigen und haben hierür auf (S+ 07) zurückgegriffen.
2.3.1
Transformation geometriser Daten
Geometrische Daten werden im groben Überblick folgendermaßen transformiert: Zunächst werden Objektkoordinaten in Weltkoordinaten umgewandelt. Das heißt die Koordinaten sind nun alle relativ zum Ursprung. Die Kamera befindet sich zu Beginn im Ursprung und wird anschließend
2.3. OPENGL
8
dadurch korrekt positioniert, dass die gesamte Welt verschoben wird. Diese Aufgaben sind in der
Modelviewmatrix MM odelV iew gebündelt. Danach wandelt die Projektionsmatrix MP rojection
diese Koordinaten in Bildschirmkoordinaten um. Mathematisch entspricht der ganze Prozess der
folgenden Rechnung, wobei v die zu transformierenden Daten sind.
MP rojection ⋅ MM odelV iew ⋅ v
bzw.
MP rojection ⋅ MV iew ⋅ MM odel ⋅ v
Von Interesse ist an dieser Stelle vor allem die Projektionsmatrix. Diese kann orthogonal und
perspektivisch konfiguriert werden, wobei wir wegen der 3D-Darstellung ausschließlich die perspektivische Projektion verwenden werden.
θ
near
far
Abbildung 2.1: Perspektivische Projektion
Abhängig von der Entfernung zum Betrachter werden Vertexdaten aussortiert. Es wird nur gezeichnet, was zwischen der Near- und der Far-Clipping-Plane liegt. Diese beiden Ebenen sind in
Abbildung 2.1 dargestellt.
Die Entfernung von Near- und Far-Plane kann beim Setzen der Perspektive angegeben werden.
Dabei ist zu beachten, dass wir es durchaus mit größeren Seitenlängen zu tun haben können, weswegen die Far-Plane-Distanz relativ groß gewählt werden sollte. Die Abstände zwischen Nearund Far-Plane bestimmen die Genauigkeit des Tiefenpuffers. Der Abstand zwischen Betrachter
und Near-Plane darf nicht zu groß sein, da sonst Objekte direkt am Betrachter nicht gezeichnet
würden, obwohl man dies erwarten würde. Je größer der Abstand zwischen den Ebenen wird,
desto schlimmer fallen optische Verzerrungen aus.
Der Winkel θ ist der Field-Of-View-Winkel, welcher in Grad an der Y -Achse orientiert ist. Abschließend ist noch zu bemerken, dass OpenGL über Stacks ür die verschiedenen Matrizen verügt, das werden wir aber erst später genauer betrachten.
2.3.2
Immediate Mode und Displaylisten
Im sogenannten Immediate Mode werden alle Befehle und Vertexdaten in jedem Frame zur GPU
übertragen. Der Immediate Mode genügt daher nur, wenn man geringe Datenmengen hat. Möchte man zum Beispiel drei kleine ader zeichnen, so ginge dies problemlos im Immediate Mode.
Möchte man hingegen dreihundert ader zeichnen, wird die Datenmenge so groß, dass mit Ruckeln zu rechnen ist, da die Übertragung der Daten ab einer gewissen Datenmenge länger dauert
als ein Frame. Bei statischen Daten helfen Displaylisten, welche eine Sammlung von Befehlen
und Vertexdaten im Speicher der Grafikkarte ablegt (siehe (DGL)). Die Übertragung der Daten
zur GPU entällt in solchen Fällen, da man die gespeicherten Befehle genau in der Reihenfolge,
in der sie gespeichert wurden, mit nur einem Aufruf ausühren kann. Lediglich der Aufruefehl
muss zur GPU geschickt werden.
9
2.4. OPENGL IN HASKELL
Displaylisten sind in der Praxis o zu statisch, weswegen o auf Vertex Buffer Objects zurückgegriffen wird, da diese nicht so statisch sind, aber ebenfalls zu einem Performanzgewinn ühren
(siehe (Ahn)). Da unsere Daten jedoch sehr statisch sind, genügen uns Displaylisten und wir
wollen auf Vertex Buffer Objects daher nicht mehr weiter eingehen.
2.4 OpenGL in Haskell
In Kapitel 2.3 wurde schon erwähnt, dass OpenGL als State-Machine definiert ist und sich daher
auf den ersten Blick mit der funktionalen Programmiersprache Haskell nicht verträgt. Wir wollen
nun kurz erläutern, warum Haskell und OpenGL letztlich doch kompatibel sind und ein kleines
Beispiel geben.
2.4.1
Monaden
Von großer Bedeutung in unserer Arbeit sind Monaden, mit denen wir uns nun kurz beschäigen
wollen. Dieser Abschni basiert weitgehend auf (Pip06) und (H+ 00).
Eine Funktion in Haskell hat keine Seiteneffekte, denn sta Befehle auszuühren, kann sie vielmehr als Ausdruck augefasst werden, sie berechnet also rein aus den Parametern den Rückgabewert. Diese starke Einschränkung gegenüber imperativen Programmiersprachen erschwert o
Erweiterungen von Funktionen. Hin und wieder ist es gerade nötig, weitere Berechnungen in derselben Funktion durchzuühren. Wir können als Abhilfe den Ergebnistyp einer Funktion erweitern um die nebensächliche Berechnung speichern zu können - einfache Ergebnistypen würden
zu Tupeln. Die Funktion
f :: a -> b
wäre erweitert um eine nebensächliche Berechnung also von der folgenden Form:
f :: a -> (m,b)
Problematisch ist nun die Komposition von Funktionen, zum Beispiel f . f. Der Rückgabetyp
von f entspricht wegen der nebensächlichen Berechnung nicht mehr dem ursprünglichen Parametertyp. Wir benötigen also eine Funktion, welche die Komposition ohne Umstand ermöglicht,
wir nennen sie hier bind. Ebenfalls sinnvoll ist eine Funktion, welche eine Funktion mit normalem Rückgabewert vom Typ b in den erweiterten Typ (m,b) überührt, wir nennen sie hier
lift.
bind :: (m,a) -> (a -> (m,b)) -> (m,b)
lift :: (a -> b) -> (m, (a -> b))
Das eben gezeigte Konzept entspricht gerade dem von Monaden. Betrachten wir den Bind-Operator
>>= und die Identitätsfunktion return von Haskell:
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
2.4. OPENGL IN HASKELL
10
Die Ähnlichkeit zu den Funktionen bind und lift ällt direkt auf. Es ist noch anzumerken, dass
die Operatoren Gesetzmäßigkeiten unterliegen, der Bind-Operator ist assoziativ zum Beispiel,
allerdings spielt dies ür die weiteren Betrachtungen keine Rolle.
Außerdem ist das m von bind und left ein gewöhnlicher Datentyp, bei >>= und return handelt
es sich um Monad-Typen - wir kommen später darauf zurück.
Das Besondere am Bind-Operator ist, dass er uns sequentielle Ausührungen erlaubt, ein Beispiel:
return "monad " >>= (\s -> head s) >>= (\t -> tail t)
In Haskell kann man die letzte Zeile jedoch auch intuitiver mithilfe der do-Notation schreiben.
Das ist sehr praktisch ür uns, da wir zahlreiche OpenGL-Anweisungen nacheinander ausühren
müssen. Es gibt außerdem noch einen Operator >>, welcher genauso wie >>= ist, bloß dass er das
Ergebnis des ersten Ausdrucks nicht weiterleitet.
1
2
3
4
5
do
let s = "monad "
t <- head s
u <- tail t
return t
Je nachdem was wir ür m einsetzen, erhält >>= eine andere Bedeutung. Setzen wir zum Beispiel
IO ein, so werden Ergebnisse einer Berechnung an die nachfolgende weitergereicht. Anschaulich
reicht >>= immer einen Zustand weiter, wobei der Zustand anschließend geändert werden kann.
Die Funktion, die letztlich die Änderung bewirkt, ist allerdings frei von Seiteneffekten. Laden wir
beispielsweise eine Datei und möchten den Inhalt in die Kommandozeile ausgeben, dann könnte
es aufgrund der Lazy-Evaluation passieren, dass der Dateiinhalt ausgegeben wird, bevor sie eingelesen wurde. Da wir aber in dem Fall mit der Monade IO arbeiten, wird durch die (indirekte)
Verwendung von >>=, >> und return die Reihenfolge festgelegt und es gibt keine Probleme mit
der Lazy-Auswertung.
Neben der IO-Monade ist die State-Monade noch von Interesse. Die IO-Monade hat eine Verbindung zur Welt außerhalb des Programms, während eine State-Monade diese Verbindung gerade
nicht hat. State-Monaden werden zahlreich vom GLUT-Paket ür Haskell eingesetzt.
Monaden sind also sehr wichtig, denn sie bringen die Besonderheiten von Haskell mit der praktischen Welt in Einklang, da man zum einen Dateien lesen kann und zum anderen Zustände im
Programm intuitiv verwalten kann. Insgesamt haben wir also aufgezeigt, dass sich OpenGL und
Haskell miteinander vertragen. Auch die von uns verwendete Bibliothek GLUT, welche wir zum
Erzeugen, Verwalten und Verknüpfen eines Programmfensters mit OpenGL verwenden, baut auf
dem Konzept von Monaden auf.
Außerdem sei noch erwähnt, dass wir hin und wieder when aus dem Modul Control.Monad benutzen, welches beim Eintreffen einer Bedingung den definierten Teil ausührt, ansonsten nichts
ausührt. Dadurch wird das Programm an einigen Stellen lesbarer.
11
2.4. OPENGL IN HASKELL
2.4.2
Beispiel zu OpenGL mit GLUT
Das folgende Programm erzeugt ein Fenster mit GLUT und zeichnet in dieses mit OpenGL ein
Rechteck. Nützlich hierür war (Joh06).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import Graphics.UI.GLUT as GLUT
main :: IO ()
main = do
(_,_) <- getArgsAndInitialize
_ <- createWindow " Beispiel zu OpenGL mit GLUT"
windowSize $= Size 400 200
displayCallback $= render
matrixMode $= Projection
loadIdentity
perspective 45 .0 ( fromIntegral (400 `div ` 200) :: GLdouble ) 0 .5 250 .0
matrixMode $= Modelview 0
currentColor $= Color4 0 0 0 1
mainLoop
drawQuads :: [( GLfloat , GLfloat , GLfloat )] -> IO ()
drawQuads p = renderPrimitive Quads $ mapM_ (\(x, y, z) -> vertex $ Vertex3 x y
z) p
render :: IO ()
render = do
clearColor $= Color4 1 1 1 1
clear [ ColorBuffer ]
loadIdentity
translate ( Vector3 0 ( -5 ) ( -2 0) :: Vector3 GLfloat )
rotate 45 (( Vector3 1 .0 0 .0 0 .0 ) :: Vector3 GLfloat )
rotate 45 (( Vector3 0 .0 1 .0 0 .0 ) :: Vector3 GLfloat )
drawQuads [(0,0,0), (0,5,0), (5,5,0), (5,0,0)]
swapBuffers
In Zeile 5 initialisieren wir GLUT, wobei der erste Rückgabewert uns den Programmnamen und
der zweite die Kommandozeilenparameter liefern würde - wir benötigen hier beides nicht. Dadurch wird die Verbindung mit dem Fenstersystem hergestellt. In Zeile 6 wird ein Fenster erzeugt, wobei der Parameter den Fenstertitel setzt und der Rückgabewert ist ein Bezeichner ür
das Fenster, den wir nicht benötigen. In Zeile 7 setzen wir die Fenstergröße, wobei windowSize
eine State-Variable ist, die intern mit IO-Monaden umgesetzt ist. Dabei wird in GLUT ($=) zum
Setzen von State-Variablen benutzt und get zum Lesen. Immer wenn GLUT feststellt, dass das
Fenster aktualisiert werden muss, wird die Funktion render aufgerufen, welche in Zeile 8 als
Callback gesetzt wurde. loadIdentity lädt die Einheitsmatrix, entspricht also anschaulich dem
Zurücksetzen der gerade gewählten Matrix. Dabei kann die Matrix, welche von den folgenden
Matrixoperationen verändern werden soll, mit MatrixMode gewählt werden. Wie schon im Abschni 2.3.1 erklärt, müssen wir die Projektionsmatrix setzen um dreidimensional arbeiten zu
können. Dies geschieht in Zeile 12 wobei wir dort den Field-Of-View-Winkel auf 45° setzen.
2.4. OPENGL IN HASKELL
12
Der zweite Parameter ist das Verhältnis aus Breite und Höhe, der drie ist die Entfernung zur
Near-Plane, der vierte die Entfernung zur Far-Plane. In Zeile 14 setzen wir die aktuelle Zeichenfarbe auf schwarz, der vierte Wert wird uns erst viel später beschäigen und soll daher erstmal nicht weiter betrachtet werden. mainLoop startet die Event-Schleife von GLUT. Sobald man
mainLoop aufgerufen hat läu der Rest des Programms über Callbacks ab - es gibt weitere Callbacks auf die wir später noch eingehen werden.
In der render-Funktion setzen wir zunächst die Hintergrundfarbe auf weiß und leeren den Fensterinhalt. Die Matrizen sind nun vorbereitet ür den eigentlichen Zeichenvorgang. Wir möchten
ein Rechteck zeichnen und haben daür die Funktion drawQuads, welche eine Liste von Tripeln
erhält, wobei je vier aufeinanderfolgende Tripel zusammengehören und die vier Eckpunkte eines
Rechtecks definieren. renderPrimitive ührt den Zeichenvorgang durch und nimmt als Eingabe die Art der Primitive und eine Reihe von Funktionsaufrufen, welche wir mit mapM_ aus der
Liste erhalten. Wir möchten allerdings schräg auf unser Rechteck schauen, daher verschieben
wir es noch mit translate und passen die Winkel bezüglich der X - und Y -Achse so an, dass
man schräg drauf schaut.
Mit swapBuffers teilen wir GLUT mit, dass die Anzeige aktualisiert werden muss.
Das erzeugte Fenster des Programms sieht wiefolgt aus:
Abbildung 2.2: GLUT-Fenster mit OpenGL
Wir können die Performanz mit Displaylisten noch optimieren, die OpenGL-Anweisungen in
render sind nämlich immer gleich. Wir definieren die Displaylist dazu in der Hauptfunktion
und übergeben die Displaylist als Parameter an die Zeichenfunktion, welche sie nur noch aufzurufen braucht. Diese Vorgehensweise entspricht genau den Schilderungen in Abschni 2.3.2.
Näheres zu den Displaylisten findet sich in (hacb),
1
2
3
4
5
6
7
8
9
10
main = do
[...]
dl <- defineNewList Compile $ do
loadIdentity
translate ( Vector3 0 ( -5 ) ( -2 0) :: Vector3 GLfloat )
rotate 45 (( Vector3 1 .0 0 .0 0 .0 ) :: Vector3 GLfloat )
rotate 45 (( Vector3 0 .0 1 .0 0 .0 ) :: Vector3 GLfloat )
drawQuads [(0,0,0), (0,5,0), (5,5,0), (5,0,0)]
13
11
12
13
14
15
16
17
18
2.4. OPENGL IN HASKELL
displayCallback $= render dl
mainLoop
render dl = do
clearColor $= Color4 1 1 1 1
clear [ ColorBuffer ]
callList dl
swapBuffers
Das erzeugte Fenster des Programms sieht genauso aus, wie das in Abbildung 2.2 gezeigte Fenster.
3
P
Da unser Dateiformat CGSI heißt, nennen wir unser Programm CGSIV. Wir möchten in
diesem Kapitel aufzeigen, was das Programm kann und erst im nächsten Kapitel darauf eingehen, wie die verschiedenen Funktionalitäten implementiert wurden. Ein Aufruf des Programms
entspricht dem Format:
cgsiviewer <dateiname > [x1 y1 x2 y2 [ modus ]]
Dabei ist nur der erste Parameter ein Pflichtparameter, alle anderen sind optional. Mit den folgenden vier Parametern kann ein Auswahlrechteck angegeben werden, welches direkt berechnet
wird beim Start. Der letzte Parameter dient zum Testen: Falls 0 angegeben ist alles normal, falls
1 angegeben, wird die Datei geladen und das Programm terminiert, falls 2 angegeben, wird die
Auswahl berechnet, und das Programm terminiert, falls 3 angegeben, wird die Auswahl berechnet und das Programm terminiert nach dem ersten Zeichendurchlauf.
Nach Laden der Datei ist zunächst nichts ausgewählt. Zunächst alle Tasten im Überblick:
Taste(n)
W
A
, ←
S
D
, →
↑
↓
Page ↑
Page ↓
Esc
X
Y
R
F
+
E
T
Effekt
Vorwärtsbewegung bezüglich der aktuellen Blickrichtung.
Bewegung nach links.
Rückwärtsbewegung bezüglich der aktuellen Blickrichtung.
Bewegung nach rechts.
Vorwärtsbewegung bezüglich der aktuellen Blickrichtung parallel zur XY -Ebene.
Rückwärtsbewegung bezüglich der aktuellen Blickrichtung parallel zur XY -Ebene.
Bewegung nach oben/unten entlang der Z -Achse.
Bewegung nach unten entlang der Z -Achse.
Beendet den GUI-Modus, falls aktiv, ansonsten terminiert das Programm.
Skalierung an der Y -Achse erhöhen.
Skalierung an der Y -Achse verringern.
Leere Auswahl betätigen.
Aktiviert/deaktiviert, dass ader Höhe 0 haben. Standardmäßig deaktiviert.
Schriweite der Navigationstasten erhöhen.
Schriweite der Navigationstasten verringern.
GUI-Modus aktivieren.
Selektions-Modus Taschenlampe aktivieren oder falls aktiviert Auswahl betätigen.
Die Blickrichtung ändert sich entsprechend zur Bewegung der Maus. Bewegt man zum Beispiel
die Maus nach oben links, so schaut man anschließend entsprechend nach oben links.
14
15
KAPITEL 3. PROGRAMMBESCHREIBUNG
Auf die Selektionsmodi wollen wir nun noch näher eingehen.
1. Tasenlampe. Nach dem Drücken der Taste T erscheint am Schnipunkt aus Blickrichtungsvektor und XY-Ebene ein Auswahlrechteck, welches mit O vergrößert und mit L
verkleinert werden kann. Ein nochmaliges Drücken von T lädt den Ausschni an der
Stelle des Rechtecks und beendet den Selektionsmodus.
Abbildung 3.1: Auswahlvorgang mit der Taschenlampe
2. Mausauswahl. Durch einen Mausklick mit der linken Taste wird die Navigation eingefroren
und man kann ein kleines Rechteck auf der XY -Ebene durch Mausbewegung verschieben.
Drückt man nochmal die linke Maustaste, so wird der erste Punkt des Auswahlrechtecks
festgelegt und man wieder durch Mausbewegung den zweiten Punkt aussuchen. Drückt
man dann nochmals die linke Maustaste, so wird der Ausschni entsprechend zum selektierten Rechteck geladen.
Abbildung 3.2: Auswahlvorgang mit der Mausauswahl
KAPITEL 3. PROGRAMMBESCHREIBUNG
16
3. Koordinateneingabe. Nach Drücken der Taste E können die beiden Eckpunkte, aus denen
das Auswahlrechteck aufgespannt werden soll, mit der Tastatur eingegeben werden. Dabei
gibt man je eine Komponente nach der anderen unter Verwendung der Zahlentasten ein
und drückt ↩ um zur Eingabe der nächsten Komponente zu kommen oder die Auswahl zu
betätigen, wenn alle Komponenten eingegeben sind. →z kann dabei verwendet werden
um eine Eingabe zu korrigieren oder falls eine Komponente gelöscht ist zur letzten zurück
zu kommen beziehungsweise den ganzen Modus zu verlassen.
Abbildung 3.3: GUI-Modus
Der GUI-Modus bietet außerdem als Zusatzinfo noch die aktuelle Position und die Farbzuordnung
auf die Achsen und kann mit Esc verlassen werden.
Außerdem ist während der Selektion Transparenz aktiv, damit man das Auswahlrechteck immer
sehen kann:
Abbildung 3.4: Auswahlvorgang mit Transparenz
4
I
In diesem Kapitel möchten wir auf die Implementierung des Programmes näher eingehen. Die
Implementierung findet sich unter der folgenden Adresse:
http://www.ki.informatik.uni-frankfurt.de/bachelor/programme/cgsi
Die Implementierung ist strukturell geordnet. Im Verzeichnis src befindet sich der reine ellcode, im Verzeichnis data sind nur Testmatrizen abgelegt und in dist befindet sich das kompilierte Programm. Wir verwenden das Build-System Cabal, im src-Verzeichnis genügt also ähnlich
zu normalen Makefiles ein Aufruf cabal configure und im Anschluss cabal build, um das
Programm zu kompilieren. Alle Kompilate werden automatisch in das dist-Verzeichnis abgelegt, auch das kompilierte Programm befindet sich dort.
Das Programm ist am Ende relativ komplex geworden, weshalb wir den Code in verschiedene
Module aufgespalten haben:
1. Common enthält in Common.Helper Hilfsfunktionen und in Common.Draw einfache Zeichenroutinen, wie zum Beispiel eine Routine, die ein Rechteck zeichnet.
2. Matrix definiert die Datenstruktur der Matrizen (Matrix.Type), bietet Laderoutinen ür
ein eigenes Dateiformat (Matrix.Load) und bietet eine Routine um Ausschnie aus einer
Matrix zu berechnen (Matrix.Draw).
3. Space bietet fundamentale OpenGL-Operationen (Space.Common), eine Kamera aus der
Ego-Perspektive (Space.Camera), grundlegende Berechnungsfunktionen im dreidimensionalen Raum (Space.Calc) und Routinen zum Zeichnen im dreidimensionalen Raum
(Space.Draw).
Wir unterteilen den Rest dieses Abschnis grob in die drei Teile Programmstruktur, Matrix und
Zeichenroutine.
4.1 Programmstruktur
Der eigentliche Programmablauf wird durch die Callbacks von GLUT bestimmt. In (haca) finden
sich genauere Informationen zu den einzelnen Callbacks, wir werden hier nur auf die nötigen
eingehen. Wir haben bereits in Abschni 2.4.2 den displayCallback kennengelernt. Wir benötigen noch weitere Callbacks, die wir gleich betrachten werden.
17
4.1. PROGRAMMSTRUKTUR
18
Die Callbacks benötigen teilweise die gleichen Daten und die Daten können von einem Callback
verändert werden, ehe sie das andere erhält. Da der Aufruf der Funktion, die einem Callback zugeordnet ist, nicht unter unserer Kontrolle liegt, müssen wir einen Weg finden, gemeinsame Daten
mit Referenzen zu speichern. In Abschni 2.4.1 wurde auf Monaden, insbesondere IO-Monaden,
eingegangen. Das Modul Data.IORef bietet veränderbare Referenzen in der IO-Monade. Dabei
übergeben wir bei der Zuweisung eines Callbacks die nötigen IORefs, wobei dann im Callback
mit get der Wert auf den die IORef zeigt gelesen und mit $= geschrieben werden kann. Mehr zu
IORefs findet sich in (hacd). Wir erreichen auf diese Weise den nötigen Informationsaustausch
zwischen den Callbacks und unser Programm kann so intuitiv programmierbar tatsächlich Zustände annehmen, welche zum Beispiel durch Tastatur und Maus gesteuert werden können.
Wir definieren also die benötigten IORefs und übergeben diese in der Startfunktion des Programms den einzelnen Callbacks. Im folgenden Ausschni legen wir eine IORef ür die Fenstergrößen an und übergeben diese an die Callbacks.
1
2
3
4
5
6
7
8
9
10
screenWidthInit = 1000
screenHeightInit = 800
screenWidth <- newIORef screenWidthInit
screenHeight <- newIORef screenHeightInit
displayCallback $= render screenWidth screenHeight [...]
reshapeCallback $= Just ( reshape screenWidth screenHeight )
passiveMotionCallback $= Just ( mouseMotion [...] screenWidth screenHeight )
keyboardMouseCallback $= Just ( keyboardMouse [...] ( screenWidth , screenHeight )
[...])
Wir möchten nun die verwendeten Callbacks etwas genauer betrachten.
1. displayCallback. Dieser Callback wurde im Abschni 2.4.2 bereits behandelt. Wir wollen auch im richtigen Programm die Projektionsmatrix möglichst selten ändern. Das Setzen dieser verlagern wir also auf die main-Funktion und den weiter unten beschriebenen
reshapeCallback sta auf die render-Funktion. Da wir aber auch zweidimensionale Texte ausgeben möchten, kommen wir nicht drumherum die Projektionsmatrix o zu ändern.
Dies lässt sich aber optimieren indem man preserveMatrix verwendet. preserveMatrix
sichert die aktuelle Matrix zwischenzeitlich durch einen Push auf den zugehörigen MatrixStack, das Ausühren der angegebenen Operation und ein Pop vom zugehörigen MatrixStack. Wir können so die Aufrufe von perspective drastisch reduzieren.
Grob läu es also wiefolgt ab:
1
2
3
4
5
6
7
8
9
10
11
12
render width height [...] = do
clearColor $= Color4 0 .8 7 0 .9 6 1 1
clear [ ColorBuffer ]
matrixMode $= Modelview 0
loadIdentity
[...]
matrixMode $= Projection
preservingMatrix $ do
loadIdentity
matrixMode $= Modelview 0
19
4.1. PROGRAMMSTRUKTUR
13
14
15
16
17
preservingMatrix $ do
loadIdentity
[...]
swapBuffers
Man behandelt also zunächst alles, was eine perspektivische Darstellung benötigt und danach alle zweidimensionalen Inhalte. Wir sichern also vor dem Umschalten in einen 2DModus sowohl die Projektions- als auch die Modelview-Matrix, zeichnen dann die Texte
und die alten Matrizen werden danach automatisch wieder geladen. Wichtig ist hierbei,
dass - wie schon im Abschni 2.4.2 - vor dem Starten des mainLoop die Perspektive einmal gesetzt wird. Wir betrachten in dem Sinne die perspektivische Projektion als normal
und das Zeichnen der Texte als Abweichung davon, das wir performant mit Aufrufen von
preservingMatrix ermöglichen.
2. reshapeCallback. Bei einer Änderung der Fenstergröße wird die Funktion aufgerufen,
welche diesem Callback zugeordnet ist.
type ReshapeCallback = Size -> IO ()
In einem solchen Fall müssen wir die eigenen Fenstergrößenwerte aktualisieren. Wir müssten entsprechend zu Abschni 2.4.2 eigentlich noch die Perspektive neu setzen, da diese
auf die Fenstergröße zugeschnien ist, da wir das - wie eben erläutert - in jedem RenderDurchlauf machen, benötigen wir das hier nicht mehr.
1
2
3
4
5
6
7
reshape :: IORef GLint -> IORef GLint -> ReshapeCallback
reshape screenWidth screenHeight sz@( Size scrW scrH) = do
screenWidth $= ( fromIntegral scrW :: GLint )
screenHeight $= ( fromIntegral scrH :: GLint )
viewport $= ( Position 0 0, sz)
mode3D scrW scrH
postRedisplay Nothing
3. passiveMotionCallback. Die Funktion, welche diesem Callback zugeordnet ist, wird bei
einer Mausbewegung aufgerufen, bei der keine Maustaste gedrückt ist. Wir benutzen diesen Callback zur Anpassung der Blickrichtung der Kamera (mehr dazu im Abschni Kamera und Bewegung). Der Callback liefert uns die Position des Mauszeigers:
type MotionCallback = Position -> IO ()
4. keyboardMouseCallback. Die Funktion, welche diesem Callback zugeordnet ist, wird sowohl bei eienr Veränderung der Tastatur als auch der Maus aufgerufen.
type KeyboardMouseCallback = Key -> KeyState -> Modifiers -> Position ->
IO ()
Der Callback liefert die gedrückte Taste der Tastatur/Maus, ob die Taste runtergedrückt
oder losgelassen wurde, ob während dem Ereignis zusätzlich Shi, Strg oder Alt gedrückt
wurde und die Mausposition, falls es sich um ein Maus-Ereignis handelt. In unserer Funktion, die dem Callback zugeordnet ist, ist ein wesentlicher Teil der Programmlogik enthalten.
4.2. MATRIX
20
Wir haben damit die grobe Struktur des Programms gezeigt, werden im Folgenden aber darauf
verzichten, immer anzugeben was sich genau in welchen Callback abspielt, da dies aus dem Kontext heraus ersichtlich ist.
4.2 Matrix
In diesem Unterkapitel beschäigen wir uns mit der Repräsentation der Matrizen und den notwendigen Operationen auf ihrer Repräsentation, um Ausschnie darstellen zu können.
4.2.1
Dateiformat
Es war von Anfang an unser Ziel, das Programm als praktisch einsetzbares Ganzes zu implementieren. Daher ist es unerlässlich ein kleines Dateiformat zu definieren, sta zum Beispiel
Testmatrizen ins Programm fest reinzukompilieren.
Unser Dateiformat heißt Compressed-Grayscale-Image (kurz: CGSI) und hat als Endung daher
bei uns immer .cgsi, wobei diese beliebig wählbar ist. Das CGSI-Format ist ein Klartextformat,
eventuell wäre ein Binärformat noch kompakter, allerdings haben wir uns aufgrund der Größenordnungen der Grafiken entschieden, auf eine Umsetzung als Binärformat zu verzichten, da dies
im ganzen gesehen nur einen kleinen Faktor ausmachen würde.
Bei einer CGSI-Datei steht in der ersten Zeile ein Nicherminal, welches das Startsymbol der
S2ASLP-Grammatik darstellt. Die weiteren Zeilen stellen die Produktionen der zugehörigen S2ASLPGrammatik dar, sind also von einer der folgenden Formen:
1. A -> B,C,D,E
entspricht der Produktion A → (
B C
).
D E
2. A -> B+C
entspricht der Produktion A → B + C .
3. A -> B*C
entspricht der Produktion A → B ⋅ C .
4. A -> c
entspricht der Produktion A → c, wobei c ∈ {0, 1, . . . , 255}.
-, >, *, + und Kommata dürfen in Namen von Nicherminalen nicht vorkommen. Das Programm
lässt auch Zahlen größer als 255 als Terminal zu, da es sich nur um die Höhe in der 3D-Darstellung
handelt und zu keinen Problemen ührt.
Das Dateiformat stellt also S2ASLP-Grammatiken dar. Die S2ASLP-Grammatik aus Beispiel 1 als
CGSI-Datei:
1
2
3
4
5
6
7
S
A1
A2
A3
A4
A5
A6
->
->
->
->
->
->
->
A1+A2
A3 ,A3 ,A4 ,A4
A4 ,A3 ,A3 ,A4
A5 ,A5 ,A5 ,A5
A6 ,A6 ,A6 ,A6
1
2
Im Kapitel 5 werden Testdateien im CGSI-Format generiert, welche mit dem Programm direkt
geladen werden können.
21
4.2. MATRIX
4.2.2
Datenstruktur
Die CGSI-Dateien werden in die Datenstruktur MatrixGrammar geladen. Dabei enthält diese Datenstruktur das Startsymbol, eine Map mit Produktionen und die Seitenlänge der quadratischen
Matrix. Besonders von Bedeutung sind die Striktheits-Deklarationen durch die Ausrufezeichen,
denn wir ügen jede Produktion der Reihe nach in die Map ein. Wenn man viele Produktionen
hat, wird der unk-Stack immer größer, sodass es letztlich zu einem Stacküberlauf kommt. Dies
liegt an der verzögerten Auswertung in Haskell und die Striktheits-Deklaration sorgt daür, dass
die Werte direkt berechnet werden, wenn sie in die Map eingeügt werden, sodass der unkStack nicht mehr überläu.
Der Datentyp Production stellt die rechte Seite einer Produktion dar und entspricht genau den
erlaubten Produktionen des Dateiformats.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type NonTerminal = String
data Production =
ProdFour NonTerminal NonTerminal NonTerminal NonTerminal -- A1,A2,A3,A4
| ProdAdd NonTerminal NonTerminal
-- A1+A2
| ProdMult NonTerminal NonTerminal
-- A1*A2
| Terminal Int
-- 0/../255
deriving Show
data MatrixGrammar = MatrixGrammar {
start :: ! NonTerminal ,
size :: ! Int ,
productions :: !( Map.Map NonTerminal Production )
} deriving Show
4.2.3
Laderoutine
Aufgrund der Einfachheit des CGSI-Dateiformats ist ür das Einlesen einer CGSI-Datei kein aufwendiger Parser notwendig. Die Laderoutine liest zunächst das Startsymbol und setzt dieses in
der Datenstruktur. Anschließend geht die Laderoutine alle Produktionen durch, überührt diese
in die passenden Datentypen und ügt die Ergebnisse in die Produktions-Map ein. Die Kommata helfen beim Einlesen der Produktionen. Zum Schluss wird noch die Seitenlänge dadurch
bestimmt, dass eine Funktion einen Pfad von Startproduktion bis zum Terminal hinabsteigt und
die bis dahin bestimmte Seitenlänge bei jeder Tiefenzunahme verdoppelt - diese Funktion ist
endrekursiv implementiert.
4.2.4
Aussnittsberenung
Die Berechnung eines Ausschnis der Matrix wird durch die Funktion drawMatrix durchgeürt.
Die Funktion befindet sich in der Datei Matrix.Draw. Um einen Ausschni zu berechnen werden
der Funktion zwei Eckpunkte, die gegenüber voneinander liegen, als Parameter mitgegeben. Der
Rückgabewert ist dann eine Liste, welche Koordinaten ür den dreidimensionalen Raum enthält.
Eine dreidimensionale Koordinate beschreibt mit den x,y -Koordinaten die Position in der Matrix
und mit der z -Koordinate den Wert der an dieser Position abgeleitet wurde.
4.2. MATRIX
22
Das Kernstück der Funktion ist calcRect'. Die Grundidee hinter calcRect' ist, dass die Berechnung der Auswahl erfolgt indem in der Rekursion direkt die richtigen Ausschnie der Matrix gewählt werden. Die Berechnung beginnt ür einen Pixel nicht immer wieder im Startsymbol. Wie
genau die Funktion jedoch arbeitet wird später ausührlich erläutert. Bevor jedoch calcRect'
ausgeührt wird, findet eine Korrektur von möglicherweise falsch übergebenen Eckpunkten sta.
Der folgende vereinfachte Codeausschni beschreibt die Korrektur:
1
2
3
4
5
calcRect (i,j) (x,y)
| i >= x && j <= y =
| i >= x && j >= y =
| i <= x && j >= y =
| otherwise = (i, j)
(x,
(x,
(i,
(x,
j) (i, y) -- Fall
y) (i, j) -- Fall
y) (x, j) -- Fall
y)
-- Fall
1
2
3
4
Es werden von calcRect vier mögliche Fälle abgedeckt, sodass calcRect' gültige Parameter
erhält. Dabei sind die Parameter gültig, wenn sich der erste übergebene Punkt entweder weiter
oben oder auf selber Höhe und weiter links oder auf selber Breite befindet.
Wir betrachten nun den ersten Fall genauer: Ist i größer gleich x und j kleiner gleich y dann heißt
es, dass sich der Punkt (i,j) weiter rechts und über dem dem Punkt (x,y) befindet, dabei wird
der Fall der Gleichheit im weiteren nicht extra aufgeührt, da er ür eine Vertauschung irrelevant
ist. Es ist jedoch wichtig dass die Gleichheit überprü wird, weil trotzdem beide Bedingungen
überprü werden müssen, wenn bei einer der beiden Gleichheit vorliegt. Damit die Punkte (i,j)
im ersten Fall richtig weitergegeben werden, müssen also i und x vertauscht werden, sodass der
erste übergebene Punkt weiter links liegt als der zweite übergebene Punkt. Da die Höhe schon
richtig ist, reicht diese Vertauschung aus.
Die Überlegungen ür die weiteren Fälle sind analog und werden kurz zusammengefasst. Im zweiten Fall befindet sich der erste Punkt weiter rechts und weiter unten, somit müssen beide Punkte
komple vertauscht werden. Im drien Fall ist der erste Punkt weiter links und weiter unten,
somit müssen nur j und y vertauscht werden, damit die Höhe des ersten Punktes richtig ist. Im
vierten Fall sind die Punkte bereits korrekt angeordnet und können genauso weitergegeben werden. Die folgende Grafik veranschaulicht die vier Fälle und die dazugehörige Korrektur, dabei ist
der Zustand nach der Korrektur grün markiert, der erste Punkt ist vor der Korrektur gelb und
der zweite Punkte ist vor der Korrektur rot:
y
x
y
x
Fall 1
y
x
Fall 2
y
x
Fall 3
Abbildung 4.1: Positionskorrektur ür die Ausschnisberechnung
Fall 4
23
4.2. MATRIX
Es ist dabei gut zu erkennen, dass nach der Korrektur weiterhin das selbe Rechteck angegeben
wird. Es geht nur darum, dass die Koordinaten in richtiger Form an calcRect' weitergegeben
werden. Ein Beispiel ür einen Fall in dem die Korrektur notwedig wäre ist, wenn bei der Mausauswahl der zweite Punkt oben links vom ersten Punkt liegt.
Zur Korrektur der Punktpositionen kommt noch eine Korrektur der Punkte an sich hinzu. Jede
Koordinate wird mit folgender Funktion verarbeitet:
1
2
3
4
corCoord c
| c < 0 = 0
| c > sideLength = sideLength
| otherwise = c
Liegt eine Koordinate beider Punkte außerhalb des Matrixbereiches, so wird sie an die Grenzen
der Matrix angepasst. Der Grund hierür ist, dass die zwei Eckpunkte die übergeben werden exakt
die Grenzen der gesamten Matrix angeben müssen, damit calcRect' die Grammatik richtig
verarbeiten kann und eine korrekte Matrix berechnet wird.
Nachdem sichergestellt ist, dass die Punkte richtig gewählt sind, wird die Berechnung basierend
auf der Grammatik gestartet. Die Funktion calcRect' hat ein Tupel mit einer Map und der gesamten Ergebnisliste als Rückgabewert. Die Map speichert Nicherminale und zwei Punkte als
Schlüssel und die Ergebnisliste des Nicherminals als Wert. Der Rückgabewert von calcRect ist
also der zweite Wert des Tupels, das calcRect' berechnet. Hierbei gibt es gemäß der Grammatik vier Hauptälle in die calcRect' eingeteilt ist. Die Fälle richten sich dabei nach der Art des
Nicherminals welches aktuell bearbeitet wird, wobei zu Beginn das Startsymbol als Parameter
angegeben wird. Die vier unterschiedlichen Fälle die in der Funktion vorkommen basieren also
darauf, ob ein Nicherminal auf vier andere Nicherminale, auf eine Addition, auf eine Multiplikation oder auf einen Basisfall abgeleitet wird. Zum besseren Verständnis kann man sich das
vereinfacht so vorstellen:
1
2
3
4
5
calcRect ' production
| calcRect ' ( ProdFour
| calcRect ' ( ProdAdd
| calcRect ' ( ProdMult
| calcRect ' ( Terminal
a b c d) = [...]
a b)
= [...]
a b)
= [...]
a)
= [...]
An dieser Stelle ist es sinnvoll, den allgemeinen Ablauf der Rekursion näher zu betrachten. Angefangen mit dem Startsymbol wird die Rekursion immer wieder auf den Nicherminalen gestartet,
auf welche das aktuelle Nicherminal abgeleitet wird. Im Falle der Multiplikation und Addition
werden zusätzlich die Ergebnislisten, welche aus den abgeleiteten Nicherminalen gewonnen
werden, multipliziert beziehungsweise addiert. Das Rekursionsende ist der Terminalfall, wobei
dieser darin besteht, dass eine Koordinate an die Ergebnisliste angehängt wird. Eine Besonderheit der Rekursion ist hierbei, dass die rekursiven Aufrufe immer ineinander gestartet werden,
falls es mehrere Aufrufe gibt. Dadurch wird sichergestellt, dass die Map alle bereits berechneten Nicherminal enthält und das wiederum ermöglicht es bereits berechnete Ergebnisse wieder
zu verwenden. Die Ergebnislisten werden jedes mal an die Ergebnisliste des nächsten rekrusiven
Aufrufs angehängt. Am Ende erhält man so die komplee Ergebnisliste in dem rekursiven Aufruf
mit dem Startsymbol.
4.2. MATRIX
24
Es gibt insgesamt neun mögliche Fälle in denen sich ein Rechteck in der Matrix befinden kann.
Abhängig davon welcher Fall vorliegt wird der Teil der Matrix selektiert in welchem das Rechteck liegt. Alle anderen Teile werden von der Berechnung ausgeschlossen. Dieses Vorgehen ist
effizienter als ür jeden Pixel die Rekursion im Startsymbol zu starten. In der Implementierung
befindet sich diese Selektion in dem Aufruf calcRect' (ProdFour a b c d), denn nur in
diesem Aufruf geht man weiter in tiefere Matrixebenen.
In folgender Grafik werden die ersten vier Fälle veranschaulicht, dabei sind die orange markierten Vierecke die Nicherminale auf denen weitere Rekursionen gestartet werden:
Fall 1
Fall 2
Fall 3
Fall 4
Abbildung 4.2: Einerälle der Ausschnisberechnung
Wenn das ausgewählte Rechteck sich komple in einem der vier Nicherminale befindet, dann
reicht es aus, die Rekursion auf diesem Nicherminal weiterlaufen zu lassen. Alle anderen Nichtterminale können von der Berechnung ausgeschlossen werden. Die nächste Grafik zeigt auf, welche Fälle möglich sind wenn das Rechteck sich über eine Fläche der Matrix erstreckt, die von zwei
Nicherminalen erzeugt wird:
Fall 5
Fall 7
Fall 6
Fall 8
Abbildung 4.3: Zweierälle der Ausschnisberechnung
In den Fällen, bei denen zwei Nicherminale betroffen sind, können die zwei anderen von der
Berechnung ausgeschlossen werden. Der letzte Fall der aureten kann ist, dass alle vier Nicherminale benötigt werden. Es gibt keine Fälle bei denen nur drei Nicherminale benötigt werden,
weil es nicht möglich ist ein Rechteck so zu wählen. Der letzte Fall ist also folgender:
Fall 9
Abbildung 4.4: Viererfall der Ausschnisberechnung
25
4.2. MATRIX
Um zu entscheiden, in welchem Teil der Matrix sich das ausgewählte Rechteck befindet, muss
die gesamte Größe der Matrix ermielt werden, zudem muss in jedem rekursiven Aufruf bekannt sein, wie groß die Matrix ist, die von dem aktuellen Nicherminal erzeugt wird. Um zu
bestimmen, welche Nicherminale vom ausgewählten Rechteck betroffen sind, wird folgende
Hilfsfunktion benutzt:
1
2
3
4
5
whichRect x y halfsize
| x < halfsize && y < halfsize = 0
| x >= halfsize && y < halfsize = 1
| x < halfsize && y >= halfsize = 2
| otherwise = 3
Dieser Codeausschni zeigt die Einteilung eines Punktes nach Platzierung im Nicherminal an.
Ist sowohl der x-, als auch der y -Wert kleiner als die halbe Seitenlänge der Matrix, die durch
ein Nicherminal erzeugt wird, so befindet sich der Punkt in dem ersten der vier abgeleiteten
Nicherminale beziehungsweise im oberen linken Viertel der Matrix, die durch das Nicherminal erzeugt wird. Der Punkt erhält dann die Bewertung 0. Ist der x-Wert größergleich der halben
Seitenlänge und der y -Wert kleiner als die halbe Seitenlänge, so befindet sich der Punkt im zweiten Nicherminal beziehungsweise im oberen rechten Viertel der Matrix und der Punkt erhält
dann die Bewertung 1. Mit analogen Überlegungen wird die 2 vergeben, wenn der Punkt sich
unten links, also im drien Nicherminal, befindet und die 3 wird vergeben wenn sich der Punkt
unten rechts, also im vierten Nicherminal befindet.
Wenn beide Punkte gemäß dieser Funktion eine Bewertung erhalten, dann kann man festlegen,
welcher der neun beschriebenen Fälle vorliegt. Haben beide Punkte die selbe Bewertung, so muss
das Rechteck komple in einem Nicherminal liegen. Wenn beide die Bewertung 0 haben, so
liegt Fall 1 vor. Analog dazu liegt Fall 2 vor wenn beide mit 1 bewertet sind, Fall 3 wenn beide
mit 2 bewertet sind und Fall 4 wenn beide mit 3 bewertet sind. Haben die Punkte verschiedene
Bewertungen, so muss es einer der restlichen ünf Fälle sein. Die Korrektur sorgt daür, dass
der zweite Punkt rechts unten vom ersten Punkt liegt und damit folgt, dass wenn Punkt 1 die
Bewertung 0 erhält, Punkt 2 nur die Bewertungen 1, 2 und 3 bekommen kann. Hat Punkt 2 die
Bewertung 1, so liegt Fall 5 vor und es muss mit der oberen Häle der Matrix weitergerechnet
werden. Hat Punkt 2 die Bewertung 2, so liegt Fall 7 vor und es muss in der linken Häle der
Matrix weitergerechnet werden. Hat Punkt 2 die Bewertung 3 so liegt Fall 9 vor und es muss in
der gesamten Matrix weitergerechnet werden. Wenn der erste Punkte die Bewertung 1 bekommt,
dann kann der zweite Punkt nur noch die Bewertung 3 erhalten. Es liegt dann Fall 6 vor und es
muss nur in der rechten Häle der Matrix weitergerechnet werden. Wenn der erste Punkt die
Bewertung 2 bekommt, dann kann der zweite Punkt nur noch die Bewertung 3 bekommen und
dann liegt der Fall 8 vor bei dem in der unteren Häle der Matrix weitergerechnet werden muss.
Um eine bessere Vorstellung dieses Vorgehens zu erhalten wird ein Beispielfall geschildert. Angenommen man hat ein Nicherminal, welches eine Matrix mit Seitenlänge 1024 generiert. Will
man jetzt den Ausschni berechnen, der durch die Punkte (256, 256) und (768, 768) beschrieben
wird, so wird Fall 9 eintreten. Denn Punkt 1 bekommt die Bewertung 0 und Punkt 2 die Bewertung 3. Die Rekursion würde dann auf allen 4 Nicherminalen weitergeührt werden. In diesen
vier Rekursionen werden jedoch jeweils die Fälle von 1 bis 4 aureten, da immer nur genau eines
der Viertel benötigt wird. Es werden dann zum Beispiel an die Rekursion auf dem ersten abgelei-
4.2. MATRIX
26
teten Nicherminal die Punkte (256, 256) und (512, 512) weitergegeben und somit wird dann
Fall 4 weiter gerechnet. So werden große Teile der Matrix von der Berechnung ausgeschlossen.
Das Beispiel wird durch folgende Darstellung beschrieben, wobei die weißen Flächen von der
Berechnung ausgeschlossen werden:
(1, 1)
(256, 256)
(768, 768)
(1024, 1024)
Abbildung 4.5: Beispiel einer Ausschnisberechnung
Die Punkte, die an rekursive Unteraufrufe weitergegeben werden, sind dabei vom Fall abhängig
und dienen dazu, die neuen Grenzen festzulegen. Sie geben jeweils die relativen Koordinaten
bezogen auf das Nicherminal an. Jedes Nicherminal hat oben links die Koordinaten (0, 0)
und unten rechts die Koordinaten (size, size), wobei size die Seitenlänge der aus dem Nicherminal resultierenden Matrix ist. Mit Angabe der Eckpunkte kann somit wie oben beschrieben
immer entschieden werden welcher Fall vorliegt. Je nach Fall werden dann die neuen Grenzen
der Nicherminale auf denen die Rekursion weiter gehen soll angepasst. Auch die Eckpunkte die
übergeben werden müssen so angepasst werden, dass sie die richtige Position im neuen Nichtterminal haben werden. Denn liegt ein Eckpunkt zum Beispiel außerhalb von dem Nicherminal
eines Unteraufrufs, so muss mindestens eine Koordinate des Eckpunktes entweder 0, oder size
des Nicherminals sein, damit der Teilausschni richtig im neuen Nicherminal liegt. In dem
Programm wird das erreicht indem bei Unteraufrufen 0 oder halfsize bei Grenzüberschreitungen
eingesetzt wird. Dabei ist halfsize die halbe Seitenlänge eines Nicherminals und jedes Nicherminal im Unteraufruf wird genau zu einer Matrix mit der halben Seitenlänge abgeleitet.
Falls ein Punkt im Nicherminal des Unteraufrufes liegt, dann muss nur darauf geachtet werden, dass er die relative Position, die er in dem Vierteil hat, beibehält wenn man davon ausgeht,
dass jedes Viertel beziehungsweise jedes Nicherminal im Unteraufruf bei (0, 0) beginnt. Für
das obere linke Viertel gilt damit, dass ein Punkt der drinnen liegt unverändert weiter gegeben
werden kann. Für das obere rechte Viertel muss von dem x-Wert halfsize abgezogen werden, ür
das untere linke Viertel muss von dem y -Wert halfsize abgezogen werden und ür das untere
rechte Viertel muss von beiden Werten halfsize abgezogen werden. Auauend auf diesen Überlegungen wird in der Implementieung bei den rekursiven Aufrufen jeder der Punkte angepasst,
jenachdem welchen Fall man betri. Diese relativen Punkte spielen eine wichtige Rolle ür die
Wiederverwendung, denn sie beschreiben was ür ein Ausschni des Nicherminals berechnet
werden soll und wenn man die Ergebnisse dazu speichert, dann kann man das komplee Nicht-
27
4.2. MATRIX
terminal oder auch teilweise berechnete Nicherminale speichern. Die Speicherung der bereits
berechneten Ergebnisse wird durch die Map ermöglicht, wobei die Schlüssel dieser Map ein Nichtterminal und zwei Punkte sind. Diese zwei Punkte sind genau die Punkte, die bei dem Aufruf, in
dem das Nicherminal verarbeitet wird, als Eckpunkte mitgegeben wurden. Dadurch lassen sich
Ergebnisse ür die exakten Ausschnie berechnen. Falls eines der Nicherminale dann wieder
mit einem gleichen Ausschni benötigt wird, so kann es wiederverwendet werden indem man
die passende Ergebnisliste verwendet. Bei allen Produktionsarten, außer dem Terminalfall, wird
überprü ob eine Wiederverwendbarkeit mit dem aktuellen Nicherminal und den mitgegeben
Punkten möglich ist. In dem Fall wenn es nicht möglich ist wird eine weitere Rekursion gestartet,
wobei das Ergebnis dieser Rekrusion mit dem Nicherminal und den Eckpunkten als Schlüssel
in der Map gespeichert wird. In dem Fall wenn es möglich ist findet ein Abbruch der Rekursion
sta und die Gesamtergebnisliste wird erweitert.
Bei der Erweiterung der gesamten Ergebnisliste ist zu beachten, dass die Koordianten der wiederverwendeten Liste bereits absolut sind. Um die richtige Position zu erhalten werden die Werte
modulo der aktuellen Seitenlänge genommen, damit man die relative Position in der Matrix des
Nicherminals hat und dann werden die absoluten Werte, an denen sich der Anfang der Matrix
befindet, draufaddiert. Die Werte die den Anfang eines Nicherminals in der kompleen Matrix
angeben werden jeweils auch mitgegeben und rekursiv aufgebaut. Das Startsymbol erhält den
absoluten Eckpunkt (0, 0). Dann werden jeweils immer wieder in jedem Viererfall die neuen absoluten Startpunkte mitgegeben. Das obere linke Nicherminal bekommt den selben Startpunkt,
beim oberen rechten wird halfsize auf den x-Wert addiert, beim unteren linken wird halfsize auf
den y -Wert addiert und beim unteren rechten wird auf beide Werte halfsize draufaddiert. In den
Basisällen ührt dieses Vorgehen genau dazu, dass der absolute Wert erreicht wird an dem sich
der Punkt in der Matrix befindet. Beim Wiederverwenden müssen die Punkte wie beschrieben
neu berechnet werden. Falls der Basisfall eintri, dann wird der berechnete Punkt einfach an die
Ergebnisliste drangehängt. Eine zusätzliche Art von Wiederverwendung wird zudem in der Addition und Multiplikation benutzt. Kommt es zu dem Fall dass die Addition oder die Multiplikation
ausgeührt werden muss und beide Nicherminale gleich sind, so wird nur eine Rekursion gestartet. Die Ergebniswerte werden danach mit sich selbst addiert beziehungsweise multipliziert.
Den implmentierten Algorithmus kann man sich vereinfacht folgendermaßen vorstellen: Das
Nicherminal ist nt, die beiden Eckpunkte sind (i,j) und (x,y), (m,n) ist der absolute Startpunkt der Nicherminalmatrix, halfsize ist die halbe Seitenlänge von der nt-Matrix, mp ist die
beschriebene Map und lst die gesamte Ergebnisliste. Die Funktion isCase bezieht sich auf die
neun vorgestellten Fälle des Viererfalls. Die Funktion save nimt das Ergebnistupel des Funktionsaufrufs entgegen, speichert in der Map die Ergebnisliste unter dem Schlüsselstring nt,i,j,x,y
ab und hängt die Ergebnisliste an lst dran. Das Tupel das von save zurückgegeben wird enthält
dann die aktuallisierte Map und die aktualisierte Ergebnisliste. Der Code sieht nun wie folgt aus,
wobei alle ungenannten Funktionen mit den vorherigen Erläuterungen selbsterklärend sind:
1
2
3
4
5
6
7
calc nt ( ProdFour a b c d) (i,j) (x,y) (m,n) hs (mp , lst)
| member ( stringOf (nt ,i,j,x,y)) mp =
(mp , lst ++ repositionPoints (mp ! ( stringOf (nt ,i,j,x,y)))
| isCase 1 =
save (calc a (prod a) (i,j) (x,y) (m,n) (hs/2) (mp ,[]))
| isCase 2 =
save (calc b (prod b) (i -h s,j) (x -h s,y) (m+hs ,n) (hs/2) (mp ,[]))
4.2. MATRIX
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| isCase 3 =
save (calc c
| isCase 4 =
save (calc d
| isCase 5 =
save (calc b
(calc a
| isCase 6 =
save (calc d
(calc b
| isCase 7 =
save (calc c
(calc a
| isCase 8 =
save (calc d
(calc c
| otherwise =
save (calc d
(calc c
(calc b
(calc a
28
(prod c) (i,j -h s) (x,y -h s) (m,n+hs) (hs/2) (mp ,[]))
(prod d) (i -hs,j -h s) (x -hs,y -h s) (m+hs ,n+hs) (hs/2) (mp ,[]))
(prod b) (0,j) (x -hs,y) (m+hs ,n) (hs/2)
(prod a) (i,j) (hs -1 ,y) (m,n) (hs/2) (mp ,[])))
(prod d) (i -hs,0) (x -hs,y -h s) (m+hs ,n+hs) (hs/2)
(prod b) (i -h s,j) (x -hs,hs -1 ) (m+hs ,n) (hs/2) (mp ,[])))
(prod c) (i,0) (x,y -h s) (m,n+hs) (hs/2)
(prod a) (i,j) (x,hs -1 ) (m,n) (hs/2) (mp ,[])))
(prod d) (0,j -h s) (x -h s,y -h s) (m+hs ,n+hs) (hs/2)
(prod c) (i,j -h s) (hs -1 ,y -h s) (m,n+hs) (hs/2) (mp ,[])))
(prod
(prod
(prod
(prod
d)
c)
b)
a)
(0,0)
(i,0)
(0,j)
(i,j)
(x -hs,y -h s)
(hs -1 ,y -h s)
(x -hs,hs -1 )
(hs -1 ,hs -1 )
(m+hs ,n+hs) (hs/2)
(m,n+hs) (hs/2)
(m+hs ,n) (hs/2)
(m,n) (hs/2) (mp ,[])))))
calc nt ( ProdAdd a b) (i,j) (x,y) (m,n) hs (mp , lst)
| member ( stringOf (nt ,i,j,x,y)) mp =
(mp , lst ++ repositionPoints (mp ! ( stringOf (nt ,i,j,x,y)))
| otherwise =
let resA = calc a (prod a) (i,j) (x,y) (m,n) hs (mp ,[])
let resB = if a == b then resA
else (calc b (prod b) (i,j) (x,y) (m,n) hs (fst resA ,[]))
let resAB = (fst resB , addAndReposLists (snd resA) (snd resB))
in save (resAB )
calc nt ( ProdMult a b) (i,j) (x,y) (m,n) hs (mp , lst)
| member ( stringOf (nt ,i,j,x,y)) mp =
(mp , lst ++ repositionPoints (mp ! ( stringOf (nt ,i,j,x,y)))
| otherwise =
let resA = calc a (prod a) (i,j) (x,y) (m,n) hs (mp ,[])
let resB = if a == b then resA
else (calc b (prod b) (i,j) (x,y) (m,n) hs (fst resA ,[]))
let resAB = (fst resB , multAndReposLists (snd resA) (snd resB))
in save (resAB )
calc _ ( Terminal a) _ _ (m,n) _ (mp ,lst) = (mp , lst ++ [(m,n,a)])
Zusammenfassend wird mit dieser Implemtierung der Ausschnisberechnung also erreicht, dass
Rekursionen nur auf den notwendigen Teilen der Matrix gestartet werden. Um das zu erreichen
wurden Korrekturmaßnahmen eingeührt. Zudem ist es wichtig dass berechnete Ergebnisse, ür
jede der neun möglichen Ausschnisarten eines Nicherminal, abgespeichert werden. Das ermöglicht eine Wiederverwendung von Ergebnissen und die Wiederverwendung ist notwendig
damit der Algorithmus effizient arbeitet.
29
4.3. GRAFIK
4.3 Grafik
4.3.1
Kamera und Bewegung
Um die Sicht auf die dreidimensionale Welt und die Bewegung in dieser zu modellieren, wird eine Kamera verwendet. Wir wollen im Folgenden den abstrakten Auau der Kamera betrachten.
Sie besteht aus den drei Komponenten Position, Flächenwinkel und Höhenwinkel. Die Positionskomponente der Kamera gibt an, wo der Betrachter sich in der dreidimensionalen Welt befindet.
Die Position der Kamera ist einfach ein Punkt der durch x-,y - und z -Koordinaten angegeben wird.
Ausgehend von dieser Position spielen dann die beiden Winkel eine Rolle, undzwar beschreiben
die Winkel die Richtung in die der Betrachter schaut. Der Flächenwinkel beschreibt eine Links-,
Rechtsdrehung und der Höhenwinkel beschreibt das Auf- und Abschauen. Wenn also genau diese
Parameter an OpenGL übermielt werden, dann ändert sich die Perspektive entsprechend und
man befindet sich an der angegebenen Position und die Blickrichtung wird den gewählten Winkeln entsprechend gedreht. Der Code der die Drehungen der beiden Winkel bewirkt sieht wie
folgt aus, dabei wurden Typkonversionen aufgrund von besserer Übersichtlichkeit weggelassen:
1
2
3
4
5
6
7
8
cameraMouseMove ( Vector2 angX angY) (oldMouseX , oldMouseY ) (mouseX , mouseY ) width
height cameraRot = do
let newAngY = angY -( ( oldMouseX -m ouseX )*720 .0 /width)
let new
= angX -( ( oldMouseY -m ouseY )*180 .0 / height )
if new < 90 && new > -9 0
then
cameraRot $= Vector2 new newAngY
else do
cameraRot $= Vector2 angX newAngY
Die Mausbewegung wird berechnet indem die Mausposition vor der Bewegung mit der Position nach der Bewegung verglichen wird. Eine Bewegung nach links oder nach rechts verändert
den Flächenwinkel angY und eine Bewegung nach unten oder oben verändert den Höhenwinkel
angX. Die Mausbewegung ist dabei so skaliert, dass die Bewegung über die volle Breite des Programmfensters eine Drehung um 720○ und die Bewegung über die volle Höhe eine Drehung um
180○ bedeutet. Diese Begrenzung wird dadurch erreicht, dass die Differenz der x-Werte mit dem
Faktor 720.0/width und die Differenz der y -Werte mit dem Faktor 180/height skaliert wird.
Bevor nun die Erläuterung der Bewegungsarten beginnt, ist es sinnvoll sich folgende trigonometrische Zusammenhänge vor Augen zu ühren:
Gegenkathete
⇔ Gegenkathete = sin(α) ⋅ Hypothenuse
Hypothenuse
Ankathete
cos(α) =
⇔ Ankathete = cos(α) ⋅ Hypothenuse
Hypothenuse
sin(α) =
Die Bewegung im dreidimensionalen Raum wird durch eine Positionsveränderung der Kamera
erreicht. Dabei wurden in dem Programm verschiedene Formen der Bewegung implementiert.
Zuerst genügt es das Modell nur mit dem Flächenwinkel zu betrachten. Der Höhenwinkel wird
ür die folgenden Bewegungsarten ignoriert. Das Modell kann man sich dann folgendermaßen
vorstellen:
4.3. GRAFIK
30
z
∢γ
x
v
y
Abbildung 4.6: Ein-Winkel-Modell der Kamera
Der blaue Winkel γ beschreibt also den Flächenwinkel und der Vektor v die Blickrichtung. Eine
Bewegung in Richtung des Vektors, also in Blickrichtung, wird durch folgende Veränderung der
Kameraposition erreicht:
newX = oldX + cos(γ) ⋅ ∣v∣
newY = oldY + sin(γ) ⋅ ∣v∣
newZ = oldZ
Im Falle, dass man um die Länge ∣v∣ in Blickrichtung laufen will, legt man laut trigonometrischen
Beziehungen genau die Länge der Gegenkathete in y -Richtung und die Länge der Ankathete in
x-Richtung zurück. Diesen Zusammenhang beschreiben auch die angegebenen Rechnungen ür
die neuen x-,y -Koordinaten. Da diese Bewegung auf einer Ebene stafinden soll, die parallel zur
XY -Ebene liegt, bleibt die z -Koordinate unverändert. Die Überlegung dahinter, solch eine Bewegung zu erlauben, liegt darin, dass man sich unabhängig davon, ob man gerade hoch oder runter
schaut, parallel zur Matrix bewegen kann. Dabei bedeutet Blickrichtung in diesem Zusammenhang, dass der eigentlich Blickrichtungsvektor auf die parallel zur XY -Ebene liegenden Ebene,
auf der man sich gerade befindet, projiziert wird. Die Bewegung nach links und nach rechts basiert auf dem gleichen Prinzip und ist auch unabhängig von dem Höhenvektor. Um die neuen x,y - und z -Koordinaten zu bestimmen, wird der Winkel γ um 90○ gedreht. Der Unterschied darin,
ob man sich vor oder zurück beziehungsweise nach links oder nacht rechts bewegt, liegt im Vorzeichen das man ür ∣v∣ festlegt. Will man also nach vorne oder nach rechts laufen so wählt man
+∣v∣, will man nach hinten oder nach links laufen, so wählt man −∣v∣. Die nächste Bewegungsart
ist die orthogonale Bewegung zur XY -Ebene beziehungsweise die Auf- und Abbewegung orthogonal zur Matrix. Diese Bewegungsart ist sowohl vom Flächen, als auch vom Höhenwinkel
unabhängig. Die gewünschte Schrigröße wird einfach von der z -Koordinate subtrahiert oder
auf den Koordinatenwert addiert, es werden also folgende Rechnungen getätigt:
newX = oldX
newY = oldY
newZ = newZ + step
31
4.3. GRAFIK
Hierbei ist auch wieder vom Vorzeichen von step abhängig ob eine Auf- oder Abbewegung stafindet. Um die letzte implementierte Bewegungsart zu erklären wird nun das Model um den Höhenwinkel erweitert:
z
∢χ
∢γ
v
x
y
Abbildung 4.7: Zwei-Winkel-Modell der Kamera
Der Blickrichtungsvektor v ist jetzt nicht mehr unabhängig vom Höhenwinkel. Die letzte Bewegungsart ist die Bewegung entlang dieses Vektors, was im dreidimensionalen Raum bedeutet,
dass man sich in tatsächlicher Blickrichtung vorwärts, oder rückwärts bewegt. Auch hier kann
man durch die trigonometrischen Zusammenhänge die Bewegung durch folgende Berechnungen
darstellen:
newX = oldX + cos(γ) ⋅ cos(χ) ⋅ ∣v∣
newY = oldY + sin(γ) ⋅ cos(χ) ⋅ ∣v∣
newZ = oldZ + sin(χ) ⋅ ∣v∣
Die Höhenveränderung der Position ist die Gegenkathete des Dreiecks mit dem Winkel χ und
wird durch die ür z angegebene Formel beschrieben. Die Bewegung auf der XY -Ebene ist jetzt
abhängig davon, wie weit man nach oben beziehungsweise unten geht. Dabei ist die Streckenlänge, die man in der XY -Ebene zurücklegt genau die Ankathete des Dreiecks mit Winkel χ.
Diese Streckelänge wird durch cos(χ) ⋅ ∣v∣ berechnet und bildet gleichzeitig die Hypothenuse des
Dreiecks mit Winkel γ . Die Länge der Ankathete vom Dreieck mit Winkel γ gibt an, wie weit
sich die Position in x-Richtung verändert und die Länge der Gegenkathete vom selben Dreieck
gibt an, wie weit sich die Position in y -Richtung verändert. Dementsprechend folgen daraus die
angegebenen Formeln ür die Berechnung der neuen Position, wobei auch hier wieder das Vorzeichen vor dem Bewegungsvektor angibt, ob eine Vor- oder Rückwärtsbewegung stafindet. In
dem Programm wurde die Berechnung der neuen Position ür die verschiedenen Bewegungsarten
folgendermaßen umgesetzt:
4.3. GRAFIK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
32
cameraMove ( Vector2 angX angY) ( Vector3 posX posY posZ) direction step =
Vector3 newPosX newPosY newPosZ
where speed
= if direction `mod ` 2 == 0 then step else -s tep
newPosX = if direction < 2
then posX+cos (angY*pi/180+pi/2)*cos(angX*pi/180)*speed else
if direction < 4
then posX+cos (angY*pi/180)*speed else
if direction < 6
then posX
else posX+cos (angY*pi/180 -p i/2)*speed
newPosY = if direction < 2 then
posY+sin (angX*pi/180)*speed else
if direction < 4
then posY else
if direction < 6
then posY+ speed
else posY
newPosZ = if direction < 2
then posZ+sin (angY*pi/180+pi/2)*cos(angX*pi/180)*speed else
if direction < 4
then posZ+sin (angY*pi/180)*speed else
if direction < 6
then posZ
else posZ+sin (angY*pi/180 -p i/2)*speed
Bis auf einige Verdrehungen aufgrund von der OpenGL-Welt sieht man an der Implementierung,
dass sie genau die beschriebenen Berechnungen durchührt. Mit Verdrehungen ist zum Beispiel
gemeint, dass nicht wirklich die Kamera-Position benötigt wird. Interessanter ist die Position
bei der alle Vorzeichen der einzelnen Komponenten negativ sind. Damit kann direkt mit den
Kamerawerten translate ausgeührt werden. Alle Bewegungsarten repräsentieren zudem eine
Zoom-Funktion. Bewegt man sich weg von der Matrix, so wird sie kleiner und man sieht bezogen
auf einen festen Abschni des Programmfensters einen größeren Bereich der Matrix. Die Matrix
wird also kleiner dargestellt und das entspricht genau dem Herauszoomen aus einem Bild. Bewegt
man sich hin zur Matrix, so wird sie größer und man sieht bezogen auf einen festen Ausschni
des Programmfesnters einen kleineren Bereich der Matrix. Die Matrix wird also größer und das
entspricht genau dem Hineinzoomen in ein Bild.
4.3.2
Zeienroutine
Wie in Abschni 2.3.2 schon erklärt, ist es sehr wichtig die Datenmengen, welche zur Grafikkarte
übertragen werden müssen, zu minimieren. Angenommen wir würden ür jedes Matrix-Element
einen ader zeichnen und den gesamten Ausschni, das heißt alle zu zeichnenden ader,
mithilfe einer Displaylist im Grafikkartenspeicher ablegen. Die Performanz ist sicher besser als
wenn man die ganzen ader in jedem Frame über den Bus zur Grafikkarte schicken würde,
dennoch häen wir zahlreiche Flächen im Grafikkartenspeicher, welche gar nicht sichtbar sind.
Die Fläche in Orange beim folgenden Beispiel ist vollkommen unabhängig von der Perspektive
stets unsichtbar, muss also nicht extra gezeichnet werden:
33
4.3. GRAFIK
Abbildung 4.8: Zwei ader mit gemeinsamer Fläche bei naiver Zeichenroutine
Man muss sich auch immer vor Augen ühren, dass eine Displaylist eine Sammlung von Kommandos und Daten ist. Ru man also eine Displaylist auf, so werden die Kommandos einfach
ausgeührt. Insbesondere würden immer wieder unnötige Flächen gezeichnet. Es ist also nicht
empfehlenswert ür jedes zu zeichnende Element einen ader zu zeichnen.
Die Funktion drawMatrix (siehe Abschni 4.2.4) liefert uns eine Liste von Tripeln (x, y, z) wobei ein Tripel daür steht, dass an Stelle (x, y) der Wert z gespeichert ist. Da wir eine Höhenkarte
darstellen möchten, entspricht der z -Wert gerade der Höhe des aders. Wir sortieren diese Liste
nun und erhalten auf diese Weise eine Ordnung. Da wir außerdem wissen, welcher Ausschni
gezeichnet werden soll, ist bekannt, wie viele Elemente in einer Zeile sind. Insgesamt können
wir uns in der Liste also wie in einer Matrix selbst orientieren. Selbstverständlich opfern wir hier
Zeit, denn das Sortieren liegt in O(n log n), wobei n die Länge der Liste ist. Diese zusätzliche
Rechenzeit ällt beim Treffen einer Auswahl an, daür reduziert sich die Rechenzeit bei jedem
Frame, wodurch das Programm in der gesamten Handhabung deutlich flüssiger wird.
Wir werden also mithilfe der Rückgabe von drawMatrix und dem Auswahlrechteck zusammenhängende Flächen zeichnen. Zur besseren Veranschaulichung betrachten wir das folgende Beispiel, welches uns den Rest des Abschnis begleiten wird. Die CGSI-Datei
1
2
3
4
5
6
S
S
A
B
N
E
->
->
->
->
->
A,B,B,A
N,E,E,N
E,N,N,E
0
100
entspricht der folgenden Darstellung, wobei die Z -Achse wegen Übersichtlichkeit weggelassen
wird und die Darstellung an der Z -Achse skaliert ist:
Anschaulich betrachten wir die Matrix nun in Form einer Reihe von Zeilen, wobei Zeilen entlang
der X -Achse ausgerichetet sind. Die oben erwähnte sortierte Liste lässt sich als Folge von Zeilen
auffassen. Haben wir also eine Zeilenlänge von k , so entspricht die erste Zeile gerade den ersten
k Elementen, die Indizes gehen also von 0 bis k − 1, die der zweiten Zeile von k bis 2k − 1,….
Außerdem entspricht ein Tripel der Liste immer einem ader, welcher durch die ersten beiden
Komponenten des Tripels und die ersten beiden Komponenten des Tripels jeweils um 1 erhöht
entspricht.
4.3. GRAFIK
34
X
Y
Abbildung 4.9: Ausgangsausschni ür Zeichenvorgang
Die Funktion drawVertices aus dem Modul Space.Draw arbeitet auf der besagten sortierten
Liste, wobei a, b und c die gerade dem aktuellen Tripel entspricht. Wir möchten die Oberseite
eines aders später mit der Farbe zeichnen, die der Höhe des aders in Graustufen entspricht.
Farben werden in Rot-Grün-Blau-Anteilen angegeben, wobei jede Komponente ein Float-Wert
zwischen 0 und 1 ist, weshalb die Farbe gerade der Höhe des aders dividiert durch 255 entspricht. Da der zu zeichnende Bereich das Rechteck renderRect ist, kann die Breite width aus
den beiden Punkten des zu zeichnenden Bereichs bestimmt werden.
1
2
3
4
5
6
7
8
9
drawVertices
drawVertices
[...]
where
a
=
b
=
c
=
col
=
width =
[] _ = return ()
(x:xs) renderRect@ ((pX ,pY) ,(qX ,qY)) =
tripFst
tripSnd
tripTrd
c/255
qX -p X +
x
x
x
1
Wir möchten nun ür je zwei benachbarte ader einer Zeile eine Verbindungsfläche zwischen
den Oberseiten herstellen. Dazu prüfen wir ob das nächste Tripel der Liste noch in der selben Zeile
ist, außerdem benötigen wir kein Rechteck, falls der nächste ader die gleiche Höhe hat. Der
folgende Code setzt dies um und in Abbildung 4.10 sind die orange eingezeichneten Flächen die
vom Code gezeichneten, wobei zu beachten ist, dass die Flächeln alle einzeln gezeichnet werden,
selbst wenn sie graphisch direkt nebeneinander liegen. Zu beachten ist, dass Y und Z -Achse
intern verdreht sind.
1
2
3
when (xs /= [] && tripFst (head xs) == a && tripTrd (head xs) /= c) $
let o = tripTrd (head xs)
in drawQuads [(a,b+1,c) ,(a+1,b+1,c) ,(a+1,b+1,o) ,(a,b+1,o)]
X
Y
Abbildung 4.10: Flächen zwischen Oberseiten von adern in Zeilen
35
4.3. GRAFIK
Solche Flächen müssen auch ür Spalten gezeichnet werden. Da wir zeilenweise durch die Matrix
wandern genügt es, zwischen der Oberseite des aktuellen Tripels und der Oberseite in der gleichen Spalte in der nächsten Zeile zu zeichnen. Da die Breite des zu zeichnenden Bereichs bekannt
ist, kann mit dieser der Zeilensprung in der Liste vollzogen werden.
1
2
3
when (width -1 < length xs && width > 0 && tripTrd (xs !! (width -1 )) /= c) $
let o = tripTrd (xs !! ( width -1 ))
in drawQuads [( a+1,b,c) ,(a+1,b+1,c) ,(a+1,b+1,o) ,(a+1,b,o)]
X
Y
Abbildung 4.11: Flächen zwischen Oberseiten von adern in Spalten
Damit sind schon die wichtigsten Flächen gezeichnet, allerdings müssen wir bei adern die am
Rand liegen noch Flächen zeichnen, da dies von den obigen Codeabschnien noch nicht abgedeckt wird. Diese Flächen müssen allerdings nur gezeichnet werden, wenn der ader eine Höhe
echt größer als 0 hat.
1
2
3
4
5
6
7
8
when (( round
( drawQuads
when (( round
( drawQuads
when (( round
( drawQuads
when (( round
( drawQuads
b == pX) && (c > 0 .0 )) $
[(a,b,0 .0 ) ,(a+1 .0 ,b,0 .0 ) ,(a+1 .0 ,b,c) ,(a,b,c)])
b == qX) && (c > 0 .0 )) $
[(a,b+1 .0 ,0 .0 ) ,(a+1 .0 ,b+1 .0 ,0 .0 ) ,(a+1 .0 ,b+1 .0 ,c) ,(a,b+1 .0 ,c)])
a == pY) && (c > 0 .0 )) $
[(a,b,0 .0 ) ,(a,b+1 .0 ,0 .0 ) ,(a,b+1 .0 ,c) ,(a,b,c)])
a == qY) && (c > 0 .0 )) $
[( a+1 .0 ,b,0 .0 ) ,(a+1 .0 ,b+1 .0 ,0 .0 ) ,(a+1 .0 ,b+1 .0 ,c) ,(a+1 .0 ,b,c)])
X
Y
Abbildung 4.12: Flächen an Rändern
Die Oberseiten müssen nun noch gezeichnet werden.
1
2
currentColor $= Color4 col col col 1
drawQuads [(a,b,c) ,(a,b+1,c) ,(a+1,b+1,c) ,(a+1,b,c)]
4.4. SELEKTION UND GUI-MODUS
36
X
Y
Abbildung 4.13: Oberseiten
drawVertices fasst die ganzen Fälle zusammen, rechts nun auch die wirkliche Anzeige:
X
X
Y
Y
Abbildung 4.14: Ergebnis von drawVertices
Zu beachten ist, dass wir die Unterseiten der ader nicht zeichnen müssen, da sie keine Rolle
in der Darstellung spielen. Wir könnten außerdem noch benachbarte Flächen zu einer Fläche
vereinen. Dies würde jedoch einen größeren algorithmischen Aufwand bedeuten, weshalb wir
auf diese weitere Optimierung aus Zeitgründen verzichten mussten.
Die erläuterte Funktion wird nur bei der Selektion aufgerufen und die OpenGL-Anweisungen
dieser in eine Displaylist geschrieben. Außerdem ist noch zu bemerken, dass man mit einem
Aufruf von scale vor dem Aufruf der Displaylist eine Skalierung vornehmen kann. Die Funktion
scale nimmt drei Parameter entgegen wobei jeder davon eine Achse skaliert. Auf diese Weise
können wir die Höhe der ader skalieren ohne erneut drawVertices aufrufen zu müssen.
Der folgende Code setzt dies um, dabei wird abhängig davon, ob der flache Darstellungsmodus
aktiviert ist, die Höhe der ader auf 0 runterskaliert oder auf die aktuell eingestellte Höhe. Die
Z -Achse skalieren wir dabei auch direkt.
1
2
3
4
5
if fl
then scale 1 .0 0 .0 (1 .0 :: GLfloat )
else scale 1 .0 divH 1 .0
callList dl
drawAxes mat_size
4.4 Selektion und GUI-Modus
Wir wollen nun auf die Implementierung der in Kapitel 3 vorgestellten Selektionsmodi und die
Implementierung des GUI-Modus etwas näher betrachten.
37
4.4.1
4.4. SELEKTION UND GUI-MODUS
Tasenlampe
Die Taschenlampe berechnet den Schnipunkt mit der XY -Ebene in Blickrichtung. Wie schon
im Abschni Kamera und Bewegung erklärt, wird die Blickrichtung in der Kamera durch Flächenund Höhenwinkel bestimmt. Um den benötigten Schnipunkt mit der XY -Ebene zu berechnen
benötigen wir also erstmal einen Blickrichtungsvektor. Diesen Vektor können wir aus Flächenund Höhenwinkel der Kamera berechnen. Dazu müssen wir einen Punkt bestimmen, welcher
z
∢χ
∢γ
v
vx vz
h
x
vy
y
Abbildung 4.15: Bestimmung des Blickrichtungsvektors
vom Vektor getroffen wird. Da die Kameraposition auf den Blickrichtungsvektor keinen Einfluss
hat, entspricht dieser Punkt gerade unserem Vektor. Betrachtet man Abbildung 4.15, so sieht man
dass die Komponenten des gesuchten Punktes direkt mit den Winkeln berechnet werden können.
Die Länge des Vektors ist frei wählbar, daher wählen wir als Länge 1, woraus folgt, dass die
Hypotenuse des zugehörigen Dreiecks gerade Länge 1 erhält. Also erhalten wir die Hypotenuse
des anderen Dreiecks wiefolgt:
h = 1 ⋅ cos χ
Nun kann der Punkt direkt berechnet werden:
vx = h ⋅ cos γ
vy = h ⋅ sin γ
vz = 1 ⋅ sin χ
Also ist der gesuchte Vektor:
⎛cos χ ⋅ cos γ ⎞
v = ⎜ cos χ ⋅ sin γ ⎟
⎝ sin χ ⎠
Der Code entspricht den Erklärungen von eben weitgehend, wir ühren wieder die nötigen Verdrehungen durch, welche durch die OpenGL-Welt bedingt sind:
4.4. SELEKTION UND GUI-MODUS
1
2
3
4
5
6
38
calcFoVVec ( Vector2 angX angY) = Vector3 x y z
where
newHypo = 1 * cos (( angX)*pi/180+pi)
z
= newHypo * sin (( angY)*pi/180+pi/2)
y
= 1 * sin (( angX)*pi/180+pi)
x
= newHypo * cos (( angY)*pi/180+pi/2)
Nun müssen wir den Schnipunkt von v und der XY -Ebene berechnen. Dazu benötigen wir eine
Geradengleichung, wobei v gerade der Richtungsvektor dieser und die Kameraposition gerade
der Stützvektor dieser ist. Da die z -Koordinate des Schnipunkts schon bekannt ist benötigen
wir ür die Berechnung des Schnipunkts keine Ebenengleichung. Seien die pi gerade die Komponenten der Kameraposition und vi gerade die Komponenten von v, dann erhalten wir:
⎛v1 ⎞
⎛x⎞ ⎛p1 ⎞
⎜y ⎟ = ⎜p2 ⎟ + t ⋅ ⎜v2 ⎟
⎝ 0 ⎠ ⎝p3 ⎠
⎝v3 ⎠
Das dazugehörige Gleichungssystem ist trivial lösbar, wir erhalten als Lösung:
p3
⋅ v1
v3
p3
y = p2 −
⋅ v2
v3
x = p1 −
Wir haben nun den Punkt der XY -Ebene gefunden, auf den der Betrachter schaut. Abhängig von
der Größe des Auswahlrechtecks können die beiden nötigen Eckpunkte des Auswahlrechtecks
nun direkt berechnet werden.
Zu beachten ist allerdings, dass die Gerade parallel zur Ebene liegen kann, in dem Fall wäre v3 = 0.
Unter Verwendung von Maybe (mehr unter (hace)) können wir diesen Fall elegant abfangen. Falls
es keinen Schnipunkt gibt, dann ühren wir die Selektion nicht durch, erst wenn der Benutzer
nicht mehr parallel zur XY -Ebene schaut kann eine Selektion durchgeührt werden.
Im Code wird die Berechnung des Schnipunkts genau wie beschrieben durchgeührt, man beachte wieder die gewohnte Achsenverdrehung in der OpenGL-Welt:
1
2
3
calcIntersectionXZ ( Vector3 posX posY posZ) ( Vector3 dirX dirY dirZ)
| dirY == 0 = Nothing
| otherwise = Just (( posX+dirX *( -( posY/dirY))), ( posZ+dirZ *( -( posY/dirY))))
4.4.2
Mausauswahl
Bisher mussten wir stets dreidimensionale Daten auf den zweidimensionalen Bildschirm bringen, wie das in Abschni 2.3.1 erläutert wurde. Allerdings haben wir nun das gegenteilige Problem: Wir wollen zweidimensionale Fensterkoordinaten in dreidimensionale Weltkoordinaten
umwandeln. Wir möchten dabei jedoch nur erreichen, dass man mit der Maus Punkte auf der
XY -Ebene auswählen kann. Seite 160 und 161 von (S+ 07) liefert uns hierür eine Lösung: Die
Funktion unProject liefert uns ür eine Fensterkoordinate eine Weltkoordinate. Dabei übergeben wir der Funktion einen dreidimensionalen Vektor, der Fensterkoordinaten und einen Tiefenwert enthält. Zur Berechnung benötigt die Funktion noch ModelView-Matrix, Projektionsmatrix
39
4.4. SELEKTION UND GUI-MODUS
und den Viewport. Der eben erwähnte Tiefenwert ist nötig, da man mit einer Koordinate im Fenster eine komplee Gerade in der Welt tri. Das ist auch nicht überraschend, da wir ja immerhin
von 2D auf 3D zurückrechnen wollen, die Lösung kann nicht eindeutig sein. Gibt man ür diesen
Tiefenwert 0 an, entspricht das der Near-Plane, gibt man als Tiefenwert 1 an, entspricht das der
Far-Plane. Also verwenden wir unProject um aus den Werten der Far- und Near-Plane einen
Vektor zu konstruieren, welcher zusammen mit der Position des Betrachters direkt eine Geradengleichung ergibt. Wie schon in Abschni 4.4.1 erläutert berechnen wir dann den Schnipunkt
zwischen dieser Geraden und der XY -Ebene.
Zu beachten ist außerdem noch dass OpenGL den Ursprung von Fensterkoordinaten unten links
setzt, während GLUT diesen oben links setzt. Besonders hilfreich war bei der Implementierung
hierbei (hacf).
1
2
3
4
5
6
7
8
9
10
11
screenCoordToWorld (mouseX , mouseY ) cameraTrans = do
vp@(_, Size _ vpH) <- get viewport
camTrans
<- get cameraTrans
mvMatrix
<- get ( matrix (Just ( Modelview 0))) :: IO ( GLmatrix GLdouble )
projMatrix <- get ( matrix (Just Projection )) :: IO ( GLmatrix GLdouble )
pn <- unProject ( Vertex3 ( fromIntegral mouseX ) ( fromIntegral (vpH -m ouseY)) 0)
mvMatrix projMatrix vp
pf <- unProject ( Vertex3 ( fromIntegral mouseX ) ( fromIntegral (vpH -m ouseY)) 1)
mvMatrix projMatrix vp
let dir = sub Vertex3 df pn pf
let camPos = (\( Vector3 x y z) -> ( Vector3 ( -x ) ( -y ) ( -z ))) camTrans
let p = calcIntersectionXZ camPos dir
return p
Die Funktion screenCoordToWorld kann nun im keyboardMouse-Callback verwendet werden
um das aktuelle Auswahlrechteck zu verändern. Dabei verwenden wir eine IORef ür das Auswahlrechteck. Ebenso benötigen wir noch eine IORef, um den aktuellen Status bei der Mausauswahl zu speichern, die Selektion läu in mehreren Phasen ab, wie es in Abbildung 3.2 gezeigt ist.
Die Anzeige des Auswahlrechtecks blendet auch die aktuelle Koordinate bei der Mausposition
und die Koordinate eines bereits gesetzten Punktes ein. Die Koordinatenberechnung erfolgt wie
gerade erklärt. Den Text wollen wir mit der Funktion drawText zeichnen (dazu gleich mehr
in Abschni 4.4.4), allerdings verlangt diese Funktion eine Koordinatenangabe in Fensterkoordinaten. Wir wissen den Schnipunkt und können dessen Fensterkoordinate miels project
berechnen. Dabei funktioniert project analog zu unProject bloß umgekehrt und nimmt die
selben Parameter. Mehr dazu findet sich auf Seite 163 in (S+ 07).
4.4.3
Transparentes Auswahlrete
Bei Taschenlampe und Mausauswahl wäre das Auswahlrechteck nicht sichtbar, wenn es sich in
Bereichen befindet, die bereits angezeigt werden - die ader überdecken das Auswahlrechteck.
Insbesondere da die Funktion drawVertices (siehe Abschni 4.3.2) zusammenhängend zeichnet,
können wir uns aber anschaulich formuliert Transparenz zunutze machen, ohne in besonders
unschöne optische Schwierigkeiten zu geraten.
Dieser Abschni basiert weigehend auf den Seiten 231ff. in (S+ 07).
4.4. SELEKTION UND GUI-MODUS
40
OpenGL verügt über keine Transparenz im klassischen Sinne. Ohne zu sehr in die Architektur einsteigen zu wollen, ist klar, dass OpenGLüber einen Puffer verügt, welcher den aktuellen
Frame enthält, soweit er bis dahin gezeichnet wurde. Daher nennt man diesen Puffer auch FrameBuffer. Wenn im Frame-Buffer bereits Primitiven gezeichnet sind und es soll noch eine gezeichnet
werden, dann würde abhängig vom Tiefenwert die neue Primitive die bereits bestehenden Primitiven überdecken oder die schon gezeichneten würde die neue überdecken.
Genau an dem Punkt können wir jedoch miels Blending ansetzen. Blending bietet uns die Möglichkeit, zu bestimmen, zu welchem Anteil neue Primitiven über den Frame-Buffer gezeichnet
werden soll. Das genaue Verhalten kann mit blendFunc festgelegt werden, welche als ersten
Parameter den Anteil ür die elle, also die noch zu zeichnenden Primitiven und als zweiten
Parameter den Anteil des Ziels, also den Anteil des Frame-Buffers erhält. Diese Funktion hat nur
Auswirkungen sofern Blending überhaupt aktiviert ist.
1
2
blend $= Enabled
blendFunc $= ( SrcAlpha , OneMinusSrcAlpha ))
Wir verwenden stets Color4-Farbwerte, wobei bei einem solchen Farbwert die ersten drei Komponenten die eigentliche Farbe im Rot-Grün-Blau-Format festlegen und die vierte einen Alphawert festlegen. Der Alphawert beschreibt dabei die Intensität, ein Alphawert von 1 entspricht
dabei voller Intensität, andere Alphawerte ühren zu Intensitäten im Verhältnis dazu. Unserer
Konfiguration der blendFunc ührt dazu, dass ür die elle der Alphawert αals Faktor beim
Blenden genommen wird und ür das Ziel wird 1 − α als Faktor beim Blenden genommen. Dies
entspricht einer transparenten Darstellung, wenn man als Alphawert zum Beispiel 0.8 wählt.
Für das Auswahlrechteck heißt das, dass wir zunächst ganz normal das Auswahlrechteck zeichnen, also mit deaktiviertem Blending. Anschließend aktivieren wir Blending mit der oben angegebenen Konfiguration und zeichnen die Matrix. Da wir danach kein Blending mehr benötigen,
deaktivieren wir es anschließend.
1
2
3
4
5
6
7
8
9
-- zeichne Auswahlrechteck
blend $= Enabled
blendFunc $= ( SrcAlpha , OneMinusSrcAlpha ))
callList dl
[...]
blend $= Disabled
Wie schon eingangs erwähnt ist dies aber keine wirkliche Transparenz. Denn die Reihenfolge
des Zeichnens der Primitiven spielt eine große Rolle. Vom Betrachter aus gesehen müsste man
die Primitiven von hinten nach vorne zeichnen, ansonsten kommt es logischerweise zu Problemen in der Darstellung. Allerdings ist eine solche Sortierung teuer, sie hängt schließlich von der
aktuellen Position und Blickrichtung der Kamera ab. Wir müssten diese Sortierung also ständig durchühren und je größer die Datenmenge, desto aufwendiger wird diese. Außerdem ist die
Reihenfolge in der Displayliste bereits festgelegt, unsere Performanzoptimierung ließe sich im
Selektionsmodus also nicht mehr verwenden. Daher haben wir uns entschlossen kleine Zeichenfehler in Kauf zu nehmen, denn am wichtigsten sind Performanz und dass man überhaupt das
Auswahlrechteck jederzeit sehen kann. Gerade diese beiden Ziele haben wir erreicht.
41
4.4. SELEKTION UND GUI-MODUS
Abbildung 4.16: Blending und seine Tücken
Man sieht links deutlich die leichten Darstellungsfehler, verursacht durch fehlende Tiefensortierung und weil man direkt entgegen der Zeichenrichtung schaut. Rechts schaut man hingegen in
Zeichenrichtung, sodass es zu solchen Fehlern nicht kommt.
4.4.4
GUI-Modus
Da die Selektionskoordinaten im GUI-Modus direkt eingegeben werden können, ist bei der Selektion im GUI-Modus nichts weiter zu beachten.
Der GUI-Modus benötigt einen IORef auf ein Tupel, das alle wesentlichen Informationen zur
GUI beinhaltet und Hilfsfunktionen, die den Umgang mit dem Tupel intuitiver gestalten. Dabei
besteht der GUI-Modus aus der Anzeige von Texten und einer Eingabemöglichkeit, die wiederum nur zu einer Anzeige von Texten ührt. In Abschni 2.4.2 haen wir schon erwähnt, dass
OpenGL auch eine orthogonale Perspektive bietet. Wir werden diese jedoch nicht nutzen, möchten wir etwas in 2D zeichnen, setzen wir einfach die Projektionsmatrix zurück und können dann
Texte ohne Weiteres zeichnen, wobei sich die Koordinaten jeweils im Intervall [−1, 1] befinden
müssen.
GLUT bietet eine direkt verwendbare Implementierung von Bitmap-Fonts, wobei bei BitmapFonts jedem definierten Zeichen eine Bitmap zugeordnet ist und bei Bedarf gezeichnet wird.
rasterPos setzt die Bildschirmposition bei der das nächste Bitmap gezeichnet werden soll (mehr
zu dem theoretischen Hintergrund in (S+ 07) ab Seite 305). Die Schri ist zu groß skaliert, weshalb
wir den Text mit scale auf die gewünschte Größe runterskalieren (siehe Abschni 7.1 in (hsO)).
renderString zeichnet den angegebenen String mit der angegebenen Schriart, wobei letztere
aus von GLUT vorgegebenen Schriarten gewählt wird (mehr dazu in (hacc)).
1
2
3
4
5
drawText (x,y) font str = do
loadIdentity
rasterPos ( Vertex2 x y :: Vertex2 GLfloat )
scale (0 .1 :: GLfloat ) (0 .1 :: GLfloat ) (0 .1 :: GLfloat )
renderString font str
4.4. SELEKTION UND GUI-MODUS
42
Mit dieser Funktion haben wir also den Grundbaustein ür den GUI-Modus gelegt. Farben können
wie gewohnt durch Setzen von currentColor verwendet werden. Den Rest vom GUI-Modus
wollen wir nicht näher betrachten, mithilfe von dem ein oder anderen IORef kann die Logik der
Eingabe einer Selektion umgesetzt werden und ansonsten zeigt der GUI-Modus nur Texte an.
5
A
5.1 Generatoren
In diesem Kapitel werden die von uns benutzten Grammatik-Generatoren erläutert. Mit Hilfe dieser Generatoren wurden Grammatiken ür das Programm generiert. Diese Grammatiken ermöglichen es später im Abschni Programmtests Testreihen durchzuühren und Rückschlüsse auf
das Programmverhalten zu ziehen. Die grundlegende Funktion eines Generators ist es Nicherminale in eine Datei zu schreiben. Dabei muss darauf geachtet werden, dass keine Nicherminale
doppelt vorkommen und das die Nicherminale auf vorhandene Nicherminale ableiten.
Als Parameter nimmt ein Generator die Höhe des Startsymbols und den Namen der zu beschreibenden Datei an. Um sich den allgemeinen Auau besser vorzustellen, folgt der Code zu einem
der Generatoren:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import System.Environment
main = do
args <- getArgs
let filename = if ( length args) > 0 then args !! 0 else "tex t. cgsi"
let h = if ( length args) > 1 then read (args !! 1) :: Int else 0
writeFile filename "A1\n"
createGrammar filename h (h + 1)
createGrammar :: String -> Int -> Int -> IO ()
createGrammar filename h maxh
| h > 0 = do
let counter = maxh -h
appendFile filename ("A"++(show counter )++" -> "++"A"++(show ( counter+ 1))++
","++"A"++(show ( counter+ 1))++","++"A"++(show ( counter+ 1))++","++"A"++(
show ( counter+ 1))++"\n")
createGrammar filename (h -1 ) maxh
| h == 0 = appendFile filename ("A"++(show maxh)++" -> "++(show 1)++"\n")
In den Zeilen 3 bis 5 werden die Parameter gesetzt und die Funktion createGrammar wird getartet. Jeder rekrusive Aufruf ührt dann zu einer Veringerung der Höhe und schreibt ein Nichtterminal in die Datei welches auf vier gleiche Nicherminale abgeleitet wird. Im Basisfall der
Rekursion wird der Terminalfall mit dem Wert 1 gebildet. Die anderen Generatoren werden auch
nach dem Prinzip gebildet, dass sie eine Grammatik auauen indem die einzelnen Höhenstufen
gebildet werden.
43
5.2. LAUFZEITANALYSE
44
Soll zum Beispiel eine völlig unkomprimierte Grammatik entstehen, dann müssen jeweils vier
neue Rekursionen in jedem Funktionsaufruf gestartet werden. Dadurch entstehen am Ende 4h
Nicherminale die auf ein Terminal ableiten und vom Startsymbol aus durch jeweils verschiedene
Pfade erreichbar sind.
Wenn mien in der Grammatik Additionsälle eingebaut werden sollen, dann kann man zum
Beispiel wie beschrieben immer jeweils 4 verschiedene Nicherminale pro Aufruf erzeugen, aber
dabei noch extra ein Nicherminal in die Datei schreiben, welches zwei der Nicherminale die
man erzeugen will addiert. Dieses extra erzeugte Nicherminal würde dann in der Zeile welche
die Ableitung beschreibt, also Zeile 14 im Beispiel, eines der üblichen Nicherminale ersetzen.
Das heißt man würde dann auf einen Additionsfall ableiten bei dem man weiß, dass die Komponenten ür die Addition in den nächsten rekursiven Aufrufen definiert werden.
Es ist schnell erkennbar, dass es viele Möglichkeiten gibt, verschiedene Grammatiken zu generieren und auch in dem Kapitel Programmtests wird noch eine weitere Grammatikart vorgestellt,
welche durch einen modifizierten Generator erzeugt wurde.
5.2 Laufzeitanalyse
Wir möchten nun die Laufzeit der Ausschnisberechnung analysieren.
Sei G eine S2ASLP-Grammatik, A ein Matrixausschni aus der von G erzeugten Matrix und
∣A∣ die Anzahl der Bildpunkte in dem Ausschni. Beim Durchlaufen der Grammatik werden auf
Grund von dem Auau der Funktion nur Pfade durchlaufen, welche zu einem benötigten Terminal ühren. Findet gar keine Wiederverwendung sta, dann muss jeder der Pfade verschieden
sein, denn sonst würde es eine Wiederverwendung geben. Dabei wird dann eine der neun möglichen Varianten eines Nicherminals nur einmal verwendet, weil direkt bei der Verwendung
alle benötigten Pfade gestartet werden. Es gibt dabei wie gesagt nur O(1) viele Varianten. Bei
jedem Besuch eines Nicherminals muss in einer Map der Größe O(∣G∣) nachgeschaut werden,
um den rechten Teil der Produktion auszulesen. Wird mindestens eine Wiederverwendung benutzt, so gibt es Nicherminalvarianten die mehrfach benötigt werden. Die Wiederverwendung
würde bewirken dass die Entstehung weiterer Pfade unterbunden wird. Das Wiederverwenden
eines schon benutzten Nicherminals kann nur von einer Nicherminalvarianten kommen, die
bisher nicht wiederverwendet werden konnte. Damit können nur O(∣G∣) viele Wiederverwendungen aureten. Insgesamt entstehen bei der Rekursion also O(∣G∣) viele Knoten. In jedem
Knoten muss in einer Map mit einer Laufzeit von O(log ∣G∣) nachgeschaut werden. Bei einer
Wiederverwendung wird zudem eine Liste der Größe O(∣A∣) verarbeitet und angehängt. Es wird
also O(∣G∣) mal in einer Map nachgeschaut und dabei kann pro Nachschauen eine Laufzeit von
O(∣A∣) entstehen. Als gesamte Laufzeit folgt daraus:
O(∣G∣ ⋅ ∣A∣ ⋅ log ∣G∣)
45
5.3. PROGRAMMTESTS
5.3 Programmtests
Es werden insgesamt drei Testvarianten durchgeührt. Dabei werden in den Tabellen in jeder Variante drei verschiedene Grammatiken getestet. Die Tests bestehen jeweils darin, Zeitmessungen
ür verschiedene Teile des Programms durchzuühren, dabei werden die Messwerte in Sekunden
angegeben. Die erste Grammatik ist voll komprimiert, die zweite teilweise und die drie garnicht.
Für jedes Nicherminal der voll komprimierten Grammatik gilt, dass es auf vier gleiche Nicherminale abgeleitet wird. Bei der unkomprimierten Grammatik wird jedes Nicherminal auf vier
verschiedene Nicherminale abgeleitet und bei der zum Teil komprimierten Grammatik wird die
obere Häle der Matrix voll komprimiert und die untere Häle der Matrix garnicht. Der Basisfall
ist bei allen Grammatiken der Wert 1. Bevor die Messwerte analysiert werden folgen vorerst die
grundlegende Systemdaten um eine Vorstellung von der Arbeitsumgebung zu haben:
Betriebssystem
Prozessor
Arbeitsspeicher
Grafikkarte
Fesplae
Windows 7 Professional 64 Bit
Intel Core i7-4770K
16 GB
NVIDIA GeForce GTX 770
Samsung SSD 840 Pro
Wie schon im Kapitel Programmbeschreibung erläutert wurde, gibt es drei Testvarianten. Bei
allen drei Testvarianten wurden die Zeitmessungen mit Hilfe des vom ghc-Compiler bereitgestellten Laufzeitsystems RTS durchgeührt (mehr dazu in (RTS)), wobei zum Zugriff auf dieses
das Programm mit der Option -rtsopts kompiliert werden muss. Ein Beispielaufruf ür einen
Test mit Testvariante 1 und einer Auswahl eines 300 × 300 großen Ausschnis würde mit der
Zeitmessungen zusammen wie folgt aussehen:
main test.cgsi 0 0 299 299 1 +RTS -s
Es folgt nun die Analyse der einzelnen Tests zu jeder Variante. Bei jeder Variante hat das Startsymbol die Höhe 9, es wird also mit einer Matrix der Größe 29 × 29 beziehungsweise 512 × 512
gearbeitet. Dabei wurde immer ein Ausschni der Größe 300 × 300 gewählt. Bei der teilweise
komprimierten Grammatik wurde der Abschni auch in der Größe gewählt, jedoch befindet er
sich in der Mie der Matrix, denn dann sind genauso viele Werte im komprimierten Teil wie im
unkomprimierten Teil.
In Variante 1 wird gemessen, wie lange die Laderoutine braucht um eine Grammatik mit allen
ihren Nicherminalen einzulesen. Zehn Testdurchläufe von jeder Grammatik ergaben:
Messung
voll
halb
nicht
1
0.17
1.45
3.04
2
0.20
1.48
2.98
3
0.22
1.47
3.04
4
0.20
1.47
3.06
5
0.22
1.45
3.04
6
0.23
1.47
3.10
7
0.22
1.48
3.04
8
0.19
1.40
3.12
9
0.20
1.48
3.15
10
0.20
1.50
3.10
Ø
0.21
1.47
3.07
5.3. PROGRAMMTESTS
46
In Variante 2 wird gemessen, wie lange die Laderoutine und die Ausschnisberechnung zusammen dauert. Zehn Testdurchläufe von jeder Grammatik ergaben folgende Messergebnisse:
Messung
voll
halb
nicht
1
0.20
1.78
3.40
2
0.22
1.76
3.35
3
0.20
1.70
3.42
4
0.22
1.72
3.39
5
0.20
1.75
3.45
6
0.22
1.76
3.37
7
0.17
1.76
3.37
8
0.20
1.78
3.42
9
0.16
1.70
3.40
10
0.22
1.78
3.39
Ø
0.20
1.75
3.40
In Variante 3 wird gemessen, wie lange die Laderoutine, die Ausschnisberechnung und das
Zeichnen der berechneten Punkte dauert. Zehn Testdurchläufe von jeder Grammatik ergaben
folgende Messergebnisse:
Messung
voll
halb
nicht
1
15.23
17.24
19.25
2
15.15
17.35
19.00
3
15.27
17.38
19.59
4
15.19
17.38
18.95
5
15.19
17.49
19.42
6
15.10
17.22
19.09
7
15.10
17.27
19.17
8
15.13
17.19
19.25
9
15.13
17.38
19.11
10
15.01
17.22
19.09
Ø
15.15
17.31
19.19
Doch nicht nur die Laufzeit ist ein wichtiger Faktor, auch der Speicherverbrauch muss betrachtet werden. Dabei wird der Wert angegeben, welcher beschreibt wie viel Platz über die Laufzeit
des gesamten Programms im Arbeitsspeicher eingenommen wurde. Der Wert bezieht sich also
nicht darauf wieviel Arbeitsspeicher das Programm dauerha beim Arbeiten einnimmt und wird
jeweils gerundet angegeben:
voll
192.3 KB
387.9 KB
442.0 MB
Variante 1
Variante 2
Variante 3
halb
760.8 MB
803.3 MB
1.405 GB
nicht
1.66 GB
1.76 GB
2.50 GB
Um die Messwerte der Laufzeit zu interpretieren, ist es sinnvoll die Zeiten der einzelnen Aktionen, also Grammatik laden, Ausschni berechnen und ader zeichnen, zu betrachten. Daür
werden die Durschniswerte in einer Grafik anschaulich gemacht. Zuerst folgt die Grafik ür
die Laderoutine, dabei ist G1 die voll komprimierte Grammatik, G2 die teilweise komprimierte
Grammatik und G3 die garnicht komprimierte Grammatik:
t/[s]
3
2
1
0
G1
G2
G3
47
5.3. PROGRAMMTESTS
Die Grafik zur Rechenroutine enthält die Differenz der Durchschniszeiten von Variante 2 und
Variante 1, also die Zeit die ür die Berechnung notwendig ist:
t/[s]
0.3
0.2
0.1
0
G1
G2
G3
Die Grafik zur Zeichenroutine enthält die Differenz der Durchschniszeiten von Variante 3 und
Variante 2, also die Zeit die ür die Zeichnung notwendig ist. Man beachte hierbei die Skalierung
der Achsen die ür eine bessere Wertzuordnung angepasst wurde:
15.8
t/[s]
15.6
15.4
15.2
15
G1
G2
G3
Ein weiterer Test soll zum einen die Produktionen mit einer Addition testen und zum anderen
zeigen, dass aufgrund von der Wiederverwendung keine exponentielle Laufzeit benötigt wird.
Eine Testdatei hat dabei die Form:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C20000
C20000 -> C19999 +
C19999 -> C19998 +
B19999 -> B19998 +
C19998 -> C19997 +
B19998 -> B19997 +
.
.
.
C3 -> C2 + B2
B3 -> B2 + C2
C2 -> C1 + B1
B2 -> B1 + C1
C1 -> A1 ,A2 ,A3 ,A4
B1 -> A1 ,A2 ,A3 ,A4
B19999
B19998
C19998
B19997
C19997
5.3. PROGRAMMTESTS
16
17
18
19
A1
A2
A3
A4
->
->
->
->
48
0
0
0
0
Die zweite Testdatei folgt der selben Struktur hat aber eine weitere Additionsstufe, wird also noch
um die folgenden Zeilen erweitert, wobei C20000 entfernt wird:
1
2
3
C20001
C20001 -> C20000 + B20000
B20000 -> B19999 + C19999
Die Anzahl der Pfade, welche aufgrund von der ersten Grammatik entstehen würden, ist 220000
und bei der zweiten Grammatik sind es 220001 Pfade, also doppelt so viele. Getestet wird jetzt die
reine Rechenzeit, das heißt die Zeit von Testmodus 1 muss wieder von der Zeit von Testmodus 2
abgezogen. Im Durchschni ergibt der Test, dass die Rechenzeit ür den Ausschni mit den Eckpunkten (0, 0) und (0, 1) in etwa genauso groß ist. Geschätzt benötigen beide Grammatiken mit
dem genannten Abschni 0, 4s Rechenzeit. Um eine Verdopplung der Zeit zu beobachten wird
nun eine Grammtik mit dem selben Auau wie beschrieben verwendet, jedoch ängt die Grammatik mit dem Nicherminal C40000 an. Die Rechenzeit, die hierbei ür den vorher benutzten
Ausschni benötigt wird, ist geschätzt 0, 8s.
Diese Tests wurden auch ür die Multiplikation und ür die anderen Ausschnisvarianten durchgeührt, wobei die selbe Entwicklung beobachtet werden konnte. Es ist klar dass je nach Testumgebung Schwankungen in den Messungen aureten können. Deshalb werden im folgenden die
Tendenzen in den Tests interpretiert:
• Aufgrund von der Wiederverwendung entstehen verschiedene Anordnungen der Ergebnislisten. Bei der Sortierung werden somit je nach Grammatik verschiedene Zeiten benötigt.
Bis auf diese Schwankungen benötigt die Zeichenroutine ungeähr immer die gleiche Zeit
um die 90000 Punkte zu zeichnen. Es besteht beim Zeichnen selbst keinerlei Abhängigkeit
zur Größe der Grammatik, oder zum Kompressionsgrad.
• Anhand der Speicherwerte ist zu erkennen, dass wirklich nur die benötigten Teile der
Grammatik verarbeitet werden. Bei der Grammatik die garnicht komprimiert ist, wird zum
Laden der Grammatik 1.65GB alloziiert, dabei werden aber ür die Berechnung nur 0.1GB
benötigt.
• Die Laderoutine verhält sich asymptotisch linear zur Größe der Grammatik.
• Die Wiederverwendung verhindert ein exponentielles Verhalten der Laufzeit. Die Laufzeit
der Ausschnisberechnung ist asymptotisch linear zur Größe der Grammatik.
• Grammatiken, welche den Regeln einer S2ASLP-Grammatik folgen, können von dem Programm ordnungsgemäß und effizient verarbeitet werden.
6
Z  A
6.1 Zusammenfassung
In der Bachelorarbeit wurde hauptsächlich die Implementierung des Programms zur Darstellung
von Grammatik-komprimierten Bildern behandelt. Dabei hat die Effizienz in der Arbeit mit den
Grammatiken und bei der grafischen Darstellung eine besondere Rolle gespielt. Insgesamt hat besonders das Konzept der Wiederverwendung die effiziente Verarbeitung der Grammatiken möglich gemacht. Bei der grafischen Darstellung hat insbesondere die Zeichenroutine und die Verarbeitung der Bildpunkte im dreidimensionalen Raum zur Effizienz beigetragen. Abschließend
haben die Tests eine effiziente Laufzeit des Programms bestätigt. Wie erwartet ist die Laufzeit
von der Größe der Grammatik abhängig und somit hat die Kompressionsrate direkte Einwirkung
auf die praktische Laufzeit.
6.2 Ausbli
Es können nur Rechtecke zur Auswahl verwendet werden, eine sinnvolle Erweiterung wäre daher eine Lasso-Auswahl. Diese würde insbesondere bei großen Selektionen die Anzahl an zu
zeichnenden aber uninteressanten adern reduzieren. Dies könnte je nach Matrix ein sehr
nützliches Hilfsmiel sein. Bei uns können nur zusammenhängende Selektionen durchgeührt
werden, sinnvoll wäre noch eine Auswahlliste, welche alte Selektionen speichert, sodass man
letztlich auch nicht zusammenhängende Selektionen durchühren könnte.
Unser Schwerpunkt lag auf der Implementierung eines benutzbaren Programms. Von besonders
großem Interesse ist nun die Generierung von vielen weiteren Matrizen zur genaueren Analyse,
die unser Programm erleichtert. Da stellen stets ein Graustufenbild beziehungsweise eine Höhenkarte durch eine Matrix dar. Was lässt sich noch alles mit solchen Matrizen darstellen – im
besonderen Hinblick auf praktische Verwendbarkeit ? Die ührt auch direkt zur Frage, inweit das
CGSI-Format oder eines mit gleichem theoretischen Hintergrund praktisch verwendbar ist. Ein
Vergleich mit anderen verlustfreien Kompressionen wäre sinnvoll. Hierzu bräuchte man allerdings ein Programm welches Grafiken anderer Dateiformate in das CGSI-Format konvertieren
kann. Sta der Varbeitung solcher komprimierten Daten wäre also ein Kompressionsverfahren
von Interesse, welches gerade diese Daten erzeugt. Anschließend könnte man verschiedene Grafiken in unser Format konvertieren und die Größen zwischen verschiedenen Formaten vergleichen. Die meisten Formate sind Binärformate, daher wäre zur besseren Vergleichbarkeit eine
Binärvariante des CGSI-Formats sinnvoll.
49
A
2.1
2.2
Perspektivische Projektion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
GLUT-Fenster mit OpenGL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
12
3.1
3.2
3.3
3.4
Auswahlvorgang mit der Taschenlampe
Auswahlvorgang mit der Mausauswahl .
GUI-Modus . . . . . . . . . . . . . . . . . .
Auswahlvorgang mit Transparenz . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
15
15
16
16
4.1
4.2
4.3
4.4
4.5
4.6
4.7
4.8
4.9
4.10
4.11
4.12
4.13
4.14
4.15
4.16
Positionskorrektur ür die Ausschnisberechnung . . . . . . . . .
Einerälle der Ausschnisberechnung . . . . . . . . . . . . . . . . .
Zweierälle der Ausschnisberechnung . . . . . . . . . . . . . . .
Viererfall der Ausschnisberechnung . . . . . . . . . . . . . . . . .
Beispiel einer Ausschnisberechnung . . . . . . . . . . . . . . . . .
Ein-Winkel-Modell der Kamera . . . . . . . . . . . . . . . . . . . .
Zwei-Winkel-Modell der Kamera . . . . . . . . . . . . . . . . . . .
Zwei ader mit gemeinsamer Fläche bei naiver Zeichenroutine
Ausgangsausschni ür Zeichenvorgang . . . . . . . . . . . . . . .
Flächen zwischen Oberseiten von adern in Zeilen . . . . . . .
Flächen zwischen Oberseiten von adern in Spalten . . . . . . .
Flächen an Rändern . . . . . . . . . . . . . . . . . . . . . . . . . . .
Oberseiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ergebnis von drawVertices . . . . . . . . . . . . . . . . . . . . . .
Bestimmung des Blickrichtungsvektors . . . . . . . . . . . . . . . .
Blending und seine Tücken . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
22
24
24
24
26
30
31
33
34
34
35
35
36
36
37
41
50
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
L
[A+ 11] In: A, Tilo u. a.: Mathematik. 2. Auflage. Spektrum Akademischer Verlag, 2011, S.
520ff.
[Ahn] A, Song H.: OpenGL Vertex Buffer Object (VBO). http://www.songho.ca/opengl/
gl_vbo.html. – zuletzt besucht am 22. Juni 2014
[DGL] DGL Wiki: Displayliste. http://wiki.delphigl.com/index.php/Displayliste. –
zuletzt besucht am 24. Mai 2014
[H+ 00] H, Paul u. a.: A Gentle Introduction to Haskell, Version 98. http://www.haskell.
org/tutorial/monads.html. Version: 2000. – zuletzt besucht am 09. Juni 2014
[haca] Hackage: Graphics.UI.GLUT.Callbacks.Window.
http://hackage.haskell.org/
package/GLUT-2.5.1.1/docs/Graphics-UI-GLUT-Callbacks-Window.html. –
zuletzt besucht am 03. Mai 2014
[hacb] Hackage: Graphics.Rendering.OpenGL.GL.DisplayLists. http://hackage.haskell.
org/package/OpenGL-2.9.2.0/docs/Graphics-Rendering-OpenGL-GLDisplayLists.html. – zuletzt besucht am 24. Mai 2014
[hacc] Hackage: Graphics.UI.GLUT.Fonts. http://hackage.haskell.org/package/GLUT2.5.1.1/docs/Graphics-UI-GLUT-Fonts.html. – zuletzt besucht am 09. Juni 2014
[hacd] Hackage: Data.IORef.
http://hackage.haskell.org/package/base-4.7.0.0/
docs/Data-IORef.html. – zuletzt besucht am 07. Juni 2014
[hace] Hackage: Data.Maybe. http://hackage.haskell.org/package/base-4.2.0.0/
docs/Data-Maybe.html. – zuletzt besucht am 07. Juni 2014
[hac] Hackage: Graphics.Rendering.OpenGL.GL.CoordTrans.
http://hackage.haskell.
org/package/OpenGL-2.5.0.0/docs/Graphics-Rendering-OpenGL-GLCoordTrans.html. – zuletzt besucht am 08. Juni 2014
[hsO] HaskellWiki: OpenGL. http://www.haskell.org/haskellwiki/OpenGL. – zuletzt
besucht am 8. April 2014
[Joh06] J, Mikael V.:
OpenGL programming in Haskell, a tutorial.
http:
//blog.mikael.johanssons.org/archive/2006/09/opengl-programmingin-haskell-a-tutorial-part-1 und http://blog.mikael.johanssons.org/
archive/2006/09/opengl-programming-in-haskell-a-tutorial-part-2,
2006. – zuletzt besucht am 07. April 2014
51
LITERATURVERZEICHNIS
52
[LSS14] L, Markus ; SS, Manfred: Processing Succinct Matrices and Vectors, Springer, Juni 2014 (Lecture Notes in Computer Science), 245-258
[OGL] Legacy OpenGL. http://www.opengl.org/wiki/Legacy_OpenGL. – zuletzt besucht
am 22. Juni 2014
[Pip06] P, Dan:
You Could Have Invented Monads! (And Maybe You Already Have.). http://blog.sigfpe.com/2006/08/you-could-have-invented-monadsand.html. Version: 2006. – zuletzt besucht am 09. Juni 2014
[RTS] Running a compiled program. http://www.haskell.org/ghc/docs/7.4.1/html/
users_guide/runtime-control.html. – zuletzt besucht am 20. Juni 2014
[S+ 07] S, Dave u. a.: OpenGL Programming Guide. 6. Auflage. Addison-Wesley Professional, 2007
[Sch13] S, Nicole: Skript: Diskrete Modellierung. http://www.tks.informatik.
uni-frankfurt.de/data/teaching/regularly/dismod/MOD-Skript.pdf.
Version: 2013. – Seite 232, zuletzt abgerufen am 19. Juni 2014
Herunterladen