In einem realitätsnahen 3D-Raum wird der Betrachter zum Akteur: Seine Aktionen führen zu Reaktionen, er hinterlässt seine Spuren in der virtuellen Realität. Mit Qt und Coin lassen sich solche animierten und interaktiven 3D-Welten schnell und einfach programmieren.
Eine dreidimensionale Szene wirkt wesentlich realistischer, wenn sie sich bewegt und der Benutzer mit ihr interagieren kann. Bereits der letzte Coin-Artikel[2] brachte Bewegung in die Szene: Die Erde rotierte unter einem Flugzeug. Der Betrachter konnte aber nicht aktiv in die Szene eingreifen, nichts verändern, sondern nur das Geschehen mit Hilfe des »SoQtExaminerViewer« aus verschiedenen Blickwinkeln betrachten.
Als interaktives Beispiel dient im Folgenden ein einfaches Zeichenprogramm. Der Benutzer malt dabei in einer 3D-Szene beliebige Punkte. Das Programm basiert auf einem Beispiel aus dem Inventor Mentor ([3], 10. Kapitel, zweites Beispiel), wurde aber erweitert und von Motif auf SoQt und Qt umgestellt.
Der Szenengraph des Malprogramms ist in Abbildung 1 zu sehen. Er benötigt nur vier Knoten, der Benutzer kann drei von ihnen dynamisch verändern (nur das Licht bleibt konstant). Durch einen Druck auf die mittlere Maustaste rotiert die Kamera um den Ursprung. Die interessantesten Knoten sind »punktKoordinaten« und »punkte«:
- »punktKoordinaten« ist ein Objekt der Klasse »SoCoordinate3«, es nimmt eine beliebige Anzahl an 3D-Koordinaten auf.
- »punkte« ist ein Objekt der Klasse »SoPointSet«, es zeichnet alle Koordinaten des »punktKoordinaten«-Knotens als Punkt in den Raum.
Mit der linken Maustaste setzt der Benutzer neue Punkte ein. Ein Klick auf die rechte Maustaste löscht alle Punkte aus der Szene.
Die richtige Wahl
Um die interaktiven Wünsche des Anwenders umzusetzen, muss das Programm einzelne Objekte im Szenengraphen auswählen und verändern. Das Beispiel fügt neue Punkte in »punktKoordinaten« ein und teilt dem Knoten »punkte« mit, dass er die Neulinge zeichnen soll.
Ein Knoten lässt sich durch seine Position im Graphen ansprechen und auswählen. Hierfür bietet jeder Gruppenknoten eine Methode »getChild()«. Abbildung 1 zeigt, wie das Programm die einzelnen Knoten über die Wurzel des Szenengraphen anspricht. Diese einfache Methode ist allerdings sehr fehleranfällig: Falls nachträglich Knoten in den Szenengraphen eingefügt werden, ändern sich die Indizes. Auch ist die Methode bei größeren Szenengraphen sehr unübersichtlich – in diesem Fall ist es besser, allen Knoten über ihre »setName()«-Methode eindeutige Namen zu geben. Ihre Position lässt sich dann durch ein Objekt der Klasse »SoSearchAction« finden und verändern.
Die Funktion »neuerPunkt()« in Listing 1 fügt einen neuen Punkt in die Szene ein. Als Parameter erhält sie einen Zeiger auf die Render-Fläche und die Koordinaten eines Punkts. Die Zeilen 5 bis 7 ermitteln die Zeiger für den dritten und vierten Knoten des Szenengraphen, ausgehend von der Wurzel. Anschließend ändern die Zeilen 10 und 12 die Eigenschaften der Knoten.
In der Szene punkten
Wenn der Benutzer Punkte in die Szene setzt, muss das Programm einen Zusammenhang zwischen der zweidimensionalen Mausposition im Fenster und der dreidimensionalen Position in der Szene herstellen. Das vorliegende Beispiel geht einen einfachen Weg: Es setzt alle Punkte auf eine Ebene, die durch den Ursprung geht.
Abbildung 2 zeigt die Geometrie des Volumens, das eine »SoPerspectiveCamera« als Ausschnitt der 3D-Szene sieht. Wenn sich die Sichtweite der Kamera bis ins Unendliche erstreckt, kann das Rendern sehr lange dauern. Deshalb schränken die Parameter »nearDistance« und »farDistance« das Volumen des Kamera-Sichtfelds ein. Im vorliegenden Beispiel ist die »nearDistance« auf den Wert »1« und die »farDistance« auf »7« gesetzt. Da die Entfernung der Kamera vom Ursprung »4« beträgt, liegt der Mittelpunkt der Szene genau in der Mitte von »nearDistance« und »farDistance«. Durch diesen Mittelpunkt verläuft auch die Ebene (grün eingezeichnet), auf der der Benutzer die Punkte zeichnen soll.
Die Entfernungseinheiten sind in Open Inventor per Default relativ zueinander angegeben. Sie können aber mit dem Knoten »SoUnit« zu einer bestimmten Längeneinheit gesetzt werden.
Die Maus im Volumen
Listing 2 bildet die x- und y-Werte der Mausposition auf diese Fläche ab. Dahinter steckt die Vorstellung, dass die Mausposition als Strahl senkrecht durch das Kameravolumen geht. Dieser Strahl ist in Abbildung 2 blau gezeichnet. Die einzelnen Schritte sind:
- Die Zeilen 7 bis 9 rechnen die Mauskoordinaten vom X11-Fenster in die Einheiten der Render-Fläche um (Wertebereich zwischen »0« und »1«). Der Ursprung liegt links unten, während ihn X11 links oben ansetzt.
- Zeile 17 holt ein Objekt der Klasse »SbViewVolume«, das den sichtbaren Ausschnitt der Szene beschreibt und auf diese Weise perspektivisch korrekte Abbildungen erlaubt.
- Zeile 21 zieht den Mauszeiger in die Länge: Aus dem Punkt wird eine Linie, die senkrecht in das Bild hineinläuft. Die Endpunkte speichert sie in »p0« und »p1«.
- Die Zeichenebene liegt auf halbem Weg in das Bild hinein, sie schneidet die Linie genau in ihrer Hälfte.
Das ist nur eine von vielen Varianten, um die Mausposition auf die Szene zu übertragen. Aufwändigere Lösungen bilden den Mauszeiger auch auf dreidimensionale Objekte ab. Mehr darüber ist in den Online-Dokumentationen von Coin[6] und Inventor Mentor zu finden[3],[4]. Besonders die Klassen »SoPickAction« und »SoPickedPoint« leisten wertvolle Dienste.

Abbildung 1: Der übersichtliche Szenengraph des 3D-Malprogramms. Die vier Kinder des Wurzelknotens lassen sich über dessen »getChild()«-Methode abfragen.

Abbildung 2: Der Betrachter blickt bei Coin durch eine Kamera in die 3D-Szene. Diese Kamera sieht nur einen Ausschnitt der Welt, begrenzt durch vier Seitenflächen und zwei Tiefenebenen (rot).
Engines, Sensoren und Callbacks
Open Inventor kennt zwei Techniken Animationen zu programmieren: Engines und Sensoren. Engines (Automaten) sind Objekte, die mit Feldern von Knoten im Szenengraphen verknüpft sind und deren Werte verändern. Die Änderung wird entweder durch eine logische Verknüpfung oder einen zeitlichen Ablauf gesteuert. Da die Engines zum Szenengraphen gehören, kann man sie mit ihrem Verhalten in Dateien speichern. In[2] wurden bereits zwei dieser Engines benutzt, um die Erdkugel rotieren zu lassen.
Sensoren sind Objekte, die Veränderungen im Szenengraphen erkennen. Als Reaktion rufen sie eine Callback-Funktion auf. Sensoren geben – verglichen mit Engines – dem Programmierer mehr Freiheit, da er eigenen Code schreiben kann. Das bedeutet aber auch, dass Sensoren nicht zum Szenengraphen gehören und dass sie sich nicht in Dateien speichern lassen.
|
Listing 1: Neuer Punkt |
01 // Neuen Punkt zur Szene hinzuzufügen
02 void neuerPunkt(SoQtRenderArea *renderFlaeche, const SbVec3f point)
03 {
04 // Zeiger auf die zu ändernden Knoten im Graphen
05 SoGroup *wurzel = (SoGroup *) renderFlaeche->getSceneGraph();
06 SoCoordinate3 *punktKoordinaten = (SoCoordinate3 *) wurzel ->getChild(2);
07 SoPointSet *punkte = (SoPointSet *) wurzel->getChild(3);
08
09 // Koordinaten hinzufügen
10 punktKoordinaten->point.set1Value(punktKoordinaten->point.getNum(), point);
11 // Der Punkte-Knoten soll alle Punkte zeichnen
12 punkte->numPoints.setValue(punktKoordinaten->point.getNum());
13 }
|
|
Listing 2: Projektion der Maus |
01 // Berechnet aus der Position der Maus
02 // einen 3D-Punkt in der Szene
03 void projektion(SoQtRenderArea *renderFlaeche,
04 int mausX, int mausY, SbVec3f &schnittpunkt)
05 {
06 // Position der Maus auf der Render-Fläche:
07 SbVec2s groesse = renderFlaeche->getSize();
08 float x = float(mausX) / groesse[0];
09 float y = float(groesse[1] - mausY) / groesse[1];
10
11 // Zeiger zur Kamera
12 SoGroup *wurzel = (SoGroup *) renderFlaeche->getSceneGraph();
13 SoCamera *kamera = (SoCamera *) wurzel->getChild(0);
14
15 // Für die Kamera sichtbares Volumen
16 SbViewVolume kameraVolumen;
17 kameraVolumen = kamera->getViewVolume();
18
19 // Linie von der Maus senkrecht durch die Szene
20 // Der andere Endpunkt entsteht durch Spiegeln an der Ebene
21 SbVec3f p0, p1;
22 kameraVolumen.projectPointToLine(SbVec2f(x,y), p0, p1);
23
24 // Der Mittelpunkt der Linie ist der gesuchte Punkt
25 // auf der Fläche, die durch den Ursprung geht
26 schnittpunkt = (p0 + p1) / 2.0;
27 }
|
Das Beispiel benutzt einen Sensor, um die Kamera in der Szene zu rotieren. Sobald der Benutzer die mittlere Maustaste drückt, schließt der Code-Ausschnitt in Listing 3a den Timer-Sensor »SoTimerSensor« an die Kamera an. Der Sensor ruft seine Callback-Funktion (Listing 3b) in regelmäßigen Abständen auf; sie dreht die Kamera schrittweise. Andere Sensoren wie »SoNodeSensor« reagieren auf Veränderungen am überwachten Knoten und rufen daraufhin ebenfalls ihre Callback-Funktion auf.
Einer der Vorteile von Callbacks ist, dass sie nicht nur den Szenengraphen beeinflussen können. Es ist zum Beispiel möglich, dass ein Sensor nach jeder Änderung die Eigenschaften des überwachten Objekts auf das Terminal schreibt. Callbacks sollten jedoch nicht zu viel Code enthalten, da sie unter Umständen sehr oft aufgerufen werden und das Programm damit bremsen.

Abbildung 3a: Zunächst funktioniert das Beispielprogramm wie eine 2D-Zeichensoftware: Man malt mit der linken Maustaste beliebige Figuren.

Abbildung 3b: Dreht der Betrachter die Kamera, offenbaren sich die 3D-Fähigkeiten des Programms. Neue Punkte landen in einer neuen Ebene.
|
Listing 3a: Sensor |
01 // Der Timer-Sensor gibt Zeitimpulse an die Kamera 02 // während die mittlere Maustaste gedrückt ist 03 ticker = new SoTimerSensor(tickerCallback, kamera); 04 ticker->setInterval(UPDATE_RATE); |
|
Listing 3b: Callback |
01 void tickerCallback(void *daten, SoSensor *)
02 {
03 // Zeiger auf die Kamera
04 SoCamera *kamera = (SoCamera *) daten;
05 SbRotation rot;
06 SbMatrix mtx;
07 SbVec3f pos;
08
09 // Kamera drehen
10 pos = kamera->position.getValue();
11 rot = SbRotation(SbVec3f(0,1,0), ROTATION_WINKEL);
12 mtx.setRotate(rot);
13 mtx.multVecMatrix(pos, pos);
14 kamera->position.setValue(pos);
15
16 // Ausrichtung der Kamera korrigieren
17 kamera->orientation.setValue(kamera ->orientation.getValue() * rot);
18 }
|
Parameter von Callbacks
Callbacks haben in Coin zwei Parameter. Der erste ist ein Zeiger vom Typ »void«, durch den die Funktion eine Referenz auf ein beliebiges Objekt erhalten kann. Listing 3b erwartet auf diesem Weg ein Objekt der Klasse »SoCamera«; die Referenz hatte Listing 3a zunächst an den Konstruktor des Sensors übergeben. Der zweite Parameter ist ein Zeiger vom Typ »SoSensor«, der Informationen vom aufrufenden Sensor weitergibt. Listing 3b nutzt diesen Parameter aber nicht.
Wenn ein Benutzer die Maus bewegt oder eine Taste drückt, registriert der X-Server diese Bewegung und meldet sie an die betreffenden Programme als so genannte Ereignisse (Events). Die finden sich auch in Qt wieder. Qt bietet über die Klasse »QEvent« einen einfachen Weg, um auf sie zu reagieren.
SoQt erlaubt es, eine Callback-Funktion an die Render-Fläche anzufügen. SoQt ruft dieses Callback immer dann auf, wenn ein Ereignis in der Form eines »QEvent« auftritt. In diesem Beispiel heißt die Funktion »ereignisHandler()« und wird mit
renderFlaeche->setEventCallback( ereignisHandler, renderFlaeche);
an die Render-Fläche angehängt. Der Code der Funktion »ereignisHandler()« ist in Listing 4 zu sehen.
Ereignisbehandlung in einem Switch-Block
Die Callback-Funktion besteht zum größten Teil aus einem Switch-Case-Konstrukt, das die verschiedenen Ereignisse abfragt. Diese Funktion sollte ebenfalls nicht zu groß sein, da sie bei jedem Ereignis aufgerufen wird und folglich das Programm stark bremsen kann. Qt bietet aber auch ausgefeiltere Wege, um einzelne Events abzufangen, ohne jedes beliebige Event auswerten zu müssen. Näheres dazu verrät die Online-Dokumentation von Qt.
Um die Ereignisse abzufangen, muss das Programm eine Ereignisschleife (Event Queue) starten, die während der gesamten Programmlaufzeit aktiv bleibt. Da diese Queue erst beim Programmende terminiert, muss man sie mit einem der letzten Befehle in »main()« starten.
Das fertige Programm steht auf[9] bereit. Nachdem es übersetzt und gestartet ist, sieht es aus, wie in Abbildung 3a und 3b zu sehen. Mit der linken Maustaste kann man beliebige zweidimensionale Figuren zeichnen und diese mit den Kontrollelementen am Rand des Fensters drehen. Aber bevor diese kleine Serie über Coin und Open Inventor endet, sollen noch einige weitere Möglichkeiten erwähnt werden.
VRML importieren
Im zweiten Teil[2] war bereits zu lesen, dass man VRML-1.0-Dateien leicht ins Open-Inventor-Format umformen kann. Die kommende Coin-Version 2.0 verspricht, auch VRML-Versionen über 1.0 besser zu unterstützen. Bei VRML-Knoten haben sich die Eigenschaften verändert, obwohl sie ihren Namen beibehalten. Um diese neuen Knoten mit Open Inventor zu benutzten, war es notwendig, ihnen neue Namen zu geben. Das gelang mit der neuen Vorsilbe »SoVRML« für Klassen, die in neueren VRML-Versionen ein anderes Verhalten haben. So beschreiben künftig »SoSphere« und »SoVRMLSphere« eine Kugelform. Mehr dazu ist in der Coin-2.0-beta-Dokumentation zu finden[6].
OpenGL lebt weiter
Wer schon 3D-Grafiken als OpenGL-Code hat, muss auf diesen nicht verzichten, wenn er auf Open Inventor umsteigt. Da Letzterer auf OpenGL basiert und auch dessen State Machine benutzt, ist es recht einfach, OpenGL-Code in eine Open-Inventor-Anwendung zu integrieren. Üblicherweise fügt man den Code in eine Callback-Funktion ein, die dann vom Szenengraphen aufgerufen wird. OpenGL-Code lässt sich leider nicht in Dateien speichern, da er nicht zum Graphen gehört.
Eine weitere Open-Inventor-Technik sind Nodekits. Sie sind eine Art Blackbox, die ihren eigenen Szenengraphen enthält – diese inneren Graphen sind nach außen aber nicht sichtbar. Sie lassen sich wie alle anderen Open-Inventor-Klassen benutzen und besitzen Eigenschaften, die der Programmierer bestimmt. Nodekits eignen sich ideal für Bibliotheken.
Komplexere Objekte
Auch komplexe Oberflächen und Geometrien, etwa Bezierkurven, sind mit Open Inventor recht einfach zu modellieren. Diese Objekte funktionieren nach folgendem Prinzip: Wie im »SoPointSet«-Knoten im obigen Beispiel müssen sich zuerst alle Punkte der Geometrie in einem oder mehreren Knoten im Szenengraphen befinden. Erst danach wird der eigentliche Geometrieknoten eingefügt, der die Form festlegt. Damit ist es leicht, bei gleich bleibenden Stützpunkten die Geometrie zu wechseln.

Abbildung 4: Mit Coin lassen sich Szenen auch als Farb-Stereografik darstellen: Mit einer Rot-Cyan-Brille sieht der Betrachter einen plastischen 3D-Kegel.
Stereografik
Ein Weg, um dreidimensionale Objekte realistischer zu machen, ist Stereografik. In der Realität sehen die beiden Augen zwei verschiedene Bilder desselben Objekts aus leicht verschiedenen Winkeln. Das Gehirn ermittelt aus den Unterschieden die dreidimensionalen Informationen. Für realistisches 3D muss ein 2D-Bildschirm zwei verschiedene Bilder an das linke und das rechte Auge senden.
Die meisten Techniken benötigen dazu eigene Hardware, zum Beispiel 3D-Brillen, spezielle Grafikkarten oder Bildschirme. Coin unterstützt unter anderem das so genannte Anaglyph Stereo: Es benutzt zwei verschiedene Farben, um beide Bilder übereinander zu zeichnen und an die Augen zu liefern. Eine Brille mit verschiedenfarbigen Gläsern trennt die Bilder wieder. Abbildung 4 zeigt ein Beispiel: Coin stellt den weißen Kegel in Rot und Cyan dar, wenn die entsprechende Stereo-Option im Menü ausgewählt ist.
Gute Online-Quellen
Wer mehr über neue Erweiterungen und Möglichkeiten von Open Inventor erfahren möchte, sollte die Webseiten von Coin[5] und TGS[7] besuchen. Die Homepage von Coin enthält nicht nur die Online-Dokumentation, sondern zudem eine gute Sammlung an Foren und FAQs. TGS entwickelt die originale Version von Open Inventor weiter und stellt ebenfalls viele nützliche Informationen und FAQs bereit. (fjl)
|
Listing 4: Ereignisbehandlung |
01 SbBool ereignisHandler(void *daten, QEvent *einEreignis)
02 {
03 // Zeiger auf die Render-Fläche
04 SoQtRenderArea *renderFlaeche = (SoQtRenderArea *) daten;
05 QMouseEvent *MausEvent;
06 SbVec3f vektor;
07 SbBool handled = TRUE;
08
09 // Welches Ereignis ist eingetreten?
10 switch(einEreignis->type()) {
11 case QEvent::MouseButtonPress:
12 // Maustaste wurde gedrückt
13 MausEvent = (QMouseEvent *) einEreignis;
14
15 if(MausEvent->button() == Qt::LeftButton) {
16 // Linke Maustaste: neuer Punkt
17 Projektion(renderFlaeche,MausEvent->x(),
18 MausEvent->y(), vektor);
19 neuerPunkt(renderFlaeche, vektor);
20 }
21 else if(MausEvent->button() == Qt::MidButton) {
22 // Mittlere Maustaste: Kamera rotieren
23 ticker->schedule();
24 }
25 else if(MausEvent->button() == Qt::RightButton) {
26 // Rechte Maustaste: alle Punkte löschen
27 loeschePunkte(renderFlaeche);
28 }
29 break;
30 case QEvent::MouseButtonRelease:
31 // Maustaste wurde losgelassen
32 MausEvent = (QMouseEvent *) einEreignis;
33 if(MausEvent->button() == Qt::MidButton) {
34 // Mittlere Maustaste: Kamera wieder ruhig
35 ticker->unschedule();
36 }
37 break;
38 case QEvent::MouseMove:
39 // Die Maus wurde bewegt
40 MausEvent = (QMouseEvent *) einEreignis;
41 if(MausEvent->state()) {
42 // Eine Taste ist gedrückt
43 Projektion(renderFlaeche,MausEvent->x(),
44 MausEvent->y(), vektor);
45 neuerPunkt(renderFlaeche, vektor);
46 }
47 break;
48 }
49 return handled;
50 }
|
|
Infos |
|
[1] Stephan Siemen, “Virtuelle Welt”: Linux-Magazin 02/03, S. 100 [2] Stephan Siemen, “Bewegte Objekte”: Linux-Magazin 03/03, S. 98 [3] Josie Wernecke, “The Inventor Mentor. Release 2”: Addison-Wesley 1994, ISBN 0-201-62495-8 [4] Zusammenfassung des Inventor Mentor von SGI: [http://www.sgi.com/software/inventor/vrml/TIMSummary.html] [5] Coin: [http://www.coin3d.org] [6] Dokumentation zu den Coin-Bibliotheken: [http://doc.coin3d.org] [7] TGS: [http://www.tgs.com] [8] Zusätzliche Informationen: [http://prswww.essex.ac.uk/stephan/3D/] [9] Dateien zum Artikel: [ftp://ftp.linux-magazin.de/pub/listings/magazin/2003/04/3d/] |
|
Der Autor |
|
Dr. Stephan Siemen arbeitet als wissenschaftlicher Mitarbeiter an der Essex University (UK). Dort entwickelt er Software zur 3D-Darstellung von Wettersystemen und unterrichtet Studenten in Computergrafik und Programmierung. Auf [8] bietet er weitere Informationen zum Thema. |





