[back to main page]

Kambi VRML extensions

Contents:

  1. Introduction
  2. Extensions
    1. Bump mapping (normalMap, heightMap, heightMapScale fields of KambiAppearance)
    2. Shadow volumes extensions
      1. Specify how lights cast shadows (fields kambiShadows and kambiShadowsMain for light nodes)
      2. Optionally specify shadow casters (KambiAppearance.shadowCaster)
    3. Shadow maps extensions
      1. Light parameters for projective texturing and shadow maps
      2. Texture mapping for projective texturing and shadow maps
      3. Automatically generated shadow maps
    4. Output events to generate camera matrix (Viewpoint.camera*Matrix events)
    5. Generating 3D tex coords in world space (easy mirrors by additional TextureCoordinateGenerator.mode values)
    6. 3D text (node Text3D)
    7. Override alpha channel detection (field alphaChannel for ImageTexture, MovieTexture and such)
    8. Movies for MovieTexture can be loaded from images sequence
    9. Automatic processing of inlined content (node KambiInline)
    10. Force VRML time origin to be 0.0 at load time (KambiNavigationInfo.timeOriginAtLoad)
    11. Executing compiled-in code on Script events (compiled: Script protocol)
    12. KambiScript (kambiscript: Script protocol)
    13. Precalculated radiance transfer (radianceTransfer in all X3DComposedGeometryNode nodes)
    14. Programmable shaders (X3D feature available also in VRML 97)
    15. Mixing VRML 1.0, 2.0, X3D nodes and features
    16. Volumetric fog (additional fields for Fog node)
    17. Special objects immune to fog (fogImmune field for Material node)
    18. Inline nodes allow to include 3D models in other handled formats (3DS, MD3, Wavefront OBJ, Collada) and any VRML/X3D version
    19. Specify triangulation (node KambiTriangulation)
    20. VRML files may be compressed by gzip
    21. Fields direction and up and gravityUp for PerspectiveCamera, OrthographicCamera and Viewpoint nodes
    22. Mirror material (field mirror for Material node)
    23. Headlight properties (node KambiHeadLight)
    24. Fields describing physical properties (Phong's BRDF) for Material node
    25. Specify octree properties (node KambiOctreeProperties, various fields octreeXxx)
    26. Extensions compatible with Avalon / instant-reality
      1. Blending factors (node BlendMode and field KambiAppearance.blendMode)
      2. Transform by explicit 4x4 matrix (MatrixTransform node)
      3. Events logger (Logger node)
      4. Teapot primitive (Teapot node)
      5. Texture automatically rendered from a viewpoint (RenderedTexture node)
      6. Plane (Plane node)
    27. VRML 1.0-specific extensions
      1. Field parts in Cone and Cylinder nodes may have value NONE
      2. Fields attenuation and ambientIntensity for light nodes
      3. Parts of Inventor in VRML
      4. Multi root node
      5. Field separate for WWWInline node

1. Introduction

This is a VRML engine, so many programs here do something non-trivial with VRML files. Not surprisingly, I needed at some point to extend what is allowed by VRML specifications, for various reasons. This page documents these extensions, so everyone can use them.

Note that some of these extensions may not be tolerated by other VRML viewers. However:

To understand specifications of these extensions you will need some basic knowledge of VRML. Here you can find official VRML 1.0 and 97 specifications if you want to educate yourself.

VRML fields and nodes are specified on this page in the convention somewhat similar to VRML 97 specification:

NodeName {
  fieldKind    FieldType   fieldName   default_value  # short comment
  ...
}

Example VRML models that use these extensions may be found in Kambi VRML test suite — look inside vrml_1/kambi_extensions/, vrml_2/kambi_extensions/, x3d/kambi_extensions/, x3d/shaders/kambi_extensions/ subdirectories.

2. Extensions

2.1. Bump mapping (normalMap, heightMap, heightMapScale fields of KambiAppearance)

Instead of Appearance node, you can use KambiApperance node that adds some new fields useful for bump mapping:

KambiAppearance {
  ... all normal Appearance fields ...
  exposedField SFNode      normalMap        NULL        # only texture nodes (ImageTexture, MovieTexture, PixelTexture) allowed
  exposedField SFNode      heightMap        NULL        # only texture nodes (ImageTexture, MovieTexture, PixelTexture) allowed
  exposedField SFFloat     heightMapScale   0.01        # must be > 0, meaningful only if heightMap specified
}
Leaf (without bump mapping)
Leaf (with bump mapping)
Lion texture (without parallax mapping)
Lion texture (with parallax mapping)

Texture specified as normalMap describes normal vector values on each texel. Normal vector values are actually encoded as colors: normal vector (x, y, z) should be encoded as RGB((x+1)/2, (y+1)/2, (z+1)/2). You can use e.g. GIMP normalmap plugin to generate such normal maps. (Hint: Remember to check "invert y" when generating normal maps, in image editing programs image Y grows down but we want Y (as interpreted by normals) to grow up, just like texture T coordinate.)

This allows bump mapping to be used. If you set BumpMappingMaximum attribute (and pass light position for bump mapping), our VRML engine will automatically do appropriate bump mapping.

normalMap is enough to use normal bump mapping ("dot product" method, done by pure multitexturing or GLSL programs, depending on OpenGL capabilities). If you additionally specify some texture as heightMap then parallax mapping (steep parallax mapping with self-shadowing, if used OpenGL will support it) will be additionally used. heightMapScale allows you to tweak the perceived height of bumps for parallax mapping.

You can test it in

Note some limitations of bump mapping:

2.2. Shadow volumes extensions

Dynamic shadows demo
Werewolves with shadows
Castle "fountain" level with shadows

These extensions describe the shadows behavior when using the shadow volumes algorithm. They are interpreted by all programs from my engine (like "The Castle", view3dscene and VRML browser components) when rendering shadows using shadow volumes.

General notes about using shadows by shadow volumes:

2.2.1. Specify how lights cast shadows (fields kambiShadows and kambiShadowsMain for light nodes)

To all VRML light nodes, we add two fields:

XxxLight {
  ...
  exposedField SFBool      kambiShadows          FALSE     
  exposedField SFBool      kambiShadowsMain      FALSE       # meaningfull only when kambiShadows = TRUE
}

The idea is that shadows are actually projected from only one light source (with shadow volumes, number of light sources is limited, since more light sources mean more rendering passes; for now, I decided to use only one light). The scene lights are divided into three groups:

  1. First of all, there's one and exactly one light that makes shadows. Which means that shadows are made where this light doesn't reach. This should usually be the dominant, most intensive light on the scene.

    This is taken as the first light node with kambiShadowsMain and kambiShadows = TRUE. Usually you will set kambiShadowsMain to TRUE on only one light node.

  2. There are other lights that don't determine where shadows are, but they are turned off where shadows are. This seems like a nonsense from "realistic" point of view — we turn off the lights, even though they may reach given scene point ? But, in practice, it's often needed to put many lights in this group. Otherwise, the scene could be so light, that shadows do not look "dark enough".

    All lights with kambiShadows = TRUE are in this group. (As you see, the main light has to have kambiShadows = TRUE also, so the main light is always turned off where the shadow is).

  3. Other lights that light everything. These just work like usual VRML lights, they shine everywhere (actually, according to VRML light scope rules). Usually only the dark lights should be in this group.

    These are lights with kambiShadows = FALSE (default).

Usually you have to experiment a little to make the shadows look good. This involves determining which light should be the main light (kambiShadowsMain = kambiShadows = TRUE), and which lights should be just turned off inside the shadow (only kambiShadows = TRUE). This system tries to be flexible, to allow you to make shadows look good — which usually means "dark, but not absolutely unrealistically black".

In "The Castle" you can experiment with this using Edit lights inside debug menu.

If no "main" light is found (kambiShadowsMain = kambiShadows = TRUE) then shadows are turned off on this model.

Trick: note that you can set the main light to have on = FALSE. This is the way to make "fake light" — this light will determine the shadows position (it will be treated as light source when calculating shadow placement), but will actually not make the scene lighter (be sure to set for some other lights kambiShadows = TRUE then). This is a useful trick when there is no comfortable main light on the scene, so you want to add it, but you don't want to make the scene actually brighter.

2.2.2. Optionally specify shadow casters (KambiAppearance.shadowCaster)

Instead of Appearance node, you can use KambiApperance node. Additional field shadowCaster says whether a shape does cast a shadow. By default all shapes cast a shadow, so typically you need to use this field only to explicitly disable shadows casting from given shape.

KambiAppearance {
  ... all normal Appearance fields, and KambiAppearance fields documented previously ...
  exposedField SFBool      shadowCaster     TRUE      
}

This is honoured by our shadow volumes implementation (that is, dynamic shadows in OpenGL) and also by our ray-tracers.

2.3. Shadow maps extensions

Scenery with shadow maps

Our engine implements also another algorithm for shadows: the shadow maps. Shadow maps work completely orthogonal to shadow volumes, which means that you can freely mix both shadow approaches (volumes and maps) within a single scene.

The engine allows you to trivially generate (by GeneratedShadowMap node) and apply (by TextureCoordinateGenerator.mode = "PROJECTION") the shadow map.

An example VRML/X3D code for a light and a shadow receiver (everything in the scene will be considered a "shadow caster" for shadow maps):

  DEF MySpot SpotLight {
    location 0 0 10
    direction 0 0 -1
    # Additional fields for projective texturing (part of shadow mapping)
    projectionNear 1
    projectionFar 20
  }

  Shape {
    appearance Appearance {
      material Material { }
      texture GeneratedShadowMap { light USE MySpot update "ALWAYS" }
    }
    geometry IndexedFaceSet {
      texCoord TextureCoordinateGenerator {
        mode "PROJECTION"
        projectedLight USE MySpot
      }
      ....
    }
  }

Note that the shadow texture will be applied in a very trivial way, just to generate intensity values (0 - in shadow, 1 - not in shadow). If you want to have some nice shading, you should use GLSL shader to sample the depth texture (like shadow2DProj(shadowMap, gl_TexCoord[0]).r) and do there any shading you want. Using shaders is generally the way to make shadow mapping both beautiful and in one pass (read: fast), and it's the way of the future anyway. You can start from a trivial fragment shader in our examples: shadow_map.fs.

2.3.1. Light parameters for projective texturing and shadow maps

Every light node gets new fields:

*Light {
  ...
  [in,out]     SFFloat     projectionNear        1           # > 0
  [in,out]     SFFloat     projectionFar         100         # anything > projectionNear
  [in,out]     SFVec3f     up                    0 1 0     
}

"projectionNear", "projectionFar" specify the near, far values for projection used when generating the shadow map texture. You should always try to make projectionNear as large as possible and projectionFar as small as possible, this will make depth precision better (keeping projectionNear large is more important for this).

"up" is the up vector of the light node when capturing the shadow map. We know the direction for DirectionalLight and SpotLight, but for shadow mapping we also need to know the up vector. Internally, it will be adjusted to be orthogonal to the directional, so you actually don't have to specify it in many cases — only make sure that it's something non-parallel to the direction.

These properties are specified at the light node, because both texture generation and texture coord generation must know them, and use the same values (otherwise results will not be of much use).

DirectionalLight gets additional fields to specify orthogonal projection rectangle (projection XY sizes) and location for projection (although directional light is conceptually at infinity and doesn't have a location, but for making a texture projection we actually need to know the light's location):

DirectionalLight {
  ...
  [in,out]     SFVec4f     projectionRectangle   -10 10 -10 10  # # left, right, bottom, top; must be left < right and bottom < top
  [in,out]     SFVec3f     projectionLocation    0 0 0       # affected by node's transformation
}

SpotLight gets additional field to specify perspective projection angle.

SpotLight {
  ...
  [in,out]     SFFloat     projectionAngle       0         
}

When projectionAngle is zero, it means to take projection angle from 2 * cutOffAngle. Whether this is Ok, depends on your needs. Note that projectionAngle is the vertical/horizontal field of view for rectangular texture, while cutOffAngle is the cone angle. This means that using cutOffAngle as projection angle would generally make the projected image smaller then perceived light cone. There's no way to choose projectionAngle and cutOffAngle to fit perfectly, since you can't perfectly fit a rectangular texture into a circle shape.

2.3.2. Texture mapping for projective texturing and shadow maps

New TextureCoordinateGenerator.mode = "PROJECTION", and new TextureCoordinateGenerator field "projectedLight" (SFNode, inputOutput, default NULL, allowed any light node). This will generate texture coordinates for projective texturing: a texture coordinate (s, t, r, q) will be generated for a fragment that is seen by the light on (s/q, t/q) position, with r/q being the depth. In other words, texture coordinates generated will contain the actual 3D geometry positions, but expressed in light's frustum coordinate system.

This can be used for any kind of projective texturing (not only for shadow maps, but for any situation when light acts like a projector for some texture).

2.3.3. Automatically generated shadow maps

New node:

GeneratedShadowMap {
  [in,out]     SFNode      metadata         NULL                  # [X3DMetadataObject]
  [in,out]     SFString    update           "NONE"                # ["NONE"|"NEXT_FRAME_ONLY"|"ALWAYS"]
  []           SFInt32     size             128                 
  []           SFNode      light            NULL                  # any light node
  [in,out]     SFFloat     scale            1.1                 
  [in,out]     SFFloat     bias             4.0                 
  []           SFString    compareMode      "COMPARE_R_LEQUAL"    # ["COMPARE_R_LEQUAL" | "COMPARE_R_GEQUAL" | "NONE"]
}

The meaning of "update" is like for the standard GeneratedCubeMapTexture: "NONE" is self-explanatory, "ALWAYS" means every frame (for the full dynamic scenes), "NEXT_FRAME_ONLY" says to update at the next frame (and afterwards change back to "NONE").

"size" is the size of the texture. This texture will be created with OpenGL internal format GL_DEPTH_COMPONENT (see ARB_depth_texture), so it's ideal for typical shadow map operations. For GLSL shader, you're best using it with sampler2DShadow.

"light" specifies the light node from which to generate the map. For now, only DirectionalLight and SpotLight are supported. Other nodes types, or NULL, will prevent the texture from generating. It's usually comfortable to "USE" here some existing light node, instead of defining new one.

"scale" and "bias" are used with glPolygonOffset to offset the scene rendered to the shadow map. This avoids ugly precision problems (related to float precision, but also to projecting shadow map texture on the screen with different resolution). In short, increase "bias" if you see strange noise instead of shadows (but don't increase too much, or you will see that shadows visibly move back).

Note that light node instanced inside GeneratedShadowMap.light or TextureCoordinateGenerator.projectedLight isn't considered a normal light, that is it doesn't shine anywhere. It should be defined anywhere in normal scene part to actually act like a normal light. Moreover, it should not be instanced many times in normal scene part, as then it's unspecified from which view we will generate shadow map.

"compareMode" allows to additionally do depth comparison on the texture. For texture coordinate (s, t, r, q), compare mode allows to compare r/q with texture2D(s/q, t/q). Combined with typical "PROJECTION" texture mapping, this is the moment when we actually decide which screen pixel is in shadow and which is not. Default value "COMPARE_R_LEQUAL" is the most useful value for standard shadow mapping, it generates 1 (true) when r/q <= texture2D(s/q, t/q), and 0 (false) otherwise. "NONE" means that comparison is not done, depth texture values are returned directly, this is useful for example for debug / demo purposes — you can view the texture as a normal grayscale (luminance) texture.

2.4. Output events to generate camera matrix (Viewpoint.camera*Matrix events)

To every viewpoint node (this applies to all viewpoints usable in our engine, including all X3DViewpointNode descendants, like Viewpoint and OrthoViewpoint, and even to VRML 1.0 PerspectiveCamera and OrthographicCamera) we add output events that provide you with current camera matrix. One use for such matrices is to route them to your GLSL shaders (as uniform variables), and use inside the shaders to transform between world and camera space.

"cameraMatrix" transforms from world-space (global 3D space that we most often think within) to camera-space (aka eye-space; when thinking within this space, you know then that the camera position is at (0, 0, 0), looking along -Z, with up in +Y). It takes care of both the camera position and orientation, so it's 4x4 matrix. "cameraInverseMatrix" is simply the inverse of this matrix, so it transforms from camera-space back to world-space.

"cameraRotationMatrix" again transforms from world-space to camera-space, but now it only takes care of camera rotations, disregarding camera position. As such, it fits within a 3x3 matrix (9 floats), so it's smaller than full cameraMatrix (4x4, 16 floats). "cameraRotationInverseMatrix" is simply it's inverse. Ideal to transform directions between world- and camera-space in shaders.

XxxViewpoint {
  ...
  [out]        SFMatrix4f  cameraMatrix            
  [out]        SFMatrix4f  cameraInverseMatrix            
  [out]        SFMatrix3f  cameraRotationMatrix            
  [out]        SFMatrix3f  cameraRotationInverseMatrix            
}

2.5. Generating 3D tex coords in world space (easy mirrors by additional TextureCoordinateGenerator.mode values)

Teapot with cube map reflections

TextureCoordinateGenerator.mode allows two additional generation modes:

  1. WORLDSPACEREFLECTIONVECTOR: Generates reflection coordinates mapping to 3D direction in world space. This will make the cube map reflection simulating real mirror. It's analogous to standard "CAMERASPACEREFLECTIONVECTOR", that does the same but in camera space, making the mirror reflecting mostly the "back" side of the cube, regardless of how the scene is rotated.
  2. WORLDSPACENORMAL: Use the vertex normal, transformed to world space, as texture coordinates. Analogous to standard "CAMERASPACENORMAL", that does the same but in camera space.

2.6. 3D text (node Text3D)

We add new node:

Text3D {
  exposedField MFString    string      []        
  exposedField SFNode      fontStyle   NULL      
  exposedField MFFloat     length      []        
  exposedField SFFloat     maxExtent   0         
  exposedField SFFloat     depth       0.1         # must be >= 0
  exposedField SFBool      solid       TRUE      
}

This renders the text, pretty much like Text node from VRML 97 (see VRML 97 specification about string, fontStyle, length, maxExtent fields). But the text is 3D: it's "pushed" by the amount depth into negative Z. The normal text is on Z = 0, the 3D text had front cap on Z = 0, back cap on Z = -Depth, and of course the extrusion (sides).

Also, it's natural to apply backface culling to such text, so we have a solid field. When true (default), then backface culling is done. This may provide much speedup, unless camera is able to enter "inside" the text geometry (in which case solid should be set to FALSE).

If depth is zero, then normal 2D text is rendered. However, backface culling may still be applied (if solid is true) — so this node also allows you to make 2D text that's supposed to be visible from only front side.

See Kambi VRML test suite, file vrml_2/kambi_extensions/text_depth.wrl for example use of this.

Compatibility:

2.7. Override alpha channel detection (field alphaChannel for ImageTexture, MovieTexture and such)

Demo of alphaChannel override

Alpha channel of your textures is fully supported, both a simple yes-no transparency (done by alpha_test in OpenGL) and full range transparency (done by blending in OpenGL, just like partially transparent materials). Internally, we have a simple and very nice algorithm that detects whether texture's alpha channel qualifies as simple yes-no or full range, see TImage.AlphaChannelType reference (default tolerance values used by VRML renderer are 5 and 0.01). There is also a special program in engine sources (see images/tools/detect_alpha_simple_yes_no.pasprogram file) if you want to use this algorithm yourself. You can also see the results for your textures if you run view3dscene with --debug-log option.

Sometimes you want to override results of this automatic detection. For example, maybe your texture has some pixels using full range alpha but you stil want to use simpler rendering by alpha_test.

So we add new field to all texture nodes (ImageTexture, MovieTexture, also Texture2 in VRML 1.0 and all future 3D texture nodes in X3D):

TextureNode {
  ... all normal TextureNode fields ...
  field        SFString    alphaChannel  "AUTO"      # "AUTO", "SIMPLE_YES_NO" or "FULL_RANGE"
}

Value AUTO means that automatic detection is used, this is the default. Value SIMPLE_YES_NO means that alpha channel, if present, will always be treated like a simple yes/no channel (only fully opaque or fully transparent pixels). FULL_RANGE means that we will always consider alpha channel as full range, and always render it using blending.

2.8. Movies for MovieTexture can be loaded from images sequence

Fireplace demo screenshot
This movie shows how it looks animated. You can also get AVI version with much better quality (4.503 MB)

For MovieTexture nodes, you can use an URL like image%d.png to load movie from a sequence of images. This will load all successive images, substituting counter in place of %d, starting from 1.

You can specify a number between % and d, like %4d, to pad counter with zeros. For example, normal %d.png results in names like 1.png, 2.png, ..., 9.png, 10.png... But %4d.png results in names like 0001.png, 0002.png, ..., 0009.png, 0010.png, ...

Such movie will always be considered to run at the speed of 25 frames per second.

A simple image filename (without %d pattern) is also accepted as a movie URL. This just loads a trivial movie, that consists of one frame and is always still...

Allowed image formats are just like everywhere in our engine — PNG, JPEG and many others, see glViewImage docs for the list.

Besides the fact that loading image sequence doesn't require ffmpeg installed, using image sequence has also one very important advantage over any other movie format: you can use images with alpha channel (e.g. in PNG format), and MovieTexture will be rendered with alpha channel appropriately. This is crucial if you want to have a video of smoke or flame in your game, since such textures usually require an alpha channel.

Samples of MovieTexture usage are inside Kambi VRML test suite, in directory vrml_2/movie_texture/.

2.9. Automatic processing of inlined content (node KambiInline)

New KambiInline node extends standard Inline node, allowing you to do something like search-and-replace automatically on inlined content.

KambiInline {
  ... all normal Inline fields ...
  exposedField MFString    replaceNames  []        
  exposedField MFNode      replaceNodes  []          # any VRML node is valid on this list
}

replaceNames specifies the node names in inlined content to search. replaceNodes are the new nodes to replace with. replaceNames and replaceNodes fields should have the same length. By default, the lists are empty and so KambiInline works exactly like standard Inline node.

An example when this is extremely useful: imagine you have a VRML file generated by exporting from some 3D authoring tool. Imagine that this tool is not capable of producing some VRML content, so you write a couple of VRML nodes by hand, and inline the generated file. For example this is your generated file, generated.wrl:

#VRML V2.0 utf8

Shape {
  geometry Box { size 1 2 3 }
  appearance Appearance {
    texture DEF Tex ImageTexture { url "test.png" }
  }
}

and this is your file created by hand, final.wrl:

#VRML V2.0 utf8

# File written by hand, because your 3D authoring tool cannot generate
# NavigationInfo node.

NavigationInfo { headlight "FALSE" }
Inline { url "generated.wrl" }

The advantage of this system is that you can get back to working with your 3D authoring tool, export as many times as you want overriding generated.wrl, and your hand-crafted content stays nicely in final.wrl.

The problem of the above example: what happens if you want to always automatically replace some part inside generated.wrl? For example, assume that your 3D authoring tool cannot export with MovieTexture node, but you would like to use it instead of ImageTexture. Of course, you could just change generated.wrl in any text editor, but this gets very tiresome and dangerous if you plan to later regenerate generated.wrl from 3D authoring tool: you would have to remember to always replace ImageTexture to MovieTexture after exporting. Needless to say, it's easy to forget about such thing, and it gets very annoying when there are more replaces needed. Here's when KambiInline comes to help. Imagine that you use the same generated.wrl file, and as final.wrl you will use

#VRML V2.0 utf8

# File written by hand, because your 3D authoring tool cannot generate
# MovieTexture node.

KambiInline {
  url "generated.wrl"
  replaceNames "Tex"
  replaceNodes MovieTexture { url "test.avi" }
}

Each time when loading final.wrl, our engine will automatically replace in the VRML graph node Tex with specified MovieTexture. Of course the "replacing" happens only in the memory, it's not written back to any file, your files are untouched. Effectively, the effect is like you would load a file

#VRML V2.0 utf8

Shape {
  geometry Box { size 1 2 3 }
  appearance Appearance {
    texture MovieTexture { url "test.avi" }
  }
}

2.10. Force VRML time origin to be 0.0 at load time (KambiNavigationInfo.timeOriginAtLoad)

By default, VRML/X3D time origin is at 00:00:00 GMT January 1, 1970 and SFTime reflects real-world time (taken from your OS). This is somewhat broken idea in my opinion, unsuitable for normal single-user games. So you can change this by using KambiNavigationInfo node:

KambiNavigationInfo {
  ... all normal NavigationInfo fields ...
  field        SFBool      timeOriginAtLoad    FALSE     
}

The default value, FALSE, means the standard VRML behavior. When TRUE the time origin for this VRML scene is considered to be 0.0 when browser loads the file. For example this means that you can easily specify desired startTime values for time-dependent nodes (like MovieTexture or TimeSensor) to start playing at load time, or a determined number of seconds after loading of the scene.

2.11. Executing compiled-in code on Script events (compiled: Script protocol)

A special Script protocol "compiled:" allows programmers to execute compiled-in code on normal Script events. "Compiled-in code" means simply that you write a piece of code in ObjectPascal and register it after creating the scene. This piece of code will be executed whenever appropriate script will receive an event (when eventIn of the Script is received, or when exposedField is changed by event, or when the script receives initialize or shutdown notifications).

This should be very handy for programmers that integrate our VRML engine in their own programs, and would like to have some programmed response to some VRML events. Using Script node allows you to easily connect programmed code to the VRML graph: you write the code in Pascal, and in VRML you route anything you want to your script.

For example consider this Script:

  DEF S Script {
    inputOnly SFTime touch_event
    inputOnly SFBool some_other_event
    inputOnly SFInt32 yet_another_event
    url "compiled:
initialize=script_initialization
touch_event=touch_handler
some_other_event=some_other_handler
" }

  DEF T TouchSensor { }
  ROUTE T.touchTime TO S.touch_event

This means that handler named touch_handler will be executed when user will activate TouchSensor. As additional examples, I added handler named script_initialization to be executed on script initialization, and some_other_handler to execute when some_other_event is received. Note that nothing will happen when yet_another_event is received.

As you see, compiled: Script content simply maps VRML event names to Pascal compiled handler names. Each line maps event_name=handler_name. Lines without = character are understood to map handler of the same name, that is simple line event_name is equivalent to event_name=event_name.

To make this actually work, you have to define and register appropriate handlers in your Pascal code. Like this:

type
  TMyObject = class
    procedure ScriptInitialization(Value: TVRMLField; const Time: TVRMLTime);
    procedure TouchHandler(Value: TVRMLField; const Time: TVRMLTime);
  end;

procedure TMyObject.ScriptInitialization(Value: TVRMLField; const Time: TVRMLTime);
begin
  { ... do here whatever you want ...

    Value parameter is nil for script initialize/shutdown handler.
  }
end;

procedure TMyObject.TouchHandler(Value: TVRMLField; const Time: TVRMLTime);
begin
  { ... do here whatever you want ...

    Value parameter here contains a value passed to Script.touch_event.
    You can cast it to appropriate field type and get it's value,
    like "(Value as TSFTime).Value".

    (Although in case of this example, Value here will always come from
    TouchSensor.touchTime, so it will contain the same thing
    as our Time.Seconds parameter. But in general case, Value can be very useful to you.)
  }
end;

  { ... and somewhere after creating TVRMLScene (or TVRMLGLScene) do this: }

  Scene.RegisterCompiledScript('script_initialization', @MyObject.ScriptInitialization);
  Scene.RegisterCompiledScript('touch_handler', @MyObject.TouchHandler);

For working example code in Pascal and VRML see example program kambi_vrml_game_engine/3dmodels.gl/examples/vrml_browser_script_compiled.pasprogram, use it to open kambi_vrml_test_suite/x3d/simple_script_tests.x3dv, and note that Pascal code reacts to clicks on TouchSensor.

2.12. KambiScript (kambiscript: Script protocol)

We have a simple scripting language that can be used inside Script nodes. See KambiScript documentation (with examples).

2.13. Precalculated radiance transfer (radianceTransfer in all X3DComposedGeometryNode nodes)

Normal OpenGL lighting
Rendering with simple ambient occlusion
Precomputed Radiance Transfer
X3DComposedGeometryNode {
  ... all normal X3DComposedGeometryNode fields ...
  exposedField MFVec3f     radianceTransfer  []        
}

The field radianceTransfer specifies per-vertex values for Precomputed Radiance Transfer. For each vertex, a vector of N triples is specified (this describes the radiance transfer of this vertex). We use Vec3f, since our transfer is for RGB (so we need 3 values instead of one). The number of items in radianceTransfer must be a multiple of the number of coord points.

Since this field is available in X3DComposedGeometryNode, PRT can be used with most of the VRML/X3D geometry, like IndexedFaceSet. Note that when using PRT, the color values (color, colorPerVertex fields) are ignored (TODO: in the future I may implement mixing). We also add this field to VRML 1.0 IndexedFaceSet, so with VRML 1.0 this works too.

For PRT to work, the object with radianceTransfer computed must keep this radianceTransfer always corresponding to current coords. This means that you either don't animate coordinates, or you animate coords together with radianceTransfer fields. TODO: make precompute_xxx work with animations, and make an example of this.

For more information, see kambi_vrml_game_engine/3dmodels.gl/examples/radiance_transfer/ demo in engine sources.

TODO: currently radianceTransfer is read but ignored by view3dscene and simple VRML browser components. This means that you have to write and compile some ObjectPascal code (see above radiance_transfer/ example) to actually use this in your games.

2.14. Programmable shaders (X3D feature available also in VRML 97)

See X3D implementation status about programmable shaders.

Since we officially support X3D now, this is not really an extension, it's just normal X3D feature. You can use it in VRML 2.0 models too, as usual our engine allows you to mix VRML versions freely.

2.15. Mixing VRML 1.0, 2.0, X3D nodes and features

Because of the way how I implemented VRML 1.0, 2.0 and X3D handling, you have effectively the sum of all VRML features available. Which means that actually you can mix VRML 1.0 and 2.0 and X3D nodes to some extent. If given node name exists in two VRML/X3D versions, then VRML/X3D file header defines how the node behaves. Otherwise, node behaves according to it's VRML/X3D specification.

For example, this means that a couple of VRML 2.0/X3D nodes are available (and behave exactly like they should) also for VRML 1.0 authors:

Also VRML 1.0 things are available in VRML 2.0, e.g. OrthographicCamera (this is one thing not available in VRML 2.0 specification; although for X3D you should rather use standard OrthoViewpoint).

Also things like GLSL shaders (from X3D) are available in VRML 97.

You can also freely include VRML 1.0 files inside VRML 2.0, or X3D, or the other way around.

2.16. Volumetric fog (additional fields for Fog node)

I add to Fog node some additional fields to allow definition of volumetric fog:
Fog {
  ...
  exposedField SFBool    volumetric                    FALSE   
  exposedField SFVec3f   volumetricDirection           0 -1 0    # any non-zero vector
  exposedField SFFloat   volumetricVisibilityStart     0       
  exposedField SFNode    alternative                   NULL      # NULL or another Fog node
}

Meaning: when volumetric is FALSE (the default), every other volumetricXxx field is ignored and Fog behaves like defined in VRML 97 spec. If volumetric is TRUE, then the volumetric fog is used.

volumetricDirection is the direction of the volumetric fog, it must be any non-zero vector (it's length doesn't matter). Every vertex of the 3D scene is projected on volumetricDirection vector, and then the resulting distance (signed distance, i.e. along the direction of this vector) of this point is used to determine fog amount. For example: in the simple case when volumetricDirection is (0, 1, 0), then the Y coordinate of every vertex determines the amount of fog. In the default case when volumetricDirection is (0, -1, 0), then the negated Y coordinate of every vertex determines the amount of fog. I will call such calculated amount of fog the FogAmount.

Now FogAmount values between volumetricVisibilityStart and volumetricVisibilityStart + visibilityRange correspond to fog color being applied in appropriately 0 (none) and 1 (full) amount. fogType determines how values between are interpolated (in the simple LINEAR case they are interpolated linearly).

Note that volumetricVisibilityStart is transformed by the Fog node transformation scaling, just like visibilityRange in VRML spec.

Note that visibilityRange must stay >= 0, as required by VRML specification. This means that volumetricDirection always specifies the direction of the fog: the more into volumetricDirection, the more fog appears. For example, if your world is oriented such that the +Y is the "up", and ground is on Y = 0, and you want your fog to start from height Y = 20, you should set volumetricDirection to (0, -1, 0) (actually, that's the default) and set volumetricVisibilityStart to -20 (note -20 instead of 20; flipping volumetricDirection flips also the meaning of volumetricVisibilityStart).

Oh, and note that in our programs for now EXPONENTIAL fog (both volumetric and not) is actually approximated by OpenGL exponential fog. Equations for OpenGL exponential fog and VRML exponential fog are actually different and incompatible, so results will be a little different than they should be.

VRML test suite has test VRMLs for this (see vrml_1/kambi_extensions/fog_volumetric/ and vrml_2/kambi_extensions/fog_volumetric/ subdirectories). Also our games malfunction and The Castle use it.

One additional field not explained yet: alternative. This will be used if current graphic output (e.g. OpenGL implementation) for any reason doesn't allow volumetric fog (or at least doesn't allow it to be implemented efficiently). Currently, this means that GL_EXT_fog_coord extension is not supported. In such case we'll look at alternative field:

alternative will also be tried if the value specified in fogType field of Fog node is not recognized.

2.17. Special objects immune to fog (fogImmune field for Material node)

New field for Material node:
Material {
  ...
  exposedField SFBool      fogImmune             FALSE     
}

When fogImmune of given object's material is TRUE, then the fog effect (specified by Fog node) is not applied to the object. Object is "immune" to fog.

This should be used only in a very special cases, when the scene looks better with the material left without fog effect. For example, I used this in The Castle for a river surface material — it's a transparent material, and the whole level is covered with a volumetric fog. It just looks better when the river surface is not affected by the fog color.

2.18. Inline nodes allow to include 3D models in other handled formats (3DS, MD3, Wavefront OBJ, Collada) and any VRML/X3D version

Inline nodes (Inline and InlineLoadControl in VRML >= 2.0 and WWWInline in VRML 1.0) allow you to include not only other VRML files, but also other 3DS, MD3, Wavefront OBJ, Collada models. Internally, all those formats are converted to VRML/X3D before displaying anyway. If you want to precisely know how the conversion to VRML/X3D goes, you can always do the explicit conversion to VRML/X3D by using "Save as VRML" view3dscene command.

Also, you can freely mix VRML versions when including. You're free to include VRML 1.0 file inside VRML 2.0 file, or X3D, or the other way around. Everything works.

This also works for jumping to scenes by clicking on an Anchor node — you can make an Anchor to any VRML version, or a 3DS, Collada etc. file.

2.19. Specify triangulation (node KambiTriangulation)

KambiTriangulation demo screenshot

New node:

KambiTriangulation {
  exposedField SFInt32   quadricSlices    -1     # {-1} + [3, infinity)
  exposedField SFInt32   quadricStacks    -1     # {-1} + [2, infinity)
  exposedField SFInt32   rectDivisions    -1     # [-1, infinity)
}

This node affects rendering of subsequent Sphere, Cylinder, Cone and Cube nodes. For VRML 1.0 you can delimit the effect of this node by using Separator node, just like with other VRML "state changing" nodes. For VRML 2.0 every grouping node (like Group) always delimits this, so it only affects nodes within it's parent grouping node (like many other VRML 2.0 nodes, e.g. DirectionalLight or sensors).

When rendering sphere, cylinder, cone or cube we will triangulate (divide the surfaces into triangles) with settings specified in last KambiTriangulation node. quadricSlices divides like pizza slices, quadricStacks divides like tower stacks, rectDivisions divides rectangular surfaces of a Cube. More precise description of this triangulation is given at description of --detail-... options in view3dscene documentation. Comments given there about so-called over-triangulating apply also here.

Special value -1 for each of these fields means that the program can use it's default value. In case of view3dscene and rayhunter they will use values specified by command-line options --detail-... (or just compiled-in values (see source code) if you didn't specify --detail-... options).

Note that this node gives only a hints to the renderer. Various algorithms and programs may realize triangulation differently, and then hints given by this node may be interpreted somewhat differently or just ignored. That said, this node is useful when you're designing some VRML models and you want to fine-tune the compromise between OpenGL rendering speed and quality of some objects. Generally, triangulate more if the object is large or you want to see light effects (like light spot) looking good. If the object is small you can triangulate less, to get better rendering time.

Test VRML file: see Kambi VRML test suite, file kambi_vrml_test_suite/vrml_2/kambi_extensions/kambi_triangulation.wrl.

2.20. VRML files may be compressed by gzip

All our programs can handle VRML files compressed with gzip.

E.g. you can call view3dscene like

      view3dscene my_compressed_vrml_file.wrl.gz
    
and you can use WWWInline nodes that refer to gzip-compressed VRML files, like
      WWWInline { name "my_compressed_vrml_file.wrl.gz" }
    

Filenames ending with .wrl.gz or .wrz are assumed to be always compressed by gzip.

Files with normal extension .wrl but actually compressed by gzip are also handled OK. Currently, there's a small exception to this: when you give view3dscene VRML file on stdin, this file must be already uncompressed (so you may need to pipe your files through gunzip -c). TODO: this is intended to be fixed, although honestly it has rather low priority now.

A personal feeling about this feature from the author (Kambi): I honestly dislike the tendency to compress the files with gzip and then change the extension back to normal .wrl. It's handled by our engine, but only because so many people do it. I agree that it's often sensible to compress VRML files by gzip (especially since before X3D, there was no binary encoding for VRML files). But when you do it, it's also sensible to leave the extension as .wrl.gz, instead of forcing it back into .wrl, hiding the fact that contents are compressed by gzip. Reason: while many VRML browsers detect the fact that file is compressed by gzip, many other programs, that look only at file extension, like text editors, do not recognize that it's gzip data. So they treat .wrl file as a stream of unknown binary data. Programs that analyze only file contents, like Unix file, see that it's a gzip data, but then they don't report that it's VRML file (since this would require decompressing).

Also note that WWW servers, like Apache, when queried by modern WWW browser, can compress your VRML files on the fly. So, assuming that VRML browsers (that automatically fetch URLs) will be also intelligent, the compression is done magically over HTTP protocol, and you don't have to actually compress VRML files to save bandwidth.

2.21. Fields direction and up and gravityUp for PerspectiveCamera, OrthographicCamera and Viewpoint nodes

Standard VRML way of specifying camera orientation (look direction and up vector) is to use orientation field that says how to rotate standard look direction vector (<0,0,-1>) and standard up vector (<0,1,0>). While I agree that this way of specifying camera orientation has some advantages (e.g. we don't have the problem with the uncertainty "Is look direction vector length meaningful ?") I think that this is very uncomfortable for humans.

Reasoning:

  1. It's very difficult to write such orientation field by human, without some calculator. When you set up your camera, you're thinking about "In what direction it looks ?" and "Where is my head ?", i.e. you're thinking about look and up vectors.
  2. Converting between orientation and look and up vectors is trivial for computers but quite hard for humans without a calculator (especially if real-world values are involved, that usually don't look like "nice numbers"). Which means that when I look at source code of your VRML camera node and I see your orientation field — well, I still have no idea how your camera is oriented. I have to fire up some calculating program, or one of programs that view VRML (like view3dscene). This is not some terrible disadvantage, but still it matters for me.
  3. orientation is written with respect to standard look (<0,0,-1>) and up (<0,1,0>) vectors. So if I want to imagine camera orientation in my head — I have to remember these standard vectors.
  4. 4th component of orientation is in radians, that are not nice for humans (when specified as floating point constants, like in VRMLs, as opposed to multiplies of π, like usually in mathematics). E.g. what's more obvious for you: "1.5707963268 radians" or "90 degrees" ? Again, these are equal for computer, but not readily equal for human (actually, "1.5707963268 radians" is not precisely equal to "90 degrees").

Also, VRML 2.0 spec says that the gravity upward vector should be taken as +Y vector transformed by whatever transformation is applied to Viewpoint node. This also causes similar problems, since e.g. to have gravity upward vector in +Z you have to apply rotation to your Viewpoint node.

So I decided to create new fields for PerspectiveCamera, OrthographicCamera and Viewpoint nodes to allow alternative way to specify an orientation:

PerspectiveCamera / OrthographicCamera / Viewpoint {
  ...
  exposedField MFVec3f     direction   []        
  exposedField MFVec3f     up          []        
  exposedField SFVec3f     gravityUp   0 1 0     
}

If at least one vector in direction field is specified, then this is taken as camera look vector. Analogous, if at least one vector in up field is specified, then this is taken as camera up vector. This means that if you specify some vectors for direction and up then the value of orientation field is ignored. direction and up fields should have either none or exactly one element.

As usual, direction and up vectors can't be parallel and can't be zero. They don't have to be orthogonal — up vector will be always silently corrected to be orthogonal to direction. Lengths of these vectors are always ignored.

As for gravity: VRML 2.0 spec says to take standard +Y vector and transform it by whatever transformation was applied to Viewpoint node. So we modify this to say take gravityUp vector and transform it by whatever transformation was applied to Viewpoint node. Since the default value for gravityUp vector is just +Y, so things work 100% conforming to VRML spec if you don't specify gravityUp field.

In view3dscene "Print current camera node" command (key shortcut Ctrl+C) writes camera node in both versions — one that uses orientation field and transformations to get gravity upward vector, and one that uses direction and up and gravityUp fields.

2.22. Mirror material (field mirror for Material node)

You can mark surfaces as being mirrors by using this field.
Material {
  ...
  exposedField MFFloat / SFFloat  mirror      0.0         # [0.0; 1.0]
}

Currently this is respected only by classic ray-tracer in view3dscene and rayhunter. Well, it's also respected by path-tracer, although it's much better to use fields describing physical properties (Phong's BRDF) for Material node when using path-tracer. In the future mirror field may be somehow respected with normal OpenGL rendering in view3dscene and others.

For VRML 1.0
This field is of multi- type (MFFloat), just like other Material fields in VRML 1.0; this way you can specify many material kinds for one shape node (like IndexedFaceSet).
For VRML 2.0
This field is of simple SFFloat type, just like other Material fields in VRML 2.0.

0.0 means no mirror (i.e. normal surface), 1.0 means the perfect mirror (i.e. only reflected color matters). Values between 0.0 and 1.0 mean that surface's color is partially taken from reflected color, partially from surface's own material color.

Note that this field can be (ab)used to specify completely unrealistic materials. That's because it's not correlated in any way with shininess and specularColor fields. In the Real World the shininess of material is obviously closely correlated with the ability to reflect environment (after all, almost all shiny materials are also mirrors, unless they have some weird curvature; both shininess and mirroring work by reflecting light rays). However, in classic ray-tracer these things are calculated in different places and differently affect the resulting look (shininess and specularColor calculate local effect of the lights, and mirror calculates how to mix with the reflected color). So the actual "shiny" or "matte" property of material is affected by shininess and specularColor fields as well as by mirror field.

2.23. Headlight properties (node KambiHeadLight)

If this node is present and headlight is turned on (e.g. because headlight field of NavigationInfo is TRUE) then this configures the headlight properties.

The default values of this node are compatible with VRML specification, that explicitly states that NavigationInfo.headlight should have intensity = 1, color = (1 1 1), ambientIntensity = 0.0, and direction = (0 0 -1).

KambiHeadLight {
  exposedField SFFloat     ambientIntensity      0           # [0.0, 1.0]
  exposedField SFVec3f     attenuation           1 0 0       # [0, infinity)
  exposedField SFColor     color                 1 1 1       # [0, 1]
  exposedField SFFloat     intensity             1           # [0, 1]
  exposedField SFBool      spot                  FALSE     
  exposedField SFFloat     spotDropOffRate       0         
  exposedField SFFloat     spotCutOffAngle       0.785398  
}

The meaning of these field should be self-explanatory: ambientIntensity, attenuation, color and intensity are the same as for PointLight or DirectionalLight in VRML 2.0. If spot is TRUE then the light makes a spot, meaning of spotDropOffRate and spotCutOffAngle is the same as in VRML 1.0 (I didn't use beamWidth from VRML 2.0 spec because it translates badly to OpenGL).

2.24. Fields describing physical properties (Phong's BRDF) for Material node

In rayhunter's path-tracer I implemented Phong's BRDF. To flexibly operate on material's properties understood by Phong's BRDF you can use the following Material node's fields:
Material {
  ...
  exposedField MFColor                           reflSpecular          []          # specular reflectance
  exposedField MFColor                           reflDiffuse           []          # diffuse reflectance
  exposedField MFColor                           transSpecular         []          # specular transmittance
  exposedField MFColor                           transDiffuse          []          # diffuse transmittance
  exposedField MFFloat (SFFloat in VRML 1.0)     reflSpecularExp       1000000     # specular reflectance exponent
  exposedField MFFloat (SFFloat in VRML 1.0)     transSpecularExp      1000000     # specular transmittance exponent
}

Short informal description how these properties work (for precise description see Phong's BRDF equations or source code of my programs):

reflectance
tells how the light rays reflect from the surface.
transmittance
tells how the light rays transmit into the surface (e.g. inside the water or thick glass).
diffuse
describe the property independent of light rays incoming direction.
specular
describe the property with respect to the light rays incoming direction (actually, it's the angle between incoming direction and the vector of perfectly reflected/transmitted ray that matters).
specular exponent
describe the exponent for cosinus function used in equation, they say how much the specular color should be focused around perfectly reflected/transmitted ray.

For VRML 1.0, all these fields have multi- type (like other fields of Material node) to allow you to specify many material kinds at once. For VRML >= 2.0 (includes X3D) only the four non-exponent fields are of multi- type, this is only to allow you to specify zero values there and trigger auto-calculation (see below). Otherwise, you shouldn't place more than one value there for VRML >= 2.0.

Two *SpecularExp fields have default values equal to 1 000 000 = 1 million = practically infinity (bear in mind that they are exponents for cosinus). Other four fields have very special default values. Formally, they are equal to zero-length arrays. If they are left as being zero-length arrays, they will be calculated as follows :

This way you don't have to use any of described here 6 fields. You can use only standard VRML fields (and maybe mirror field) and path tracer will use sensible values derived from other Material fields. If you will specify all 6 fields described here, then path tracer will completely ignore most other Material colors (normal diffuseColor, specularColor etc. fields will be ignored by path tracer then; only emissiveColor will be used, to indicate light sources).

You can use kambi_mgf2inv program to convert MGF files to VRML 1.0 with these six additional Material fields. So you can easily test my ray-tracer using your MGF files.

These fields are used only by path tracer in rayhunter and view3dscene.

2.25. Specify octree properties (node KambiOctreeProperties, various fields octreeXxx)

Octree visualization

Like most 3D engines, Kambi VRML game engine uses a smart tree structure to handle collision detection in arbitrary 3D worlds. The structure used in our engine is the octree, with a couple of special twists to handle dynamic scenes. See documentation chapter "octrees" for more explanation.

There are some limits that determine how fast the octree is constructed, how much memory does it use, and how fast can it answer collision queries. While our programs have sensible and tested defaults hard-coded, it may be useful (or just interesting for programmers) to test other limits — this is what this extension is for.

In all honesty, I (Michalis) do not expect this extension to be commonly used... It allows you to tweak an important, but internal, part of the engine. For most normal people, this extension will probably look like an uncomprehensible black magic. And that's Ok, as the internal defaults used in our engine really suit (almost?) all practical uses.

If the above paragraph didn't scare you, and you want to know more about octrees in our engine: besides documentation chapter "octrees" you can also take a look at the (source code and docs) of the TVRMLScene.Spatial property and units VRMLTriangle, VRMLTriangleOctree and VRMLShapeOctree.

A new node:

KambiOctreeProperties {
  field        SFInt32  maxDepth              -1      # must be >= -1
  field        SFInt32  leafCapacity          -1      # must be >= -1
}

Limit -1 means to use the default value hard-coded in the program. Other values force the generation of octree with given limit. For educational purposes, you can make an experiment and try maxDepth = 0: this forces a one-leaf tree, effectively making octree searching work like a normal linear searching. You should see a dramatic loss of game speed on non-trivial models then.

To affect the scene octrees you can place KambiOctreeProperties node inside KambiNavigationInfo node. For per-shape octrees, we add new fields to Shape node:

KambiNavigationInfo {
  ...
  field        SFNode  octreeRendering            NULL    # only KambiOctreeProperties node
  field        SFNode  octreeDynamicCollisions    NULL    # only KambiOctreeProperties node
  field        SFNode  octreeVisibleTriangles     NULL    # only KambiOctreeProperties node
  field        SFNode  octreeCollidableTriangles  NULL    # only KambiOctreeProperties node
}
X3DShapeNode (e.g. Shape) {
  ...
  field        SFNode  octreeTriangles            NULL    # only KambiOctreeProperties node
}

See the API documentation for classes TVRMLScene and TVRMLShape for precise description about what each octree is. In normal simulation of dynamic 3D scenes, we use only octreeRendering, octreeDynamicCollisions and Shape.octreeTriangles octrees. Ray-tracers usually use octreeVisibleTriangles.

We will use scene octree properties from the first bound NavigationInfo node (see VRML/X3D specifications about the rules for bindable nodes). If this node is not KambiNavigationInfo, or appropriate octreeXxx field is NULL, or appropriate field within KambiOctreeProperties is -1, then the default hard-coded limit will be used.

Currently, it's not perfectly specified what happens to octree limits when you bind other [Kambi]NavigationInfo nodes during the game. With current implementation, this will cause the limits to change, but they will be actually applied only when the octree will be rebuild — which may happen never, or only at some radical rebuild of VRML graph by other events. So if you have multiple [Kambi]NavigationInfo nodes in your world, I advice to specify in all of them exactly the same octreeXxx fields values.

2.26. Extensions compatible with Avalon / instant-reality

We handle some Avalon / instant-reality extensions. See instant-reality and in particular the specifications of Avalon extensions.

Please note that I implemented this all looking at Avalon specifications, which are quite terse. (I didn't actually run instant-reality program (closed-source, not installable under current Debian testing).) Please report any incompatibilities.

2.26.1. Blending factors (node BlendMode and field KambiAppearance.blendMode)

Various blend modes with transparent teapots

We add new field to KambiAppearance node: blendMode (SFNode, NULL by default, inputOutput). It's allowed to put there BlendMode node to specify blending mode done for partially-transparent objects. BlendMode node is not X3D standard, but it's specified by Avalon: see BlendNode specification.

From Avalon spec, our engine supports a subset of fields: srcFactor, destFactor, color, colorTransparency. Note that some values require newer OpenGL versions, we will eventually fallback on browser-specific blending modes (you can set them explicitly in view3dscene).

For example:

  appearance KambiAppearance {
    material Material {
      transparency 0.5
    }
    blendMode BlendMode {
      srcFactor "src_alpha" # actually this srcFactor is the default
      destFactor "one"
    }
  }

Example above sets blending to an often-desired equation where the order of rendering doesn't matter. It's very useful for models with complex 3D partially-transparent objects, otherwise traditional approach (src_alpha and one_minus_src_alpha) may cause rendering artifacts.

2.26.2. Transform by explicit 4x4 matrix (MatrixTransform node)

MatrixTransform: supported matrix field, and the standard X3DGroupingNode fields.

This is analogous to Transform node, but specifies explicit 4x4 matrix. Note that VRML 1.0 also had MatrixTransform node (we also handle it), although specified a little differently. Later VRML 97 and X3D removed the MatrixTransform node from official specification — this extension fills the gap.

Note that this node was removed from specifications for a good reason. Our engine can invert your matrix internally (this is needed for some things, like bump mapping), but still it's difficult to extract particular features (like scaling factor) from such matrix. Currently our engine extracts scaling factors by very naive method. (Although this is planned to be fixed using unmatrix.c algorithm.) The bottom line is: You are well adviced to try to express all transformations using stardard Transform node.

This node may be useful when you really have no choice (for example, when converting from Collada files that have transformation written as explicit 4x4 matrix, it's natural to convert it to VRML MatrixTransform).

2.26.3. Events logger (Logger node)

Logger: supported level, logFile, enabled fields and write inputOnly event.

Logger node demo

An extremely useful debugger when playing with VRML / X3D routes and events. The idea is simple: whatever is sent to write input event is logged on the console. write event has special type (Avalon calls this XFAny) that allows to receive any VRML field type.

Other properties allow to control logging better. When enabled is false, nothing is logged. level controls the amount of logged info:

  1. nothing,
  2. log sending field name, type, timestamp,
  3. additionally log received value,
  4. additionally log sending node name, type.

logFile, when non-empty, specifies the filename to write log information to. (When logFile is empty, it's all simply dumped on standard output, i.e. usually console.) As a security measure (you really do not want to allow an author of X3D file to overwrite arbitrary files without asking user), in my implementation only the basename of the logFile matters, the file is always saved into current directory. Moreover, filename is like view3dscene_logger_XXX_%d.log, where "view3dscene" is the name of the program, "XXX" is the name specified in logFile, and "%d" is just next free number. This way logger output file is predictable, and should never overwrite your data.

These security measures were added by my implementation — Avalon spec simply says that logFile is the name of the file, I don't know how they handled security problems with logFile.

2.26.4. Teapot primitive (Teapot node)

Teapot node demo

A teapot. Useful non-trivial shape for testing various display modes, shaders and such.

Compatibility with Avalon Teapot: we support size and solid fields from Avalon. Fields texCoord and manifold are our own (Kambi engine) extensions.

Teapot : X3DGeometryNode {
  [in,out]     SFNode      metadata    NULL        # [X3DMetadataObject]
  []           SFVec3f     size        3 3 3     
  []           SFBool      solid       TRUE      
  []           SFBool      manifold    FALSE     
  [in,out]     SFNode      texCoord    NULL        # [TextureCoordinateGenerator]
}

The "size" field allows you to scale the teapot, much like the standard Box node. The default size (3, 3, 3) means that the longest size of teapot bounding box is 3.0 (all other sizes are actually slightly smaller). Changing size scales the teapot (assuming that size = 3 means "default size").

The "texCoord" field may contain a TextureCoordinateGenerator node specifying how texture coordinates are generated. Very useful to quickly test various texture coordinate generators (e.g. for cube env mapping) on teapot. When texCoord is not present but texture coordinates are required (because appearance specifies a texture), we will generate default texture coords (using the same alrgoithm as for IndexedFaceSet).

The "solid" field has standard meaning: if true (default), it's assumed that teapot will never be seen from the inside (and backface culling is used to speed up rendering).

The "manifold" field allows you to force teapot geometry to be correctly closed (2-manifold, where each edge has exactly 2 neighboring faces). This is useful if you want to use shadow volumes to cast shadow of this teapot.

For the sake of VRML / X3D standards, I do not really advice using this node... VRML developers should spend their time better than to implement such nodes of little practical use :), and it's possible to make the same thing with a PROTO. But it's useful for testing purposes.

2.26.5. Texture automatically rendered from a viewpoint (RenderedTexture node)

RenderedTexture demo

Texture rendered from a specified viewpoint in the 3D scene. This can be used for a wide range of graphic effects, the most straighforward use is to make something like a "security camera" or a "portal", through which a player can peek what happens at the other place in 3D world.

This is mostly compatible with Avalon RenderedTexture specification. We do not support all Avalon fields, but the basic fields and usage remain the same.

RenderedTexture : X3DTextureNode {
  [in,out]     SFNode      metadata              NULL             # [X3DMetadataObject]
  [in,out]     MFInt32     dimensions            128 128 4 1 1  
  [in,out]     SFString    update                "NONE"           # ["NONE"|"NEXT_FRAME_ONLY"|"ALWAYS"]
  [in,out]     SFNode      viewpoint             NULL             # [X3DViewpointNode] (VRML 1.0 camera nodes also allowed)
  []           SFNode      textureProperties     NULL             # [TextureProperties]
  []           SFBool      repeatS               TRUE           
  []           SFBool      repeatT               TRUE           
  []           SFBool      repeatR               TRUE           
  [in,out]     MFBool      depthMap              []             
}

First two numbers in "dimensions" field specify the width and the height of the texture. (Our current implementation ignores the rest of dimensions field. Also, in current implementation, you should treat dimensions field as initializeOnly, i.e. do not change it's value after world is loaded.)

"update" is the standard field for automatically generated textures (works the same as for GeneratedCubeMapTexture or GeneratedShadowMap). It says when to actually generate the texture: "NONE" means never, "ALWAYS" means every frame (for fully dynamic scenes), "NEXT_FRAME_ONLY" says to update at the next frame (and afterwards change back to "NONE").

"viewpoint" allows you to explicitly specify viewpoint node from which to render to texture. Default NULL value means to render from the current camera (this is equivalent to specifying viewpoint node that is currently bound). Yes, you can easily see recursive texture using this, just look at the textured object. It's quite fun :) (It's not a problem for rendering speed — we always render texture only once in a frame.) You can of course specify other viewpoint node, to make rendering from there.

"textureProperties" is the standard field of all texture nodes. You can place there a TextureProperties node to specify magnification, minification filters (note that mipmaps, if required, will always be correctly automatically updated for RenderedTexture), anisotropy and such.

"repeatS", "repeatT", "repeatR" are also standard for texture nodes, specify whether texture repeats or clamps. For RenderedTexture, you may often want to set them to FALSE. "repeatR" is for 3D textures, useless for now.

"depthMap", if it is TRUE, then the generated texture will contain the depth buffer of the image (instead of the color buffer as usual). (Our current implementation only looks at the first item of MFBool field depthMap. Also, in current implementation, you should treat depthMap as initializeOnly, i.e. do not change it's value after world is loaded.)

2.26.6. Plane (Plane node)

Avalon Plane node. You should instead use Rectangle2D node from X3D 3.2 when possible, this is implemented only for compatibility.

Our current implementation doesn't support anything more than size and solid fields. So it's really equivalent to Rectangle2D inside our engine, the only difference being that Plane.solid is TRUE by default (for Rectangle2D spec says it's FALSE by default).

2.27. VRML 1.0-specific extensions

2.27.1. Field parts in Cone and Cylinder nodes may have value NONE

This way every possible value is allowed for parts field. This is comfortable for operating on these nodes, especially from programs — there is no special "forbidden" value.

2.27.2. Fields attenuation and ambientIntensity for light nodes

Lights that have a position, i.e. PointLight and SpotLight nodes, have the field attenuation. The meaning of this field is exactly the same as in VRML 97. I allow this for VRML 1.0 because this is really useful, and because the default value of this field (1,0,0) assures that standard VRML 1.0 files are interpreted correctly.

Moreover, all lights have ambientIntensity field, also defined exactly like in VRML 97. However, when reading VRML 1.0 files, we treat default value of ambientIntensity as -1 (while VRML 97 specification gives 0). And when rendering, we treat lights with ambientIntensity < 0 specially: we treat them like ambientIntensity = intensity. This way:

  1. in VRML 1.0 when you specified ambientIntensity value, or in VRML 97: ambientIntensity is treated following VRML 97 specification. So rendered light ambient color is color * ambientIntensity.
  2. in VRML 1.0 when you didn't specify ambientIntensity: calculations are compatible with standard VRML 1.0 behavior (although it was not really stated clearly in VRML 1.0 spec...). So rendered light ambient color is color * intensity.

2.27.3. Parts of Inventor in VRML

Some Inventor-specific things are allowed:

These things allow me to handle many Inventor 1.0 files. They also allow me to handle many broken VRML 1.0 files that sometimes falsely claim that they are VRML 1.0 while in fact they use some Inventor-specific features.

For completely unrecognized nodes, our engine can always omit them (even without any VRML >= 2.0 (protos) or VRML 1.0 ("fields", "isA") extensibility features), so most Inventor files can be at least partially handled and displayed.

2.27.4. Multi root node

VRML 1.0 file may have any number of root nodes (VRML 1.0 spec requires that there is exactly one root node). I implemented this because
  1. There are many invalid VRML 1.0 files on the Internet that use this extension (partially because it's normal VRML 97 feature, and many VRML viewers allow this)
  2. This is allowed in VRML 97.
  3. This was very easy to implement :)

2.27.5. Field separate for WWWInline node

I'm adding new field:
WWWInline {
  ...
  exposedField SFBool      separate    TRUE      
}

To explain this field, let's create an example. Assume you have file 1.wrl with following contents:

#VRML V1.0 ascii
Material { diffuseColor 1 0 0 }
And a file 2.wrl with following contents:
#VRML V1.0 ascii
Group {
  WWWInline { name "1.wrl" }
  Cube { }
}

Question: what material is used by the cube ? The red material (defined in 1.wrl) or the default material ? In other words, do the state changes inside 1.wrl "leak outside" of WWWInline node ?

The answer (stated by VRML specification, and followed by our programs when separate is TRUE (the default)) is that the cube uses the default material. Not the red material. In other words, state changes do not "leak" outside. This is definitely a sensible behavior. This is safer for the author of VRML files (you're not so "vulnerable" to changes done in included files). And it allows to delay loading of inlined file until it's really needed (e.g. is potentially visible). Effectively, this means that WWWInline behaves a little like a Separator node. File 2.wrl is equivalent to

#VRML V1.0 ascii
Group {
  Separator {
    Material { diffuseColor 1 0 0 }
  }
  Cube { }
}

On the other hand, when you set field separate to FALSE, the cube will be red. Every state change done inside inlined file will affect the things defined after WWWInline node. Effectively, this means that WWWInline behaves a little like a Group node. Two files below are equivalent:

#VRML V1.0 ascii
Group {
  WWWInline { name "1.wrl" separare FALSE }
  Cube { }
}
#VRML V1.0 ascii
Group {
  Group {
    Material { diffuseColor 1 0 0 }
  }
  Cube { }
}

Generally, setting field separate to FALSE is a little dangerous (because you have to be careful what you include), but it also allows you to do various tricks.

Test VRML file: see VRML test suite, file vrml_1/kambi_extensions/inline_not_separate.wrl.