Sie sind auf Seite 1von 40

HOCHSCHULE FÜR TECHNIK, WIRTSCHAFT UND KULTUR LEIPZIG

Einbindung und Nutzung von 3DConnexion


6dof-Eingabegeräten für auf OpenSG-
basierende Anwendungen in der 3D-
Visualisierung
Ein Master-Projekt am Helmholtz-Zentrum für
Umweltforschung GmbH - UFZ

B.Sc. Lars Bilke


28.02.2008

Betreuer:
Dr. Björn Zehner (UFZ)
Prof. Dr. Ing. habil. Dieter Vyhnal (HTWK Leipzig)
Inhaltsverzeichnis
Abbildungsverzeichnis ................................................................................................................................... 3
1. Einführung ............................................................................................................................................. 4
2. OpenSG .................................................................................................................................................. 5
2.1. Überblick........................................................................................................................................ 5
2.2. Features.......................................................................................................................................... 6
2.2.1. Performance .......................................................................................................................... 6
2.2.2. Multi-Threading .................................................................................................................... 6
2.2.3. Clustering .............................................................................................................................. 7
2.2.4. Erweiterbarkeit ..................................................................................................................... 8
2.2.5. Multi-Plattform ..................................................................................................................... 8
2.3. Ausblick ..........................................................................................................................................8
3. VRPN – Virtual Reality Peripheral Network........................................................................................ 9
3.1. Eingabegeräte-Abstraktion ........................................................................................................... 9
3.2. VRPN-Verbindungsaufbau ......................................................................................................... 10
3.3. Vorteile des Client/Server-Aufbaus ............................................................................................ 11
3.4. Logging.......................................................................................................................................... 11
4. VRPN benutzen ..................................................................................................................................... 11
4.1. Der allgemeine VRPN-Server ...................................................................................................... 11
4.2. Einen eigenen Server schreiben ................................................................................................. 12
4.3. Eine einfache Client-Anwendung ............................................................................................... 13
4.4. SpaceNavigator-Klasse................................................................................................................ 14
4.4.1. Implementierung als VRPN-Client .................................................................................... 16
4.4.2. Implementierung mithilfe des Singleton-Entwurfsmusters ............................................ 16
4.4.3. Initialisierung und Aufbauen einer Verbindung zum VRPN-Server ............................... 16
4.4.4. Bereitstellen des aktuellen Zustands des SpaceNavigators .............................................. 17
4.4.5. Verschiedene Funktionsmodi............................................................................................. 18
4.4.6. Achsenbeschränkung .......................................................................................................... 18
4.4.7. Invertierung der Achsen ..................................................................................................... 19
4.4.8. Standard-Buttonbelegung .................................................................................................. 19
4.4.9. Die Klasse benutzen ............................................................................................................ 19
4.4.10. Beispielanwendung: Bewegen und Drehen eines Würfels in OpenSG ............................20
4.5. Der SpaceNavigatorSSM ............................................................................................................. 24
4.5.1. Ableiten von SimpleSceneManager ................................................................................... 24
4.5.2. Auswählen der Hoch-Achse ............................................................................................... 26
4.5.3. Kamerasteuerung mit fester Höhe über Grund ................................................................28

2
4.5.4. Auswählen von Objekten per Mausklick ...........................................................................30
4.5.5. Ausgewählte Objekte rotieren und verschieben ................................................................ 32
4.5.6. Auf der Oberfläche bewegen............................................................................................... 34
4.5.7. Optimierte Bewegung auf der Oberfläche ......................................................................... 34
4.5.8. Weitere Funktionen ............................................................................................................ 35
4.5.9. Beispielanwendung: Bewegung auf einem Stadtmodell ................................................... 35
4.5.10. Beispielanwendung: Bewegung auf einem Landschaftsmodell ....................................... 36
4.6. Installation und Testen im VR-Pool der HTWK Leipzig........................................................... 37
4.6.1. Den SpaceNavigator-Server einrichten ............................................................................. 37
4.6.2. Die Testanwendungen (SpaceNavigator-Client) einrichten .............................................38
4.7. VRPN erweitern ...........................................................................................................................38
5. CD-Inhalt .............................................................................................................................................38
5.1. Ordner Client ...............................................................................................................................38
5.2. Ordner download ........................................................................................................................38
5.3. Ordner Server ..............................................................................................................................38
5.4. Ordner SpaceNavigator ............................................................................................................. 39
5.5. Ordner opensg ............................................................................................................................. 39
5.6. Ordner vrpn ................................................................................................................................. 39
Literaturverzeichnis .....................................................................................................................................40

Abbildungsverzeichnis
Abbildung 1 - Eingabegeräte von 3DConnexion, links Space Navigator, rechts Space Pilot ..................... 4
Abbildung 2 - Schematischer Aufbau des Visualisierungszentrums im UFZ ............................................. 5
Abbildung 3 - Anwendungen nutzen ein Eingabegerät über verschiedene Klassen ................................ 10
Abbildung 4 - Die Verarbeitung der Space Navigator-Eingaben .............................................................. 17
Abbildung 5 - Der Aufbau der Szene ........................................................................................................... 21
Abbildung 6 - Die Beispielanwendung zum Drehen eines Würfels .......................................................... 24
Abbildung 7 – links: OpenSG-Koordinatensystem (Y-Achse ist Hochachse), rechts: rechtshändiges
Koordinatensystem mit Z-Achse als Hochachse ........................................................................................ 27
Abbildung 8 - Ein Objekt wird mithilfe der Maus ausgewählt und mit dem SpaceNavigator gedreht und
angehoben .................................................................................................................................................... 36

3
1. Einführung
Im Rahmen des als Pflichtmodul im 3. Fachsemester im Masterstudiengang Medien-
informatik durchzuführenden Master-Projektes stellt die vorliegende Arbeit eine praktische
Einführung in die Programmierung mit der netzwerkbasierten Eingabegeräte-Bibliothek
VRPN1 dar. Es wird gezeigt, wie 6dof2-Eingabegeräte von 3DConnexion (siehe Abbildung 2)
in VRPN eingebunden werden und Programmen über das Netzwerk Zugriff darauf verschafft.

Abbildung 1 - Eingabegeräte von 3DConnexion, links Space Navigator, rechts Space Pilot

Ziel der am Helmholtz Zentrum für Umweltforschung (UFZ) in Leipzig durchgeführten


praktischen Tätigkeiten war es, das SpaceNavigator-Eingabegerät von 3DConnexion (eine Art
„3D-Maus“) in das Szenegraphen-System OpenSG3 zur Steuerung der Kameraperspektive
und der Positionierung von Objekten im dreidimensionalen Raum einzubinden. Als
besondere Anforderung sollte die Eingabe netzwerkgestützt sein, d.h. dass das Eingabegerät
nicht unbedingt an dem gleichen Rechner angeschlossen sein muss, auf dem die zu steuernde
Anwendung läuft, sondern die Eingabedaten über das Netzwerk verteilt werden können.

Dies ist eine typische Anforderung im Bereich der Virtual Reality und der Visualisierung. VR-
Systeme bestehen meistens aus mehreren Rechnern, die über ein Netzwerk zu einem
Rechencluster verbunden werden. Der Rechencluster ist meistens räumlich vom
Displaysystem getrennt. Es wäre sehr unpraktisch Eingabegeräte immer an den Rechner
anschließen zu müssen, auf dem die Anwendung gerade läuft. Mit einer netzwerkgestützten
Eingabemethode können die Eingabegeräte an einen Rechner angeschlossen werden, der z.B.
in der Nähe des Displaysystems steht. Abbildung 2 verdeutlicht einen beispielhaften Aufbau
des Visualisierungszentrums im UFZ.

1 VRPN – Virtual Reality Peripheral Network


2 6dof – six degrees of freedom, dt.: sechs Freiheitsgrade
3 OpenSG – Open Scenegraph
4
Abbildung 2 - Schematischer Aufbau des Visualisierungszentrums im UFZ

Im Vorführ-Raum
Raum befindet sich der Rechner an dem das Eingabegerät angeschlossen
angeschlosse ist. Der
eigentliche Anwendungsrechner kann aber räumlich vom Vorführ-Raum
Vorführ Raum getrennt über das
Netzwerk die Eingaben des Benutzers bekommen. Das Rendering erfolgt wiederum ebenfalls
räumlich getrennt auf dem Render-Cluster.
Render Das Render-Cluster
Cluster steht über das
da Netzwerk in
Verbindung mit dem Anwendungsrechner.

2. OpenSG
2.1. Überblick
OpenSG ist ein portables Szenengraphen4-System
System für die Erstellung von Echtzeit-Grafik-
Echtzeit
Anwendungen, in erster Linie für Virtual Reality.. OpenSG wird als OpenSource entwickelt
und kann frei verwendet werden. Es läuft auf zahlreichen Plattformen und Betriebssystemen.
Betriebssystemen
OpenSG baut auf dem hardwarenahen OpenGL auf, nutzt es zur Grafikausgabe und bildet
dabei eine höhere Abstraktionsschicht. Dabei
bei verfügt der Szenengraph über die komplette
Struktur der Szene und kann mit dieser Information die Berechnung der Grafik auf einem
hohen Level optimieren.

4 Ein Szenengraph ist eine objektorientierte Datenstruktur, die häufig in Grafik-Anwendungen


Grafik Anwendungen genutzt
wird, um die logische und oft auch räumliche Anordnung der darzustellenden zwei- zwei oder
dreidimensionalen Szene zu beschrieben.
5
OpenSG ist für keinen bestimmten Anwendungstyp entwickelt worden, sondern eignet sich
sowohl für Architektur-Visualisierungen als auch für hochdynamische interaktive virtuelle
Welten.

2.2. Features
2.2.1. Performance
Um die ständig steigende Rechengeschwindigkeit von aktueller Grafikhardware optimal
auszunutzen, ist es sehr wichtig, die Hardware ausreichend schnell mit zu verarbeitenden
Daten zu beliefern. Die Rechengeschwindigkeit entwickelt sich schneller als die
Speicherbandbreiten zwischen dem Hauptspeicher und der Grafikhardware. Aus diesem
Grund muss die Grafikhardware mit großen und homogenen Daten versorgt werden. Jeder
zu transferierende Datenblock verursacht einen gewissen Initialisierungsoverhead.
Außerdem kommt es zu sogenannten State-Änderungen, wie z.B. das Wechseln der
verwendeten Textur oder das Laden eines Shaders5, die ebenfalls an der Performance zehren.
OpenSG minimiert diesen Setup-Overhead, indem zu zeichnende Objekte, sortiert nach
State-Änderungen6 und Render-Eigenschaften, gezeichnet werden.
Der Szenengraph hat einen globalen Überblick über seinen Inhalt. Im Gegensatz dazu
verarbeitet der Grafiktreiber immer nur eine sehr kleine Menge an Daten. Der Szenengraph
kann Optimierungen auf einer höheren Ebene vornehmen.
OpenSG leitet aus der Szenengraph-Hierarchie eine Bounding Box-Hierarchie ab, um schnell
entscheiden zu können, welche Teil der Szene für den Betrachter sichtbar sind und welche
Teilbäume komplett von der weiteren Berechnung ausgeschlossen werden können.
Der nun auf die sichtbaren Objekte reduzierte Szenengraph wird nach State-Änderungen
sortiert und dann gerendert. Diese Reorganisation des Szenengraphen kann einen
signifikanten Performance-Schub zu Folge haben und die effiziente Ausnutzung der
Grafikhardware als hochparallelisierte Architektur erst ermöglichen. [Ope08]

2.2.2. Multi-Threading
Neue Prozessorgenerationen unterscheiden sich derzeit nicht mehr maßgeblich durch eine
höhere Taktfrequenz von ihren Vorgängern, sondern durch ihre immer mehr auf
Rechenparallelität abzielende Architektur. Die Taktfrequenzen steigen auf Grund des
Erreichens von physikalischen Grenzen (eine höhere Taktfrequenz erfordert auch eine
kleinere Strukturdichte innerhalb des Prozessors) weit weniger stark als noch vor ein paar
Jahren. Um trotzdem leistungsfähigere Prozessoren zu entwickeln werden diese mit immer
mehr Rechenkernen ausgestattet. So sind heute 2-Kern-Prozessoren im Heim- und 4- bis 8-
Kern-Prozessoren im Workstationbereich gängig. Nun stellt sich die Herausforderung
Anwendungen in der Entwicklung so zu konzipieren, dass sie einen größtmöglichen Nutzen
aus der neuen Mehrkern-Hardware ziehen.
Multi-Threading stellt die Softwareentwicklung vor neue Probleme. Jeder Thread rechnet
unabhängig von den anderen und weiß nicht, was diese gerade machen. Aufgrund der
Unabhängigkeit der Prozesse kann nicht vermieden werden, dass ein Prozess schreibend auf

5 Ein Shader ist Programmcode, der für die Grafikberechnung direkt auf der Grafik-Hardware
ausgeführt wird.
6 Unter State-Änderungen fallen z.B. das Wechseln einer Textur, das Ändern der Kameramatrix, …
6
Daten zugreift, während ein anderer lesend auf diese zugreift. Dieses Problem kann
umgangen werden, wenn man das Schreiben auf Daten während der Parallelverarbeitung
verbietet. Das Rendern einer Szene ist eine Operation, die auf diesem Wege realisiert wird.
Die hochparallele Architektur moderner Grafikkarten arbeitet dabei mit einem Datensatz
und verarbeitet diesen parallel (bis zu 320 sogenannter Stream-Prozessoren7) und benötigt
dabei nur lesenden Zugriff. Knotenbasiertes Locking (d.h. schreibt ein Thread auf Daten in
einem Knoten, so ist dieser für andere Threads sowohl lese- als auch schreibgeschützt) ist
eine alternative Methode, um gleichzeitiges Schreiben und Lesen auf den gleichen Daten zu
vermeiden. Dies kann jedoch zu Problemen führen, wenn Threads die Knotenstruktur
verändern (also Kindknoten anhängen / abhängen). Locking kann nur vermieden werden,
wenn jeder Thread eine eigene Kopie der benötigten Daten hat, was aber zu einem sehr
großen Speicherbedarf führen kann.
In einem Szenengraphen gibt es Daten, die zahlreich vorkommen und in Vektor-Strukturen
gespeichert werden, wie Vertices, Normalen, Farben und Texturkoordinaten, und skalare
Daten, wie z.B. die Farbe eines Materials oder eine Transformationsmatrix.
In OpenSG greifen alle Threads auf dieselben Vektor-Daten (sogenannte MultiFields) zu und
jeder Thread verfügt über eine eigene Kopie der skalaren Daten (sogenannte SingleFields).
Somit beträgt der zusätzliche Speicherbedarf nur einen Bruchteil der gesamten Szenengröße.
Wenn ein Thread nun auf einem Vektor aus einem geteilten Speicherbereich schreibt, so
erhält er eine Kopie und schreibt auf dieser Kopie. Der Originalvektor bleibt unangetastet
und wird auch nicht gesperrt. Die Daten der einzelnen Threads müssen jedoch immer wieder
zu einem konsistenten Zustand synchronisiert werden. Dazu werden alle Datenänderungen,
die ein Thread vorgenommen hat in einer Liste verzeichnet und mithilfe der Änderungslisten
der anderen Threads synchronisiert. Dabei werden immer nur die geänderten Daten von
Thread zu Thread kopiert. Da pro Thread meistens nur kleine Datenmengen geändert werden
hält sich der verursachte erhöhte Speicher- und Rechenzeitbedarf in Grenzen. Der
Anwendungsprogrammierer muss OpenSG jedoch immer mitteilen, wenn er im
Programmcode Daten ändert, die zwischen den Threads geteilt werden. Dies geschieht durch
zusätzliche Funktionsaufrufe und man greift auf die Daten über spezielle Zeiger, die auf die
konkrete Kopie der Daten des gerade aktuellen Threads verweisen, zu. [Voß02]

2.2.3. Clustering
Clustering bietet die Möglichkeit, normale PCs mit leistungsfähigen Grafikkarten zu einem
leistungsfähigen und flexibel erweiterbaren Grafiksystem, speziell für VR- und Visuali-
sierungs-Anwendungen, zusammen zu schließen. Diese werden meist über ein
Projektionssystem mit mehreren Projektoren (meist ein Projektor pro Render-PC im Cluster)
sichtbar gemacht.
Auch in diesem Fall stellen sich dieselben Probleme wie im Bereich Multi-Threading. Das
was auf einem PC gemacht wird (Rendern eines Ausschnitts der Szene oder Änderungen an
der Szene) muss den anderen Rechner mitgeteilt werden. Alle Instanzen der Anwendung
müssen auf allen PCs synchronisiert werden.
Aufgrund der Multi-Threading-Eigenschaften speichert OpenSG bereits Änderungen am
Szenengraphen und den zugrundeliegenden Daten pro Thread auf einem einzelnen PC und
damit auch pro PC im Rendercluster. Diese Änderungen müssen nun noch in ein

7 ATI-Grafikchip R600, verbaut in der Grafikkarte Radeon HD2900 XT


7
netzwerkfähiges Format umgewandelt werden. Dazu wird anfangs jedem Datenfeld eine
eindeutige ID zugewiesen. Wird nun ein Datenfeld von einem PC geändert, so sendet er ein
Netzwerkpaket an die anderen PCs im Cluster mit der Feld-ID und den geänderten Daten als
Inhalt. Die anderen PCs setzen diese Änderungen an ihrer Instanz der Anwendung um und
arbeiten somit immer auf einem für alle identischen Szenengraphen.
Dies geschieht alles unsichtbar für den Anwendungsprogrammierer. Man muss nur eine
spezielle ClusterWindow-Klasse von OpenSG benutzen. OpenSG kümmert sich dann um eine
gleichmäßige Auslastung der einzelnen Cluster-PCs und um die abschließende Bildkom-
position. [Ope08]
Eine ausführliche Beschreibung Cluster-Implementierung von OpenSG ist unter [Rot05] zu
finden.

2.2.4. Erweiterbarkeit
Jede OpenSG-Klasse und jeder OpenSG-Typ kann verändert werden und es ist möglich,
komplett neue Klassen zu schreiben, die sich vollständig in OpenSG integrieren und nicht die
ursprüngliche OpenSG-Bibliothek beeinflussen oder verändern. Dazu setzt OpenSG
fortgeschrittene Programmiermethoden wie Entwurfsmuster (Besucher-, Fabrik- und
Prototyp-Muster) und Reflektion ein. [Ope08]

2.2.5. Multi-Plattform
OpenSG basiert auf plattformunabhängigen Bibliotheken wie OpenGL und Boost und
unterstützt plattformspezifische Fenstersysteme. OpenSG läuft auf Windows-, Linux-, Mac
OSX- und Solaris-Betriebssystemen und auf verschiedenen Plattformen wie PDAs (mit
OpenGL ES), PCs, Clustern und Grafik-Workstations. Gibt es eine OpenGL-Implementierung
auf dem System, so ist theoretisch auch OpenSG lauffähig. [Ope08]

2.3. Ausblick
OpenSG 2 ist derzeit in Entwicklung und soll viele Verbesserungen und Erweiterungen
erfahren. Trotzdem wird die Generalität von OpenSG beibehalten, so dass es auch weiterhin
für ein breites Feld von möglichen Anwendungen genutzt werden kann. Es soll
benutzerfreundlicher werden und die Komplexität eines ausgereiften Grafiksystems vor dem
Benutzer so gut wie möglich verbergen. Die Benutzung von multi-thread-sicheren Daten soll
für den Benutzer vereinfacht werden (Zugriff über normale C-Pointer) und insgesamt
recheneffizienter werden. Multi-Pass-Algorithmen wie Schatten-Berechnung und HDR8-
Rendering sollen besser in das System integriert werden. Dynamisch erzeugte Shader sollen
das Zusammensetzen von Shadern über einzelne Shader-Bausteine, die im Szenengraphen
hinterlegt werden, ermöglichen. Außerdem soll das ganze Render-System im Hinblick auf
den zukünftigen OpenGL-Standard 3.0 ausgelegt werden. Der Quellcode soll durch die
Einbindung eines Unit-Testing-Systems robuster werden. Da OpenSG auf Open Source und
der freiwilligen Arbeit basiert, kann es noch eine Zeitlang dauern bis OpenSG 2 erscheint.
OpenSG 1.8 erschien im Juli 2007 und es sind erst etwa ein Drittel der für OpenSG geplanten
Features implementiert.

8 HDR – High Dynamic Range


8
3. VRPN – Virtual Reality Peripheral Network
VRPN ist eine Klassenbibliothek und eine Sammlung von Server-Anwendungen, die eine
geräteunabhängige, netzwerkgestützte Schnittstelle zwischen Anwendungen und einer
Anzahl an Eingabegeräten (z.B. Tracker, Buttons, Mäuse, Joysticks) in Virtual Reality-
Systemen zur Verfügung stellt.
VRPN wurde entwickelt, um folgenden Problemen gerecht zu werden:
• VR-Systeme, die aus mehreren Rechnern zusammengesetzt sind, z.B. Render-Cluster,
benötigen Zugriff auf Eingabegeräte, die physikalisch an verschiedene Rechner an
verschiedenen Standorten angeschlossen sind. Dabei wäre es umständlich, die
Eingabegeräte immer an die Rechner anschließen zu müssen, an denen die
Eingabedaten verarbeitet werden sollen. Außerdem könnte in diesem Fall das
Eingabegerät auch nur an einem Rechner benutzt werden.
• Einige VR-Eingabegeräte (vor allem Tracker) arbeiten zuverlässiger wenn sie
kontinuierlich angeschaltet sind und benötigen eine längere Startprozedur.
• Verschiedene Eingabegeräte bieten teilweise sehr verschiedene Schnittstellen, obwohl
sie über eine ähnliche Funktionalität verfügen. In einigen Fällen gibt es nicht für alle
Betriebssysteme auch Treiber für den Betrieb der Eingabegeräte.
• VR-Anwendungen haben die Anforderung einer möglichst niedrigen Latenz zwischen
dem Zeitpunkt der Eingabe und dem Feedback durch das VR-System. Außerdem
sollen alle Ereignisse (z.B. das Drücken eines Buttons) einem Zeitpunkt zugeordnet
werden können und die Zeit soll zwischen Server- und Client-Anwendung
synchronisiert werden.
VRPN bietet eine Client-Server-Architektur, mit der es möglich ist, ein Eingabegerät an einen
Rechner anzuschließen, auf diesem den VRPN-Server zu starten und an einem anderen über
Ethernet verbundenen Rechner auf die Daten des Eingabegerätes über einen VRPN-Client
zuzugreifen. [Tay01]

3.1. Eingabegeräte-Abstraktion
Konkrete Eingabegeräte setzen sich aus einem oder mehreren abstrahierten Eingabetypen
zusammen. Jeder Typ spezifiziert eine konsistente Schnittstelle, die für alle Eingabegeräte
gleich ist, die diesen Typ implementieren. Folgende Typen sind in VRPN enthalten:
• Tracker sendet seine Position und Orientierung im Raum sowie Geschwindigkeit-
und Beschleunigungswerte.
• Button sendet Drücken- und Loslassen-Ereignisse für einen oder mehrere Buttons.
• Analog sendet einen oder mehrere analoge Werte.
• Dial senden Informationen über fortführende Rotationen.
• ForceDevice ermöglicht der Clientseite, Oberflächen und Kräftefelder
dreidimensional zu spezifizieren.
Um nun ein Eingabegerät zu verwenden, werden die einzelnen Möglichkeiten des Gerätes auf
Eingabetypen abgebildet. So kann man z.B. einen handelsüblichen Joystick mit zwei Tasten
auf zwei Button-Typen und zwei Analog-Typen abbilden. Eine Client-Anwendung geht mit
den Typen einzeln um, sodass es für die Anwendung keine Rolle spielt, ob nun ein Joystick

9
mit zwei Buttons oder eine Maus mit zwei Buttons angeschlossen ist, da sie beide dieselben
Typen implementieren. Somit kann die Anwendung mit allen Eingabegeräten genutzt
werden, die zwei Analogs und zwei Buttons implementieren.
Obwohl die einzelnen Typen eines Eingabegerätes logisch getrennt sind, werden sie trotzdem
auf eine Netzwerkverbindung abgebildet.
Von einem Eingabegerät ausgehend können können mehrere logische Eingabetypen abgeleitet
werden. So kann z.B. ein frei drehbares Rad benutzt werden, um eine Rotation darzustellen
(Typ Dial) oder um einen Wert zu liefern (Typ Analog). Eingabetypen können
könne außerdem in
Schichten angeordnet sein, d.h. die Ausgabe eines Typs kann Eingabe eines anderen sein.
So sendet die vrpn_Joystik-Klasse
Klasse z.B. einen analogen Wert für jede Joystick-Achse.
Joystick Die
vrpn_Analog_Fly-Klasse
Klasse nimmt diese Werte als Eingaben und gibt Tracker-Daten
Tracker aus, um in
VR-Anwendungen
Anwendungen durch die 3D-Szene
3 zu fliegen. Die Client-Seite
Seite kann nun entscheiden, ob
sie die Analog- oder die Tracker-Daten
Tracker oder beide benutzt. [Tay01]

3.2. VRPN-Verbindungsaufbau
Verbindungsaufbau
Der Server öffnet einen festgelegten UDP9-Port
Port für Verbindungsanfragen von Clients. Der
Client öffnet einen verfügbaren
TCP10-Port
Port und sendet eine UDP- UDP
Anfrage an den Server, um eine
Verbindung auf diesem TCP-Port
TCP
aufzubauen. Der Client fragt dann
den TCP-Port
Port ab, ob der Server
geantwortet hat. Wenn die
Verbindung über TCP aufgebaut ist,
werden die Versionen abgeglichen
sowie die Zeit synchronisiert.
Außerdem wird eine separate
separat UDP-
Verbindung zum schnellen
Datenaustausch aufgebaut. Die
eigentlichen Daten der Eingabegeräte
werden dann über UDP ausgetauscht.
Dies hat zur Folge, dass zwar einzelne
Datenpakete verloren gehen können,
aber dafür kann mit einer hohen Abbildung 3 - Anwendungen nutzen ein Eingabegerät
über verschiedene Klassen
Ein

Geschwindigkeit gesendet
esendet werden.
TCP wäre für Echtzeitanwendungen
zu langsam.
Wird die Verbindung durch den Client oder den Server getrennt, so wartet jeweils die andere
Seite auf eine neue Verbindungsaufnahme. Fällt z.B. der Server kurzzeitig aus, so muss die
Client-Anwendung
Anwendung nicht neu gestartet werden, was bei langen Startzeiten der Client- Client
Anwendung sehr vorteilhaft ist. [Tay01]

9 UDP – User Datagram Protocol


10 TCP – Transmission Control Protocol
10
3.3. Vorteile des Client/Server-Aufbaus
Die Aufteilung in 2 Prozesse nach der Client/Server-Architektur von VRPN ist von Vorteil,
wenn:
• die Prozesse stark unterschiedliche Update-Raten haben,
• die Server- oder Client-Initialisierung lang dauert,
• die genauen Zeitpunkte der Ereignisse wichtig sind oder
• der Server ständigen Zugriff auf das Eingabegerät benötigt.
So könnte man z.B. die Daten eines PHANTOMs mit 1000 kHz abfragen, um eine Grafik-
Anwendung zu steuern, die mit 60 Hz läuft. Würde man das PHANTOM auch nur 60-mal
pro Sekunde abfragen, so würde kleine Bewegung wahrscheinlich gar nicht registriert werden
oder es könnte z.B. Tastendrücke vollständig verloren gehen.
Im Falle einer langwierigen Initialisierung eines Servers, kann dieser einfach laufen gelassen
werden und die Anwendung kann sich dann sofort mit ihm verbinden.
Einige Eingabegeräte benötigen eine ständige Verbindung zum Server oder müssen mit einer
bestimmten Frequenz abgefragt werden.
Zum einfachen Testen können ein VRPN-Server und der zugehörige Client auch auf
demselben Rechner in zwei separaten Prozessen gestartet werden. Werden der Server und
der Client in ein und demselben Prozess gestartet, so werden Ereignisnachrichten direkt an
die dafür vorgesehene Callback-Funktion des Clients übermittelt und gehen nicht über das
(in diesem Fall nur auf den lokalen Rechner beschränkte) Netzwerk. [Tay01]

3.4. Logging
VRPN verfügt über eine Logging-Funktion, die alle Nachrichten in einer Datei speichern
kann. Dabei kann sowohl auf Server- als auch auf Client-Seite mitgeschnitten werden. Ein
Client kann sich mit einer bestehenden Log-Datei verbinden und diese Daten anstelle einer
normalen Serververbindung verarbeiten. Somit ist es möglich, aufgezeichnete Bewegungen /
Benutzereingaben erneut, als eine Art Replay-Funktion, zu verwenden. Es ist dafür keine
Codeänderung nötig.
Weitere Informationen sind unter [VRP08] zu finden.

4. VRPN benutzen
4.1. Der allgemeine VRPN-Server
VRPN liegt ein universeller Server mit dem Namen vrpn_server.exe bei. Ab Version 7.14
wird auch der 3DConnexion SpaceNavigator und SpacePilot unterstützt. Um den VRPN-
Server mit dem SpaceNavigator zu nutzen, öffnet man die vrpn.cfg und entfernt das
Kommentarzeichen in der Zeile #vrpn_3DConnexion_Navigator device0. Man braucht
optional nur noch den Namen von device0 in z.B. SpaceNav ändern und kann dann den
Server starten. Der kompilierte Server befindet sich im Release-Verzeichnis des Projektes.

11
4.2. Einen eigenen Server schreiben
Nachfolgend wird ein einfacher Server für den 3DConnexion SpaceNavigator Schritt für
Schritt erstellt.
In VRPN ist bereits eine Klasse enthalten, die die Funktionalität des SpaceNavigators in
VRPN implementiert. Die Klasse ist in der Datei vrpn_3DConnexion.h definiert und
implementiert die VRPN-Schnittstellen Button und Analog.
Als erstes wird die Headerdatei eingebunden. Und es folgt die Deklaration zweier globaler
Variablen, die Netzwerkverbindung und eine Instanz der SpaceNavigator-Klasse.
#include <vrpn_3DConnexion.h>

vrpn_Connection *connection;
vrpn_3DConnexion_Navigator *nav;

Vor der main()-Funktion wird eine Shutdown-Funktion definiert, die den Referenzzähler der
Verbindung um eins verringert:
void shutdown()
{
if(connection)
connection->removeReference(); connection = NULL;
}

Nun folgt die main()-Funktion. Als erstes wird der Port definiert, über den die
Netzwerkverbindung hergestellt werden soll. Dabei kann die Konstante
vrpn_DEFAULT_LISTEN_PORT_NO benutzt werden, die von VRPN aktuell mit dem Wert 3883
definiert wird. Dieser Port wurde VRPN von der Internet Assigned Numbers Authority
zugewiesen.
int port = vrpn_DEFAULT_LISTEN_PORT_NO;

Nun wird die Verbindung mit dem zuvor festgelegten Port erzeugt.
connection = new vrpn_Connection(port);

Anschließend erstellt man die Klasseninstanz des SpaceNavigators:


if((nav = new vrpn_3DConnexion_Navigator("Gerätename@Computername", connection)) ==
NULL)
{
printf("Fehler: kann Gerät nicht erstellen\n");
return -1;
}

Hierbei übergibt man dem Klassenkonstruktor einen String, der einen frei wählbaren
Gerätnamen sowie den Namen des Computers, auf dem der Server läuft beinhaltet, z.B.
„SpaceNav@viswork01“. Als zweiter Parameter wird die eben erstellte Verbindung
übergeben.
Nun kann die Hauptschleife des Servers betreten werden, in der die Hauptschleifen der
SpaceNavigator-Klasse und der Verbindung aufgerufen werden sowie die Anwendung bei
Auftreten eines Fehlers heruntergefahren wird:
while (true)
{
nav->mainloop();
connection->mainloop();
if(!connection->doing_okay())

12
shutdown();
}
Shutdown();

In den Schleifen werden alle Ereignisse verarbeitet und an evtl. verbundene Clients über das
Netzwerk gesendet.
Der komplette Quellcode befindet sich im Projektordner SpaceNavigator/
SpaceNavigatorServer.
Der Server führt die Hauptschleife so oft wie möglich aus. Das führt dazu, dass die
Prozessorbelastung stark steigt, so dass ein Kern des Prozessors voll ausgelastet wird. Ein
Experimentieren mit der Funktion vrpn_SleepMsecs(double dMsecs), die dafür sorgt, dass
die Hauptschleife nur alle dMsecs ausgeführt wird, führte immer zu falschen Ausgaben des
SpaceNavigators (Werte springen hin und her). Für die endgültige Benutzung des
SpaceNavigators ist daher der Einsatz des allgemeinen VRPN-Servers zu empfehlen. Bei
diesem kann über die Kommandozeile der Parameter -millisleeep übergeben werden, wobei
sich Werte bis 5 bewährt haben. Der im Release-Verzeichnis enthaltene Server
(vrpn_server.exe) ist bereits standardmäßig auf 5 Millisekunden eingestellt und muss nicht
unbedingt mit dem Parameter gestartet werden.

4.3. Eine einfache Client-Anwendung


Nun wird eine Client-Anwendung entwickelt, die sich mit dem eben besprochenem Server
verbindet und die vom SpaceNavigator gesendeten Daten auf der Konsole ausgibt.
Wie bereits beschrieben, verbindet sich die Client-Anwendung nicht mit einem
SpaceNavigator als solchem, sondern mit abstrahierten Eingabetypen. Im Falle des
SpaceNavigators, der über 6 Achsen (Typ Analog mit 6 Werten) und 2 Knöpfe (Typ Button
mit 2 Einträgen) verfügt, mit einem Analog- und einem Button-Objekt. Zur Nutzung dieser
Objekte bindet man zuerst die entsprechenden Header ein:
#include "vrpn_Button.h"
#include "vrpn_Analog.h"

In der main()-Funktion wird zuerst eine Instanz der vrpn_Button_Remote-Klasse erzeugt. Als
Parameter wird derselbe String übergeben, wie auch im Server bei der Erstellung der
SpaceNavigator-Klasse angegeben wurde, also z.B. „SpaceNav@viswork01“.
vrpn_Button_Remote *button = new vrpn_Button_Remote("SpaceNav@viswork01");

Anschließend wird eine Callback-Funktion für den Button registriert. Diese wird immer
aufgerufen, wenn eine Nachricht vom Server über einen veränderten Zustand eines Buttons
eintrifft:
button->register_change_handler(NULL, (vrpn_BUTTONCHANGEHANDLER)handleButtons);

Die Callback-Funktion hat nun den Namen handleButtons.


Das Gleiche wird nun noch einmal für die vrpn_Analog_Remote-Klasse durchgeführt:
vrpn_Analog_Remote *analog = new vrpn_Analog_Remote("SpaceNav@viswork01");
analog->register_change_handler(NULL, (vrpn_ANALOGCHANGEHANDLER)handleAnalogs);

Anschließend werden die Hauptschleifen der beiden Eingabeobjekte immer wieder


aufgerufen:
while (1)
{

13
button->mainloop();
analog->mainloop();
}

Nun werden die Callback-Funktionen definiert. Da die Button-Callback-Funktion vom Typ


vrpn_BUTTONCHANGEHANDLER ist, hat sie folgende Signatur:

void CALLBACK handleButtons(void *, vrpn_BUTTONCB buttonData)

In der Funktion steht nun über den Parameter buttonData der Zustand des aktuell
veränderten Buttons bereit. buttonData.button enthält die Buttonnummer und
buttonData.state enthält den Zustand. Dabei steht 1 für gedrückt und 0 für nicht gedrückt.
Diese Information kann nun auf der Konsole ausgegeben werden:
printf("Button: %d, Button state: %d\n\n", buttonData.button, buttonData.state);

Die Analog-Callback-Funktion hat folgende Signatur:


void CALLBACK handleAnalogs(void *, vrpn_ANALOGCB analogData)

In analogData stehen nun Informationen zur Anzahl der Werte (analogData.numChannels)


sowie die Werte selber (analogData.channel[]) zur Verfügung. Man kann nun in einer
Schleife alle Werte auslesen und auf der Konsole ausgeben:
for(int i = 0; i < analogData.num_channel; i++)
printf("Channel %d: %f\n", i, analogData.channel[i]*1000);
printf("\n");

Die Werte liegen dabei im Bereich [-1, 1] und werden in diesem Beispiel auf den Bereich [-
1000, 1000] skaliert.
Der komplette Quellcode ist im Projektordner SpaceNavigator/SpaceNavigatorClient zu
finden.

4.4. SpaceNavigator-Klasse
Aufbauend auf dem vorgestellten VRPN-Client wird eine SpaceNavigator-Klasse entwickelt.
Diese soll folgende Anforderungen erfüllen:
1. Implementierung als VRPN-Client
2. Implementierung mithilfe des Singleton-Entwurfsmusters
3. Initialisierung und Aufbauen einer Verbindung zum VRPN-Server
a. Setzen der nach oben zeigenden Achse (Y – normal, Z – für Anwendungen aus
den Geowissenschaften)
4. Bereitstellen des aktuellen Zustands des SpaceNavigators
a. Bereitstellen der Translationswerte
b. Bereitstellen der Rotationswerte
c. Bereitstellen des Zustands der Buttons
5. Verschiedene Funktionsmodi:
a. Translation und Rotation
b. Nur Translation
c. Nur Rotation
6. Achsenbeschränkung
a. Nur Achse mit höchstem Ausschlag relevant
b. Kombinierbar mit den Funktionsmodi
7. Invertierung der Achsen
8. Standard-Buttonbelegung

14
Folgendes Listing zeigt die Headerdatei der SpaceNavigatorClient-Klasse:
#pragma once

#include <vrpn_Button.h>
#include <vrpn_Analog.h>

class SpaceNavigatorClient
{
public:
enum SpaceNavigatorMode
{
TRANSROT = 0,
TRANS,
ROT
};

enum SpaceNavigatorAxes
{
X = 1,
Y,
Z,
rX,
rY,
rZ
};

bool buttons[8];
float x, y, z;
float rx, ry, rz;

static SpaceNavigatorClient* Instance();


void init(char *deviceNamem, SpaceNavigatorAxes axis = Y);
void mainloop();
void setDomination(bool dominating);
void switchDomination();
void setMode(SpaceNavigatorClient::SpaceNavigatorMode mode);
void switchMode();
void setDefaultButtonBehaviour(bool enable);
void invertAxis(SpaceNavigatorAxes axisToInvert);

protected:
SpaceNavigatorClient();
~SpaceNavigatorClient();

private:
vrpn_Button_Remote *_button;
vrpn_Analog_Remote *_analog;
SpaceNavigatorAxes _upAxis;
bool _dominating;
SpaceNavigatorMode _mode;
bool _defaultButtonBehaviour;
float _invertAxes[6];
static SpaceNavigatorClient* _spacenavigator;

static void CALLBACK _handleButtons(void *, vrpn_BUTTONCB buttonData);


static void CALLBACK _handleAnalogs(void *, vrpn_ANALOGCB analogData);
};

15
4.4.1. Implementierung als VRPN-Client
Analog zum VRPN-Client-Beispiel verfügt die Klasse über jeweils ein vrpn_Button_Remote-
und ein vrpn_Analog_Remote-Objekt sowie über die zugehörigen Callback-Funktionen, die
immer aufgerufen werden, wenn der Client eine neue Nachricht vom Server über einen
veränderten Zustand des Eingabegerätes erhält.

4.4.2. Implementierung mithilfe des Singleton-Entwurfsmusters


Man kann nur ein SpaceNavigator-Eingabegerät über VRPN ansprechen, wodurch sich die
Implementierung der Klasse nach dem Singleton-Entwurfsmuster anbietet. Ein Singleton
stellt sicher, dass von einer Klasse nur eine Instanz erzeugt werden kann und stellt einen
globalen Zugriff auf diese sicher (siehe auch [Gam95] ).
Um das Entwurfsmuster zu implementieren, muss der Konstruktor als protected deklariert
werden und die Erzeugung der Klasse in eine öffentliche, statische Funktion verlagert
werden. Ein Zugriff auf die Klasse erfolgt immer über diese Funktion. Die Funktion gibt
einen Zeiger auf die Instanz der Klasse zurück. Wurde die Klasse noch nicht instanziiert, so
wird der Konstruktor aufgerufen.
SpaceNavigatorClient* SpaceNavigatorClient::Instance()
{
if(_spaceNavigator == 0)
_spaceNavigator = new SpaceNavigatorClient();

return _spaceNavigator;
}

4.4.3. Initialisierung und Aufbauen einer Verbindung zum VRPN-Server


Mithilfe der init()-Funktion wird die Verbindung zum VRPN-Server aufgebaut. Man
übergibt der Funktion als ersten Parameter den Verbindungsstring, der auch im Server
angegeben wurde, „Gerätname@Rechnername“, z.B. „SpaceNav@viswork01“. Innerhalb der
Funktion werden die Analog- und Button-Objekte wie im vorherigen Beispiel erzeugt und die
zugehörigen Callback-Funktionen registriert. Als optionaler Parameter kann die nach oben
zeigende Achse auch auf die Z-Achse gesetzt werden. Wird dieser Parameter nicht
angegeben, so zeigt die Y-Achse nach oben.
void init(char *deviceName, SpaceNavigatorAxes axis)
{
button = new vrpn_Button_Remote(deviceName);
button->register_change_handler(NULL,
(vrpn_BUTTONCHANGEHANDLER)SpaceNavigatorClient::handleButtons);

analog = new vrpn_Analog_Remote(deviceName);


analog->register_change_handler(NULL,
(vrpn_ANALOGCHANGEHANDLER)SpaceNavigatorClient::handleAnalogs);

if(axis == Z)
upAxis = axis;
else
upAxis = Y;
}

16
4.4.4. Bereitstellen des aktuellen Zustands des SpaceNavigators
Der aktuelle Zustand des SpaceNavigators wird in folgenden öffentlichen Variablen
Variabl
gespeichert:
bool buttons[2];
float x, y, z;
float rx, ry, rz;

Die Werte werden in den Callback-Funktionen


Callback Funktionen aktualisiert. Folgendes Listing zeigt die
Button-Callback-Funktion:
void CALLBACK handleButtons(void *, vrpn_BUTTONCB buttonData)
{
>buttons[buttonData.button] = (bool)buttonData.state;
spacenavigator->buttons[buttonData.button]


}

Bewegung des Space Navigators

vrpn_ANALOGCB analogData
channel[0] channel
channel[2] channel[1] channel[3] channel[5] channel[4]

SpaceNavigatorClient
x y z rx ry rz

SpaceNavigatorClient::
SpaceNavigatorClient::invertAxes[]
normal invertiert normal normal invertiert normal

Abbildung 4 - Die Verarbeitung der Space Navigator-Eingaben


Navigator Eingaben

In der Analog-Callback-Funktion
Funktion werden ebenso die einzelnen Werte in den öffentlichen
Variablen gespeichert:
void CALLBACK handleAnalogs(void *, vrpn_ANALOGCB analogData)
{
spacenavigator->x
>x = analogData.channel[0] * spacenavigator->invertAxes[0];
>invertAxes[0];
spacenavigator->y
>y = analogData.channel[2] * spacenavigator->invertAxes[1];
spacenavigator >invertAxes[1];
spacenavigator->z
>z = analogData.channel[1] * spacenavigator->invertAxes[2];
spacenavigator >invertAxes[2];
spacenavigator->rx
>rx = analogData.channel[3] * spacenavigator->invertAxes[3];
spacenavigator >invertAxes[3];
spacenavigator->ry
>ry = analogData.channel[5] * spacenavigator->invertAxes[4];
spacenavigator >invertAxes[4];
spacenavigator->rz
>rz = analogData.channel[4] * spacenavigator->invertAxes[5];
spacenavigator >invertAxes[5];
}

17
In der nichtöffentlichen Variable invertAxes ist für jede Achse gespeichert, ob sie invertiert
werden soll. In Abbildung 4 wird die Zuordnung der Space Navigator-Achsen verdeutlicht.

4.4.5. Verschiedene Funktionsmodi


Es gibt 3 verschiedene Funktionsmodi:
• Translation und Rotation
• Nur Translation
• Nur Rotation
Der erste Modus entspricht dem Standardverhalten, wie es bereits im vorigen Abschnitt in
der Callback-Funktion implementiert wurde.
Der Translations-Modus entspricht ebenfalls dem ersten Modus, nur dass die Rotationswerte
nicht ausgelesen werden sondern diese auf 0 gesetzt werden:
spacenavigator->x = analogData.channel[0] * spacenavigator->invertAxes[0];
spacenavigator->y = analogData.channel[2] * spacenavigator->invertAxes[1];
spacenavigator->z = analogData.channel[1] * spacenavigator->invertAxes[2];
spacenavigator->rx = spacenavigator->ry = spacenavigator->rz = 0;

Der Rotationsmodus verhält sich vergleichbar:


spacenavigator->rx = analogData.channel[3] * spacenavigator->invertAxes[3];
spacenavigator->ry = analogData.channel[5] * spacenavigator->invertAxes[4];
spacenavigator->rz = analogData.channel[4] * spacenavigator->invertAxes[5];
spacenavigator->x = spacenavigator->y = spacenavigator->z = 0;

Es ist standardmäßig der erste Modus aktiv. Der aktive Modus kann mit den Funktionen
switchMode() und setMode(SpaceNavigatorMode mode) gesetzt werden.

4.4.6. Achsenbeschränkung
Es ist möglich, die Werte des Navigators auf eine Achse zu beschränken. Es wird dann nur
die Achse betrachtet, die den höchsten Absolutwert hat. Damit kann man genauer mit dem
SpaceNavigator navigieren. Die Funktionen switchDomination() und setDomination(bool
dominating) schalten diese Funktion an und aus. Am Beispiel des Standard-Funktions-
Modus (Translation und Rotation) wird im Folgenden die Implementierung der
Achsenbeschränkung gezeigt:
if(spacenavigator->dominating)
{
float max = analogData.channel[0];
int index = 0;
for(int i = 1; i < 6; i++)
{
if(abs(analogData.channel[i]) > abs(max))
{
index = i;
max = analogData.channel[i];
}
}
spacenavigator->x = spacenavigator->y = spacenavigator->z = 0;
spacenavigator->rx = spacenavigator->ry = spacenavigator->rz = 0;

switch(index)
{
case 0: spacenavigator->x = max * spacenavigator->invertAxes[0]; break;

18
case 2: spacenavigator->y = max * spacenavigator->invertAxes[1]; break;
case 1: spacenavigator->z = max * spacenavigator->invertAxes[2]; break;
case 3: spacenavigator->rx = max * spacenavigator->invertAxes[3]; break;
case 5: spacenavigator->ry = max * spacenavigator->invertAxes[4]; break;
case 4: spacenavigator->rz = max * spacenavigator->invertAxes[5]; break;
}
}

Die Implementierung für die anderen beiden Modi ist ähnlich.

4.4.7. Invertierung der Achsen


In der nichtöffentlichen Variable invertAxes werden die zu invertierenden Achsen
gespeichert. Standardmäßig werden die Y-Achse und die Rotation um die Y-Achse invertiert,
um ein Verhalten der SpaceNavigator-Steuerung analog zu der im SpaceNavigator-Handbuch
zu erreichen. Dies bleibt dem Benutzer der Klasse aber verborgen. Er benutzt nur die
Funktion invertAxis(SpaceNavigatorAxis axisToInvert) und gibt dabei als Parameter die zu
invertierende Achse ein. Er muss sich nicht darum kümmern, ob eine Achse bereits invertiert
wurde, was der folgende Quellcode zeigt:
void SpaceNavigatorClient::invertAxis(SpaceNavigatorAxes axisToInvert)
{
if(axisToInvert < 7 && axisToInvert > 0)
{
if(invertAxes[axisToInvert - 1] == 1.f)
invertAxes[axisToInvert - 1] = -1.f;
else
invertAxes[axisToInvert -1] = 1.f;
}
}

4.4.8. Standard-Buttonbelegung
Die beiden Knöpfe des SpaceNavigators sind mit folgenden Funktionen belegt:
• Linker Knopf: Wechseln des Funktionsmodus
• Rechter Knopf: Wechseln der Achsenbeschränkung
Diese Belegung kann mit der Funktion setDefaultButtonBehaviour(bool enable) aus- und
eingeschaltet werden. Die Belegung wird in der Button-Callback-Funktion implementiert:
if(spacenavigator->defaultButtonBehaviour)
{
if(spacenavigator->buttons[0])
spacenavigator->switchMode();

if(spacenavigator->buttons[1])
spacenavigator->switchDomination();
}

Der komplette Quellcode der SpaceNavigator-Klasse ist im Projektordner SpaceNavigator/


SpaceNavigatorClient zu finden.

4.4.9. Die Klasse benutzen


Um die Klasse zu benutzen muss im Quellcode die zugehörige Headerdatei eingebunden
werden:
#include "../SpaceNavigator/SpaceNavigatorClient.h"

19
Außerdem benötigt man zum Zugriff auf die Objektinstanz der Klasse einen Zeiger:
SpaceNavigatorClient* spaceNavigator;

Die statische Funktion SpaceNavigatorClient::Instance() gibt einen Zeiger auf die


SpaceNavigator-Klasse zurück und die Initialisierungs-Funktion muss aufgerufen werden:
spaceNavigator = SpaceNavigatorClient::Instance();
spaceNavigator->init("SpaceNav@viswork01");

Bevor man auf die Werte des SpaceNavigators zugreifen kann, muss erst die Hauptschleife
der Klasse ausgeführt werden, z.B. in der GLUT-Display-Funktion:
spaceNavigator->mainloop();

Anschließend kann auf die aktuellen Werte über die öffentlichen Variablen der Klasse
zugegriffen werden, z.B.:
float bewegungXAchse = spaceNavigator->x;

Alternativ kann man auch immer über die statische Instance()-Funktion auf die Klasse
zugreifen. In diesem Fall benötigt man keinen Zeiger auf die Klasse.
float bewegungXAchse = SpaceNavigatorClient::Instance()->x;

4.4.10. Beispielanwendung: Bewegen und Drehen eines Würfels in


OpenSG
In diesem Abschnitt wird eine einfache OpenSG-Anwendung entwickelt, in der man einen
Würfel mithilfe des SpaceNavigators verschieben und rotieren kann. Es wird der
SimpleSceneManager, eine Hilfsklasse aus OpenSG, genutzt, um die Anwendung mit wenig
Code zu erstellen. Der SimpleSceneManager kümmert sich um Dinge wie das Erzeugen und
Positionieren der Kamera, das Erzeugen des Viewports, die Behandlung von Tastendrücken
und Mauseingaben.
Am Anfang des Quellcodes werden alle benötigten Header eingebunden, dazu zählt auch der
Header der erstellten SpaceNavigator-Klasse:
#include <OpenSG/OSGConfig.h>
#include <OpenSG/OSGGLUT.h>
#include <OpenSG/OSGSimpleGeometry.h>
#include <OpenSG/OSGGLUTWindow.h>
#include <OpenSG/OSGSimpleSceneManager.h>

#include "../SpaceNavigator/SpaceNavigatorClient.h"

Um nicht immer den Namensraum osg:: vor jedes OpenSG-Objekt schreiben zu müssen,
verwendet man den OpenSG-Namensraum:
OSG_USING_NAMESPACE

Nun werden die globalen Variablen deklariert. Es wird ein Zeiger auf die Instanz der Space-
Navigator-Klasse, auf den SimpleSceneManager sowie auf ein Transform-Objekt, indem
später die Transformation des Würfels gespeichert wird, angelegt.

20
SpaceNavigatorClient* spaceNavigator;
SimpleSceneManager* mgr; Node n
TransformPtr tMain;

Die Setup-Funktion
Funktion von GLUT muss vor den anderen Group::create()
Funktionen des Programms deklariert werden.
int setupGLUT(int *argc, char *argv[]); Node
mainTrans
Anschließend wird in der createScene()-Funktion der
Würfel mitsamt zugehöriger Transformation erstellt. Transform tMain
Die Funktion gibt einen NodePtr zurück, an den die
Transformation sowie der Würfel angehängt sind.
Matrix m
Zuerst wird eine Matrix-Variable
Variable angelegt:
NodePtr createScene(void)
Node box
{
Matrix m;

Anschließend wird die Box-Geometrie


Box mit der Geometry makeBox

OpenSG-Funktion makeBox() erzeugt.


Abbildung 5 - Der Aufbau der Szene
NodePtr box = makeBox(20, 20, 20, 1, 1, 1);

Für die globale TransformPtr-Variable wird ein Transform-Objekt


Objekt erzeugt und dessen
Transformationsmatrix als Identitätsmatrix initialisiert:
tMain = Transform::create();
beginEditCP(tMain, Transform::MatrixFieldMask);
m.setIdentity();
tMain->setMatrix(m);
endEditCP(tMain, Transform::MatrixFieldMask);

Es wird ein weiterer Knoten erzeugt,


erzeugt an den der Knoten
en mit der Box angehangen wird und
dessen Core auf das globale Transform-Objekt
Transform zeigt.
NodePtr mainTrans = Node::create();
beginEditCP(mainTrans, Node::CoreFieldMask | Node::ChildrenFieldMask);
mainTrans->setCore(tMain);
>setCore(tMain);
mainTrans->addChild(box);
>addChild(box);
endEditCP(mainTrans,
EditCP(mainTrans, Node::CoreFieldMask
Node::CoreFieldMask | Node::ChildrenFieldMask);

Nun erzeugt man den Wurzelknoten des Szenengraphen und hängt den zuletzt erzeugten
Knoten als Kind an.
NodePtr n = Node::create();
beginEditCP(n, Node::CoreFieldMask | Node::ChildrenFieldMask);
Node::ChildrenFieldMa
n->setCore(Group::create());
>setCore(Group::create());
n->addChild(mainTrans);
>addChild(mainTrans);
endEditCP(n, Node::CoreFieldMask
Node::CoreFieldM | Node::ChildrenFieldMask);

Dieser Wurzelknoten wird von der Funktion zurückgegeben:


return n;
}

Damit ist der Szenengraph (siehe Abbildung 5) vollständig erstellt. Wie bereits erwähnt,
kümmert sich der SimpleSceneManager um die Erstellung und Positionierung einer Kamera.
In der main()-Funktion
Funktion wird als erstes das SpaceNavigator-Objekt
SpaceNavigator Objekt erstellt und initialisiert.
Zur Initialisierung muss der Name des SpaceNavigators auf dem Server und der
Rechnername des Servers als Kommandozeilen-Parameter
Parameter übergeben werden:
werden
21
int main(int argc, char **argv)
{
spaceNavigator = SpaceNavigatorClient::Instance();
spaceNavigator->init(connectionString);

char *connectionString;
if(argc > 1)
connectionString = argv[1];
spaceNavigator->init(connectionString);

osgInit(argc, argv);

int winid = setupGLUT(&argc, argv);


GLUTWindowPtr gwin = GLUTWindow::create();
gwin->setId(winid);
gwin->init();

Die createScene()-Funktion wird aufgerufen.


NodePtr scene = createScene();

Der SimpleSceneManager wird erzeugt. An ihn werden das GLUT-Fenster sowie der
Wurzelknoten der Szene übergeben. Die Funktion showAll() positioniert die Kamera
automatisch so, dass der Würfel sichtbar ist.
mgr = new SimpleSceneManager;
mgr->setWindow(gwin);
mgr->setRoot(scene);
mgr->showAll();
mgr->setStatistics(true);

Schließlich startet man die GLUT-Hauptschleife und übergibt somit GLUT die Kontrolle über
die Anwendung.
glutMainLoop();

return 0;
}

Man kann jedoch Callback-Funktionen definieren, die von GLUT bei speziellen Ereignissen
aufgerufen werden. Die Display-Funktion ist hierbei sicherlich die wichtigste. In dieser kann
die Szene verändert und schließlich gerendert werden. In dieser Anwendung wird also die
Box über den TransformPtr mithilfe der Eingaben des SpaceNavigator bewegt und rotiert und
am Ende gerendert. In der Display-Funktion wird erst die Hauptschleife des SpaceNavigators
ausgeführt:
void display(void)
{
spaceNavigator->mainloop();

Es werden einige Variablen für die folgenden Anweisungen angelegt. Über die beiden
Gleitkommavariablen transFactor und rotFactor kann die Sensitivität gesteuert werden.
Matrix t, r, old;
float transFactor = 1.f;
float rotFactor = 0.05f;
Vec3f oldTrans, oldScale;
Quaternion oldRot, oldScaleRot;

22
Die bisherige Transformation des Würfels wird mithilfe der Matrix-Funktion getTransform()
in den Hilfsvariablen gespeichert. Diese Transformation ist in der globalen Transform-
Variablen gespeichert.
tMain->getMatrix().getTransform(oldTrans, oldRot, oldScale, oldScaleRot);

In der Matrix old wird nun eine Transformation erzeugt, die der alten Translation
entspricht.
old.setTransform(oldTrans);

In der Matrix t wird die neue Translation erzeugt. Dabei werden die Werte des
SpaceNavigators ausgelesen und mit dem Transformationsfaktor multipliziert:
t.setTranslate(spaceNavigator->x * transFactor, spaceNavigator->y * transFactor,
spaceNavigator->z * transFactor);

Nun werden die neuen Rotationen erzeugt. Dazu wird jeweils ein Quaternion erzeugt,
welches die Rotation um eine Achse darstellt und auf den jeweiligen Rotationswert des
SpaceNavigators zurückgreift. Entsprechend fließt hier der Rotationsfaktor mit ein. Die drei
Quaternions für die drei Rotationen werden miteinander multipliziert, was einer Verkettung
der einzelnen Rotationen entspricht.
Quaternion rot=Quaternion(Vec3f(0,0,1), (3.14159*spaceNavigator->rz)*rotFactor);
rot.mult(Quaternion(Vec3f(0,1,0), (3.14159 * spaceNavigator->ry ) * rotFactor));
rot.mult(Quaternion(Vec3f(1,0,0), (3.14159 * spaceNavigator->rx ) * rotFactor));

Die resultierende Rotation wird mit der alten Rotation multipliziert und in der Matrix r wird
die finale Rotationsmatrix erzeugt:
rot.mult(oldRot);
r.setRotate(rot);

Die Transformationsmatrizen werden miteinander multipliziert was ebenso einer Verkettung


der Transformationen entspricht. Die Matrizen müssen folgendermaßen multipliziert
werden: alte Translation * Translation * Rotation.
old.mult(t);
old.mult(r);

Schließlich wird die Transformationsmatrix des Transform-Objekts der Box auf die finale
Transformationsmatrix gesetzt:
beginEditCP(tMain);
tMain->setMatrix(old);
endEditCP(tMain);

Natürlich wird die Szene mithilfe des SimpleSceneManagers gerendert.


mgr->redraw();
}

Nun muss nur noch die anfangs erwähnte setupGLUT()-Funktion implementiert werden. Die
GLUT-Initialisierung wird aufgerufen und der Displaymodus festgelegt.
int setupGLUT(int *argc, char *argv[])
{
glutInit(argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE);

Mit glutCreateWindow() wird das eigentliche Programmfenster erzeugt. Als Parameter wird
der Fenstername übergeben.

23
int winid = glutCreateWindow("OpenSG
First App");

Abschließend wird die Display-Funktion als


GLUT-Callback registriert und die GLUT-Idle-
CALLBACK-Funktion ebenfalls auf die Display-
Funktion gesetzt.
glutDisplayFunc(display);
glutIdleFunc(display);

return winid;
}

In dem Programm kann man nun den Würfel


mithilfe des SpaceNavigators bewegen und
rotieren. Die Transformation erfolgt dabei
immer relativ zum ursprünglichen OpenSG-
Koordinatensystem. Abbildung 6 - Die Beispielanwendung zum
Drehen eines Würfels
Das Programm muss über die Kommandozeile
in folgender Weise gestartet werden:
SpaceNavigatorTestObjectMode.exe SpaceNav@Rechnername

Der vollständige Quellcode ist im Projektordner SpaceNavigator/SpaceNavigator-


TestObjectMode zu finden.

4.5. Der SpaceNavigatorSSM


Um die Funktionalität der SpaceNavigator-Klasse schnell und einfach in neuen
Anwendungen nutzen zu können, wird der in OpenSG enthaltene SimpleSceneManager
abgeleitet und mit neuer Funktionalität erweitert, der SpaceNavigatorSSM. Man kann ihn
anstatt des normalen Scene Managers verwenden.
Der SpaceNavigatorSSM soll folgende Funktionen implementieren:
1. Abgeleitet von SimpleSceneManager  bereits bestehende Anwendungen, die den
SimpleSceneManager nutzen, können ohne großen Aufwand mit der neuen
Funktionalität versehen werden.
2. Auswählen der Hoch-Achse: wahlweise Y- (Standard) oder Z-Achse
3. Kamerasteuerung mit fester Höhe über Grund (frameratenunabhängig)
4. Auswählen von Objekten per Mausklick
5. Ausgewählte Objekte können mit dem SpaceNavigator bewegt und rotiert werden (die
Kamera ist in diesem Moment fest)
6. Standardmäßig wird der WalkNavigator benutzt, um sich auf einer spezifizierten
Oberfläche bewegen zu können (die Funktionalität des TrackballNavigators bleibt
aber weiterhin erhalten, man kann zwischen beiden umschalten)
7. Performanceoptimierte Bewegung auf einer Oberfläche mit der Klasse Triangle-
ElevationGrid

4.5.1. Ableiten von SimpleSceneManager


Der SpaceNavigatorSSM ist vom OpenSG SimpleSceneManager öffentlich abgeleitet, d.h. er
erbt alle Funktionen und Variablen.
24
class SpaceNavigatorSSM : public osg::SimpleSceneManager

Es werden hauptsächlich neue Funktionen hinzugefügt, aber auch Funktionen überschrieben


wie setRoot(), showAll(), mouseButtonPress() und initialize().
Neue Variablen werden als private Felder deklariert und sind nur durch Get- und Set-
Methoden zugänglich. Die Headerdatei des SpaceNavigatorSSM verdeutlicht den Aufbau der
Klasse:
#pragma once

#include <OpenSG/OSGGLUT.h>
#include <OpenSG/OSGSimpleSceneManager.h>
#include <OpenSG/OSGSimpleMaterial.h>
#include <OpenSG/OSGMaterialGroup.h>
#include <OpenSG/OSGComponentTransform.h>
#include "../SpaceNavigator/SpaceNavigatorClient.h"
#include "../TriangleElevationGrid/TriangleElevationGrid.h"

OSG_USING_NAMESPACE

class SpaceNavigatorSSM : public SimpleSceneManager


{
public:
SpaceNavigatorSSM(char *deviceName, bool zUpAxis = false);
void setRoot(osg::NodePtr root);
void showAll(void);
SpaceNavigatorClient* getSpaceNavigator();
void updateCameraMovement();
void setDefaultButtonBehaviour(bool enable);
void setHeightControl(bool enable);
void switchHeightControl();
void setObjectPicking(bool enable);
void switchObjectPicking();
void setTranslationFactor(float factor);
void setRotationFactor(float factor);
void setCameraPosition(osg::Pnt3f position);
void mouseButtonPress(osg::UInt16 button, osg::Int16 x, osg::Int16 y);
void initWalkNavGroundCollision(NodePtr groundNode);
void initElevationGrid(char *gridFile, int numXCells = 10, int numYCells = 10);
void setGroundDistance(float distance);
NodePtr getNodeByName(NodePtr rootNode, const Char8 *nodeName);

protected:
void SpaceNavigatorSSM::initialize();

private:
SpaceNavigatorClient _spaceNavigator;
float _translationFactor;
float _rotationFactor;
bool _zUpAxis;
bool _heightControl;
float _groundDistance;
bool _objectPicking;
osg::NodePtr _pickedObjectNode;
int _elapsedTime;
CElevationGridBase *_grid;
Bool _useElevationGrid;
};

25
Die Variable _spaceNavigator ist die Instanz der Klasse SpaceNavigatorClient, sie bietet also
Zugriff auf das SpaceNavigator-Eingabegerät. Die Variable _rotationFactor ist eine
Gleitkommazahl, über die die Empfindlichkeit der Rotationseingaben festgelegt werden
kann. Analog wird _translationFactor für die Empfindlichkeit der Translation verwendet. In
_elapsedTime wird die seit dem Initialisieren des GLUT-Systems vergangene Zeit gespeichert.
Nun folgen einige Statusvariablen. In ihnen wird gespeichert, ob die Z-Achse die nach oben
zeigende Achse ist (_zUpAxis), ob der Abstand zum Boden gleichbleibend oder über eine
Achse des SpaceNavigators gesteuert werden kann (_heightControl) und ob man mit der
Maus Objekte selektieren kann (_objectPicking). Ein Verweis auf die Transformation des
ausgewählten Objektes wird in _pickedObjectNode gespeichert. Die aktuelle Höhe der
Kamera über dem Boden wird in der Variable _groundDistance gespeichert. In _grid wird
eventuell eine spezielle Struktur zur Kollisionserkennung gespeichert und die Variable
_useElevationGrid bestimmt, ob diese Struktur zur Kollisionsabfrage genutzt wird (siehe
auch 4.5.7).
Im Folgenden wird die eigentliche Implementierung der Klasse erklärt.
Dem Konstruktor wird der Verbindungsstring des SpaceNavigator-Eingabegerätes (so wie er
auch im SpaceNavigator-Server angegeben wurde) und optional, ob die Z-Achse als nach
oben zeigende Achse benutzt werden soll, übergeben.
SpaceNavigatorSSM::SpaceNavigatorSSM(char *deviceName, bool zUpAxis)
{

Entsprechend dem zUpAxis-Parameter wird der SpaceNavigator initialisiert und die private
Variable _zUpAxis gesetzt.
if(zUpAxis)
SpaceNavigatorClient::Instance()->init(deviceName, SpaceNavigatorClient::Z);
else
SpaceNavigatorClient::Instance()->init(deviceName);
_zUpAxis = zUpAxis;

Die restlichen Variablen werden ebenfalls initialisiert. Die Bodendistanz wird auf 2 und der
Rotationsfaktor auf 1 gesetzt. Die Höhe über dem Boden wird auf fest eingestellt und der
Objektauswahlmodus aktiviert:
_groundDistance = 2.0f;
_rotationFactor = _translationFactor = 1.f;
_heightControl = false;
_objectPicking = true;
_grid = NULL;
_useElevationGrid = false;
}

4.5.2. Auswählen der Hoch-Achse


Die Hoch-Achse wurde bereits im Konstruktor gesetzt. Das Koordinatensystem ist ein links-
händiges Koordinatensystem, wenn die Y-Achse die Hochachse ist. Ist die Z-Achse die
Hochachse, so handelt es sich um ein rechtshändiges Koordinatensystem (siehe Abbildung
7).

26
Abbildung 7 – links: OpenSG-Koordinatensystem (Y-Achse ist Hochachse), rechts: rechtshän-
diges Koordinatensystem mit Z-Achse als Hochachse

Die setRoot()-Methode, die vom SimpleSceneManager geerbt wird, wird überschrieben, um


die Hoch-Achse in der dem SimpleSceneManager zugehörigen Navigator-Klasse zu setzen.
void SpaceNavigatorSSM::setRoot(osg::NodePtr root)
{
this->initialize();

SimpleSceneManager::setRoot(root);

if(this->_zUpAxis)
this->getNavigator()->setUp(Vec3f(0, 0, 1));
}

Außerdem wird in der setRoot()-Methode die ebenfalls geerbte und überschrieben Methode
initialize() aufgerufen. Diese ruft einerseits die originale Initialize-Methode der
Basisklasse auf und setzt andererseits den Modus der Navigator-Klasse auf Navigator::WALK.
void SpaceNavigatorSSM::initialize()
{
SimpleSceneManager::initialize();

this->getNavigator()->setMode(Navigator::WALK);
}

Dieser WALK-Modus implementiert das „Laufen“ über einen festzulegenden Boden. Man
befindet sich mit der Kamera dann immer in einem festen Abstand zum Boden und kann sich
somit z.B. frei auf einer Landschaft bewegen und die Kamera folgt dabei dem Geländeprofil.
Die ebenfalls geerbte showAll()-Methode wird nur dahingehend abgeändert, dass nun auch
die Hoch-Achse berücksichtigt wird. Im Folgenden werden nur die Änderungen gezeigt.
void SpaceNavigatorSSM::showAll(void)
{

Real32 dist;
if(_zUpAxis)
dist = osgMax(d[0],d[2]) / (2 * osgtan(_camera->getFov() / 2.f));
else
dist = osgMax(d[0],d[1]) / (2 * osgtan(_camera->getFov() / 2.f));

Vec3f up = getNavigator()->getUp();
Pnt3f at;

27
if(_zUpAxis)
at = Pnt3f((min[0] + max[0]) * .5f,(min[2] + max[2])*.5f,(min[1]+max[1])*.5f);
else
at = Pnt3f((min[0] + max[0]) * .5f,(min[1] + max[1])*.5f,(min[2]+max[2])*.5f);

Pnt3f from=at;
if(_zUpAxis)
from[1]+=(dist+fabs(max[1]-min[1])*0.5f);
else
from[2]+=(dist+fabs(max[2]-min[2])*0.5f);

_navigator.set(from,at,up);


}

Die Variablen min und max beinhalten die Ausdehnung der Szene. Die Variable d ist die
Differenz von min und max.

4.5.3. Kamerasteuerung mit fester Höhe über Grund


Herzstück der Klasse ist die updateCameraAndMovement()-Methode, die die Kamerasteuerung
implementiert. Wenn diese Klasse in einem Programm benutzt wird, muss diese Methode
einmal pro Frame aufgerufen werden. Um die Kamerabewegung frameratenunabhängig zu
machen, wird die seit dem letzten Frame vergangene Zeit benötigt und mit deren Hilfe
skaliert. Das bedeutet, dass die Kamerabewegung immer mit der gleichen Geschwindigkeit
abläuft, egal, ob die Anwendung mit 20 oder mit 60 Bildern pro Sekunde läuft. Würde man
dies nicht berücksichtigen, so würde sich die Geschwindigkeit der Kamerabewegungen z.B.
mit steigender Bildrate erhöhen.
Die vergangene Zeit wird mithilfe der GLUT-Methode glutGet(GLUT_ELAPSED_TIME) und der
beim letzten Frame gespeicherten Zeit berechnet.
void SpaceNavigatorSSM::updateCameraMovement()
{
int newElapsedTime = glutGet(GLUT_ELAPSED_TIME);
int frameTime = (newElapsedTime - _elapsedTime); // in ms
_elapsedTime = glutGet(GLUT_ELAPSED_TIME);

Anhand der berechneten Zeit und der gespeicherten Faktoren wird die Empfindlichkeit der
Translation und Rotation des SpaceNavigators angepasst.
float transFactor = _translationFactor * (frameTime / 1000.0);
float rotFactor = _rotationFactor * (frameTime / 800.0);
float rotFactorObject = rotFactor * 3.0f;

Die SpaceNavigator-Hauptschleife muss natürlich auch einmal pro Frame aufgerufen


werden:
SpaceNavigatorClient* spaceNavigator = SpaceNavigatorClient::Instance();
spaceNavigator->mainloop();

Ein Zeiger auf den WalkNavigator der Basisklasse wird unter dem Namen wnav gespeichert:
WalkNavigator* wnav = _navigator.getWalkNavigator();

Anschließend wird zwischen dem WALK- und dem TRACKBALL-Modus unterschieden,


wobei im Falle des TRACKBALL-Modus nix weiter getan wird, denn diese Funktionalität ist
bereits in der Basisklasse implementiert.

28
switch(this->getNavigator()->getMode())
{
case Navigator::WALK:

case Navigator::TRACKBALL:

break;
}
}

Im WALK-Modus wird ebenso unterschieden, ob gerade ein Objekt ausgewählt ist (siehe
4.5.4) oder die Kamera mit den Eingaben des SpaceNavigators gesteuert wird.
if(_pickedObjectNode != NullFC)
{

}

else
{

}
break;

Zur Kamerabewegung wird auf Funktionalität des WalkNavigators zurückgegriffen. So kann


man mit rotate() die Kamera um 2 Achsen rotieren, mit forward() die Kamera vor und
zurück bewegen und mit right() die Kamera nach rechts und links bewegen. Der anfangs
berechnete Translations- oder Rotationsfaktor wird mit dem entsprechenden Wert des
SpaceNavigators multipliziert und das Ergebnis an eine dieser Funktionen übergeben.
Entsprechend der eingestellten Hoch-Achse werden die richtigen SpaceNavigator-Achsen in
den Funktionen verwendet:
if(_zUpAxis)
{
wnav->rotate(spaceNavigator->rz*-rotFactor,spaceNavigator->rx*-rotFactor);
wnav->forward(spaceNavigator->y * transFactor);
}
else
{
wnav->rotate(spaceNavigator->ry*-rotFactor,spaceNavigator->rx*-rotFactor);
wnav->forward(_spaceNavigator->z * transFactor);
}
wnav->right(_spaceNavigator->x * -transFactor);

Gegebenenfalls wird noch die Höhe der Kamera über dem Boden mithilfe der am
SpaceNavigator gemachten Eingabe aktualisiert. Hierbei werden die SpaceNavigator-Achsen
wieder entsprechend der Hoch-Achse verwendet. Außerdem wird sichergestellt, dass man
nicht unterhalb des Bodens gelangen kann:
if(_heightControl)
{
if(_zUpAxis)
_groundDistance += spaceNavigator->z * transFactor;
else
_groundDistance += spaceNavigator->y * transFactor;
if(_groundDistance < 0.1f) _groundDistance = 0.1f;
wnav->setGroundDistance(_groundDistance);
}
}

29
4.5.4. Auswählen von Objekten per Mausklick
Ein Objekt soll beim Drücken der linken Maustaste ausgewählt werden und beim Drücken
der rechten Maustaste soll die Auswahl beendet werden. Um das ausgewählte Objekt wird für
eine bessere Sichtbarkeit eine Umgebungsbox (bounding box) eingeblendet. Die von der
Basisklasse geerbte Methode mouseButtonPress() wird überschrieben. Diese Methode wird
immer aufgerufen wenn eine Maustaste gedrückt wurde.
void SpaceNavigatorSSM::mouseButtonPress(UInt16 button, Int16 x, Int16 y)
{

Wurde die linke Maustaste gedrückt und ist der Objektauswahlmodus aktiv, so wird ein
Strahl ausgehend vom angeklickten Pixel in die Szene gesendet und damit ein Intersection-
Objekt gebildet. Nun wird getestet, ob der Strahl mit einem Objekt in der Szene kollidiert.
Dazu wird die apply()-Methode aufgerufen und ihr der Wurzelknoten des Szenengraphen
übergeben. Wenn ein Objekt getroffen wurde wird ein Verweis darauf in der Variablen
_pickedObjectNode gespeichert.
switch (button)
{
case MouseLeft:
if(_objectPicking)
{
Line ray = calcViewRay(x, y);
IntersectAction *iAct = IntersectAction::create();
iAct->setLine(ray);
iAct->apply(this->getRoot());
if(iAct->didHit())
{
_pickedObjectNode = iAct->getHitObject();

Nun wird der Szenengraph ausgehend vom getroffen Objekt nach oben durchlaufen, bis man
auf einen Transformations-Knoten stößt. Dieser wird wiederum in _pickedObjectNode
gespeichert. In der Variablen wird also nicht auf das Objekt selber, sondern auf den
zugehörigen Transformationsknoten verwiesen. Sind z.B. mehrere Objekte an einen
Transformationsknoten angehängt, so werden nach diesem Verfahren auch alle Objekte
ausgewählt.
while(!_pickedObjectNode->getCore()->getType().
isDerivedFrom(Transform::getClassType()))
{
if(_pickedObjectNode->getParent() != this->getRoot())
_pickedObjectNode = _pickedObjectNode->getParent();

Wurde der ganze Szenengraph bis zum Wurzelknoten durchlaufen und es wurde kein
Transformations-Knoten gefunden, so wird ein neuer Transformations-Knoten erzeugt und
an Stelle des gewählten Objektes in den Szenengraphen eingefügt. Das gewählte Objekt wird
als Kind an den neuen Transformations-Knoten angehängt.
else
{
NodePtr pickedObject = iAct->getHitObject();
TransformPtr newTransform = Transform::create();
Matrix m;
m.setIdentity();
beginEditCP(newTransform, Transform::MatrixFieldMask);
newTransform->setMatrix(m);
endEditCP(newTransform, Transform::MatrixFieldMask);

30
NodePtr newTransformNode = Node::create();

beginEditCP(newTransformNode, Node::CoreFieldMask);
newTransformNode->setCore(newTransform);
endEditCP(newTransformNode, Node::CoreFieldMask);

NodePtr pickedObjectParent = pickedObject->getParent();

addRefCP(pickedObject);

beginEditCP(pickedObjectParent);
pickedObjectParent->replaceChildBy(pickedObject,
newTransformNode);
endEditCP(pickedObjectParent);

beginEditCP(newTransformNode);
newTransformNode->addChild(pickedObject);
endEditCP(newTransformNode);

subRefCP(pickedObject);

_pickedObjectNode = newTransformNode;
}

Die Umgebungsbox wird mit der Methode setHighlight() der Basisklasse angezeigt. Die
Umgebungsbox gehört zum ausgewählten Transformationsknoten und ist so groß wie alle als
Kindknoten angehängten Objekte zusammen. Im Normalfall entspricht die Umgebungsbox
aber der Umgebungsbox des ausgewählten Objekts.
this->setHighlight(_pickedObjectNode);
}
}

Abschließend wird der Maustastendruck an den Navigator der Basisklasse weitergeleitet, um


auch den TrackballNavigator nutzen zu können.
_navigator.buttonPress(Navigator::LEFT_MOUSE,x,y);
break;

Dies geschieht ebenso für die anderen Maustastendrücke:


case MouseMiddle:
_navigator.buttonPress(Navigator::MIDDLE_MOUSE,x,y);
break;

case MouseUp:
_navigator.buttonPress(Navigator::UP_MOUSE,x,y);
break;

case MouseDown:
_navigator.buttonPress(Navigator::DOWN_MOUSE,x,y);
break;

Wird die rechte Maustaste gedrückt, so wird die Variable _pickedObjectNode auf den Null-
Zeiger gesetzt, die Anzeige der Umgebungsbox wieder ausgeschaltet und der
Maustastendruck weitergeleitet:
case MouseRight:
if(_objectPicking)
_pickedObjectNode = NullFC;

31
this->setHighlight(NullFC);
_navigator.buttonPress(Navigator::RIGHT_MOUSE,x,y);
break;
}

Die restlichen Codezeilen entsprechen denen in der überschriebenen Methode:


_mousebuttons |= 1 << button;
_lastx = x;
_lasty = y;
}

4.5.5. Ausgewählte Objekte rotieren und verschieben


Das Rotieren und Verschieben von Objekten geschieht in der updateCameraAndMovement()-
Methode. Es wurden jedoch deutliche Änderungen im Gegensatz zum Code aus der
Beispielanwendung mit dem Würfel gemacht. Die Verschiebung geschieht immer relativ zur
Orientierung der Kamera. Verschiebt man also ein Objekt „nach hinten“, so bewegt es sich
auch wirklich von einem weg und nicht entlang einer festen Achse des Koordinatensystems.
Dies ist deutlich intuitiver. Außerdem sollte es ebenso möglich sein, dass Objekte
transformiert werden können die im Graphen unter mehreren Transformationen angeordnet
sind. Die Transformation wird relativ zur Kamera ausgeführt und in eine Transformation
relative zum ersten Transformationsknoten umgerechnet. Die übergeordneten
Transformationen bleiben also unangetastet. Zuerst werden einige temporäre Variablen
angelegt.
if(_pickedObjectNode != NullFC)
{
Vec3f dummy1, dummy2;
Quaternion rotation, dummy3;

Anschließend wird die Rotation der Kamera in Weltkoordinaten in der Matrix camToWorld
gespeichert.
Matrix camToWorld = getCamera()->getBeacon()->getToWorld();
camToWorld.getTransform(dummy1, rotation, dummy2, dummy3);
camToWorld.setIdentity();
camToWorld.setRotate(rotation);

Nun wird die Rotation des Objektes in Weltkoordinaten in der Matrix objectToWorld
gespeichert und transponiert, also die inverse gebildet, da die Rotationsmatrix orthogonal ist.
Matrix objectToWorld = _pickedObjectNode->getToWorld();
objectToWorld.getTransform(dummy1, rotation, dummy2, dummy3);
objectToWorld.setIdentity();
objectToWorld.setRotate(rotation);
objectToWorld.transpose();

Der Verschiebungsvektor dv wird mithilfe der Eingaben des SpaceNavigators in Abhängigkeit


zur Hoch-Achse erstellt. In den beiden Variablen dvworld und dvobject wird der Vektor
später in andere Koordinatensystemen umgewandelt.
Vec3f dv;
if(_zUpAxis)
dv.setValues(spaceNavigator->x*transFactor,spaceNavigator->z*transFactor,
spaceNavigator->y * transFactor);
else
dv.setValues(spaceNavigator->x*transFactor,spaceNavigator->y*transFactor,
spaceNavigator->z * transFactor);
Vec3f dvworld;

32
Vec3f dvobject;

Nun wird eine Transformationsmatrix camToObject erstellt, die eine Transformation von
Kamera- in Objektkoordinaten repräsentiert. Dazu wird die transponierte Objekt zu Welt-
Rotation (objectToWorld) mit der Kamera zu Welt-Rotation (camToWorld) multipliziert.
Matrix camToObject;
camToObject.setIdentity();
camToObject.mult(objectToWorld);
camToObject.mult(camToWorld);

Der Verschiebungsvektor wird in Objektkoordinaten umgerechnet und die drei


Kameraachsen werden in cameraREx erstellt.
camToObject.mult(dv, dvobject);

Vec3f cameraRE1(1, 0, 0);


Vec3f cameraRE2(0, 1, 0);
Vec3f cameraRE3(0, 0, 1);

Die Kameraachsen werden nun in Objektkoordinaten transformiert und das Ergebnis in


objectREx gespeichert.

Vec3f objectRE1;
Vec3f objectRE2;
Vec3f objectRE3;
camToObject.mult(cameraRE1, objectRE1);
camToObject.mult(cameraRE2, objectRE2);
camToObject.mult(cameraRE3, objectRE3);

Nun werden die einzelnen Rotationen um die drei Achsen mithilfe der Eingaben des
SpaceNavigators, der transformierten Vektoren und in Abhängigkeit zur Hoch-Achse in
Quaternions erstellt.
Quaternion qx(objectRE1, spaceNavigator->rx * rotFactorObject);
Quaternion qy, qz;
if(_zUpAxis)
{
qy.setValueAsAxisRad(objectRE2, spaceNavigator->rz * rotFactorObject);
qz.setValueAsAxisRad(objectRE3, spaceNavigator->ry * rotFactorObject);
}
else
{
qy.setValueAsAxisRad(objectRE2, spaceNavigator->ry * rotFactorObject);
qz.setValueAsAxisRad(objectRE3, spaceNavigator->rz * rotFactorObject);
}

Die Quaternions werden miteinander und mit der alten Rotation des Objektes durch
multiplizieren verknüpft.
Matrix transform = (TransformPtr::dcast(_pickedObjectNode->getCore()))
->getMatrix();
transform.getTransform(dummy1, rotation, dummy2, dummy3);
rotation.mult(qx);
rotation.mult(qy);
rotation.mult(qz);

Die finale Transformationsmatrix transform wird durch die Verknüpfung alte Transforma-
tion * neue Verschiebung * neue Rotation in Objektkoordinaten erzeugt.
Matrix m;
m.identity();

33
m.setTranslate(dvobject);
transform.mult(m);
transform.setRotate(rotation);

Anschließend wird im Falle eines normalen Transformations-Knoten die Transformations-


Matrix gesetzt. Im Falle einer ComponentTransform müssen die Translation und die
Rotation einzeln gesetzt werden.
if(_pickedObjectNode->getCore()->getType().isDerivedFrom
(ComponentTransform::getClassType()))
{
// set translation and rotation separately
ComponentTransformPtr compTrans = ComponentTransformPtr::dcast
(_pickedObjectNode->getCore());
beginEditCP(compTrans);
compTrans->setTranslation(Vec3f(transform[3][0],transform[3][1],
transform[3][2]));
compTrans->setRotation(rotation);
endEditCP(compTrans);
}
else if(_pickedObjectNode->getCore()->getType().isDerivedFrom
(Transform::getClassType()))
{
// set final matrix
TransformPtr transCore = TransformPtr::dcast(_pickedObjectNode
->getCore());
beginEditCP(transCore, Transform::MatrixFieldMask);
transCore->setMatrix(transform);
endEditCP(transCore, Transform::MatrixFieldMask);
}

4.5.6. Auf der Oberfläche bewegen


Ist der Walk-Modus aktiv, kann man sich in einem festgelegten Abstand über die Oberfläche
eines Objektes bewegen. Dies ist bereits im WalkNavigator der Basisklasse implementiert.
Man aktiviert diesen Modus mit dem Aufruf der Funktion SpaceNavigatorSSM::
initWalkNavGroundCollision(). Man übergibt der Funktion einen Szenenknoten, der den
Boden enthält. Die Höhe über Grund wird über SpaceNavigatorSSM::setGroundDistance()
geregelt.
Mit dem Aufruf setHeightControl(true) aktiviert man die Steuerung der Höhe über den
SpaceNavigator. Hebt man den SpaceNavigator, so steigt die Höhe und drückt man den
SpaceNavigator nach unten so sinkt die Höhe.

4.5.7. Optimierte Bewegung auf der Oberfläche


Aufgrund der sehr einfachen Implementierung der Bodenkollisionsabfrage im
WalkNavigator, ist dieses Verfahren für komplexere Bodengeometrien nicht praktikabel.
Deswegen wurde ein intelligenteres Verfahren über die Klasse CTriangleElevationGrid
(geschrieben von Björn Zehner) implementiert. Dabei wird die Geometrie in ein regelmäßiges
Gitter aufgeteilt. Man aktiviert diese Art der Kollisionserkennung mit dem Boden mit dem
Aufruf der Methode SpaceNavigatorSSM::initElevationGrid(). Der Methode wird ein
Dateiname übergeben, in der die Bodengeometrie enthalten ist. Außerdem gibt man an, in
wie viele Zellen die Bodengeometrie aufgeteilt wird. Je mehr Zellen desto performanter ist
die Kollisionserkennung aber desto mehr Speicher wird auch benötigt. Geeignete Werte für

34
die Zellenanzahl sind daher stark von der Bodengeometrie abhängig. Dieses Verfahren
funktioniert allerdings bisher nur mit Geometrie, bei der die Z-Achse die Hoch-Achse ist.

4.5.8. Weitere Funktionen


Mithilfe der Methode setCameraPosition() kann man den Standpunkt der Kamera auch
manuell festlegen. Die Orientierung der Kamera wird dabei beibehalten.
void SpaceNavigatorSSM::setCameraPosition(osg::Pnt3f position)
{
Navigator* nav = getNavigator();

nav->setFrom(position);

nav->setAt(nav->getAt() + position - nav->getFrom());


}

Über set/switchHeightControl kann man die Steuerung der Höhe über den SpaceNavigator
aktivieren. Über set/switchObjectPicking kann der Objektauswahlmodus aktiviert werden.
Der Rotationsfaktor kann über setRotationFactor gesetzt werden. Der Translationsfaktor
kann über setTranslationFactor gesetzt werden. Die Methode setDefaultButtonBehaviour()
legt die Funktionalität der beiden Buttons des SpaceNavigators fest (siehe Kapitel 4.4.8).
Der gesamte Quellcode der SpaceNavigatorSSM-Klasse ist im Projektordner
SpaceNavigator/SpaceNavigatorSSM zu finden.

4.5.9. Beispielanwendung: Bewegung auf einem Stadtmodell


Zuerst wird die Headerdatei der Klasse eingebunden.
#include "../SpaceNavigatorSSM/SpaceNavigatorSSM.h"

Außerdem benötigt man einen (globalen) Zeiger auf die Instanz der Klasse.
SpaceNavigatorSSM *mgr;

Im Folgenden wird nur die main()- und die keyboard()-Funktion erklärt. Der komplette
Quellcode ist im Projektordner SpaceNavigator/SpaceNavigatorTestSSM zu finden.
Zu Begin der main()-Funktion wird OpenSG initialisiert und ein GLUT-Fenster erzeugt.
int main(int argc, char **argv)
{
osgInit(argc,argv);

int winid = setupGLUT(&argc, argv);

GLUTWindowPtr gwin= GLUTWindow::create();


gwin->setId(winid);
gwin->init();

Die Szene, bestehend aus einem virtuellen Straßenabschnitt, wird aus einer Datei gelesen.
NodePtr scene = SceneFileHandler::the().read("../osg/City ohne.osb");

Der Szenenmanager wird erzeugt und der Variablen zugewiesen. Der Verbindungsstring zum
SpaceNavigator wird dabei von der Kommandozeile gelesen.
char* connectionString;
if(argc > 1)
connectionString = argv[1];
mgr = new SpaceNavigatorSSM(connectionString, false);

35
Dem Manager werden das GLUT-Fenster und der Wurzelknoten der Szene übergeben.
mgr->setWindow(gwin );
mgr->setRoot (scene);

Die Bodenkollision des WalkNavigators wird aktiviert. Dabei wird der Initialisierungs-
funktion der Knoten mit dem Namen „ground“ übergeben und die Höhe über Grund auf 5
gesetzt.
mgr->initWalkNavGroundCollision(mgr->getNodeByName(scene, „ground“));
mgr->setGroundDistance(5.0f)

Die Kamera wird automatisch mit der Methode showAll() platziert und ausgerichtet sowie
die Standardbutton-Belegung des SpaceNavigators aktiviert.
mgr->showAll();
mgr->setDefaultButtonBehaviour(true);

Die GLUT-Anwendungsschleife wird betreten.


glutMainLoop();

return 0;
}

In der keyboard()-Methode wird die Tastenbelegung festgelegt. Mit ´T´ ändert man den
Navigationsmodus auf Trackball, wodurch man die Kamera mit Hilfe der Maus um die Szene
rotieren kann. ´W´ schaltet zurück in den Walk-Modus. Mit ´H´ aktiviert und deaktiviert
man die Höhenverstellung.
Befindet man sich im Walk-Modus, kann man mit dem Mauszeiger und der linken Maustaste
Objekte auswählen und sie dann mit dem SpaceNavigator verschieben und drehen (siehe
Abbildung 8). Per Rechtsklick wird das Objekt wieder deselektiert und man kann sich
mithilfe des SpaceNavigators wieder durch die Szene bewegen.

Abbildung 8 - Ein Objekt wird mithilfe der Maus ausgewählt und mit dem SpaceNavigator gedreht
und angehoben

4.5.10. Beispielanwendung: Bewegung auf einem Landschaftsmodell


Dieses Beispiel ist analog zum vorherigen mit dem Unterschied, dass ein hügeliges, aus über
50000 Dreiecken bestehendes Landschaftsmodell lädt. Im Folgenden werden nur
Unterschiede im Code zum vorherigen Beispiel erklärt. Die Szene wird geladen.
Scene = SceneFileHandler::the().read("../osg/ground_example_2000.osb");

Das Modell wurde mit der Z-Achse als Hoch-Achse modelliert. Deswegen wird als zweiter
Parameter bei der Erzeugung des SceneManagers true übergeben.

36
mgr = new SpaceNavigatorSSM(computerName, true);

Anstatt der Standard-Kollisionserkennung des WalkNavigators wird die optimierte


Kollisionserkennung der Klasse CTriangleElevationGrid benutzt. Dabei wird das
Landschaftsmodell in 15x15 gleichgroße Teile unterteilt. Zur Kollisionserkennung wird
immer nur der Teil getestet, in dem sich die Kamera befindet.
mgr->initElevationGrid("../osg/ground_example_2000.osb", 15, 15);

Schließlich werden die Höhe über dem Boden, die Empfindlichkeit der Bewegung und
Rotation sowie die Startposition der Kamera an die Szene angepasst.
mgr->setGroundDistance(20);
mgr->setTranslationFactor(0.2f);
mgr->setRotationFactor(0.5f);
mgr->showAll();
mgr->setCameraPosition(Vec3f(-50, 0, 50));

4.6. Installation und Testen im VR-Pool der HTWK Leipzig


Im VR-Pool der HTWK ist die Verbindung der Anwendung zum Eingabegerät über das
Netzwerk ebenfalls sehr nützlich. So kann die über den SpaceNavigator zu steuernde
Anwendung auf dem Rechner laufen, der das Stereo-Display ansteuert. Dieser ist hinter dem
Display positioniert. Der SpaceNavigator kann dann an einen Rechner der vor dem Display
steht angeschlossen werden und somit die Anwendung bequem gesteuert werden.

4.6.1. Den SpaceNavigator-Server einrichten


Der SpaceNavigator-Server muss auf dem Rechner installiert und gestartet werden, an den
der SpaceNavigator angeschlossen wird. Dazu kopiert man das Verzeichnis Server von der
Abgabe-CD in ein Verzeichnis auf Festplatte.
Man hat die Möglichkeit den allgemeinen VRPN-Server benutzen. Dazu startet man die Datei
vrpn_server.exe. Anschließend kann man bereits die Anwendung auf dem zweiten Rechner
starten. Sind die Bewegungen in der Anwendung ruckelig, so empfiehlt sich stattdessen der
Einsatz der SpaceNavigatorServer.exe, da diese ohne Pause die Eingaben des
SpaceNavigators verarbeitet während der VRPN-Server dies nur alle 5ms tut. Die
SpaceNavigatorServer.exe wird über die entsprechende .bat-Datei gestartet. Dies ist nötig,
weil die der Server dem Eingabegerät einen eindeutigen String zuweisen muss, der aus einem
Namen und dem Rechnernamen des Server-PCs bestehen muss. Die .bat-Datei übergibt der
Anwendung den Verbindungsstring wie folgt:
SpaceNavigatorServer.exe SpaceNav@computername

Wird der SpaceNavigator an den Rechner mit dem EOS-Tracking-System angeschlossen, so


müsste die .bat-Datei folgendermaßen aussehen:
SpaceNavigatorServer.exe SpaceNav@eos-ir-tracking

Alternativ kann man die Anwendung auch über die Konsole starten und den entsprechenden
Verbindungsstring übergeben oder die .bat-Datei editieren und einen anderen Computer
eintragen.
Nun wartet der Server auf eine Verbindungsanfrage von einer Anwendung.

37
4.6.2. Die Testanwendungen (SpaceNavigator-Client) einrichten
Die Testanwendungen können auf dem Display-PC installiert werden, indem einfach der
Ordner Client von der Abgabe-CD in ein Verzeichnis auf der Festplatte kopiert wird. Eine
Anwendung wird über die entsprechende .bat-Datei aufgerufen.
Außerdem ist es wichtig, dass sich die beiden verwendeten Rechner in derselben Netzwerk-
Arbeitsgruppe befinden (im VR-Pool ist dies WORKGROUP für den Display- und den EOS-
Tracking-Rechner und ARBEITSGRUPPE für die restlichen PCs).
Wird der SpaceNavigator an den Rechner vor dem Display angeschlossen, so müsste die .bat-
Datei folgendermaßen aussehen:
SpaceNavigatorTestSSM.exe SpaceNav@eos-ir-tracking

Nachdem der Server gestartet wurde, kann auch die Anwendung gestartet werden.

4.7. VRPN erweitern


Da im VR-Pool und auch im UFZ der SpacePilot von 3DConnexion zum Einsatz kommt
wurde der Standard-Server von VRPN erweitert. Auf die Implementierung im Speziellen wird
hier nicht weiter eingegangen. Die geänderten VRPN-Quellcode-Dateien liegen im
Verzeichnis SpaceNavigator/vrpn. Die hinzugefügten Zeilen sind jeweils mit dem
Kommentar // Changed: 22.01.08, Bilke gekennzeichnet.
Der kompilierte Server liegt im Release-Verzeichnis des Projektes. Die zugehörige
Konfigurationsdatei sieht nun folgendermaßen aus:
vrpn_3DConnexion_Navigator SpaceNav
#vrpn_3DConnexion_Traveller SpaceTrav
#vrpn_3DConnexion_Pilot SpacePilot
#vrpn_3DConnexion_Explorer SpaceExp

Je nachdem, welches Eingabegerät man verwendet, so entfernt man das Kommentarzeichen


# in der jeweiligen Zeile. Im obigen Falle wird der SpaceNavigator verwendet.
Die Dateien im Server- und Client-Ordner sind bereits auf die Verwendung des SpacePilot
angepasst.

5. CD-Inhalt
5.1. Ordner Client
Dieser Ordner enthält die Dateien, die im VR-Pool auf den Display-Rechner kopiert werden
müssen.

5.2. Ordner download


Dieser Ordner enthält die 3DConnexion-Treiber (3DxSoftware_v3-5-5.exe), den OpenSG-
Quellcode (opensg_dailybuild.070704.win-source.tgz) und den VRPN-Quellcode
(vrpn_07_13.zip).

5.3. Ordner Server


Dieser Ordner enthält die Dateien, die im VR-Pool auf den EOS-Tracking-Rechner kopiert
werden müssen.

38
5.4. Ordner SpaceNavigator
Dieser Ordner enthält die Projektordner der einzelnen Klassen und Beispielprogramme, die
Projektdatei (SpaceNavigator.sln) sowie die kompilierte Projekte (Ordner Debug und
Release). Im Unterordner vrpn sind die geänderten VRPN-Quellcode-Dateien zu finden.

5.5. Ordner opensg


Dieser Ordner enthält die kompilierte OpenSG-Bibliothek sowie den zugehörigen Quellcode.

5.6. Ordner vrpn


Dieser Ordner enthält die kompilierte VRPN-Bibliothek sowie den zugehörigen Quellcode.

39
Literaturverzeichnis
[Gam95] Gamma, Erich und al., et. 1995. Design Patterns - Elements of Reusable
Object-Oriented Software. s.l. : Addison Wesley, 1995.
[Ope08] 2008. OpenSG-Homepage. [Online] Januar 2008. http//opensg.vrsource.org.
[Rot05] Roth, Marcus. 2005. Paralle Bildberechnung in einem Netzwerk von
Workstations. Disertation. Darmstdt : Technische Universität Darmstadt, 2005.
[Tay01] Taylor, Russel M. und al., et. 2001. VRPN: A Device-Independent, Network-
Transparent VR Peripheral System. University of North Carolina at Chapel Hill : s.n., 2001.
[Voß02] Voß, G., et al. 2002. A Multi-thread Safe Foundation for Scene Graphs and its
Extensions to Clusters. Fourth Eurographics Workshop on Parallel Graphics and
Visualization. 2002, S. 33-37.
[VRP08] 2008. VRPN-Homepage. [Online] Januar 2008.
http://www.cs.unc.edu/Research/vrpn/.

40

Das könnte Ihnen auch gefallen