Universität Leipzig Institut für Informatik Abteilung Bild- und Signalverarbeitung Prof. Dr. Gerik Scheuermann Sommersemester 2007 Computergrafik-Vorlesung Betreuer: Heike Jänicke Tutorial - CGViewer 1 Einleitung Der CGViewer ist ein 3D-Modellierungsprogramm, mit dem Szenen gestaltet, dargestellt, gespeichert und geraytraced werden können. Das Programm ist in C++ geschrieben und verwendet die Bibliotheken QT und libQGLViewer. 2 Struktur des Viewers Der Viewer besteht im wesentlichen aus vier Klassen: Viewer, Widget, Scene und Primitive. Die Klasse Viewer stellt die GUI bereit. Sie ist von QMainWindow abgeleitet (Anmerkung: Alle Klassen die mit Q beginnen gehören zu QT). QMainWindow stellt grundlegende Funktionen für die Gestaltung einer GUI zur Verfügung. In den Viewer werden die Menüs, die Buttons, die Statusleiste und die Widgets, die die Szene darstellen, eingefügt, vgl. Abb. 1. Die Widgets sind von QGLViewer abgeleitet. Der QGLViewer beinhaltet Funktionalitäten zum Anzeigen und Manipulieren von OpenGL Graphiken. Der Viewer enthält vier Widgets, wobei jedes eine andere Sicht auf die Szene bereitstellt. Die Szene ist in der Klasse Scene gespeichert. Scene verwaltet die einzelnen Objekte, sowie die Manipulation der Szene und einzelner Objekte. Die einzelnen Objekte einer Szene heißen Primitive. Primitive können einfache Objekt wie Dreiecke oder Kugeln, aber auch komplexe Kompositionen von Primitiven wie Autos oder ein Tisch sein. Eine Szene kann aus beliebig vielen Primitiven bestehen. 3 Signals and Slots Die vier Klassen sind unabängig voneinander, d.h., dass die Szene zum Beispiel nicht direkt vom Viewer manipuliert werden kann. Zum Austausch von Informationen wird der “Signal/Slot-Mechanismus” von QT verwendet. Dieser verbindet Funktionen verschiedener Klassen, ähnlich wie bei einem Telefonnetz. Nachdem zwei Teilnehmer verbunden wurden, kann der Sender ein Signal abschicken, welches vom Empfänger empfangen wird. Das Signal wird allgemein ausgeschickt, und wird an alle Empfänger gesendet, die mit dem Sender verbunden sind. Wird also der Rotations-Button im Viewer mit der Slot-Funktion 1 Abbildung 1: Der CGViewer nach dem Start. Buttons/Men s Scene translate rotate scale delete clone Viewer commitMessage update updateView commitMessage addPrimitive emitSelected em itS ele cte d Widget Primitive Abbildung 2: Signals und Slots in CGViewer. Pfeile zeigen, dass eine Verbindung zwischen diesen Klassen besteht. Die Klasse am Anfang des Pfeiles ist der Sender, die am Ende (Pfeilspitze) ist der Empfänger. Die nebenstehenden Namen geben die entsprechenden Funktionen an. 2 rotate() in der Szene verbunden, so wird jedesmal, wenn der Benutzer den Button drückt, die entsprechende Methode der Szene aufgerufen. Abb. 2 gibt einen Überblick über die wichtigsten Verbindungen im CGViewer. 4 Ein neues Primitiv einbauen Im folgenden soll ein neues Primitiv, ein Würfel, in das Programm eingebaut werden. Der Nutzer soll die Möglichkeit haben, dieses durch Eingabe über das Menü oder durch Drücken eines Buttons zu erzeugen. Anschließend wird das Primitiv aufgebaut und im Ursprung des Koordinatensystems eingefügt. Anmerkung: Kompiliert wird vorerst einfach mit “make”. Wie das Makefile erzeugt/erweitert wird folgt später. Anpassen der GUI Damit der Nutzer einen Würfel zeichnen kann sollen ein Menüeintrag und ein Button bereitgestellt werden. • Fügen Sie in der Methode createMenu() in viewer.cpp einen neuen Eintrag zum Erzeugen eines Würfels ein. • Zeichnen Sie mit dem Programm KIconEdit ein Icon für den Würfel und speichern Sie dieses unter cube.xpm im Ordner CGViewer ab. • Füge Sie das Icon mittels #include in den Viewer ein. “cube” kann nun zum erstellen einer QPixmap verwendet werden. • Fügen Sie das Icon im Menü ein, und bauen Sie in der Funktion createButtons() einen neuen Button für den Würfel ein. Sowohl der Menüeintrag, als auch der Button brauchen einen Empfänger und einen Slot, die die Aktion bearbeiten. Empfänger des Ereignisses ist die Szene. Ein Slot zum Erzeugen des Würfels muss neu programmiert werden. • Fügen Sie einen neuen Slot (addCube()) in die Klasse Scene ein. • Lassen Sie den Slot addCube() etwas auf der Kommandozeile ausgeben, und testen Sie, ob alles richtig verbunden ist. Neue Klassen einfügen Bisher konnte das Programm einfach mit make compiliert werden. Werden neue Klassen eingefügt, muss das Makefile entsprechend angepasst werden. QT hat ein Tool, das dies automatisch macht: qmake. Damit qmake weiß, welche Klassen verwendet werden, benötigt es ein Profile. Dies ist für den CGViewer unter cgviewer.pro gespeichert. qmake sucht automatisch nach einer Datei mit dem Prefix .pro. Soll nun eine neue Klasse eingefügt werden, müssen Header- und 3 Source-File in das Profile eingetragen werden. Danach muss qmake aufgerufen werden, um ein neues Makefile zu erzeugen. Mit make wird das Programm anschließend kompiliert. • qmake PREFIX=/u/.../CGPraktikum/QGLViewer • make PREFIX gibt den Pfad an, wo nach der QGLViewer Bibliothek gesucht werden soll. Wenn Klassen nicht erkannt werden oder Fehler auftreten, können alle kompilierten Dateien mit • make clean gelöscht werden. Nutzereingaben abfragen Bisher hat der Nutzer mitgeteilt, dass er einen Würfel erstellen möchte. Es ist allerdings noch nicht bekannt welche Farbe er hat. Um diese Informationen zu erhalten, wird ein Dialog geöffnet, in den die relevanten Größen eingetragen werden können. • Erstellen Sie den Rumpf einer neuen Klasse CubeDialog in cubeDialog.cpp und cubeDialog.h (analog zu SphereDialog). Der Dialog ist von QDialog abgeleitet. Fügen Sie vorerst nur den Slot accept() ein. Dieser muss vorhanden sein, da die Methode in QDialog pure virtual ist. • Tragen Sie die neuen Dateien in das Profile ein, fügen Sie den Dialog in die Methode addCube() in die Szene ein, und kompilieren Sie. • Erweitern Sie den Dialog um die benötigten Felder (Material, OK-Button und Cancel-Button). • Hat der Benutzer den OK-Button gedrückt, so wird die Methode accept() aufgerüfen. Diese muss nun das ausgewählte Material übertragen. Der Dialog wird mit QDialog::accept() automatisch geschlossen. Einen Würfel einfügen Nun ist bekannt, wie sich der Nutzer den Würfel vorstellt. Um anschließende Transformationen zu erleichtern, werden alle neuen Primitive in den Würfel mit den Ecken (±1, ±1, ±1) eingepasst. So kann das Objekt um den Ursprung rotiert werden und gleichzeitig kann der Faktor für die Skalierung auf eine bestimmte Größe leicht berechnet werden. Jedes Primitiv ist in einer eigenen Klasse implementiert, die von der virtuellen Basisklasse Primitive abgeleitet ist. 4 • Erstellen Sie den Rumpf der neuen Klasse Cube (cube.cpp, cube.h) und fügen Sie diese in das Profile ein. • Erzeugen Sie einen neuen Würfel in der Methode addCube() in Scene. Damit der Würfel auch angezeigt werden kann, müssen die Begrenzungsflächen initialisiert werden und die render()-Methode implementiert werden. Für den Raytracer werden später trianguliert Oberflächen benötigt, weshalb alle Primitive aus Dreiecken aufgebaut werden sollen. • Fügen Sie in die Klasse Cube eine neue Methode initTriangles() ein, die vom Konstruktor aufgerufen wird. • Erzeugen sie in dieser Klasse neue Vertices für die Ecken des Würfels auf (±1, ±1, ±1). Fügen Sie die Vertices in die Vertexliste der Klasse ein. • Erstellen Sie die begrenzenden Dreiecke der Fläche, indem Sie dem Konstruktor die zugehörigen Vertices und Normalen übergeben. Die Normalen der Vertices einer Seitenfläche sind alle gleich und entsprechen der Normalen auf die Fläche. Alle Dreiecke sind gegen den Uhrzeigersinn zu orientieren, d.h. wenn man von vorne auf das Dreieck sieht, und die Vertices durchläuft, beschreibt dies eine Drehung entgegen dem Uhrzeigersinn. Fügen Sie die Dreiecke in den std::vector triangles ein. Jede Primitive hat eine eigene Methode render(), die angibt, wie das Objekt zu zeichnen ist. • Setzen Sie die Materialeigenschaften mit glMaterialfv(...). • glPushMatrix(); fügt eine neue Matrix in den Transformationsstack ein, so dass die globale Transformationsmatrix nicht verändert wird. • Multiplizieren Sie die Matrix mit der Transformationsmatrix des Primitivs. Die oberste Matrix das Stacks ist die Matrix für die globalen Transformationen des Objekts. • Wenn der Würfel ausgewählt wurde, soll er in einer anderen Farbe gezeichnet werden. Fragen Sie dies analog zur Klasse Sphere ab, und ändern Sie die Materialeigenschaften entsprechend. • Sollen Primitive mit OpenGL gezeichnet werden, werden diese in einen glBegin( TYP ); ... glEnd(); Block eingefügt. In diesem Block werden die zugehörigen Vertices und Normalen spezifiziert. Für alle Primitive ist der TYP GL TRIANGLES. Dadurch werden je drei aufeinanderfolgende Vertices als ein Dreieck aufgefasst. Durchlaufen Sie nun alle Dreiecke des Würfels und fügen Sie für jeden Eckpunkt Normale und Position ein. Dazu benötigen Sie die Befehle: glNormal3fv( double[3] );und glVertex3fv( double[3] ); Beachten Sie, dass zuerst die Normale und dann der Vertex spezifiziert werden muss. 5 Die render() Funktionen werden von den draw()-Methoden der Szene aufgerufen. Davon gibt es zwei Stück. Die eine rendert lediglich die Primitive, die andere gibt zusätzlich jedem Primitiv einen Namen, damit dieses ausgewählt werden kann. Die draw()-Methoden werden von den Widgets aufgerufen, die für die OpenGL-Graphik zuständig sind. Signale mit dem Würfel verbinden Damit das Primitiv auch manipuliert werden kann, müssen noch die Signals und Slots verbunden werden. Das Primitiv soll darauf reagieren, wenn eine Rotation, Translation oder eine Skalierung ausgeführt wird. Jedes Signal schickt den Namen des ausgewählten Objekts mit. Das Objekt soll nur transformiert werden, wenn es selbst oder keines ausgewählt ist. Die Transformationen sind bereits in der Basisklasse Primitive implementiert. • Verbinden Sie die entsprechenden Signals und Slots: connect( sender, SIGNAL(signal(...), empfänger, SLOT(slot(...) ); Das Primitiv soll die Möglichkeit haben mittels des Signals commitMessage( const char * ) Nachrichten für die Statusleiste an die Szene zu schicken, die diese dann an den Viewer weiterleitet. Verbinden Sie entsprechend. Abschließend muss den Widgets noch mitgeteilt werden, dass sich etwas geändert hat, und die Szene neu gezeichnet werden muss. Dies geschieht durch das Signal needUpdate(). Signale werden durch den Befehl emit abgeschickt. Objekte clonen Zum leichteren Erstellen einer Szene gibt es die Möglichkeit Objekte zu clonen, so dass z.B. ein Autorad nur einmal gebaut werden muss und dann einfach kopiert werden kann. In der Methode copyPrimitive() in Scene werden Primitive geclont. Dazu wird die Methode clone() des Primitives benötigt. Diese ist in der Basisklasse pure virtual und muss für den Würfel implementiert werden. • Schreiben Sie einen Copy-Konstruktor, der einen Würfel übergeben bekommt, und einen neuen äquivalenten erzeugt. • Erzeugen Sie in der clone()-Methode von Cube eine kopierte Version des aktuellen Würfels, und verschieben Sie diese wenn gewünscht (centerPrimitive) in den Ursprung. • Geben Sie das neu erstellte Objekt zurück. Testen Wenn alles richtig implementiert ist, sollte Sie jetzt einen neuen Würfel erzeugen können und diesem ein Material zuweisen. Testen Sie die verschiedenen Transformationen und versuchen Sie das Objekt zu clonen und zu entfernen. 6