Aus Linux-Magazin 03/2003

Interaktive 3D-Welten mit Coin und Qt - Teil 2

Auch hochfliegende Ideen lassen sich mit Coin und wenigen Zeilen C++ schnell umsetzen: Aus einfachen Objekten mit Texturen und komplexen VRML-Modellen entsteht eine animierte 3D-Welt.

3D-Objekte wirken erst mit einer Textur (Oberflächengestaltung) realistisch. Wer mit Coin und C++ arbeitet (siehe Kasten “Rückblick”), kann diesen Effekt sehr einfach nutzen. Die Objekte muss man dafür nicht mühsam von Hand programmieren: Open Inventor und damit auch Coin können VRML-1.0-Dateien einbinden, für die es ausgereifte grafische Modellierwerkzeuge gibt. Durch Animation haucht der Entwickler seiner Welt Leben ein.

Rückblick

Der erste Teil dieser kleinen Coin-Serie[1] beschrieb, wie mit Hilfe von Coin und SoQt dreidimensionale Grafiken in C++ entstehen. Coin[2] ist ein freier Klon von Open Inventor (SGI[3] und TGS[4]), der auf OpenGL aufbaut. Die Beispiele in diesem und im vorangegangenen Artikel enthalten keinen Coin-spezifischen Code. Die Programme sollten daher mit jeder Open-Inventor-Version und jedem Klon laufen, mit Ausnahme der Bindung zum Fenstersystem: Hier kommt Qt statt Motif zum Einsatz.

Ein Vorteil von Open Inventor gegenüber OpenGL ist, dass Ersterer 3D-Szenen durch einen Szenengraphen beschreibt. Der Szenengraph ist als Baumstruktur aufgebaut; jedes 3D-Element ist in einem eigenen Knoten enthalten. Wie und wo Coin einen Knoten anzeigt hängt von seiner Position im Graphen ab. So ist es möglich, eine Szene (oder Teile davon) zu speichern oder ihr Verhalten (Interaktion und Animation) zu programmieren.

Bindeglied der verschiedenen Teile einer 3D-Welt ist der Szenengraph. Für ihn gilt “Teile und herrsche”: Eine komplexe Szene ist einfacher zu programmieren, wenn sie in kleinere Teile zerlegt ist. Beim Programmieren einer Szene ist es ratsam, zu Beginn eine Liste mit allen benötigten Objekten und deren Eigenschaften zu erzeugen. Für jede Geometrie ist dann ein Gruppenknoten anzulegen. In den fügt man zuerst die gewünschten Transformationen ein (etwa Rotation oder Skalierung), ergänzt die Materialeigenschaften (etwa die Farbe) und beschreibt erst dann die eigentliche Geometrie. Die Reihenfolge ist wichtig, weil Open Inventor zum Rendern (darstellen) auf OpenGL zurückgreift.

Beim Unterteilen der Szene sollte man darauf achten, dass sich möglichst viele Objekte mehrfach verwenden lassen. Das spart Code und ist übersichtlicher. Eine Möglichkeit ist es, Funktionen zu schreiben, die eigene kleine Szenengraphen oder Gruppen über einen Zeiger zurückgeben. Auf diese Weise lassen sich die Komponenten einfach in den Szenengraphen einhängen.

Beim Programmieren ist es üblich, wiederkehrende Teile des Codes in Bibliotheken auszulagern. Das trifft auch auf die Grafikprogrammierung zu: Für vorgefertigte Beschreibungen von wiederkehrenden, komplexeren Objekten lassen sich Dateien einsetzen. Die beiden gängigsten Varianten Dateien einzubinden, sind dreidimensionale Szenen, die durch eigene Szenengraphen beschrieben werden, sowie 2D-Bilder (in diesem Zusammenhang auch Texturen genannt). Letztere werden als Oberfläche auf Objekte abgebildet.

Texturen

Texturen sollen Objekte realistischer aussehen lassen. Open Inventor kennt zwei Varianten: Texturen können durch eine Matrix im Speicher beschrieben sein oder als zweidimensionale Bilder in Dateien vorliegen. Ersteres ist sehr aufwändig und in der Regel nicht nötig. Um eine Textur aus einer Datei zu laden, erzeugt das Programm zuerst ein Objekt der Klasse »SoTexture2«. Dieses Objekt muss dann den Namen der Bilddatei erfahren:

SoTexture2 *texture = new SoTexture2;
texture->filename.setValue("textur.rgb");

Das Texturobjekt muss im Szenengraphen vor dem Objekt eingefügt sein, auf das es abgebildet wird. Open Inventor kennt zwei Wege, Texturen auf Objekte zu legen: Er kann eine Textur einzeln (der Default-Fall) oder mehrfach auf eine Oberfläche zeichnen. Coin nutzt die »simage«-Bibliothek[5], um Texturen zu laden, und unterstützt damit folgende Formate: JPEG (».jpg«), GIF (».gif«), Targa (».tga«), PIC (».pic«), SGI-RGB (».rgb«, ».bw«) und XWD (».xwd«).

Listing 1 zeigt Code, der eine Kugel mit der Weltkarte überzieht. Der Code ist als Funktion implementiert, die einen Zeiger vom Typ »SoSeparator« zurückliefert. An diesem Gruppenknoten hängen eine Textur »SoTexture2« und eine Kugel »SoSphere«. Die Datei »weltkarte.rgb« steht auf[8] bereit; sie stammt ursprünglich aus den Beispielen des Inventor Mentor. Abbildung 1 zeigt das Resultat dieser Funktion, eingebunden in einen Szenengraphen und gerendert mit »SoQtExaminer«.

Listing 1: Erde zeichnen

01 SoSeparator* zeichneErde()
02 {
03    // Objekte für Gruppenknoten und Textur
04    SoSeparator *erde = new SoSeparator;
05    SoTexture2  *textur_erde = new SoTexture2;
06 
07    // Name der Texturdatei
08    textur_erde->filename = "weltkarte.rgb";
09 
10    // Textur an den Gruppenknoten hängen
11    erde->addChild(textur_erde);
12 
13    // Kugel an den Gruppenknoten hängen
14    // => Textur auf die Kugeloberfläche zeichnen
15    erde->addChild(new SoSphere);
16 
17    return erde;
18 }
Abbildung 1: Die Erde in 3D. Für diese Grafik genügen eine Kugel und die passende Oberflächentextur.

Abbildung 1: Die Erde in 3D. Für diese Grafik genügen eine Kugel und die passende Oberflächentextur.

Externe Geometrien hinzufügen

Wie erwähnt kann in einer Szene ein Objekt mehrfach, an verschiedenen Stellen, in unterschiedlichen Größen und Ausrichtungen erscheinen. Da Open Inventor die Form unabhängig von der Lage, der Orientierung und dem Aussehen (etwa der Farbe) in einem Objekt beschreibt, kann es der Programmierer beliebig oft verwenden. Wie das Stuhl-Beispiel im ersten Artikel[1] zeigte, sind diese Objekte nur einmal nötig.

Die Beschreibung von Geometrien erfordert gerade bei komplizierteren Formen viel Code. Um ein größeres Projekt übersichtlicher zu gestalten, bietet es sich an, diese komplexeren Objekte in Dateien auszulagern. Hierfür kennt Open Inventor ein eigenes Dateiformat, ähnlich VRML. Die Endung der Dateinamen ist ».iv«, der Inhalt kann entweder aus Ascii-Zeichen oder aus binären Daten bestehen. Für kleinere Szenen empfiehlt es sich, die Daten in (lesbarem) Ascii zu speichern. Für größere Files ist das kompakte Binärformat besser geeignet, da die Dateien kleiner sind und Coin sie schneller lädt.

Die in Listing 2 vorgestellte Funktion »ladeGeometrie()« gibt einen Zeiger auf den Szenengraphen zurück, der in einer Datei gespeichert ist. Die Funktion enthält mehrere Fehlerbehandlungen, um nur gültige Szenen zu laden.

Listing 2: Geometrie aus Datei laden

01 SoSeparator* ladeGeometrie(const char *dateiname)
02 {
03    // Wurzel des Szenengraphen
04    SoSeparator *datei_szene = new SoSeparator;
05 
06    // Handler für Open-Inventor-Datei
07    SoInput myScene;
08 
09    // Szenendatei öffnen
10    if (!myScene.openFile(dateiname))
11    {
12       printf("Fehler beim Laden der Datei '%s'n", dateiname);
13       return NULL;
14    }
15 
16    // Hat die Datei ein gültiges Format?
17    if (!myScene.isValidFile())
18    {
19       printf("Die Datei '%s' ist keine gültige Inventor-Datein", dateiname);
20       return NULL;
21    }
22 
23    // Szene lesen und an den Gruppenknoten 'datei_szene' hängen
24    datei_szene = SoDB::readAll(&myScene);
25 
26    if (datei_szene == NULL)
27    {
28       printf("Fehler beim Lesen der Datei '%s'n", dateiname);
29       myScene.closeFile();
30       return NULL;
31    }
32 
33    // Datei schließen
34    myScene.closeFile();
35 
36    return datei_szene;
37 }

Einige Beispiele für Open-Inventor-Dateien sind auf[8] zu finden. Dort liegt auch »boeing767.iv« mit der Geometrie eines Boeing-767-Jets. Abbildung 2 stellt die Szene im »SoQtExaminer« dar. Ein Blick in die Ascii-Textversion zeigt, wie aufwändig die Beschreibung dieser Szene ist.

Abbildung 2: Das Flugzeugmodell ist in einer Textdatei beschrieben, die von einem Coin-Programm geöffnet und in den Szenengraphen eingebunden wird.

Abbildung 2: Das Flugzeugmodell ist in einer Textdatei beschrieben, die von einem Coin-Programm geöffnet und in den Szenengraphen eingebunden wird.

In der Grafik-Programmierung ist es üblich, komplexe Objekte (etwa Gestalten und Fahrzeuge) in Modellierprogrammen zu entwickeln. Die Objekte exportiert der Designer in das jeweilige Dateiformat (Open Inventor ».iv« oder VRML ».wrl«). Diese Files lädt das Coin-Programm zur Laufzeit. Es gibt eine Menge Modellier-Tools, viele sind kostenlos für den privaten Gebrauch. Sie unterstützen meist mehrere Dateiformate.

VRML

Nicht alle Modellierprogramme können 3D-Objekte als Open-Inventor-Datei exportieren. Die meisten Tools kennen aber das weit verbreitete VRML-Format (Virtual Reality Modeling Language), das auch Open Inventor zugrunde liegt. Entsprechend einfach ist es, VRML-Dateien in Open-Inventor-Files zu konvertieren. Um zum Beispiel eine Ascii-formatierte VRML-1.0-Datei in das Open-Inventor-Format zu verwandeln, genügt es, die erste Zeile anzupassen:

#VRML V1.0 ascii

Für Open Inventor muss diese Zeile so aussehen:

#Inventor V2.0 ascii

Alle Bezeichnungen in VRML 1.0 und Open Inventor sind identisch. Neuere VRML-Versionen enthalten allerdings Objekte, die Open Inventor nicht kennt. Die geplante Coin-Version 2.0 wird aber auch neuere VRML-Versionen unterstützen. Falls eine Modellier-Software weder VRML noch Open Inventor unterstützt, helfen vielleicht 3D-Dateikonverter wie 3dc[6]. Unter[7] sind weitere Tools aufgeführt.

Animationen

Bis jetzt wurden nur statische Objekte betrachtet. Der Benutzer konnte zwar die Szenen von mehreren Seiten betrachten, weil die Programme dafür die Klasse »SoQtExaminerViewer« benutzten. Open Inventor bietet aber auch Klassen an, um Animationen in den Szenen einzufügen. Dazu sind zwei Schritte nötig: Zuerst muss der Programmierer ein Objekt anlegen, das die Veränderungen in der Animation auslöst (triggert). Dieses Objekt heißt Engine. Es gibt im Wesentlichen zwei Gruppen von Engines: Zähler und Rechner. Das Ausgangssignal der Engine wird im zweiten Schritt mit einer Eigenschaft eines Objekts im Szenengraphen verknüpft. Diese Eigenschaft passt sich dann laufend den Vorgaben der Engine an.

Um die Weltkugel von Abbildung 1 um ihre eigene Achse rotieren zu lassen, fügt man zunächst ein Objekt der Klasse »SoRotationXYZ« vor der Kugel in den Graphen ein. Die Klasse hat zwei Felder: Rotationsachse (axis) und Rotationswinkel (angle). Mit einer Engine verbunden erzeugt der Rotationswinkel die gewünschte Animation.

Listing 3 erzeugt eine Knotengruppe, die mit der Funktion »zeichneErde()« aus Listing 1 eine rotierende Erde darstellt. Eine Engine der Klasse »SoTimerCounter« erzeugt die verschiedenen Winkel. Allerdings müssen alle Winkel in Open Inventor als Radialzahlen (und damit als Fließkommawert) angegeben sein, »SoTimerCounter« liefert aber nur natürliche (ganzen) Zahlen. Das Umformen der Gradzahlen in Radialwerte gelingt mit »SoCalculator«. Diese Engine formt Eingabewerte mit gängigen arithmetischen Ausdrücken um.

Listing 3: Rotation der Erde

01 #include <Inventor/Qt/SoQt.h>
02 #include <Inventor/Qt/viewers/SoQtExaminerViewer.h>
03 #include <Inventor/SoInput.h>
04 #include <Inventor/nodes/SoSeparator.h>
05 #include <Inventor/nodes/SoSpotLight.h>
06 #include <Inventor/nodes/SoScale.h>
07 #include <Inventor/nodes/SoTexture2.h>
08 #include <Inventor/nodes/SoTranslation.h>
09 #include <Inventor/nodes/SoRotationXYZ.h>
10 #include <Inventor/nodes/SoSphere.h>
11 #include <Inventor/engines/SoTimeCounter.h>
12 #include <Inventor/engines/SoCalculator.h>
13 
14 // Code von Listing 1 und 2 hier einfügen
15 
16 int main(int argc, char ** argv)
17 {
18   // SoQt initialisieren (erzeugt ein Qt-Fenster)
19   QWidget *fenster = SoQt::init("main");
20 
21   // Szenengraph erstellen
22   SoSeparator *wurzel = new SoSeparator;
23   wurzel->ref();
24 
25   // Szene ausleuchten: Spotlight erzeugt auch Schatten
26   SoSpotLight *licht = new SoSpotLight;
27    licht->location.setValue(0,0,2);
28    licht->direction.setValue(0,0,-1);
29    licht->cutOffAngle = 1.5;
30   wurzel->addChild(licht);
31 
32   // Gruppenknoten für die rotierende Erde
33   SoSeparator *erde = new SoSeparator;
34 
35   // Rotationsknoten setzen
36   SoRotationXYZ *erdrotation = new SoRotationXYZ;
37    erdrotation->axis.setValue("Y");
38   erde->addChild(erdrotation);
39 
40   // Erde in die Szene einfügen
41   erde->addChild(zeichneErde());
42 
43   // Zähler setzen
44   SoTimeCounter *zaehler = new SoTimeCounter;
45    zaehler->max=360;
46    zaehler->step=1;
47    zaehler->frequency=0.03;
48 
49   // Werte umrechnen: Grad -> Rad
50   SoCalculator *umrechner = new SoCalculator;
51    umrechner->a.connectFrom(&zaehler->output);
52    umrechner->expression.set1Value(0,"oa=a/(2*M_PI)");
53 
54   // Zähler mit Erdrotationsknoten verbinden
55   erdrotation->angle.connectFrom(&umrechner->oa);
56 
57   // Gruppenknoten der Erde einfügen
58   wurzel->addChild(erde);
59 
60   // Gruppenknoten für das Flugzeug anlegen
61   SoSeparator *flugzeug = new SoSeparator;
62 
63   // Flugzeug aus dem Mittelpunkt der Szene schieben
64   SoTranslation *flughoehe = new SoTranslation;
65    flughoehe->translation.setValue(0,0,1.2);
66   flugzeug->addChild(flughoehe);
67 
68   // Flugzeug verkleinern und um 90¡ drehen
69   SoScale *skalierung = new SoScale;
70    skalierung->scaleFactor.setValue(0.0025,0.0025,0.0025);
71   flugzeug->addChild(skalierung);
72   SoRotationXYZ *flugrichtung = new SoRotationXYZ;
73    flugrichtung->axis.setValue("Y");
74    flugrichtung->angle = 1.5707963;
75   flugzeug->addChild(flugrichtung);
76 
77   // Flugzeuggeometrie aus Datei lesen
78   flugzeug->addChild(ladeGeometrie("boeing767.iv"));
79 
80   // Flugzeug in die Szene einfügen
81   wurzel->addChild(flugzeug);
82 
83   // Betrachter erzeugen
84   SoQtExaminerViewer *b = new SoQtExaminerViewer(fenster);
85    b->setSceneGraph(wurzel);
86    b->setHeadlight(FALSE);
87    b->show();
88 
89   // Fensters anzeigen und auf "Exit" warten
90   SoQt::show(fenster);
91   SoQt::mainLoop();
92 
93   // Betrachter und die Referenz zur Szene löschen
94   delete b;
95   wurzel->unref();
96 
97   return 0;
98 }

Abgehoben

Um das Beispiel zu vervollständigen, enthält die Szene auch noch das Flugzeug aus Abbildung 2. Das Ergebnis zeigt Abbildung 3, den Szenengraphen dazu Abbildung 4. Er enthält keine Beschreibungen von Geometrien oder deren Aussehen. Beides steht entweder in einer externen Datei (über »ladeGeometrie()« gelesen) oder wird in einer Funktion beschrieben (hier »zeichneErde()«). Die verwendeten Knoten sind Gruppen und Transformationen.

Open Inventor bietet viele weitere Möglichkeiten, um mit diesen und anderen Engines Animationen zu programmieren. Die Online-Dokumentation von Coin[2] beschreibt detailliert alle Engines mit ihren Auswirkungen und Möglichkeiten. (fjl)

Abbildung 3: Ein Flugzeug fliegt in 3D über die Erde, die sich dabei unter ihm dreht. Mit Coin ist es leicht, diese Animation zu programmieren.

Abbildung 3: Ein Flugzeug fliegt in 3D über die Erde, die sich dabei unter ihm dreht. Mit Coin ist es leicht, diese Animation zu programmieren.

 

Infos

[1] Stephan Siemen, “Virtuelle Welt – Interaktive 3D-Welten mit Coin und Qt”: Linux-Magazin 02/03, S. 100

[2] Coin: [http://www.coin3d.org]

[3] Open-Source-Variante von Open Inventor: [http://oss.sgi.com/projects/inventor/]

[4] TGS: [http://www.tgs.com]

[5] Simage-Bibliothek: [ftp://ftp.coin3d.org/pub/coin/src/]

[6] 3dc, ein 3D-Konverter: [http://www.on-the-web.ch/3dc/]

[7] Zusätzliche Informationen: [http://prswww.essex.ac.uk/stephan/3D/]

[8] Dateien zum Artikel: [ftp://ftp.linux-magazin.de/pub/listings/magazin/2003/03/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[7] bietet er weitere Informationen zum Thema.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben