VRML processing and rendering engine

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

Goals
1. Overview of VRML
1.1. First example
1.2. Fields
1.2.1. Field types
1.2.2. Placing fields within nodes
1.2.3. Examples
1.3. Children nodes
1.3.1. Group node examples
1.3.2. The Transform node
1.3.3. Other grouping nodes
1.4. DEF / USE mechanism
1.4.1. VRML file as a graph
1.5. VRML 1.0 state
1.5.1. Why VRML 2.0 is better
1.6. Other important VRML features
1.6.1. Inline nodes
1.6.2. Texture transformation
1.6.3. Navigation
1.6.4. IndexedFaceSet features
1.6.5. Prototypes
1.6.6. Beyond what is implemented
2. Reading, writing, processing VRML scene graph
2.1. TVRMLNode class basics
2.2. The sum of VRML 1.0 and 2.0
2.3. Reading VRML files
2.4. Writing VRML files
2.4.1. DEF / USE mechanism when writing
2.4.2. Determining VRML version when writing
2.4.3. VRML graph preserving
2.5. Constructing and processing VRML graph by code
2.6. Traversing VRML graph
2.7. Shape nodes features
2.7.1. Bounding boxes
2.7.2. Triangulating
2.8. WWWBasePath property
2.9. Defining your own VRML nodes
2.10. Flat scene
2.10.1. List of shape+state pairs
2.10.2. Various comfortable routines
2.10.3. Caching
3. Octrees
3.1. Collision detection
3.2. How octree works
3.2.1. Checking for collisions using the octree
3.2.2. Constructing octree
3.3. Similar data structures
4. Ray-tracer rendering
4.1. Using octree
4.2. Classic deterministic ray-tracer
4.3. Path-tracer
4.4. RGBE format
4.5. Generating light maps
5. OpenGL rendering
5.1. VRML lights rendering
5.1.1. Lighting model
5.1.2. Rendering lights separately
5.2. Basic OpenGL rendering
5.2.1. OpenGL resource cache
5.2.2. Specialized OpenGL rendering routines vs Triangulate approach
5.3. Flat scene for OpenGL
5.3.1. Material transparency using OpenGL alpha blending
5.3.2. Material transparency using polygon stipple
5.3.3. Display lists strategies
5.3.4. Shape+state granularity
6. Animation
6.1. How does it work
6.2. Comparison with VRML interpolator nodes
6.3. Future plans
6.3.1. Perform interpolation at rendering time
6.3.2. Handling of VRML interpolator nodes
7. Shadows
7.1. Quick overview how to use shadow volumes in our engine
7.2. Inspecting models with shadow_volume_test
7.3. Ghost shadows
7.4. Problems with BorderEdges (models not 2-manifold)
7.4.1. Lack of shadows (analogous to ghost shadows)
7.4.2. Not closed silhouettes due to BorderEdges
7.4.3. Invalid capping for z-fail method
8. Links
8.1. VRML specifications
8.2. Author's resources

List of Figures

1.1. VRML 1.0 sphere example
1.2. VRML 2.0 sphere example
1.3. Cylinder example, rendered in wireframe mode (because it's unlit, non-wireframe rendering would look confusing)
1.4. VRML points example: yellow point at the bottom, blue point at the top
1.5. A cube and a sphere in VRML 1.0
1.6. An unlit box and a sphere in VRML 2.0
1.7. A box and a translated sphere
1.8. A box, a translated sphere, and a translated and scaled sphere
1.9. Two cones with different materials
1.10. A box and a translated sphere using the same texture
1.11. Three columns of three spheres
1.12. Faces, lines and point sets rendered using the same Coordinate node
1.13. Spheres with various material in VRML 1.0
1.14. An example how properties “leak out” from various grouping nodes in VRML 1.0
1.15. Our earlier example of reusing cone inlined a couple of times, each time with a slight translation and rotation
1.16. Textured cube with various texture transformations
1.17. Viewpoint defined for our previous example with multiplied cones
1.18. Three towers with various creaseAngle settings
2.1. Four spheres in mixed VRML 1.0 and 2.0 code
2.2. Different triangulations example (wireframe view)
2.3. Different triangulations example (Gouraud shading)
2.4. Different triangulations example (ray-tracer rendering)
3.1. A sample octree constructed for a scene with two boxes and a sphere
3.2. A nasty case when a box is considered to be colliding with a frustum, but in fact it's outside of the frustum
4.1. lets_take_a_walk scene, side view
4.2. lets_take_a_walk scene, top view
4.3. Generated ground texture
4.4. lets_take_a_walk scene, with ground texture. Side view
4.5. lets_take_a_walk scene, with ground texture. Top view.
5.1. Rendering without the fog (camera frustum culling is used)
5.2. Rendering with the fog (only objects within the fog visibility range need to be rendered)
5.3. The ghost creature on this screenshot is actually very close to the player. But it's transparent and is rendered incorrectly: gets covered by the ground and trees.
5.4. The transparent ghost rendered correctly: you can see that it's floating right before the player.
5.5. Material transparency with random stipples
5.6. Material transparency with regular stipples
5.7. All the trees visible on this screenshot are actually the same tree model, only moved and rotated differently.
5.8. The correct rendering of the trees with volumetric fog. Using roSeparateShapeStates optimization.
5.9. The wrong rendering of the trees with volumetric fog. Using roSeparateShapeStatesNoTransform optimization.
7.1. Fountain level, no shadows
7.2. Fountain level, shadows turned on
7.3. Fountain level, edges marked
7.4. Fountain level, only edges
7.5. Ghost shadows
7.6. Lack of shadows, problem analogous to ghost shadows
7.7. A cylinder capped at the top, open at the bottom
7.8. Cylinder open at the bottom with shadow quads
7.9. Cylinder open at the bottom with shadow edges
7.10. Good shadow from a single triangle
7.11. Good shadow from a single triangle, with shadow volumes drawn
7.12. Bad shadow from a single triangle

Goals

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.

Chapter 1. Overview of VRML

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").

1.1. First example

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.

  1. 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.

  2. 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).

Figure 1.1. VRML 1.0 sphere example

VRML 1.0 sphere example

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.

Figure 1.2. VRML 2.0 sphere example

VRML 2.0 sphere example

1.2. Fields

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.

1.2.1. Field types

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:

SFFloat

A 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.

SFBool

A 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, SFVec3f

Vector 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.

SFColor

Syntax 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.

SFRotation

Four floating point values specifying rotation around an axis. First three values specify an axis, fourth value specifies the angle of rotation (in radians).

SFImage

This 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.

SFString

A 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].

SFNode

This 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:

  1. An opening bracket ([).

  2. 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.

  3. A closing bracket (]). Note that you can omit both brackets if your MF-field has exactly one value.

1.2.2. Placing fields within nodes

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.

1.2.3. Examples

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)

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 ] }
  }
}

Figure 1.4. VRML points example: yellow point at the bottom, blue point at the top

VRML points example: yellow point at the bottom, blue point at the top

1.3. Children nodes

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.

1.3.1. Group node examples

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 }
}

Figure 1.5. A cube and a sphere in VRML 1.0

A cube and a sphere in VRML 1.0

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:

  1. In VRML 2.0 we have to wrap Sphere and Box nodes inside a Shape node.

  2. Node Cube from VRML 1.0 was renamed to Box in VRML 2.0.

  3. 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:

Figure 1.6. An unlit box and a sphere in VRML 2.0

An unlit box and a sphere in VRML 2.0

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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”.

1.3.2. The Transform node

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 { }
  }
}

Figure 1.7. A box and a translated sphere

A box and a translated 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 { }
  }
}

Figure 1.8. A box, a translated sphere, and a translated and scaled sphere

A box, a translated sphere, and a translated and scaled sphere

1.3.3. Other grouping nodes

  • 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.

1.4. DEF / USE mechanism

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
  }
}

Figure 1.9. Two cones with different materials

Two cones with different materials

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 { }
  }
}

Figure 1.10. A box and a translated sphere using the same texture

A box and a translated sphere using the same texture

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 }

Figure 1.11. Three columns of three spheres

Three columns of three spheres

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
    }
  }
}

Figure 1.12. Faces, lines and point sets rendered using the same Coordinate node

Faces, lines and point sets rendered using the same Coordinate node

1.4.1. VRML file as a graph

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 “invisibleGroup 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.

1.5. VRML 1.0 state

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 { }
}

Figure 1.13. Spheres with various material in VRML 1.0

Spheres with various material in VRML 1.0

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:

  1. At the beginning, use glPushAttrib (saving all OpenGL attributes that can be changed by VRML nodes) and glPushMatrix (for both modelview and texture matrices).

  2. Then process all children nodes of Separator.

  3. 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 { }
}

Figure 1.14. An example how properties “leak out” from various grouping nodes in VRML 1.0

An example how properties leak out from various grouping nodes in VRML 1.0

1.5.1. Why VRML 2.0 is better

There are some advantages of VRML 1.0 “state” approach:

  1. 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.

  2. 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.

  1. 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.

  2. 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.

  3. 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”.

1.6. Other important VRML features

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.

1.6.1. Inline nodes

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.

An example:

#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

Our earlier example of reusing cone inlined a couple of times, each time with a slight translation and rotation

1.6.2. Texture transformation

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 { }
  }
}

Figure 1.16. Textured cube with various texture transformations

Textured cube with various texture transformations

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 { }
  }
}

1.6.3. Navigation

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" }

Figure 1.17. Viewpoint defined for our previous example with multiplied cones

Viewpoint defined for our previous example with multiplied cones

1.6.4. IndexedFaceSet features

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
    }
  }
}

Figure 1.18. Three towers with various creaseAngle settings

Three towers with various creaseAngle settings

1.6.5. Prototypes

Prototypes

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.

External prototypes

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.

1.6.6. Beyond what is implemented

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:

Interpolator nodes

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.

Sensors, events, scripting

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

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.

Chapter 2. Reading, writing, processing VRML scene graph

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.

2.1. TVRMLNode class basics

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.

2.2. The sum of VRML 1.0 and 2.0

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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
    ]
  }
}

Figure 2.1. Four spheres in mixed VRML 1.0 and 2.0 code

Four spheres in mixed VRML 1.0 and 2.0 code

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.

2.3. Reading VRML files

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.

2.4. Writing VRML files

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.

2.4.1. DEF / USE mechanism when writing

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:

  1. 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.

  2. 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.

2.4.2. Determining VRML version when writing

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