Tutorial - CGViewer 1 Einleitung 2 Struktur des Viewers 3 Signals

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