Copyright © 2006, 2007 Michalis Kamburelis
You can redistribute and/or modify this document under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
Table of Contents
List of Figures
Coordinate nodecreaseAngle settingsroSeparateShapeStates
optimization.roSeparateShapeStatesNoTransform
optimization.This thesis describes the implementation of a 3D engine based on the VRML language.
The VRML language is used to define 3D environments. It will be described in detail in Chapter 1, Overview of VRML. It has many advantages over other 3D languages:
The specification of the language is open.
The language is implementation-neutral, which means that it's not “tied” to any particular rendering method or library. It's suitable for real-time rendering (e.g. using OpenGL or DirectX), it's also suitable for various software methods like ray-tracing. This neutrality includes the material and lighting model described in VRML 2.0 specification.
Inventor, an ancestor of the VRML, lacked such neutrality. Inventor was closely tied to the OpenGL rendering methods, including the OpenGL lighting model.
The language is quite popular and many 3D authoring programs can import and export models in this format. Author of this document can recommend the open-source Blender modeler.
The language can describe geometry of 3D objects with all typical properties like materials, textures and normal vectors.
The language is not limited to 3D objects. Other important environment properties, like lights, the sky, the fog, viewpoints, collision properties and many other can be expressed.
The language is easy to extend. You can easily add your own nodes and fields (and I did, see the list of my VRML extensions).
Implementation goals were to make an engine that
Uses VRML. Some other 3D file formats are also supported (like 3DS, MD3, Wavefront OBJ and Collada) by silently converting them to VRML graph.
Allows to make a general-purpose VRML browser. See view3dscene.
Allows to make more specialized programs, that use the engine and VRML models as part of their job. For example, a game can use VRML models for various parts of the world:
Static environment parts (like the ground and the sky) can be stored and rendered as one VRML model.
Each creature, each item, each “dynamic” object of the world (door that can open, building that can explode etc.) can be stored and rendered as a separate VRML model.
When rendering, all these VRML objects can be rendered within the same frame, so that user sees the complete world with all objects.
Example game that uses my engine this way is “The Castle”.
Using the engine should be as easy as possible, but at the same time OpenGL rendering must be as fast as possible. This means that a programmer gets some control over how the engine will optimize given VRML model (or part of it). Different world parts may require entirely different optimization methods:
static parts of the scene,
parts of the scene that move (or rotate or scale etc.) only relatively to the static parts,
parts of the scene that frequently change inside (e.g. a texture changes or creature's arm rotates).
All details about optimization and animation methods will be given in later chapters (see Chapter 5, OpenGL rendering and Chapter 6, Animation).
The primary focus of the engine was always on 3D games, but, as described above, VRML models can be used and combined in various ways. This makes the engine suitable for various 3D simulation programs (oh, and various game types).
The engine is open-source (licensed on GNU General Public License).
Developed in object-oriented language. For me, the language of choice is ObjectPascal, as implemented in the Free Pascal compiler.
Table of Contents
This chapter is an overview of VRML concepts. It describes the language from the point of view of VRML author. It teaches how a simple VRML files look like and what are basic building blocks of every VRML file. It's intended to be a simple tutorial into VRML, not a complete documentation how to write VRML files. If you want to learn how to write non-trivial VRML files you should consult VRML specifications.
This chapter also describes main differences between VRML 1.0 and 2.0. Our engine handles both VRML versions, and in the future X3D will be supported too. X3D is the successor of VRML 2.0 format (you can think of it as "VRML 3.0").
VRML files are normal text files, so they can be viewed and edited in any text editor. Here's a very simple VRML 1.0 file that defines a sphere:
#VRML V1.0 ascii
Sphere { }The first line is a header. It's purpose is to identify
VRML version and encoding used. Oversimplifying things a little,
every VRML 1.0 file will start with the exact same line:
#VRML V1.0 ascii.
After the header comes the actual content.
Like many programming languages, VRML language is a free-form
language, so the amount of whitespace in the file doesn't really matter.
In the example file above we see a declaration of a node
called Sphere. “Nodes” are the building
blocks of VRML: every VRML file specifies a directed graph of nodes.
After specifying the node name (Sphere),
we always put an opening brace (character {),
then we put a list of fields and
children nodes of our node,
and we end the node by a closing brace (character }).
In our simple example above, the Sphere
node has no fields specified and no children nodes.
The geometry defined by this VRML file is a sphere centered at the origin of coordinate system (i.e. point (0, 0, 0)) with a radius 1.0.
Why the sphere is centered at the origin ?
Spheres produces by a Sphere node
are always centered at the origin — that's defined by
VRML specifications.
Don't worry, we can define spheres centered at any point,
but to do this we have to use other nodes that will move
our Sphere node — more on this later.
Why the sphere radius is 1.0 ?
This is the default radius of spheres produced
by Sphere node. We could change it by
using the radius field of a Sphere
node — more on this later.
Since the material was not specified, the sphere will use the default material properties. These make a light gray diffuse color (expressed as (0.8, 0.8, 0.8) in RGB) and a slight ambient color ((0.2, 0.2, 0.2) RGB).
An equivalent VRML 2.0 file looks like this:
#VRML V2.0 utf8
Shape {
geometry Sphere { }
}
As you can see, the header line is now different. It indicates VRML version as 2.0 and encoding as utf8 [1].
In VRML 2.0 we can't directly use a Sphere
node. Instead, we have to define a Shape node
and set it's geometry field to our desired
Sphere node. More on fields and children nodes
later.
Actually, our VRML 2.0 example is not equivalent to VRML 1.0 version: in VRML 2.0 version sphere is unlit (it will be rendered using a single white color). It's an example of a general decision in VRML 2.0 specification: the default behavior is the one that is easiest to render. If we want to make the sphere lit, we have to add a material to it — more on this later.
Every VRML node has a set of fields.
A field has a name, a type, and a default value. For example,
Sphere node has a field named radius,
of type SFFloat, that has a default value of 1.0.
There are many field types defined by VRML specification. Each field type specifies a syntax for field values in VRML file, and sometimes it specifies some interpretation of the field value. Example field types are:
SFFloatA float value. Syntax is identical to the syntax
used in various programming languages, for example
3.1415926 or 12.5e-3.
SFLong (in VRML 1.0), SFInt32 (in VRML 2.0)A 32-bit integer value. As you can see, the name was changed in VRML 2.0 to indicate clearly the range of allowed values.
SFBoolA boolean value. Syntax: one word, either
FALSE or TRUE.
Note that VRML is case-sensitive. In VRML 1.0 you could
also write the number 0 (for FALSE)
or 1 (for TRUE), but this additional syntax
was removed from VRML 2.0 (since it's quite pointless).
SFVec2f, SFVec3fVector of 2 or 3 floating point values. Syntax is to
write them as a sequence of SFFloat values, separated by
whitespace. The specification doesn't say how these vectors
are interpreted: they can be positions, they can be directions etc.
The interpretation must be given for each case when some node
includes a field of this type.
SFColorSyntax is exactly like SFVec3f,
but this field has a special interpretation: it's an RGB
(red, green, blue) color specification. Each component must
be between 0.0 and 1.0. For example, this is a yellow color:
1 1 0.
SFRotationFour floating point values specifying rotation around an axis. First three values specify an axis, fourth value specifies the angle of rotation (in radians).
SFImageThis field type is used to specify image content
for PixelTexture node in VRML 2.0
(Texture2 node in VRML 1.0).
This way you can specify texture content directly in VRML file,
without the need to reference any external file.
You can create grayscale, grayscale with alpha, RGB or RGB with alpha
images this way. This is sometimes comfortable, when you
must include everything in one VRML file, but beware that
it makes VRML files very large (because the color values are specified
in plain text, and they are not compressed in any way).
See VRML specification for exact syntax of this field.
SFStringA string, enclosed in double quotes.
If you want to include double quote in a string, you
have to precede it with the backslash (\)
character, and if you want to include the backslash in a string
you have to write two backslashes. For example:
"This is a string."
"\"To be or not to be\" said the man."
"Windows filename is
c:\\3dmodels\\tree.wrl"
Note that in VRML 2.0 this string can contain characters encoded in utf8 [2].
SFNodeThis is a special VRML 2.0 field type that contains
other node as it's value (or a special value NULL).
More about this in Section 1.3, “Children nodes”.
All names of field types above start with SF,
which stands for “single-value field”. Most of these field types
have a counterpart, “multiple-value field”, with a name
starting with MF. For example MFFloat,
MFLong, MFInt32,
MFVec2f and MFVec3f.
The MF-field value is a sequence of any number
(possibly zero) of single field values. For example,
MFVec3f field specifies any number of 3-component
vectors and can be used to specify a set of 3D positions.
Syntax of multiple-value fields is:
An opening bracket ([).
A list of single field values separated by commas (in VRML 1.0) or whitespaces (in VRML 2.0). Note that in VRML 2.0 comma is also a whitespace, so if you write commas between values your syntax is valid in all VRML versions.
A closing bracket (]).
Note that you can omit both brackets if your MF-field has
exactly one value.
Each node has a set of fields given by VRML specification. VRML file can specify value of some (maybe all, maybe none) node's fields. You can always leave the value of a field unspecified in VRML file, and it always is equivalent to explicitly specifying the default value for given field.
VRML syntax for specifying node fields is simple:
within node's braces ({ and })
place field's name followed by field's value.
Let's see some examples of specifying field values.
Sphere node has a field named
radius of type SFFloat
with a default value 1.0. So the file below is exactly
equivalent to our first sphere example in previous section:
#VRML V1.0 ascii
Sphere {
radius 1
}
And this is a sphere with radius 2.0:
#VRML V1.0 ascii
Sphere {
radius 2
}
Here's a VRML 2.0 file that specifies a cylinder that should be rendered
without bottom and top parts (thus creating a tube), with a
radius 2.0 and height 4.0. Three SFBool
fields of Cylinder are used:
bottom, side,
top (by default all are TRUE,
so actually we didn't have to write side TRUE).
And two SFFloat fields, radius
and height, are used.
Remember that in VRML 2.0 we can't just write the Cylinder
node. Instead we have to use the Shape node.
The Shape node has a field geometry
of type SFNode. By default, value of this field
is NULL, which means that no shape is defined.
We can place our Cylinder node as a value
of this field to correctly define a cylinder.
#VRML V2.0 utf8
Shape {
geometry Cylinder {
side TRUE
bottom FALSE
top FALSE
radius 2.0
height 10.0
}
}Figure 1.3. Cylinder example, rendered in wireframe mode (because it's unlit, non-wireframe rendering would look confusing)

Here's a VRML 2.0 file that specifies two points.
Just like in the previous example, we had to use a Shape node
and place PointSet node in it's geometry
field. PointSet node, in turn, has two more
SFNode fields: coord
(that can contain Coordinate node)
and color (that can contain Color node).
Coordinate node has a point field
of type MFVec3f — these are positions
of defined points. Color node has a
color field of type MFColor —
these are colors of points, specified in the same order as in
the Coordinate node.
Note that PointSet and Color
nodes have the same field name: color.
In the first case, this is an SFNode field,
in the second case it's an MFVec3f field.
#VRML V2.0 utf8
Shape {
geometry PointSet {
coord Coordinate { point [ 0 -2 0, 0 2 0 ] }
color Color { color [ 1 1 0, 0 0 1 ] }
}
}Now we're approaching the fundamental idea of VRML: some
nodes can be placed as a children of other nodes. We already
saw some examples of this idea in VRML 2.0 examples
above: we placed various nodes inside geometry
field of Shape node. VRML 1.0 has a little different
way of specifying children nodes (inherited from Inventor format)
than VRML 2.0 and X3D — we will see both methods.
In VRML 1.0, you just place children nodes inside the parent node. Like this:
#VRML V1.0 ascii
Group {
Sphere { }
Cube { width 1.5 height 1.5 depth 1.5 }
}Group is the simplest grouping node.
It has no fields, and it's only purpose is just to treat a couple of nodes
as one node.
Note that in VRML 1.0 it's required that a whole VRML file consists of exactly one root node, so we actually had to use some grouping node here. For example the following file is invalid according to VRML 1.0 specification:
#VRML V1.0 ascii
Sphere { }
Cube { width 1.5 height 1.5 depth 1.5 }
Nevertheless the above example is handled by many VRML engines, including our engine described in this document.
In VRML 2.0, you don't place children nodes directly
inside the parent node. Instead you place children nodes inside
fields of type SFNode (this contains
zero (NULL) or one node) or
MFNode (this contains any number (possibly zero)
of nodes). For example, in VRML 2.0 Group
node has an MFNode field children,
so the example file in VRML 2.0 equivalent to previous example looks like
this:
#VRML V2.0 utf8
Group {
children [
Shape { geometry Sphere { } }
Shape { geometry Box { size 1.5 1.5 1.5 } }
]
}Syntax of MFNode is just like for other multiple-valued
fields: a sequence of values, inside brackets ([
and ]).
Example above also shows a couple of other differences between VRML 1.0 and 2.0:
In VRML 2.0 we have to wrap Sphere and
Box nodes inside a Shape node.
Node Cube from VRML 1.0 was renamed
to Box in VRML 2.0.
Size of the box in VRML 2.0 is specified
using size field of type SFVec3f,
while in VRML 1.0 we had three fields (width,
height, depth) of type
SFFloat.
While we're talking about VRML versions differences, note also that
in VRML 2.0 a file can have any number of root nodes. So actually
we didn't have to use Group node in our example,
and the following would be correct VRML 2.0 file too:
#VRML V2.0 utf8
Shape { geometry Sphere { } }
Shape { geometry Box { size 1.5 1.5 1.5 } }
To be honest, we have to point one more VRML difference: as was mentioned before, in VRML 2.0 shapes are unlit by default. So our VRML 2.0 examples above look like this:
To make them lit, we must assign a material
for them. In VRML 2.0 you do this by placing a Material
node inside material field of Appearance
node. Then you place Appearance node inside
appearance field of
appropriate Shape node. Result looks like this:
#VRML V2.0 utf8
Group {
children [
Shape {
appearance Appearance { material Material { } }
geometry Sphere { }
}
Shape {
appearance Appearance { material Material { } }
geometry Box { size 1.5 1.5 1.5 }
}
]
}
We didn't specify any Material node's fields,
so the default properties will be used. Default VRML 2.0 material properties
are the same as for VRML 1.0: light gray diffuse color and a slight
ambient color.
As you can see, VRML 2.0 description gets significantly more verbose than VRML 1.0, but it has many advantages:
The way how children nodes are specified
in VRML 2.0 requires you to always write an SFNode
or MFNode field name
(as opposed to VRML 1.0 where you just write the children nodes).
But the advantages are obvious: in VRML 2.0 you can explicitly
assign different meaning to different children nodes
by placing them within different fields. In VRML 1.0 all the
children nodes had to be treated in the same manner —
the only thing that differentiated children nodes was their
position within the parent.
As mentioned earlier, the default behavior of various VRML 2.0 parts is the one that is the easiest to render. That's why the default behavior is to render unlit, and you have to explicitly specify material to get lit objects.
This is a good thing, since it makes VRML authors more conscious about using features, and hopefully it will force them to create VRML worlds that are easier to render. In the case of rendering unlit objects, this is often perfectly acceptable (or even desired) solution if the object has a detailed texture applied.
Placing the Material
node inside the SFNode field of
Appearance, and then placing
the Appearance node inside
the SFNode field of Shape
may seem like
a “bondage-and-discipline language”, but it allows
various future enhancements of the language without breaking
compatibility. For example you could invent a node that allows
to specify materials using a different properties (like
by describing it's BRDF function, useful for rendering realistic images)
and then just allow this node as a value for the material
field.
Scenario described above actually happened.
First versions of VRML 97 specification didn't include
geospatial coordinates support, including a node
GeoCoordinate.
A node IndexedFaceSet has a field
coord used to specify a set of points for
geometry, and initially you could place a
Coordinate node there.
When specification of geospatial coordinates support was formulated
(and added to VRML 97 specification as optional for VRML browsers),
all that had to be changed was to say that now you can place
GeoCoordinate everywhere where earlier
you could use only Coordinate.
The Shape
node in VRML 2.0 contains almost whole information needed to render
given shape. This means that it's easier to create a VRML rendering engine.
We will contrast this with VRML 1.0 approach that requires
a lot of state information in Section 1.5, “VRML 1.0 state”.
Let's take a look at another grouping node:
VRML 2.0 Transform
node. This node specifies a transformation (a mix
of a translation, a rotation and a scale) for all it's children nodes.
The default field values are such that no transformation actually
takes place, because by default we translate by (0, 0, 0) vector,
rotate by zero angle and scale by 1.0 factor. This means that
the Transform node with all fields left as default
is actually equivalent to a Group node.
Example of a simple translation:
#VRML V2.0 utf8
Shape {
appearance Appearance { material Material { } }
geometry Box { }
}
Transform {
translation 5 0 0
children Shape {
appearance Appearance { material Material { } }
geometry Sphere { }
}
}Note that a child of a Transform node
may be another Transform node. All transformations
are accumulated. For example these two files are equivalent:
#VRML V2.0 utf8
Shape {
appearance Appearance { material Material { } }
geometry Box { }
}
Transform {
translation 5 0 0
children [
Shape {
appearance Appearance { material Material { } }
geometry Sphere { }
}
Transform {
translation 5 0 0
scale 1 3 1
children Shape {
appearance Appearance { material Material { } }
geometry Sphere { }
}
}
]
}#VRML V2.0 utf8
Shape {
appearance Appearance { material Material { } }
geometry Box { }
}
Transform {
translation 5 0 0
children Shape {
appearance Appearance { material Material { } }
geometry Sphere { }
}
}
Transform {
translation 10 0 0
scale 1 3 1
children Shape {
appearance Appearance { material Material { } }
geometry Sphere { }
}
}A Switch node
allows you to choose only one (or none) from
children nodes to be in the active (i.e. visible, participating
in collision detection etc.) part of the scene.
This is useful for various scripts and it's also useful
for hiding nodes referenced later — we will see
an example of this in Section 1.4, “DEF / USE mechanism”.
A Separator and a
TransformSeparator nodes in VRML 1.0.
We will see what they do in Section 1.5, “VRML 1.0 state”.
A LOD node
(the name is an acronym for level of detail)
specifies a different versions of the same object.
The intention is that all children nodes represent the same object,
but with different level of detail: first node is the most
detailed one (and difficult to render, check for collisions etc.),
second one is less detailed, and so on, until the last node
has the least details (it can even be empty, which can be
expressed by a Group node with
no children).
VRML browser should choose the appropriate children to render
based on the distance between the viewer and designated
center point.
Unfortunately, note that LOD is not implemented in our engine yet — for now we always use the most detailed version. As far as VRML specification is concerned, this is acceptable (although non-optimal) behavior.
VRML nodes may be named and later referenced. This allows
you to reuse the same node (which can be any VRML node type —
like a shape, a material, or even a whole group) more than once. The syntax
is simple: you name a node by writing
DEF <node-name> before node type.
To reuse the node, just write USE <node-name>.
This mechanism is available in all VRML versions.
Here's a simple example
that uses the same Cone
twice, each time with a different material color.
#VRML V2.0 utf8
Shape {
appearance Appearance {
material Material { diffuseColor 1 1 0 }
}
geometry DEF NamedCone Cone { height 5 }
}
Transform {
translation 5 0 0
children Shape {
appearance Appearance {
material Material { diffuseColor 0 0 1 } }
geometry USE NamedCone
}
}Using DEF / USE mechanism makes your VRML files
smaller and easier to author, and it also allows
VRML implementations to save resources (memory, loading time...).
That's because VRML implementation can allocate the node
once, and then just copy the pointer to this node.
VRML specifications are formulated to make this
approach always correct, even when mixed with features like
scripting or sensors. Note that some nodes
can “pull” additional data with them
(for example ImageTexture nodes will load
texture image from file), so the memory saving may be even larger.
Consider these two VRML files:
#VRML V2.0 utf8
Shape {
appearance Appearance {
texture DEF SampleTexture
ImageTexture { url "sample_texture.png" }
}
geometry Box { }
}
Transform {
translation 5 0 0
children Shape {
appearance Appearance {
texture USE SampleTexture
}
geometry Sphere { }
}
}#VRML V2.0 utf8
Shape {
appearance Appearance {
texture ImageTexture { url "sample_texture.png" }
}
geometry Box { }
}
Transform {
translation 5 0 0
children Shape {
appearance Appearance {
texture ImageTexture { url "sample_texture.png" }
}
geometry Sphere { }
}
}Both files above look the same when rendered, but in the first case VRML implementation loads the texture only once, since we know that this is the same texture node [3].
Note that the first node definition, with DEF
keyword, not only names the node, but also includes it in the file.
Often it's more comfortable to first define a couple of named
nodes (without actually using them) and then use them.
You can use the Switch node for this
— by default Switch node doesn't
include any of it's children nodes, so you can write
VRML file like this:
#VRML V2.0 utf8
Switch {
choice [
DEF RedSphere Shape {
appearance Appearance {
material Material { diffuseColor 1 0 0 } }
geometry Sphere { }
}
DEF GreenSphere Shape {
appearance Appearance {
material Material { diffuseColor 0 1 0 } }
geometry Sphere { }
}
DEF BlueSphere Shape {
appearance Appearance {
material Material { diffuseColor 0 0 1 } }
geometry Sphere { }
}
DEF SphereColumn Group {
children [
Transform { translation 0 -5 0 children USE RedSphere }
Transform { translation 0 0 0 children USE GreenSphere }
Transform { translation 0 5 0 children USE BlueSphere }
]
}
]
}
Transform { translation -5 0 0 children USE SphereColumn }
Transform { translation 0 0 0 children USE SphereColumn }
Transform { translation 5 0 0 children USE SphereColumn }
One last example shows a reuse of Coordinate
node. Remember that a couple of sections earlier we defined
a simple PointSet. PointSet
node has an SFNode field named coord.
You can place there a Coordinate node.
A Coordinate node, in turn, has a point
field of type SFVec3f that allows you to specify
point positions. The obvious question is “Why all this complexity ?
Why not just say that coord field is
of SFVec3f type and directly include the point
positions ?”. One answer was given earlier when talking
about grouping nodes: this allowed VRML specification for painless
addition of GeoCoordinate as an alternative
way to specify positions. Another answer is given by the example
below. As you can see, the same set of positions may be used
by a couple of different nodes[4].
#VRML V2.0 utf8
Switch {
choice DEF TowerCoordinates Coordinate {
point [
4.157832 4.157833 -1.000000,
4.889094 3.266788 -1.000000,
......
]
}
}
Shape {
appearance Appearance { material Material { } }
geometry IndexedFaceSet {
coordIndex [
63 0 31 32 -1,
31 30 33 32 -1,
......
]
coord USE TowerCoordinates
}
}
Transform {
translation 30 0 0
children Shape {
geometry IndexedLineSet {
coordIndex [
63 0 31 32 63 -1,
31 30 33 32 31 -1,
......
]
coord USE TowerCoordinates
}
}
}
Transform {
translation 60 0 0
children Shape {
geometry PointSet {
coord USE TowerCoordinates
}
}
}
Now that we know all about children relationships and DEF / USE mechanism, we can grasp the statement mentioned at the beginning of this chapter: every VRML file is a directed graph of nodes. It doesn't have cycles, although if we will forget about direction of edges (treat it as an undirected graph), we can get cycles (because of DEF / USE mechanism).
Note that VRML 1.0 file must contain exactly one root node,
while VRML 2.0 file is a sequence of any number of root nodes.
So, being precise, VRML graph doesn't have to be a connected graph.
But actually our engine when reading VRML file with many
root nodes just wraps them in an “invisible”
Group node. This special Group
node acts just like any other group node, but it's not written
back to the file (when e.g. using our engine to pretty-print VRML files).
This way, internally, we always see VRML file as a connected graph,
with exactly one root node.
In previous sections most of the examples were given only in VRML 2.0 version. Partially that's because VRML 2.0 is just newer and better, so you should use it instead of VRML 1.0 whenever possible. But partially that was because we avoided to explain one important behavior of VRML 1.0. In this section we'll fill the gap. Even if you're not interested in VRML 1.0 anymore, this information may help you understand why VRML 2.0 was designed the way it was, and why it's actually better than VRML 1.0. That's because part of the reasons of VRML 2.0 changes were to avoid the whole issue described here.
Historically, VRML 1.0 was based on Inventor file format,
and Inventor file format was
designed specifically with OpenGL implementation in mind.
Those of you who do any programming in OpenGL know that OpenGL
works as a state machine. This means
that OpenGL remembers a lot of “global” settings
[5]. When you want to render
a vertex (aka point) in OpenGL, you just call one simple command
(glVertex), passing only point coordinates.
And the vertex is rendered (along with a line or even a triangle
that it produces with other vertexes). What color does the vertex
has ? The last color specified by glColor
call (or glMaterial, mixed with lights).
What texture coordinate does it have ? Last texture coordinate
specified in glTexCoord call. What texture
does it use ? Last texture bound with glBindTexture.
We can see a pattern here: when you want to know what property
our vertex has, you just have to check what value we last assigned
to this property. When we talk about OpenGL state, we talk
about all the “last glColor”,
“last glTexCoord” etc. values
that OpenGL has to remember.
Inventor, and then VRML 1.0, followed a similar approach.
“What material does a sphere use ?” The one specified in the last
Material node. Take a look at the example:
#VRML V1.0 ascii
Group {
# Default material will be used here:
Sphere { }
DEF RedMaterial Material { diffuseColor 1 0 0 }
Transform { translation 5 0 0 }
# This uses the last material : red
Sphere { }
Transform { translation 5 0 0 }
# This still uses uses the red material
Sphere { }
Material { diffuseColor 0 0 1 }
Transform { translation 5 0 0 }
# Material changed to blue
Sphere { }
Transform { translation 5 0 0 }
# Still blue...
Sphere { }
USE RedMaterial
Transform { translation 5 0 0 }
# Red again !
Sphere { }
Transform { translation 5 0 0 }
# Still red.
Sphere { }
}Similar answers are given for other questions in the form “What is used ?”. Let's compare VRML 1.0 and 2.0 answers for such questions:
What texture is used ?
VRML 1.0 answer:
Last Texture2 node.
VRML 2.0 answer:
Node specified in enclosing Shape
appearance's texture field.
What coordinates are used by
IndexedFaceSet ?
VRML 1.0 answer:
Last Coordinate3 node.
VRML 2.0 answer:
Node specified in coord field
of given IndexedFaceSet.
What font is used by by AsciiText node
(renamed to just Text in VRML 2.0) ?
VRML 1.0 answer:
Last FontStyle node.
VRML 2.0 answer:
Node specified in fontStyle field
of given Text node.
So VRML 1.0 approach maps easily to OpenGL.
Simple VRML implementation can just traverse the scene graph,
and for each node do appropriate set of OpenGL calls.
For example, Material node will correspond
to a couple of glMaterial and glColor
calls. Texture2 will correspond to binding
prepared OpenGL texture. Visible shapes will cause
rendering of appropriate geometry, and so last
Material and Texture2
settings will be used.
In our example with materials above you can also
see another difference between VRML 1.0 and 2.0,
also influenced by the way things are done in OpenGL:
the way Transform node is used.
In VRML 2.0, Transform affected it's children.
In VRML 1.0, Transform node is not supposed to
have any children. Instead, it affects all subsequent nodes.
If we would like to translate last example to VRML 2.0,
each Transform node would have to be placed
as a last child of previous Transform node,
thus creating a deep nodes hierarchy. Alternatively, we could
keep the hierarchy shallow and just use
Transform { translation 5 0 0 ... }
for the first time, then Transform { translation 10 0 0 ... },
then Transform { translation 15 0 0 ... } and so on.
This means that simple VRML 1.0 implementation will just call
appropriate matrix transformations when processing Transform
node. In VRML 1.0 there are even more specialized
transformation nodes. For example a node Translation
that has a subset of features of full Transform node:
it can only translate. Such Translation
has an excellent, trivial mapping to OpenGL: just call
glTranslate.
There's one more important feature of OpenGL
“state machine” approach: stacks. OpenGL has a matrix
stack (actually, three matrix stacks for each matrix type) and
an attributes stack. As you can guess, there are nodes in VRML 1.0
that, when implemented in an easy way, map perfectly
to OpenGL push/pop stack operations: Separator
and TransformSeparator. When you use
Group node in VRML 1.0, the properties
(like last used Material and Texture2,
and also current transformation and texture transformation)
“leak” outside of Group node,
to all subsequent nodes.
But when you use Separator,
they do not leak out: all transformations and “who's the last
material/texture node” properties are unchanged
after we leave Separator node.
So simple Separator implementation in OpenGL
is trivial:
At the beginning, use glPushAttrib
(saving all OpenGL attributes that can be changed by VRML nodes)
and glPushMatrix (for both modelview and
texture matrices).
Then process all children nodes of
Separator.
Then restore state by glPopAttrib
and glPopMatrix calls.
TransformSeparator is a cross between
a Separator and a Group:
it saves only transformation matrix, and the rest of the state
can “leak out”. So to implement this in OpenGL,
you just call glPushMatrix (on modelview matrix)
before processing children and glPopMatrix
after.
Below is an example how various VRML 1.0 grouping nodes allow
“leaking”. Each column starts with a standard
Sphere node. Then we enter some
grouping node (from the left: Group,
TransformSeparator and Separator).
Inside the grouping node we change material, apply scaling transformation
and put another Sphere node — middle
row always contains a red large sphere. Then we exit
from grouping node and put the third Sphere node.
How does this sphere look like depends on used grouping node.
#VRML V1.0 ascii
Separator {
Sphere { }
Transform { translation 0 -3 0 }
Group {
Material { diffuseColor 1 0 0 }
Transform { scaleFactor 2 2 2 }
Sphere { }
}
# A Group, so both Material change and scaling "leaks out"
Transform { translation 0 -3 0 }
Sphere { }
}
Transform { translation 5 0 0 }
Separator {
Sphere { }
Transform { translation 0 -3 0 }
TransformSeparator {
Material { diffuseColor 1 0 0 }
Transform { scaleFactor 2 2 2 }
Sphere { }
}
# A TransformSeparator, so only Material change "leaks out"
Transform { translation 0 -3 0 }
Sphere { }
}
Transform { translation 5 0 0 }
Separator {
Sphere { }
Transform { translation 0 -3 0 }
Separator {
Material { diffuseColor 1 0 0 }
Transform { scaleFactor 2 2 2 }
Sphere { }
}
# A Separator, so nothing "leaks out".
# The last sphere is identical to the first one.
Transform { translation 0 -3 0 }
Sphere { }
}There are some advantages of VRML 1.0 “state” approach:
It maps easily to OpenGL.
Such easy mapping may be also quite efficient.
For example, if two nodes use the same Material
node, we can just change OpenGL material once (at the time
Material node is processed).
VRML 2.0 implementation must remember last set
Material node to achieve this purpose.
It's flexible. The way transformations are specified in VRML 2.0 forces us often to create deeper node hierarchies than in VRML 1.0.
And in VRML 1.0 we can easier share materials, textures, font styles
and other properties among a couple of nodes. In VRML 2.0 such reusing
requires naming nodes by
DEF / USE mechanism.
In VRML 1.0 we can simply let a couple of nodes have the same
node as their last Material (or similar) node.
But there are also serious problems with VRML 1.0 approach, that VRML 2.0 solves.
The argumentation about “flexibility” of VRML 1.0 above looks similar to argumentation about various programming languages (...programming languages that should remain nameless here...), that are indeed flexible but also allow the programmer to “shoot himself in the foot”. It's easy to forget that you changed some material or texture, and accidentally affect more than you wanted.
Compare this with the luxury of VRML 2.0
author: whenever you start writing a Shape
node, you always start with a clean state: if you don't specify
a texture, shape will not be textured, if you don't specify
a material, shape will be unlit, and so on. If you want to know
how given IndexedFaceSet will look like
when rendered, you just have to know it's
enclosing Shape node. More precisely,
the only things that you have to know for VRML 2.0 node
to render it are
enclosing Shape node,
accumulated transformation from
Transform nodes,
and some “global” properties: lights that affect this shape and fog properties. I call them “global” because usually they are applied to the whole scene or at least large part of it.
On the other hand, VRML 1.0 author or reader (human or program)
must carefully analyze the code before given node, looking for last
Material node occurrence etc.
The argumentation about “simple VRML 1.0 implementation” misses the point that such simple implementation will in fact suffer from a couple of problems. And fixing these problems will in fact force this implementation to switch to non-trivial methods. The problems include:
OpenGL stacks sizes are limited,
so a simple implementation will limit allowed depth
of Separator and TransformSeparator
nodes.
If we will change OpenGL state each time we process a state-changing node, then we can waste a lot of time and resources if actually there are no shapes using given property. For example this code
Separator {
Texture2 { filename "texture.png" }
}
will trick a naive implementation into loading from file and then loading to OpenGL context a completely useless texture data.
This seems like an irrelevant problem, but it will become a large problem as soon as we will try to use any technique that will have to render only parts of the scene. For example, implementing material transparency using OpenGL blending requires that first all non-transparent shapes are rendered. Also implementing culling of objects to a camera frustum will make many shape nodes in the scene ignored in some frames.
Last but not least: in VRML 1.0, grouping nodes
must process their children in order,
to collect appropriate state information needed to render
each shape. In VRML 2.0, there is no such requirement.
For example, to render a Group node
in VRML 2.0, implementation can process and render
children nodes in any order. Like said above,
VRML 2.0 must only know about current transformation
and global things like fog and lights. The rest of information
needed is always contained within appropriate Shape
node.
VRML 2.0 implementation can even ignore some
children in Group node if it's
known that they are not visible.
Example situations when implementation should be able to freely choose which shapes (and in what order) are rendered were given above: implementing transparency using blending, and culling to camera frustum.
More about the way how we solved this problem for both VRML 1.0 and 2.0 in Section 2.10, “Flat scene”. More about OpenGL blending and culling to frustum in Section 5.3, “Flat scene for OpenGL”.
Now that we're accustomed with VRML syntax and concepts, let's take a quick look at some notable VRML features that weren't shown yet.
A powerful tool of VRML is the ability to include one
model as a part of another. In VRML 2.0 we do this by
Inline node. It's url
field specifies the URL (possibly relative) of VRML file to load.
Note that our engine doesn't actually support URLs right now
and treats this just as a file name.
The content of referenced VRML file is placed at the position
of given Inline node. This means that you
can apply transformation to inlined content. This also
means that including the same file more than once is sensible
in some situations. But remember the remarks in Section 1.4, “DEF / USE mechanism”:
if you want to include the same file more than once, you should
name the Inline node and then just reuse it.
Such reuse will conserve resources.
url field is actually MFString
and is a sequence of URL values, from the most to least preferred one.
So VRML browser will try to load files from given URLs in order,
until a valid file will be found.
In VRML 1.0 the node is called WWWInline,
and the URL (only one is allowed, it's SFString
field) is specified in the field name.
When using our engine you can mix VRML versions and include VRML 1.0 file from VRML 2.0, or the other way around. Moreover, you can include 3DS and Wavefront OBJ files too.
#VRML V2.0 utf8
DEF MyInline Inline { url "reuse_cone.wrl" }
Transform {
translation 1 0 0
rotation 1 0 0 -0.2
children [
USE MyInline
Transform {
translation 1 0 0
rotation 1 0 0 -0.2
children [
USE MyInline
Transform {
translation 1 0 0
rotation 1 0 0 -0.2
children [
USE MyInline
Transform {
translation 1 0 0
rotation 1 0 0 -0.2
children [
USE MyInline
] } ] } ] } ] }Figure 1.15. Our earlier example of reusing cone inlined a couple of times, each time with a slight translation and rotation

VRML allows you to specify a texture coordinate transformation. This allows you to translate, scale and rotate visible texture on given shape.
In VRML 1.0, you do this by Texture2Transform
node — this works analogous to Transform,
but transformations are only 2D. Texture transformations in VRML 1.0
accumulate, just like normal transformations. Here's an example:
#VRML V1.0 ascii
Group {
Texture2 { filename "sample_texture.png" }
Cube { }
Transform { translation 3 0 0 }
Separator {
# translate texture
Texture2Transform { translation 0.5 0.5 }
Cube { }
}
Transform { translation 3 0 0 }
Separator {
# rotate texture by Pi/4
Texture2Transform { rotation 0.7853981634 }
Cube { }
}
Transform { translation 3 0 0 }
Separator {
# scale texture
Texture2Transform { scaleFactor 2 2 }
Cube { }
Transform { translation 3 0 0 }
# rotate texture by Pi/4.
# Texture transformation accumulates, so this will
# be both scaled and rotated.
Texture2Transform { rotation 0.7853981634 }
Cube { }
}
}Remember that we transform texture coordinates, so e.g. scale 2x means that the texture appears 2 times smaller.
VRML 2.0 proposes a different approach here:
We have similar TextureTransform node, but we can
use it only as a value for textureTransform field
of Appearance. This also means that there
is no way how texture transformations could accumulate.
Here's a VRML 2.0 file equivalent to previous VRML 1.0 example:
#VRML V2.0 utf8
Shape {
appearance Appearance {
texture DEF SampleTexture
ImageTexture { url "sample_texture.png" }
}
geometry Box { }
}
Transform {
translation 3 0 0
children Shape {
appearance Appearance {
texture USE SampleTexture
# translate texture
textureTransform TextureTransform { translation 0.5 0.5 }
}
geometry Box { }
}
}
Transform {
translation 6 0 0
children Shape {
appearance Appearance {
texture USE SampleTexture
# rotate texture by Pi/4
textureTransform TextureTransform { rotation 0.7853981634 }
}
geometry Box { }
}
}
Transform {
translation 9 0 0
children Shape {
appearance Appearance {
texture USE SampleTexture
# scale texture
textureTransform TextureTransform { scale 2 2 }
}
geometry Box { }
}
}
Transform {
translation 12 0 0
children Shape {
appearance Appearance {
texture USE SampleTexture
# scale and rotate the texture.
# There's no way to accumulate texture transformations,
# so we just do both rotation and scaling by
# TextureTransform node below.
textureTransform TextureTransform {
rotation 0.7853981634
scale 2 2
}
}
geometry Box { }
}
}
You can specify various navigation information using
the NavigationInfo node.
type field describes
preferred navigation type. You can “EXAMINE”
model, “WALK” in the model (with collision detection
and gravity) and “FLY” (collision detection,
but no gravity).
avatarSize
field sets viewer (avatar) sizes. These typically have to be
adjusted for each world to “feel right”.
Although you should note that VRML generally
suggests to treat length 1.0 in your world as “1 meter”.
If you will design your VRML world following this assumption,
then default avatarSize will feel quite adequate,
assuming that you want the viewer to have human size in your world.
Viewer sizes are used for collision detection.
Viewer size together with
visibilityLimit may be also used
to set VRML browsers Z-buffer near and far clipping planes.
This is the case with our engine. By default our engine tries to calculate
sensible values for near and far based on scene bounding box size.
You can also specify moving speed
(speed field), and whether head light is on
(headlight field).
To specify default viewer position and orientation in the
world you use Viewpoint node. In VRML 1.0,
instead of Viewpoint you have
PerspectiveCamera and
OrthogonalCamera (in VRML 2.0 viewpoint
is always perspective). Viewpoint and camera nodes may be generally
specified anywhere in the file. The first viewpoint/camera node
found in the file (but only in the active part of the file —
e.g. not in inactive children of Switch)
will be used as the starting position/orientation.
Note that viewpoint/camera nodes
are also affected by transformation.
Finally, note that my VRML viewer view3dscene has a useful function to print VRML viewpoint/camera nodes ready to be pasted to VRML file, see menu item “Console” -> “Print current camera node”.
Here's an example file. It defines a viewpoint (generated
by view3dscene) and a navigation info
and then includes actual world geometry from other file
(shown in our earlier example
about inlining).
#VRML V2.0 utf8
Viewpoint {
position 11.832 2.897 6.162
orientation -0.463 0.868 0.172 0.810
}
NavigationInfo {
avatarSize [ 0.5, 2 ]
speed 1.0
headlight TRUE
}
Inline { url "inline.wrl" }IndexedFaceSet nodes (and a couple of other
nodes in VRML 2.0 like ElevationGrid) have
some notable features to make their rendering better and
more efficient:
You can use non-convex faces if you set
convex field to FALSE.
It will be VRML browser's responsibility to correctly
triangulate them. By default faces are assumed to be
convex (following the general rule that the default behavior
is the easiest one to handle by VRML browsers).
By default shapes are assumed to be
solid which allows to use backface
culling when rendering them.
If you don't supply pre-generated normal vectors
for your shapes, they will be calculated by the VRML
browser. You can control how they will be calculated
by the creaseAngle field: if the angle
between adjacent faces will be less than specified
creaseAngle, the normal vectors in
appropriate points will be smooth. This allows you
to specify preferred “smoothness” of the
shape. In VRML 2.0 by default creaseAngle is zero
(so all normals are flat; again this follows the rule that
the default behavior is the easiest one for VRML browsers).
See example below.
For VRML 1.0 the creaseAngle,
backface culling and convex faces settings are controlled by
ShapeHints node.
All VRML shapes have some sensible default texture mapping.
This means that you don't
have to specify texture coordinates if you want the texture
mapped. You only have to specify some texture.
For IndexedFaceSet the default
texture mapping adjusts to shape's bounding box (see
VRML specification for details).
Here's an example of the creaseAngle
use. Three times we define the same geometry in IndexedFaceSet
node, each time using different creaseAngle values.
The left tower uses creaseAngle 0, so all
faces are rendered flat. Second tower uses creaseAngle 1
and it looks good — smooth where it should be.
The third tower uses creaseAngle 4,
which just means that normals are smoothed everywhere (this case
is actually optimized inside our engine, so it's calculated
faster) — it looks bad, we can see that normals are
smoothed where they shouldn't be.
#VRML V2.0 utf8
Viewpoint {
position 31.893 -69.771 89.662
orientation 0.999 0.022 -0.012 0.974
}
Switch {
choice DEF TowerCoordinates Coordinate {
point [
4.157832 4.157833 -1.000000,
4.889094 3.266788 -1.000000,
........
]
}
}
Transform {
children Shape {
appearance Appearance { material Material { } }
geometry IndexedFaceSet {
coordIndex [
63 0 31 32 -1,
31 30 33 32 -1,
........
]
coord USE TowerCoordinates
creaseAngle 0
}
}
}
Transform {
translation 30 0 0
children Shape {
appearance Appearance { material Material { } }
geometry IndexedFaceSet {
coordIndex [
63 0 31 32 -1,
31 30 33 32 -1,
........
]
coord USE TowerCoordinates
creaseAngle 1
}
}
}
Transform {
translation 60 0 0
children Shape {
appearance Appearance { material Material { } }
geometry IndexedFaceSet {
coordIndex [
63 0 31 32 -1,
31 30 33 32 -1,
........
]
coord USE TowerCoordinates
creaseAngle 4
}
}
}These constructions define new VRML nodes in terms of already available ones. The idea is basically like macros, but it works on VRML nodes level (not on textual level, even not on VRML tokens level) so it's really safe.
These constructions define syntax of new VRML nodes, without defining their implementation. The implementation can be specified in other VRML file (using normal prototypes mentioned above) or can be deduced by particular VRML browser using some browser-specific means (for example, a browser may just have some non-standard nodes built-in). If a browser doesn't know how to handle given node, it can at least correctly parse the node (and ignore it).
For example, many VRML browsers handle some non-standard VRML nodes. If you use these nodes and you want to make your VRML files at least readable by other VRML browsers, you should declare these non-standard nodes using external prototypes.
Even better, you can provide a list of proposed implementations for each external prototype. They are checked in order, VRML browser should chose the first implementation that it can use. So you can make the 1st item a URN that is recognized only by your VRML browser, and indicanting built-in node implementation. And the 2nd item may point to a URL with another VRML file that at least partially emulates the functionality of this non-standard node, by using normal prototype. This way other VRML browsers will be able to at least partially make use of your node.
Our engine handles prototypes and external prototypes perfectly
(since around September 2007). We have some non-standard VRML extensions
(see
Kambi VRML game engine extensions list),
and they can be declared as external prototypes
with URN like
"urn:vrmlengine.sourceforge.net:node:KambiTriangulation".
So other VRML browsers should be able to at least parse them.
There are some very notable VRML 97 features that I didn't describe in this document, simply because they are not implemented yet. To name just a few:
They define animation by interpolation of appropriate sets of values. My engine also allows to do animations, also by interpolating, so the internal approach is actually the same. However the way to specify the animations for my engine is not by VRML nodes, but instead by providing two or more VRML models with the same structure. My approach has some advantages and some disadvantages when compared to VRML interpolators, the details will be explained in Chapter 6, Animation.
VRML 97 specification includes a great support for extending content with any kind of external language. Detailed description of Java and ECMAScript (JavaScript) bindings is given by the specification, and it's expected that other languages could use similar approaches. X3D specification pushes this even further, by describing external language interface in a way that is “neutral” to actual programming language (which means that it should be applicable to pretty much all existing programming languages).
Scripts can be invoked on various events, and the events can in turn be generated by various nodes. In particular, sensor nodes are special kind of nodes that were designed only to generate events in particular situations.
My engine doesn't support any kind of scripting for now. My initial approach was directed at making special programs (like games ...) that simply use the VRML engine, so any logic was expressed in normal ObjectPascal code that was later compiled.
Of course, it would be great to implement scripting and move as much of this logic as possible to VRML files. For VRML authors, this is also the way to not be tied to any particular VRML engine. Although for really large programs there's no way that whole logic could be moved into a scripting language...
NURBS curves and surfaces. Optional in VRML 97 specification (but obviously terribly useful, so their lack in current implementation deserves mention).
[1] VRML 2.0 files are always encoded using plain text in utf8. There was a plan to design other encodings, but it was never realized for VRML 2.0. VRML 2.0 files distributed on WWW are often compressed with gzip, we can say that it's a “poor-man's binary encoding”.
X3D (VRML 2.0 successor) filled the gap by specifying three encodings available: “classic VRML encoding” (this is exactly what VRML 2.0 uses), an XML encoding and a binary encoding.
[2] But also note that our engine doesn't support utf8
yet. In particular, when rendering Text node,
the string is treated as a sequence of 8-bit characters
in ISO-8859-1 encoding.
[3] Actually, in the second case, our engine can also figure out that this is the same texture filename and not load the texture twice. But the first case is much “cleaner” and should be generally better for all decent VRML implementations.
[4] I do not cite full VRML source
code here, as it includes a long list of coordinates and indexes generated by
Blender exporter. See VRML files distributed with this document:
full source is in the file examples/reuse_coordinate.wrl.
[5] Actually, they are remembered for each OpenGL context. And, ideally, they are partially “remembered” on graphic board. But we limit our thinking here only to the point of view of a typical program using OpenGL.
Table of Contents
This and the following chapters will describe how our VRML engine works. We will describe used data structures and algorithms. Together this should give you a good idea of what our engine is capable of, where are it's strengths and weaknesses, and how it's all achieved.
In this document we should not go
into details about some ObjectPascal-specific language constructs
or solutions — this would be too low-level stuff, uninteresting
from a general point of view. If you're an ObjectPascal programmer
and you want to actually use my engine then you may find it helpful
to study
source code (especially example programs in examples
subdirectories) and
units reference while reading this document.
If you only want to read this document, everything that you need
is some basic idea about object-oriented programming.
The base class of our engine is the TVRMLNode
class, not surprisingly representing a VRML node. This is an abstract
class, for all specific VRML node types we have some descendant
of TVRMLNode defined. Naming convention for non-abstract
node classes is like TNodeCoordinate class for VRML
Coordinate node type.
Every VRML node has it's fields available in it's
Fields property. You can also access
individual fields by properties named like FdXxx,
for example FdPoint is a property of
TNodeCoordinate class that represents
point field of Coordinate node.
VRML 1.0 children nodes are accessed by Children
and ChildrenCount properties.
For VRML 2.0 this is not needed, since you access all children nodes
by accessing appropriate SFNode and MFNode
fields. A convenience properties named
SmartChildren and SmartChildrenCount
are defined: for “normal” VRML 2.0 grouping nodes
(this mostly means nodes with MFNode field named
children) the SmartChildrenXxx
properties operate on appropriate MFNode, for other
nodes they operate on VRML 1.0 ChildrenXxx properties.
Because of DEF / USE mechanism
each node may be a children (“children” both in the
VRML 1.0 and 2.0 senses) of more than one node. This means that
we cannot use some trivial destructing strategy. When we destruct
some node's instance, we cannot simply destruct all it's children,
because they are possibly used in other nodes. The simple solution
to this is to keep track in each node about it's parents.
Each node has properties ParentNodes
and ParentNodesCount that track information about
all the nodes that use it in VRML 1.0 style (i.e. on
TVRMLNode.Children list). And properties
ParentFields
and ParentFieldsCount that track information about
all the SFNode and MFNode
fields referencing this node. The children node is automatically destroyed
when it has no parents — which means that
both ParentNodesCount and
ParentFieldsCount are zero.
Effectively, we implemented reference-counting.
And as a bonus, ParentXxx properties are sometimes
helpful when we want to do some “bottom-to-top”
processing of VRML graph (although this should be generally avoided,
“top-to-bottom” processing is much more in the spirit
of the VRML graph).
Classes for VRML nodes specific to particular VRML version
get a suffix _1 or _2 representing
their intended VRML version. For example, we have
TNodeIndexedFaceSet_1 (for VRML 1.0) and
TNodeIndexedFaceSet_2 (for VRML 2.0) classes.
Such nodes always have their ForVRMLVersion method
overridden to indicate in what VRML version they are allowed to be used.
For example, when parser starts reading IndexedFaceSet
node, it creates either TNodeIndexedFaceSet_1 or
TNodeIndexedFaceSet_2, depending on VRML version
indicated in the file header line. Note that this separation between VRML versions
is done only when reading VRML nodes from file. When processing
VRML nodes graph by code you can freely mix VRML nodes from various
VRML versions and everything will work, including writing nodes
back to VRML file (although if you mix VRML versions too carelessly
you may get VRML file that can only be read back by my engine, and not
by other engines that may be limited to only VRML 1.0 or only VRML 2.0).
More on this later in Section 2.2, “The sum of VRML 1.0 and 2.0”.
The result of parsing any VRML file is always a single
TVRMLNode instance representing the root node of the given file.
If the file had more than one root node [6]
then our engine wraps them in an additional Group
node. More precisely, additional instance of TNodeGroupHidden_1
or TNodeGroupHidden_2 is created. They descend
from TNodeGroup_1 and TNodeGroup_2,
accordingly, and so can be always treated as 100% normal Group
nodes. At the same time, VRML writing code can take special precautions
to not record these “fake” group nodes back to VRML file.
Our engine handles both VRML 1.0 and VRML 2.0. As we have seen in Chapter 1, Overview of VRML, there are important differences between these VRML versions. The way how I decided to handle both VRML versions is the more difficult, but also more complete approach. Effectively, you have the sum of VRML 1.0 and 2.0 features available.
I decided to avoid trying to create some internal conversions from VRML 1.0 to VRML 2.0, or VRML 2.0 to 1.0, or to some newly invented internal format. I wanted to have a full, flexible, 100% conforming to VRML 1.0 and VRML 2.0 specifications engine. And the fact is that any conversion along the way will likely cause problems — ideologically speaking, that's because there is always something lost, or at least difficult to recover, when a complicated conversion is done.
Practically here are some reasons why a simple conversion between VRML 1.0 and VRML 2.0 is not possible, in any direction:
VRML 2.0 specification authors intentionally wanted to simplify some things that people (both VRML world authors and VRML browser implementors) thought were unnecessarily complicated in VRML 1.0. This causes problems for a potential converter from VRML 1.0 to 2.0, since it will have trouble to express some VRML 1.0 constructs. For example:
In VRML 1.0 you can specify multiple materials for a single geometry node. In VRML 2.0 each geometry node uses at most one material. So a potential converter from VRML 1.0 to 2.0 may need to split geometry nodes.
In VRML 1.0 you can accumulate
texture transformations (Texture2Transform
nodes). In VRML 2.0 you can't (you can only place one
TextureTransform node in the
Appearance.textureTransform field).
So a potential converter must accumulate texture transformations
on it's own. And this is not trivial in a general case,
because you can't directly specify texture transformation matrix
in VRML 2.0. Instead you have to express texture transformation
in terms of one translation, one rotation and one scaling.
In VRML 1.0 you can specify any 4x4 matrix transformation
using MatrixTransformation node.
This is not possible at all in VRML 2.0. In
VRML 2.0 geometry transformation must be specified in terms of
translations, rotations and scaling.
In VRML 1.0 you can limit
which geometry nodes are affected by PointLight or
SpotLight by placing light nodes at particular points
in the node hierarchy. That's because in VRML 1.0 light nodes work just
like other “state changing” nodes: they affect
all subsequent nodes, until blocked by the end of the
Separator node.
In VRML 2.0 this doesn't work.
You cannot control what parts of the scene are
affected by light nodes by placing light nodes at some
particular place in the node hierarchy. Instead,
you have to use radius field of light nodes.
This means that some VRML 1.0 tricks are simply not possible.
OrthographicCamera
is not possible to express using VRML 2.0 standard nodes.
Summary: in certain cases translating VRML 1.0 to 2.0 can be very hard or even impossible. If we want to handle VRML 1.0 perfectly, we can't just write a converter from VRML 1.0 to 2.0 and then define every operation only in terms of VRML 2.0.
On the other hand, VRML 2.0 also includes various things
not present in VRML 1.0. This includes many new nodes,
that often cannot be expressed at all in VRML 1.0:
all sensors, scripts, interpolators, special things like
Collision and Billboard.
Moreover, VRML 2.0 uses SFNode
(with possible NULL value) and
MFNode, and generally
reduces the state that needs to be remembered when
processing VRML graph. This means that many
existing features have to be expressed differently.
For example consider specifying normals for
IndexedFaceSet. In VRML 2.0
everything that decides about how generated normals
are supplied are the normal
and normalIndex fields of given
IndexedFaceSet node.
We take advantage of the SFNode
field type, and say that whole Normal
node may be just placed within normal field of
IndexedFaceSet.
So we just keep whole knowledge inside IndexedFaceSet
node.
On the other hand, in VRML 1.0 we have to use the value of
last NormalBinding node. This says whether we
should use the last Normal node, and how.
Potential VRML 2.0 to 1.0 converter would have to make a lot of effort to “deconstruct” VRML 2.0 shape properties back to VRML 1.0 state nodes. This makes conversion difficult to revert (e.g. when we want to write VRML 2.0 content back to file).
That's why I decided to support in my engine the sum of all VRML features.
For example, VRML 1.0 nodes can have direct children nodes, so I support it
(by Children property of TVRMLNode).
VRML 2.0 nodes can have children nodes through SFNode
and MFNode fields, so I support it too.
I'm not trying hard to “combine”
these two ideas (direct children nodes and children inside
MFNode) into one
— I just implement and handle them both
[7].
In some cases this approach forces me to do more work. For example, for many routines that calculate bounding boxes of shape nodes, I had to prepare three routines:
Common implementation, as a static procedure inside the
VRMLNodes unit. This handles actual calculation
and as parameters expects already calculated properties of given
shape node. As a simple example, when calculating bounding box
of a cube, we expect to get three parameters describing cube's sizes in
X, Y and Z dimension.
VRML 1.0 implementation in VRML 1.0-specific
node version that calls the common implementation,
after preparing parameters for common implementation.
As a simple example, TNodeCube_1 (VRML 1.0 cube shape)
just uses it's FdWidth, FdHeight
and FdDepth as appropriate sizes.
And VRML 2.0 implementation in VRML 2.0-specific
node version, that also calls the common implementation after
preparing it's parameters.
As a simple example, TNodeBox (VRML 2.0 cube shape)
accesses three items of it's FdSize field to get
the appropriate sizes.
In our simple example above we talked about a cube shape,
and the whole issue with calculating three size values differently
for VRML 1.0 and 2.0 was actually trivial.
But the point is that for some nodes,
like IndexedFaceSet, this is much harder.
For VRML authors this “sum” approach means that when reading VRML 1.0, many VRML 2.0 constructs (that not conflict with anything in VRML 1.0) are allowed, and the other way around too. That's why you can actually mix VRML 1.0 and 2.0 code in my engine. Consider this strange file:
#VRML V2.0 utf8
Separator {
DEF VRML2Cube Shape {
appearance Appearance { material Material { } }
geometry Sphere { }
}
Translation { translation 3 0 0 }
USE VRML2Cube
Transform {
translation 3 0 0
children [
USE VRML2Cube
Translation { translation 3 0 0 }
USE VRML2Cube
]
}
}This file uses VRML 2.0 sphere that is transformed using
both VRML 1.0 approach (Translation node that affects
all subsequent nodes) and VRML 2.0 approach (Transform
node that affects all it's children). And everything works:
VRML 1.0 nodes are handled according to VRML 1.0 specification,
VRML 2.0 according to VRML 2.0 specification. Transformations,
no matter which VRML version was used to specify them, affect all shapes.
The file's header line
says that it's supposed to be VRML 2.0, and this means that when
we have a node name that is possible in both VRML specifications
(but must be handled differently in each version), for example
Transform node, file header decides which version
of this node (TNodeTransform_1 or
TNodeTransform_2) is created when parsing this file.
This also means that you have many VRML 2.0 features available
in VRML 1.0. VRML 2.0 nodes like Background,
Fog and many others, that express features not available
at all in standard VRML 1.0, may be freely placed inside VRML 1.0 models
when using our engine.
Also including (using WWWInline or
Inline nodes) VRML 1.0 files within VRML 2.0
files (and the other way around) is possible. Each VRML file will
be parsed taking into account it's own header line, and then included
content is actually placed as a children node of including
WWWInline or Inline node.
So you get VRML graph hierarchy with nodes mixed from both VRML versions.
You can create a node using CreateParse
constructor to parse the node. Or you can initialize node
contents by parsing it using Parse method.
However, these both approaches require you to
first prepare appropriate TVRMLLexer
instance and a list of read node names.
There are comfortable routines like ParseVRMLFile
that take care of this for you. They create appropriate lexer,
and may create also suitable TStream instance to read
given file content.
Some details about parsing:
Our VRML lexer is a unified lexer for both VRML 1.0 and 2.0. Most of the VRML 1.0 and 2.0 lexical syntax is identical, minor differences can be handled correctly by a lexer because it always knows VRML header line of the given file. So it knows what syntax to expect.
Note that VRML version of the file
from where the node was read is not saved anywhere in
the TVRMLNode instance. This information
is lost after parsing has ended and lexer is destroyed.
This is intentional.
Only while parsing, ForVRMLVersion
method mentioned earlier may be used to decide which node classes
to create based on VRML version indicated in the file's header line.
This creates a question when saving VRML nodes
back to file: what VRML header to write ?
We will solve it by SuggestedVRMLVersion
method, described in Section 2.4.2, “Determining VRML version when writing”.
To properly handle
DEF / USE mechanism
we keep a list of known node names while parsing.
After a node with DEF clause
is parsed we add the node name and it's reference to
NodeNameBinding
list that is passed through all parse routines.
When a USE clause is encountered,
we just search this list for appropriate node name.
Simple VRML rules of DEF / USE
behavior make this approach correct. Remember that VRML name
scope is not modeled after normal programming languages,
where name scope of an identifier is usually limited
to the structure (function, class, etc.) where this identifier
is declared. In VRML, name scope always spans to the end
of whole VRML file (or to the next DEF
occurrence with the same name, that overrides previous
name). Also, the name scope is always limited to the current file
— for example, you cannot use names defined in other VRML
files (that you included by Inline nodes,
or that include you).
(Prototypes and external prototypes in VRML 2.0 are designed
to allow reusing VRML code between different VRML files.)
The simple trick with adding our name to NodeNameBinding
after the node is fully parsed prevents
creating loops in our graph, in case supplied VRML file is invalid.
SaveToStream method of TVRMLNode
class allows you to save node contents (including children nodes) to any stream.
Just like for reading, there are also more comfortable routines for writing
called SaveToVRMLFile.
When writing we also keep track of all node names defined
to make use of DEF / USE mechanism. If we want to write a named node,
we first check NodeNameBinding list whether the same name
with the same node was already written to file. If yes, then we
can place a USE statement, otherwise we have
to actually write the node's contents and add given node to
NodeNameBinding list.
The advantages of above NodeNameBinding
approach is that it always works correctly.
Even for node graphs created by code
(as opposed to node graphs read earlier from VRML file).
If node graph was obtained by reading VRML file, then the
DEF / USE statements will be correctly written back to the file, so there will
not be any unnecessary size bloat. But note that in some cases
if you created your node graph by code then some node contents
may be output more than once in the file:
First of all, that's because we can use DEF / USE mechanism only for nodes that are named. For unnamed nodes, we will have to write them in expanded form every time. Even if they were nicely shared in node graph.
Second of all, VRML name scope is weak
and if you use the same node name twice, then you may
force our writing algorithm to write node in expanded
form more than once (because you “overridden”
node name between the first DEF clause
and the potential place for corresponding USE clause).
So if you process VRML nodes graph by code and you want to maximize the chances that DEF / USE mechanism will be used while writing as much as it can, then you should always name your nodes, and name them uniquely.
It's not hard to design a general approach that will always
automatically make your names unique. VRML 97 annotated
specification suggests adding to the node name an _ (underscore)
character followed by some integer for this purpose. For example,
in our engine you can enumerate all nodes (use EnumerateNode
method), and for each node that is used more than once (you can
check it easily: such node will have ParentNodesCount + ParentFieldsCount > 1)
you can append '_' + PtrUInt(Pointer(Node)) to the
node name. The only problem with this approach (and the reason why
it's not done automatically) is that you will have to strip
these suffixes later, if you will read this file back
(assuming that you want to get the same node names).
This can be easily done (just remove everything following the last underscore
in the names of multiply instantiated nodes).
But then if you load the created VRML file into some other
VRML browser, you may see these internal suffixes anyway.
That's why my decision was that by default such behavior is not done.
So the generated VRML file will always have exactly the same node
names as you specified.
Since our engine supports various VRML versions, there appears
a question when writing VRML files: what VRML version to indicate
in created file header ? One solution would be to save for this purpose
in TVRMLNode version numbers of the original VRML
file from where the node was read.
But our engine must also allow easy construction
of VRML files by code, so not every node is obtained from parsing
some file. Adding some public fields to TVRMLNode
or parameters for SaveToVRMLFile to explicitly
indicate desired version is one simple solution, but it's tiresome
— it's another piece of information that will have to be
figured out and provided when constructing VRML files by code.
Finally the approach we take in our engine places the burden on
implementation of each TVRMLNode descendant.
If you only create nodes of already defined classes, everything should just magically
work. Every TVRMLNode descendant can override
SuggestedVRMLVersion method to indicate
it's desired VRML version, and how strong is the preference
(SuggestionPriority parameter).
The idea is that node decides about the desired VRML version
by collecting desired VRML version of all it's children and then
adding his own preference. And there is a SuggestionPriority
parameter, so that VRML files that use our engine extension
that allows to mix VRML 1.0 and 2.0 constructions
(see Section 2.2, “The sum of VRML 1.0 and 2.0”) will also be written
correctly, i.e. using the closest VRML version for them.
When writing VRML file, the SuggestedVRMLVersion method
of root node is called and used to determine header line for the VRML file.
This means that if you have VRML nodes graph (either read from a file or constructed by code) using only VRML 1.0 features then it will be correctly written as VRML 1.0 file. Same for a file using o