PD Dr. David Sabel Institut für Informatik Fachbereich Informatik und Mathematik Johann Wolfgang Goethe-Universität Frankfurt am Main Praktikum BKSPP Wintersemester 2014/15 Aufgabenblatt Nr. 3 Abgabe: Dienstag, 16. Dezember 2014 Auf diesem Aufgabenblatt wird OpenGL1 verwendet, um graphische (2D und 3D) Ausgaben zu erzeugen. Als Haskell-Bibliotheken zur Anbindung an OpenGL und GLUT2 werden die entsprechenden Cabal-Pakete • OpenGL (http://hackage.haskell.org/package/OpenGL) und • GLUT (http://hackage.haskell.org/package/GLUT) verwendet. Die Haskell-Platform beinhaltet diese Bibliotheken bereits. Eventuell muss auf dem eigenen Rechner die GLUT Bibliothek nachinstalliert werden, z.B.: • FreeGLUT unter Linux http://freeglut.sourceforge.net/ oder • GLUT for Windows (http://www.xmission.com/∼nate/glut.html) Es sollte ausreichen die Zip-Datei unter http://user.xmission.com/∼nate/glut/glut-3.7.6-bin.zip herunterzuladen und die Datei glut32.dll in das Verzeichnis %WinDir%\System zu kopieren. Testen auf verschiedenen System ergab, dass das Starten eines OpenGL-Fenster aus dem ghci heraus manchmal zu Abstürzen führt. In diesem Fall empfiehlt es sich das Programm zu kompilieren. Im Allgemeinen sollte ein Aufruf der Form ghc --make -o output.exe Input.hs wobei Input.hs das Programmfile ist, funktionieren. Anschließend kann das Programm output.exe ausgeführt werden. Als erster Einstieg zur OpenGL-Programmierung mit Haskell kann die Seite http://www.haskell.org/haskellwiki/Opengl dienen, über die z.B. das OpenGL Tutorial 1 (http://www.haskell.org/haskellwiki/OpenGLTutorial1) und OpenGL Tutorial 2 (http://www.haskell.org/haskellwiki/OpenGLTutorial2) zu finden sind. Eine sehr gute Einführung bietet das Tutorial von Sven Eric Panitz, welches über http://www.cs.hs-rm.de/∼panitz/hopengl frei verfügbar ist. 1 2 OpenGL steht für Open Graphics Library, die Webseite von OpenGL ist http://www.opengl.org/ GLUT steht für OpenGL Utility Toolkit (siehe auch http://www.opengl.org/resources/libraries/glut/ 1 1 Grundgerüst für ein OpenGL-Programm in Haskell Wir betrachten ein einfaches Grundgerüst für ein in Haskell implementiertes OpenGLProgramm. Zunächst werden die beiden Bibliotheken für OpenGL und für GLUT importiert: import Graphics.UI.GLUT import Graphics.Rendering.OpenGL Das Hauptprogramm erhält folgendes Gerüst main = do (prgName,_) <- getArgsAndInitialize createWindow prgName windowSize $= Size 800 800 displayCallback $= mainDisplay mainLoop Hierbei wird in der ersten monadischen Aktion getArgsAndInitialize GLUT initialisiert und an prgName der Aufruf-Name des Programms gebunden. Im zweiten Schritt wird mittels createWindow prgName ein Fenster mit dem Titel des Programmnamens erzeugt. In Zeile 3 wird mit dem Operator $= der OpenGL-Variablen windowSize die Größe 800x800 zugewiesen, d.h. das Fenster wird auf diese Größe eingestellt. In Zeile 4 wird mit displayCallback $= mainDisplay der OpenGL-Variablen displayCallback die Funktion mainDisplay zugewiesen (diese wird später definiert). Hierbei ist displayCallback ein (eingebauter) Ereignis-Handler, der das Ereignis des Anzeigens des Fensters übernimmt. Durch unsere Zuweisung wird bei jedem Anzeigen und Wiederanzeigen des Fensters, die mainDisplay-Funktion aufgerufen, sie bestimmt also den Fensterinhalt. Schließlich wird in der letzten Zeile mit mainLoop der GLUT-Ereignisprozesser gestartet, der sämtliche Ereignisse abfängt und an die vorher definierten Handler übergibt (in diesem Fall wurde nur der displayCallback-Handler vorher registriert). Es fehlt nun noch die Implementierung der Funktion mainDisplay, die den Fensterinhalt beschreibt. Eine ganz einfache Definition ist: mainDisplay = clear [ColorBuffer] In diesem Fall wird der Fensterinhalt gelöscht. Ruft man das Programm insgesamt auf, erhält man ein schwarzes Fenster. Wir betrachten nun einige einfache Funktionen zum Anzeigen von Punkten, Linien, Streckenzügen und Polygonen. Wir definieren hierfür zunächst zwei Typsynonyme type Coord = (GLfloat,GLfloat,GLfloat) type ColoredCoord = (GLfloat,GLfloat,GLfloat,Color4 GLfloat) 2 Der Typ Coord stellt ein 3-Tupel von GLfloat-Werten dar. Seine Bedeutung ist eine 3dimensionale Koordinate. Jeder der einzelnen drei Werte sollte hierbei einen Wert zwischen -1 und 1 besitzen, damit er innerhalb des Fensters liegt. Der Typ ColoredCoord ist ein 4-Tupel, wobei die ersten 3 Komponenten wieder X-, Y-, und Z-Koordinaten sind und der vierte Wert eine Farbe darstellt. Für die Farben benutzen wir den vordefinierten Typ Color4 GLfloat, der RGBA-Farbwerte darstellen kann. RGBA steht für Red-Green-BlueAlpha, mit dem Datenkonstruktor Color4 können Farben erzeugt werden, z.B. red = Color4 1 0 0 1 green = Color4 0 1 0 1 blue = Color4 0 0 1 1 Alle 4 Werte sollten zwischen 0 und 1 liegen. Die ersten 3 Werte geben den Rot-, Grün, bzw. Blau-Anteil an, der letzte Wert (der Alpha-Wert) die Transparenz der Farbe. Eine Funktion zum Anzeigen einer Liste von Punkten (vom Typ Coord) kann in Haskell nun programmiert werden als displayPoints points = renderPrimitive Points $ mapM_ (\(x, y, z)-> vertex $ Vertex3 x y z) points Die wesentliche Bibliotheksfunktion ist hierbei renderPrimitive, die einfache Grafikobjekte erzeugen kann. Mit vertex $ Vertex3 x y z werden die Punkte vom Typ Coord noch in für renderPrimitive passende Objekte (dies sind monadische IO-Aktionen) konvertiert. Eine erweiterte Variante ist die folgende Funktion, die farbige Punkte (vom Typ ColoredCord) anzeigen kann: displayCPoints points = renderPrimitive Points $ mapM_ (\(x, y, z, c) -> do currentColor $= c vertex $ Vertex3 x y z) points Der Aufbau gleicht fast displayPoints, mit der Ausnahme, dass für jeden Punkt eine erweiterte IO-Aktion erzeugt wird: Bevor der eigentliche Punkt mit vertex ... erzeugt wird, wird die aktuelle Farbe mit currentColor $= c auf den entsprechenden Farbwert gesetzt. Eine Funktion zur Anzeige von Linien kann programmiert werden als displayLines points = renderPrimitive Lines $ mapM_ (\(x, y, z)-> vertex $ Vertex3 x y z) points Hierbei werden je zwei Punkte aus der Liste der übergebenen Punkte als Endpunkte einer Linie verwendet. Die Funktion 3 displayLineStrip points = renderPrimitive LineStrip $ mapM_ (\(x, y, z)-> vertex $ Vertex3 x y z) points zeichnet einen Streckenzug durch die übergebenen Koordinaten. Ein (konvexes) farbiges Polygon (gegeben durch Koordinaten und eine Farbe) kann gezeichnet werden mit displayPolygon points color = do currentColor $= color renderPrimitive Polygon $ mapM_ (\(x, y, z)-> vertex $ Vertex3 x y z) points Wir können die mainDisplay-Funktion z.B. wie folgt abändern, um verschiedene Objekte zu zeichnen. Hierbei sollte am Ende ein Aufruf von flush erfolgen, um alle Zwischenbuffer sicher zu leeren, und damit zu garantieren, dass wirklich alle Objekte gezeichnet werden. mainDisplay = do -- Hintergrundfarbe festlegen clearColor $= Color4 1 1 1 1 -- Fensterinhalt l"oschen clear [ColorBuffer] -- Farbe auf Schwarz setzen currentColor $= Color4 0 0 0 1 -- Punkte anzeigen displayPoints beispielPunkte -- farbige Punkte anzeigen displayCPoints beispielFarbpunkte -- rotes Polygon anzeigen displayPolygon beispielPunkte red -- aktuelle Farbe auf gr"un umstellen currentColor $= green -- Linien zeichnen displayLines beispielPunkte2 currentColor $= blue -- Linien zeichnen displayLineStrip beispielPunkte3 flush beispielPunkte :: [Coord] beispielPunkte = [(-0.25, 0.25, 0.0), (0.75, 0.35, 0.0), (0.75, -0.15, 0.0), (-0.75, -0.25, 0.0)] 4 beispielPunkte2 :: [Coord] beispielPunkte2 = [(-0.3, 0.1, 0.0), (0.8, 0.1, 0.0), (0.5, -0.7, 0.0), (-0.4, -0.1, 0.0)] beispielPunkte3 :: [Coord] beispielPunkte3 = [(-0.3, 0.1, 0.0), (-0.1, 0.1, 0.0), (0.3, 0.4, 0.0), (0.8, 0.1, 0.0), (0.5, 0.7, 0.0), (0.4, -0.1, 0.0)] beispielFarbpunkte :: [ColoredCoord] beispielFarbpunkte = [(-0.5, 0.5, 0.0, red), (0.5, -0.5, 0.0,green), (-0.5, -0.5, 0.0,blue), (0.5, 0.5, 0.0,Color4 1 1 1 1)] Ausführung des Programms ergibt das folgende Fenster 5 Aufgabe 1 Implementieren Sie in Haskell ein OpenGL-Programm zum Zeichnen von Funktionsgraphen. Dabei sollte eine Funktion plot :: GLfloat -> (GLfloat -> GLfloat) -> GLfloat -> GLfloat -> [Coord] plot delta f start stop = ... erstellt werden, wobei delta eine Schrittweite, f eine Funktion auf GLfloat-Werten, und start und stop das zu betrachtende Intervall (X-Werte) markieren. plot berechnet die Koordinaten des Funktionsgraphen im Intervall [start,stop] mit einer Schrittweite von delta. Implementieren Sie anschließend eine Funktion bestScale :: [Coord] -> [Coord], die eine Liste von Koordinaten erhält und diese so skaliert, dass alle Punkte im Fenster liegen, d.h. X,Y,ZKoordinaten der Ausgabe Koordinaten liegen im Intervall [−1, 1]. Kombinieren Sie plot und bestScale und lassen sie verschiedene Funktiongraphen zeichnen. Als Beispiel betrachten wir die Exponentialfunktion im Bereich -5 bis 10: test = let delta = 0.01 start = -5 stop = 10 f x = 2**x in plot delta f start stop Die folgenden zwei Abbildungen zeigen links das Ergebnis ohne Skalierung und rechts die Ausgabe bei Verwendung von bestScale: 6 2 Zweidimensionale Turtlegrafiken Im Folgenden verwenden wir Haskells OpenGL-Anbindung, um zweidimensionale Turtle-Grafiken zu erzeugen. Wir implementieren hierfür keine eigene Turtle-Sprache, sondern betten diese Sprache als Datentyp in Haskell ein. Der Zustand einer (zweidimensionalen) Schildkröte (Turtle) besitzt die folgenden Attribute: • Position im zweidimensionalen Raum als X- und Y-Koordinaten • einen Winkel, der die Laufrichtung der Schildkröte angibt (im Gradmaß) • Ein Flag, das angibt, ob die Schildkröte beim Laufen zeichnet oder nicht zeichnet. • Eine Farbe, die angibt, mit welcher Farbe die Schildkröte zeichnet. In Haskell stellen wir eine Schildkröte durch den Datentyp Turtle dar, wobei wir die Record-Syntax verwenden: data Turtle = Turtle { xpos :: GLfloat, ypos :: GLfloat, angle :: GLfloat, painting :: Bool tcolor :: Color4 GLfloat } Eine Schildkröte kann die folgenden Kommandos verarbeiten: • Gehen: Die Schildkröte bewegt sich um einen übergebenen Wert vorwärts (in Richtung des eingestellten Winkels), ist der übergebene Wert negativ, so läuft die Schildkröte rückwärts. Ist painting auf True gesetzt, so zeichnet die Schildkröte den gegangenen Weg. • Drehen: Die Schildkröte dreht sich im Uhrzeigersinn, um den übergebenen Winkel. Ist der Winkel negativ, so dreht sich die Schildkröte gegen den Uhrzeigersinn. • Stift an/aus: Das Flag (Zeichnen ja/nein) wird geändert. • Farbe setzen: Der Wert von tcolor wird auf einen übergebenen Farbwert gesetzt. In Haskell stellen wir Schildkröten-Aktionen durch einen Datentypen dar: data TurtleAction = Move GLfloat | Turn GLfloat | FlipPaint | SetColor (Color4 GLfloat) 7 Aufgabe 2 Implementieren Sie eine Funktion applyAction :: TurtleAction -> Turtle -> Turtle die eine Schildkrötenaktion und eine Schildkröte erwartet und den Nachfolgezustand der Schildkröte berechnet. Hinweise: • Beachten Sie, dass die Gehen-Aktion die Weglänge erhält. Um daraus entsprechende X- und Y -Koordinaten zu berechnen, sollten sie die in Haskell eingebauten trigonometrischen Funktionen sin und cos verwenden, wobei sie den Winkel hierfür vom Gradmaß ins Bogenmaß umrechnen müssen. Hierfür gilt die Formel3 : winkelImGradmaß = (winkelImBogenmaß/π) · 180 • Für die Berechnung der Koordinaten könnte folgendes Bild hilfreich sein: c sin(α) = a/c cos(α) = b/c a • α b Ein Schildkrötenprogramm ist eine Sequenz von Schildkrötenaktionen. In Haskell kann ein solches Programm durch folgenden Typ dargestellt werden: type TurtleProgram = [TurtleAction] Aufgabe 3 Implementieren Sie in Haskell ein Programm, dass eine initiale Schildkröte und ein Schildkrötenprogramm erwartet, und die von der Schildkröte bei Ausführung des Schildkrötenprogramms erzeugte Grafik mittels OpenGL darstellt. Hinweise: • Im Grunde müssen Sie nur die Linien berechnen und darstellen, die durch jene MoveOperationen erzeugt werden, bei denen der Stift an ist. • Es könnte durchaus hilfreich sein, zunächst eine Funktion zum Darstellen farbiger Linien (die evtl. durch ein neues Typsynonym dargestellt werden) zu schreiben (als Vorlage können dafür die Funktionen displayCPoints und displayLines dienen). • Sie können die Funktion applyAction aus der vorherigen Aufgabe verwenden. 3 Eine Näherung der Zahl π ist in Haskell durch die Funktion pi bereits vordefiniert. 8 Aufgabe 4 Implementieren Sie in Haskell ein Schildkrötenprogram (vom Typ TurtleProgram), welches das folgende „Haus vom Nikolaus“ ohne Ausschalten des Stifts zeichnet: Aufgabe 5 Implementieren Sie in Haskell eine rekursive Funktion nEck, die eine Zahl n erwartet und ein Schildkrötenprogram (vom Typ TurtleProgram) generiert, welches ein gleichmäßiges nEck zeichnet. Die folgenden Abbildungen zeigen Aufrufe für n = 8 und n = 15: Aufgabe 6 Implementieren Sie in Haskell eine rekursive Funktion nnEck, die eine Zahl n erwartet und ein Schildkrötenprogram (vom Typ TurtleProgram) generiert, das n viele n-Ecke im Kreis herum zeichnet. D.h. vom Mittelpunkt aus: Nach Zeichnen eines n-Ecks wird das nächste n-Eck um (360/n) Grad gedreht gezeichnet. Die folgenden Abbildungen zeigen Aufrufe für n = 3, n = 8 und n = 20, wobei die Größe der einzelnen n-Ecke angepasst wurde. 9 Aufgabe 7 Implementieren Sie in Haskell eine rekursive Funktion pBaum, die ein Schildkrötenprogram (vom Typ TurtleProgram) generiert, das einen so genannten Pythagorasbaum zeichnet. Die Konstruktion (siehe linkes Bild unten) beginnt hierbei mit einem Quadrat, auf welches in einem rechtwinkligen Dreieck rekursiv zwei Dreiecke gezeichnet werden, z.B. für die Winkel 30 und 60 Grad. Führt man diese Konstruktion rekursiv weiter bis die Quadrate ziemlich klein werden, erhält man den Pythagorasbaum wie im mittleren Bild gezeigt. Schöner wird der Baum, wenn man nur die linke und die rechte Seite jedes Quadrats zeichnet und noch eine Färbung der Quadrate vornimmt (rechtes Bild). 10 3 Dreidimensionale Turtlegrafiken In diesem Abschnitt wird das bisherige Turtle-Interface auf drei Dimensionen erweitert. Neben den bereits aus dem Zweidimensionalen bekannten Attributen, erhält eine dreidimensionale Schildkröte die folgenden zusätzlichen Eigenschaften: • Für die Position im dreidimensionalen Raum zusätzlich zu den X- und YKoordinaten eine Z-Koordinate. • Der bisherige Winkel gibt eine Laufrichtung bzgl. der X- und Y-Achse an. Nun wird ein weiterer Winkel hinzugefügt, der die Laufrichtung bezüglich der X- und ZAchse angibt. Dies entspricht im Wesentlichen so genannten Kugelkoordinaten. Unser Haskell-Datentyp wird dementsprechend erweitert in: data Turtle = Turtle { xpos :: GLfloat, ypos :: GLfloat, angle :: GLfloat, painting :: Bool, tcolor :: Color4 GLfloat, -- neu: zpos :: GLfloat, angleXZ :: GLfloat } Damit sich die Schildkröte im 3-dimensionalen-Raum bewegen kann, wird als neue Aktion das Drehen des Winkels angleXZ hinzugefügt. Die Interpretation der GehenOperation ändert sich natürlich auch, da beide Winkel beachtet werden müssen. Das folgende Bild zeigt die Kugelkoordinaten, wobei α der Winkel zwischen der X- und der Y-Achse ist und β der Winkel zwischen der X-Y-Ebene und der Z-Achse ist. 11 z β α y x Sei r die Länge des blauen Pfeils. Dann kann man die Längen der Projektionen auf die einzelnen Achsen wie folgt berechnen (diese benötigen sie für die Implementierung der Gehen-Operation!): x = r · cos α · sin β y = r · sin α · sin β z = r · cos(β) Aufgabe 8 Implementieren Sie in Haskell ein Modul Turtle3D, welches das zweidimensionale Schildkröteninterface auf drei Dimensionen erweitert und Schildkrötenprogramme ausführen (und korrekt darstellen) kann. Schildkrötenprogramme sind hierbei Listen von Elementen des Typs TurtleAction, der um die TurnXZ-Aktion erweitert wurde, d.h. data TurtleAction = Move GLfloat | Turn GLfloat | FlipPaint | SetColor (Color4 GLfloat) | TurnXZ GLfloat Hinweise: Im CVS-Repository ist ein erweitertes Basisprogramm namens Basis3D.hs nebst zwei Hilfsmodulen (aus der Anleitung von S.E. Panitz) eingecheckt. Dieses erlaubt Interaktion: Durch drücken der Pfeiltasten kann navigiert, durch Drücken von + und - gezoomt werden. Verwenden Sie dieses Programm als Rahmenprogramm für das Schildkrötenprogramm. Aufgabe 9 Implementieren Sie in Haskell eine Funktion wuerfel :: GLfloat -> [TurtleProgram] 12 die eine Länge erwartet und ein Schildkrötenprogramm zur Erzeugung eines dreidimensionalen Würfels erzeugt. Eine Ausgabe zeigt folgendes Bild: Aufgabe 10 Implementieren Sie in Haskell eine Funktion pyramide, die eine Zahl w erwartet und ein Schildkrötenprogramm erzeugt, welches eine Stufenpyramide zusammengesetzt aus Würfeln zeichnet, wobei die Grundfläche aus w × w Würfeln besteht. Das linke Bild zeigt eine Pyramide für w = 5. Das rechte Bild eine Pyramide für w = 25, wobei die einzelnen Ebenen in verschiedenen Farben gezeichnet wurden: Aufgabe 11 Impementieren Sie in Haskell eine Funktion meinhaus, welches ein Schildkrötenprogramm generiert, dass ein Haus mit möglichst vielen Details zeichnet. Das schönste Haus wird prämiert! 13