Goethe Universität Frankfurt am Main Institut ür Informatik Sommersemester 2014 Baelorarbeit Implementierung einer interaktiven dreidimensionalen Anzeige von Bildaussnitten von Grammatik-komprimierten Bildern in der funktionalen Programmiersprae 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 Hilfsmiel 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 Abschnie 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 Aueilung 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 Programmbesreibung 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 Ausschnisberechnung . . . . . 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 Abbildungsverzeinis 50 Literaturverzeinis 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 komplee 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, Ausschnie 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 Ausschnien 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 Nicherminalsymbolen beziehungsweise Variablen oder Nicherminalen. • 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 Auau 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 Nicherminal von G gelten, dass es genau eine Produktion ür dieses Nichtterminal gibt, sodass die Ersetzung eines Nicherminals eindeutig ist. Jeder Produktion dieser Matrix kann eine Höhe n ∈ N zugeteilt werden. Seien A und Ai beliebige Nicherminale von G. Sei a ein Terminal von G. Für ein Nicherminal 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 Nicherminals n gibt dabei an, dass die Größe der entsprechenden Matrix 2n ×2n ist. Dieser Zusammenhang folgt aus dem Auau 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 stafinden 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 Nicherminale 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 Nicherminal der Grammatik und seien alle Si mit i ∈ N Nicherminale 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 ermielt 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 Nicherminal geührt hat und dieses Ergebnis wiederverwendet, falls dieses Nicherminal 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 Nicherminal A1 berechnet. A1 wird auf vier Nicherminale 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 Nicherminal A3 weitergerechnet, dabei werden halfsize = 1 und P = (1, 2) mitgegeben. • A3 wird auch auf vier Nicherminale abgeleitet und mit x = halfsize und y > halfsize folgt, dass mit A5 dem drien abgeleiteten Nicherminal 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 Nicherminals 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 Nicherminal, 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 Nicherminale in der Grammatik dargestellt wird. Das bedeutet auch, dass Nicherminale 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 Nicherminalen wird bei perfekter Kompression eine Matrix mit 4h Einträgen beschrieben. Die Grammatik, die solch eine Kompression durchührt, leitet angefangen vom Startsymbol jedes Nicherminal auf vier gleiche Nicherminale ab. Das geht solange weiter, bis ein Nicherminal auf ein Terminal abgeleitet wird. Falls jedoch keine Kompression vorliegt und jeder Eintrag der Matrix durch ein Nicherminalsymbol repräsentiert wird, dann ist die Anzahl der Nicherminale sogar größer als die Anzahl der Matrixeinträge, denn schon allein die Anzahl der Nicherminale 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 zwisen 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 Schnipunkte mit Ebenen berechnen kann. Für unsere Zwecke eignet sich die Darstellung in Parameterform gut. Definition 5 (Geradengleiung 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 (Ebenengleiung 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 Schnistelle zwischen Soware 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 geometriser 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 Aufruefehl 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 drie 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. Schriweite der Navigationstasten erhöhen. Schriweite 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. Tasenlampe. Nach dem Drücken der Taste T erscheint am Schnipunkt 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 Ausschnie 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 Abschnis 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 zugeschnien 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 Ausschnie 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 Nicherminal, 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 Nicherminalen 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 Aussnittsberenung Die Berechnung eines Ausschnis 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 Ausschnie 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 drien 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 Ausschnisberechnung 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 Nicherminale und zwei Punkte als Schlüssel und die Ergebnisliste des Nicherminals 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 Nicherminals 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 Nicherminal auf vier andere Nicherminale, 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 Nicherminalen gestartet, auf welche das aktuelle Nicherminal abgeleitet wird. Im Falle der Multiplikation und Addition werden zusätzlich die Ergebnislisten, welche aus den abgeleiteten Nicherminalen 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 Nicherminal 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 komplee 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 Nicherminale auf denen weitere Rekursionen gestartet werden: Fall 1 Fall 2 Fall 3 Fall 4 Abbildung 4.2: Einerälle der Ausschnisberechnung Wenn das ausgewählte Rechteck sich komple in einem der vier Nicherminale befindet, dann reicht es aus, die Rekursion auf diesem Nicherminal 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 Nicherminalen erzeugt wird: Fall 5 Fall 7 Fall 6 Fall 8 Abbildung 4.3: Zweierälle der Ausschnisberechnung In den Fällen, bei denen zwei Nicherminale betroffen sind, können die zwei anderen von der Berechnung ausgeschlossen werden. Der letzte Fall der aureten kann ist, dass alle vier Nicherminale benötigt werden. Es gibt keine Fälle bei denen nur drei Nicherminale 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 Ausschnisberechnung 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 ermielt werden, zudem muss in jedem rekursiven Aufruf bekannt sein, wie groß die Matrix ist, die von dem aktuellen Nicherminal erzeugt wird. Um zu bestimmen, welche Nicherminale 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 Nicherminal an. Ist sowohl der x-, als auch der y -Wert kleiner als die halbe Seitenlänge der Matrix, die durch ein Nicherminal erzeugt wird, so befindet sich der Punkt in dem ersten der vier abgeleiteten Nicherminale beziehungsweise im oberen linken Viertel der Matrix, die durch das Nicherminal 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 Nicherminal 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 drien Nicherminal, befindet und die 3 wird vergeben wenn sich der Punkt unten rechts, also im vierten Nicherminal 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 Nicherminal 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äle der Matrix weitergerechnet werden. Hat Punkt 2 die Bewertung 2, so liegt Fall 7 vor und es muss in der linken Häle 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äle 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äle der Matrix weitergerechnet werden muss. Um eine bessere Vorstellung dieses Vorgehens zu erhalten wird ein Beispielfall geschildert. Angenommen man hat ein Nicherminal, 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 Nicherminalen weitergeührt werden. In diesen vier Rekursionen werden jedoch jeweils die Fälle von 1 bis 4 aureten, 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 Nicherminal 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 Ausschnisberechnung 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 Nicherminal an. Jedes Nicherminal hat oben links die Koordinaten (0, 0) und unten rechts die Koordinaten (size, size), wobei size die Seitenlänge der aus dem Nicherminal 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 Nicherminale 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 Nicherminal eines Unteraufrufs, so muss mindestens eine Koordinate des Eckpunktes entweder 0, oder size des Nicherminals sein, damit der Teilausschni richtig im neuen Nicherminal 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 Nicherminals und jedes Nicherminal im Unteraufruf wird genau zu einer Matrix mit der halben Seitenlänge abgeleitet. Falls ein Punkt im Nicherminal 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 Nicherminal 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. Auauend 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 Nicherminals berechnet werden soll und wenn man die Ergebnisse dazu speichert, dann kann man das komplee Nicht- 27 4.2. MATRIX terminal oder auch teilweise berechnete Nicherminale 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 Nicherminal verarbeitet wird, als Eckpunkte mitgegeben wurden. Dadurch lassen sich Ergebnisse ür die exakten Ausschnie berechnen. Falls eines der Nicherminale 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 Nicherminal 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 Nicherminal 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 Nicherminals hat und dann werden die absoluten Werte, an denen sich der Anfang der Matrix befindet, draufaddiert. Die Werte die den Anfang eines Nicherminals in der kompleen 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 Nicherminal 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 Nicherminale 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 Nicherminal ist nt, die beiden Eckpunkte sind (i,j) und (x,y), (m,n) ist der absolute Startpunkt der Nicherminalmatrix, 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 Ausschnisberechnung 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 Ausschnisarten eines Nicherminal, 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 Auau 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 übermielt 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 stafinden 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 Schrigröß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 stafindet. 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 stafindet. 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 Zeienroutine 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 Abschnis 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 Codeabschnien 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 Tasenlampe Die Taschenlampe berechnet den Schnipunkt 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 Schnipunkt 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 Schnipunkt 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 Schnipunkts schon bekannt ist benötigen wir ür die Berechnung des Schnipunkts 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 Schnipunkt 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 Schnipunkts 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 komplee 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 Schnipunkt 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 Schnipunkt und können dessen Fensterkoordinate miels 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 Auswahlrete 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 miels 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 haen 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 Schriart, wobei letztere aus von GLUT vorgegebenen Schriarten 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 Nicherminale in eine Datei zu schreiben. Dabei muss darauf geachtet werden, dass keine Nicherminale doppelt vorkommen und das die Nicherminale auf vorhandene Nicherminale ableiten. Als Parameter nimmt ein Generator die Höhe des Startsymbols und den Namen der zu beschreibenden Datei an. Um sich den allgemeinen Auau 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 Nicherminale 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 auauen 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 Nicherminale die auf ein Terminal ableiten und vom Startsymbol aus durch jeweils verschiedene Pfade erreichbar sind. Wenn mien in der Grammatik Additionsälle eingebaut werden sollen, dann kann man zum Beispiel wie beschrieben immer jeweils 4 verschiedene Nicherminale pro Aufruf erzeugen, aber dabei noch extra ein Nicherminal in die Datei schreiben, welches zwei der Nicherminale die man erzeugen will addiert. Dieses extra erzeugte Nicherminal würde dann in der Zeile welche die Ableitung beschreibt, also Zeile 14 im Beispiel, eines der üblichen Nicherminale 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 Ausschnisberechnung 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 Auau 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 Nicherminals 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 Nicherminals 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 Nicherminalvarianten die mehrfach benötigt werden. Die Wiederverwendung würde bewirken dass die Entstehung weiterer Pfade unterbunden wird. Das Wiederverwenden eines schon benutzten Nicherminals kann nur von einer Nicherminalvarianten kommen, die bisher nicht wiederverwendet werden konnte. Damit können nur O(∣G∣) viele Wiederverwendungen aureten. 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 drie garnicht. Für jedes Nicherminal der voll komprimierten Grammatik gilt, dass es auf vier gleiche Nicherminale abgeleitet wird. Bei der unkomprimierten Grammatik wird jedes Nicherminal auf vier verschiedene Nicherminale abgeleitet und bei der zum Teil komprimierten Grammatik wird die obere Häle der Matrix voll komprimiert und die untere Häle 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 Fesplae 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 Ausschnis 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 Mie 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 Nicherminalen 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 Ausschnisberechnung 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 Ausschnisberechnung 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 Durschniswerte 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 Durchschniszeiten 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 Durchschniszeiten 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 Auau wie beschrieben verwendet, jedoch ängt die Grammatik mit dem Nicherminal 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 Ausschnisvarianten durchgeührt, wobei die selbe Entwicklung beobachtet werden konnte. Es ist klar dass je nach Testumgebung Schwankungen in den Messungen aureten 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 Ausschnisberechnung 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 Hilfsmiel 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 Ausschnisberechnung . . . . . . . . . Einerälle der Ausschnisberechnung . . . . . . . . . . . . . . . . . Zweierälle der Ausschnisberechnung . . . . . . . . . . . . . . . Viererfall der Ausschnisberechnung . . . . . . . . . . . . . . . . . Beispiel einer Ausschnisberechnung . . . . . . . . . . . . . . . . . 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 ; SS, 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