Kleine Einführung in VRML

Wie sieht eine VRML-Datei aus? VRML ist ein Klartextformat mit einer an Programmiersprachen orientierten Syntax. Man kann somit eine VRML-Datei mit einem beliebigen Texteditor schreiben, und verwendet dabei einleuchtende Worte wie Box, Color, rotation u.s.w., sowie Klammern und natürlich auch Zahlen. Zunächst aber benötigt man eine Headerzeile, die zur Zeit immer so aussieht

#VRML V2.0 utf8

Jetzt kommt eine Liste von sogenannten Knoten. Diese haben die Syntax

Knotentypname{Knotenattribut1 Wert1 Knotenattribut2 Wert2 ....}

Es gibt 54 verschiedene Knotentypen, die jeweils eine Anzahl von speziellen Attributen haben. Die Werte der Attribute sind selbst wieder Knoten oder Zahlenwerte, Listen oder boolesche Werte. Z.B. gibt es den Knotentyp Shape, der die Attribute geometry und appearence hat. Der Wert des geometry-Attributs ist einer von 10 Elementargeometrie-Knoten wie z.B. Box oder Sphere. Der Wert des appearence-Attributes ist immer ein Appearence-Knoten. Dieser hat die Attribute material, texture und textureTransform. Eine kleine VRML-Datei, die eine roten Würfel zeigt, sieht folgendermaßen aus:
#VRML V2.0 utf8

Shape{
geometry
Box{size 1 1 1}
appearance
Appearance{
material
Material{
diffuseColor 1 0 0
}
}
}

[Teste das Beispiel!]

Die räumliche Platzierung eines solchen Objektes ist keine Eigenschaft, die innerhalb des Objektes festgelegt wird. Mit Hilfe des Transform-Knotens macht man eine Koordinaten-Transformation und in diesen Knoten wird dann das Shape oder ein komplexeres Gebilde eingehängt. Das sieht z.B. so aus:
#VRML V2.0 utf8

Transform{
children [ Diverses ]
translation 2 0.5 0
rotation 0 1 0 0.7854
scale 1 2 2
}

[Teste das Beispiel!]

Der Wert des children-Attributs ist im allgemeinen eine in eckigen Klammern eingeschlossene Liste von irgendwelchen Knoten. Diese können wie oben Shapes sein, das können aber auch selbst wieder Transform-Knoten sein, oder ganz etwas anderes.

Neben den Elementargeometrien Box und Sphere, gibt es noch Cylinder und Cone sowie IndexedFaceSet, IndexedLineSet und IndexedPointSet, zwei etwas obskurere Dinge mit den Namen EvaluationGrid und Extrusion und schließlich Text. Ganz allgemeine, auf Polygonen aufgebaute Körper, realisiert man mit dem Knotentyp IndexedFaceSet.
#VRML V2.0 utf8

Shape{ geometry
IndexedFaceSet{
coord Coordinate{
point [
0 0 0
0 0 1
0 1 0
0 1 1
1 0 0
1 0 1
1 1 0
1 1 1

]
}
coordIndex [
0 1 3 2 -1
6 7 5 4 -1
0 2 6 4 -1
5 7 3 1 -1
0 4 5 1 -1
3 7 6 2 -1

]
colorPerVertex FALSE
color Color{
color [
0 0 1
0 1 0
0 1 1
1 0 0
1 0 1
1 1 0

]
}
}
}

[Teste das Beispiel!]

Bei einem EvaluationGrid-Knoten wird aus einer Matrix von Y-Werten eine Fläche über einem regulären Gitter in der X-Z-Ebene gebildet.
#VRML V2.0 utf8

Shape{
geometry ElevationGrid{
xDimension 4
zDimension 4
xSpacing 0.5
zSpacing 0.5
height [
0.1 0.1 0.1 0.1
0.2 0.3 0.5 0.7
0.3 0.6 0.9 1.2
0.4 0.5 0.7 0.9
]
solid FALSE
}
Appearance Appearance{
material Material{
diffuseColor 0.5 0.5 0.5
}
}
}

[Teste das Beispiel!]

Ein Extrusion-Knoten zieht eine Fläche, in dem sich ein ebenes Polygon im Raum bewegt und sich dabei vielleicht noch Größe und Orientierung des ursprünglichen Polygons ändern. So ist z.B. eine sich nach oben verjüngende Sechsecksäule denkbar, oder ein in sich verdrilltes Schleifenband.
#VRML V2.0 utf8

Shape{
geometry Extrusion{
spine [
0 0 0
0 1 0
0 2 0
]
crossSection [
0 1.
.866 .5
.866 -.5
0 -1.
-.866 -.5
-.866 .5
0 1.
]
scale [
1 1
0.5 0.5
0.5 0.5
]
}
appearance Appearance{
material Material{
diffuseColor 0.5 0.5 1
}
}
}

[Teste das Beispiel!]

Das Aussehen der Objekte wird entweder durch eine Textur (ein Pixelbild) oder durch ein Material bestimmt.
#VRML V2.0 utf8


Shape{
geometry
Box{size 1 1 1}
appearance
Appearance{
texture
ImageTexture{
url ßtroh.gif"
}
}
}

[Teste das Beispiel!]

Ein Material hat die RGB-Attribute diffuseColor, emissiveColor und specularColor, sowie die Scalar-Attribute ambientIntensity, shininess und transparency, die Werte zwischen 0 und 1 annehmen können. Für alle Attribute in allen VRML-Knoten gelten Default-Werte, so daß es hier für einfache Bedürfnisse reicht, den Wert für diffuseColor anzugeben. "Glänzende" Objekte erzielt man mit den Parametern shininess und specularColor.
#VRML V2.0 utf8


Shape{
geometry
Sphere{radius 1}
appearance
Appearance{
material
Material{
diffuseColor 1 0 0
specularColor 1 1 1
shininess 1
}
}
}

[Teste das Beispiel!]

Durch Setzen von emissiveColor bekommen Materialien ein "Eigenleuchten". Damit sind wir beim Thema Licht. Sehen kann man normalerweise (d.h. solange man keine selbstleuchtenden Objekte hat) nur etwas, wenn man Lichtquellen hat. Dazu gibt es die Knotentypen DirectionalLight, PointLight und SpotLight. Letztlich ist dies aber mehr etwas für die ausgefuchste Szenengestaltung, denn im VRML-Brower "brennt" per Default ein direktionales Licht aus der Richtung des Betrachters. Dieses "head light" hat gegenüber den Lichtern, die sich in der Szene befinden, den Vorteil, daß es sich nicht mit der Szene bewegt. Es leuchtet also immer diejenigen Flächen an, die vom Betrachter gesehen werden. Andernfalls könnte der Betrachter leicht im Dunkeln stehen, wenn nach einigen Bewegungen des Objektes oder Bewegungen in der Szene das einzige Licht direkt von vorne kommt. Dann werden nämlich nur die Flächen erleuchtet, die dem Betrachter gerade abgewandt sind.

VRML bietet die Möglichkeit Knoten mit Namen zu versehen, um danach mehrfache Kopien diese Knotens zu erzeugen.
#VRML V2.0 utf8

DEF S Shape{
geometry
Box{size 1 1 1}
appearance
Appearance{
material
Material{
diffuseColor 1 0 0
}
}
}

Transform{
children [USE S]
translation 2 0 0
}

[Teste das Beispiel!]

Die Benamung von Knoten ist auch erforderlich, wenn sie sich gegenseitig Nachrichten zusenden sollen. Mit solchen Nachrichten, man spricht auch von Ereignissen oder von Events, lassen sich manche Attribute der Knoten einer Szene durch Interaktion oder zeitabhängig verändern. Der Betrachter einer Szene kann z.B. auf ein Objekt klicken, um eine Lampe einzuschalten.
#VRML V2.0 utf8

Group {children [
Shape {
geometry Sphere {radius 1}
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
}
DEF T TouchSensor {}
] }

DEF L PointLight {
location 0 3 0
on FALSE
}

ROUTE T.isActive TO L.on

[Teste das Beispiel!]

Mit Hilfe eines Group-Knotens wird eine rote Kugel mit einem TouchSensors-Knoten verbunden, der den Namen T erhält. Dadurch bekommt der Betrachter die Möglichkeit, durch Klicken auf die Kugel ein Ereignis auszulösen. Durch die ROUTE-Vereinbarung wird der Wert des Feldes isActive des Touchsensors an das Attribut on des Punktlichtes L gesandt. Letzteres hatten wir initial auf FALSEgesetzt, was bedeutet, daß das Licht aus ist. Klickt man nun auf die Kugel, geht das Licht an. Läßt man die Maustaste wieder los, geht das Licht wieder aus, denn isActive von T ändert wieder seinen Wert und wird wieder FALSE. Dieser neue Wert wird ebenfalls an on von L weitergegeben.

Die Möglichkeiten einer Eventsteuerung auf diesem Niveau sind mit VRML alleine allerdings sehr beschränkt. Mit einem Script-Knoten kann man aber die Eventsteuerung durch eine andere Programmiersprache unterstützen. Die VRML-Spezifikation schreibt hier keine spezielle Sprache vor. Es gelten lediglich ein paar Grundvoraussetzungen an die Sprache. Wichtig ist allerdings, daß der darstellende VRML-Browser die Programmiersprache interpretieren kann. Es bieten sich Java und Javascript an, die in dem WWW/VRML-Umfeld weit verbreitet sind und von fast allen Browsern verstanden werden. Mit Javascript läst sich der Programmcode direkt in die VRML-Datei schreiben. Wir ergänzen nun das obige Beispiel durch einen Script-Knoten, in dem man sich das Ereignis des Mausklickes merkt. Das Licht soll nach loslassen der Maustaste anbleiben, und erst beim zweiten Klicken wieder erlöschen.
#VRML V2.0 utf8

Group {children [
Shape {
geometry Sphere {radius 1}
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
}
DEF T TouchSensor {}
] }

DEF L PointLight {
location 0 3 0
on FALSE
}

DEF S Script {
eventIn SFBool isKlick
eventOut SFBool isMerk
url " javascript:
function initialize() {isMerk = false;}
function isKlick(value,ts) {
if (value) { isMerk = !isMerk }
}
"
}

ROUTE T.isActive TO S.isKlick ROUTE S.isMerk TO L.on

[Teste das Beispiel!]

In einem Script-Knoten können mehrere eventIn- und eventOut-Felder vereinbart werden. Darüber wird das Script für die normale VRML-Eventsteuerung zugänglich. Für jedes eventIn-Feld soll es dann in dem Programm (das ist einfach als String der Wert des Attributes url) eine Funktion (Unterprogramm, Methode) geben, die immer dann ausgeführt wird, wenn das Script ein entsprechendes Event erhält. Dieser Funktion werden dann zwei Parameter übergeben. Einmal der Wert, den das Ereignis übergibt, und zum anderen ein Zeitstempel. Den Zeitstempel haben wir oben nicht verwendet. Die Funktion initialize wird zu Beginn (beim Einlesen der VRML-Datei in den Browser) einmal ausgeführt. Wir klicken nun auf die rote Kugel. Das Attribut isAktive des Touchsensors ändert seinen Wert; er wird TRUE. Dieser Wert wird als Event an das Script S geschickt, wo die Funktion isKlick ausgeführt wird. Der übergebene Wert ist TRUE. Somit fällt die if-Abfrage positive aus. Das Feld isMerk wird ümgeknipst". isMerk ist aber ein eventOut-Feld und es ist mit ROUTE vereinbart, daß eine Änderung von isMerk an das Attribut on des Punktlichtes übergeben wird. Beim Loslassen der Maustaste wird wieder die Funktion isKlick aufgerufen, bekommt aber jetzt den Wert FALSE übergeben. Somit passiert nichts. Erst beim nächsten Klick wird isMerk wieder verändert.

Die in einem Script-Knoten vereinbarten Routinen können auch direkt auf die Felder eines anderen Knotens zugreifen. Entsprechende ROUTE-Anweisungen entfallen dann. In dem Script-Knoten muß dazu ein Feld vom Typ SFNode vereinbart werden. Als Wert erhält das Feld einen vorher mit DEF benamten Knoten. Außerdem muß das Script-Attribute directOutput auf TRUE gesetzt werden.
#VRML V2.0 utf8

Group {children [
Shape {
geometry Sphere {radius 1}
appearance Appearance {
material Material {
diffuseColor 1 0 0
}
}
}
DEF T TouchSensor {}
] }

DEF L PointLight {
location 0 3 0
on FALSE
}

DEF S Script {
eventIn SFBool isKlick
field SFNode light USE L
directOutput TRUE
url " javascript:
function initialize() {light.on = false;}
function isKlick(value,ts) {
if (value) { light.on = !light.on }
}
"
}

ROUTE T.isActive TO S.isKlick

[Teste das Beispiel!]

Als letztes wollen wir ein einfaches Beispiel für eine Bewegung geben: ein rotierender Würfel. Dazu benötigen wir zunächst einen TimeSensor. Für diesen setzen wir das Attribut loop auf TRUE und das Attribut cycleInterval auf 4. Der TimeSensor besitzt außerdem ein eventOut-Feld fraction, das kontinuierlich, innerhalb von 4 Sekunden, das Intervall [0,1] linear durchfährt. Dieser sich ändernde Wert wird mittels einer ROUTE-Vereinbarung an einen OrientationInterpolator gesendet. Der hat dann die Aufgabe, die Skalarwerte auf jeweils 4 Werte für eine Rotation abzubilden. (Eine Rotation wird durch einen Richtungsvektor und einen Winkel bestimmt.) Die sich so kontinuierlich ändernden Rotationswerte müssen jetzt nur noch an das rotation-Attribut eines Transform-Knotens gesandt werden.
#VRML V2.0 utf8

DEF R Transform {
children [
Shape {
geometry Box {size 1 1 1}
appearance Appearance {
material Material {
diffuseColor 0 0 1
}
}
}
]
}

DEF T TimeSensor {
loop TRUE
cycleInterval 4
}

DEF O OrientationInterpolator {
key [0 0.5 1]
keyValue [1 1 1 0 1 1 1 3.1416 1 1 1 6.2832]
}

ROUTE T.fraction TO O.fraction ROUTE O.value TO R.rotation

[Teste das Beispiel!]

Hier haben wir also eine Drehung um den Vektor [1 1 1]. Die Rotationswinkel haben die Werte 0, 3.142 und 6.283, die bei den fraction-Werten 0, 0.5 und 1 angenommen werden. Die Zwischenwerte werden linear interpoliert, damit eine gleichmäßige Bewegung entsteht. Neben dem OrientationInterpolator gibt es noch weitere Interpolatoren, um z.B. Translationen oder Farben zu animieren. Desweiteren kann man solche Bewegungen auch mit Hilfe von Script-Knoten erzeugen.

Desweiteren gibt es die Möglichkeit, eine Bewegung interaktive mit der Maus zu erzeugen. Dazu dienen die Knoten CylinderSensor, SphereSensor und PlaneSensor. Im folgenden Beispiel wird mit dem linken Würfel ein CylinderSensor verbunden. Die Rotationen, die er aussendet werden auf den rechten Würfel übertragen.
#VRML V2.0 utf8

DEF S Shape{
geometry
Box{size 1 1 1}
appearance
Appearance{
material
Material{
diffuseColor 1 0 0
}
}
}

Transform{
children [
USE S
DEF C CylinderSensor{}
]
translation 2 0 0
}

DEF T Transform{
children [
USE S
]
translation -2 0 0
}

ROUTE C.rotation_changed TO T.rotation

[Teste das Beispiel!]

Im folgenden Beispiel wird ein Billboard-Knoten eingesetzt. Bewegt sich der Betrachter in der Scene, oder bewegt der Betrachter das Gesamtobjekt relativ zu seinem eigenen Standpunkt, so führen die Teilobjekte des des Billboard-Knotens eine Rotation um die angegebene Achse aus, sodaß immer die Gleiche Seite dieser Objekte zum Betrachter zeigt.
#VRML V2.0 utf8

Billboard{
axisOfRotation 0 1 0
children [
DEF S Shape{
geometry
Box{size 1 1 1}
appearance
Appearance{
material
Material{
diffuseColor 1 0 0
}
}
}
]
}

Transform{
children [
USE S
]
translation 2 0 0
}

Transform{
children [
USE S
]
translation -2 0 0
}

[Teste das Beispiel!]


Burkhard Wald, 0201 / 183-2921,wald@hrz.uni-essen.de, 04.05.1998