Light and sound

While you can now do all the main tasks of building a VR world, there are a few more elements that will greatly improve the quality of your creations. In this unit, you will explore a number of these interesting features. Specifically, you will learn how to add sound and lights to your world, as well as changing the background and point of view. Finally you will look at adding fog effects to your world.

Lighting Basics

Real Audio

Lighting is an important aspect of any visual experience. So far you've used the default light that most VRML browsers supplies. This light is essentially a miner's hat attached to the user's virtual forehead. It shoots a light beam straight out and reflects against any shapes in its path.

Lighting effects are among the most computationally challenging parts of 3d graphics. It is possible to calculate exactly how various lights and surfaces work together to give an incredible illusion of a lighting model, but such calculations are very intensive, and can bog down even the fastest computers. (However, the technique called 'ray tracing' is well worth exploring. You can get amazing effects if you aren't tied to real-time interaction. Check out www.povray.org for an example of a very powerful free ray-tracing engine.) In a VR environment, real time interaction is the king. Real ray-tracing (the process of accurately mapping light rays in a scene) cannot be done quickly enough on today's computers to give the illusion of real time. VRML uses a very simplified lighting model to give reasonable effects. If you understand this model, you can often overcome its weaknesses.

The basic idea of VRML's lighting model is this: If a light ray is perpendicular to the surface of an object, that surface is brightly lit. The more nearly parallel a light ray is to the surface, the darker that surface appears. As an example of this phenomenon, look back at one of the earliest worlds you built:
Color Box
As you can rotate the box, you can see this effect. The only light source in this scene is coming from directly behind your viewpoint. Whatever side of the box is most nearly perpendicular to your line of sight is very brightly lit. Any sides that are oblique to your line of sight are much darker.

While the 'miners light' approach is reasonable for simple worlds, you've undoubtedly seen its weaknesses. In the actual world, light comes from multiple light sources, and it bounces off nearly every object, causing an ambient lighting effect. The simple lighting model of VRML does not directly reflect this fact. In fact, VRML does not even cast shadows, because even this calculation would greatly slow down the rendering process.

Fortunately, it is possible to compensate for the weaknesses in VRML's color model by adding your own lighting. You have three primary types of lighting to add: pointlights, spotlights, and directional lighting. We'll look at an example of each. Many of these examples light up a simple world composed of a series of white "ping pong balls." For comparison purposes, you may want to look at pingpong.wrl in its default state with now special lighting.
pingPong.wrl

spotlights

The spotlight is the basic lighting source. You're probably familiar with spotlights from the theater. Theatrical spots are large cans that can throw a bright beam of light on the stage. Spotlights can be aimed at a specific point, and they can be adjusted to throw a wide or narrow beam. Often theatrical spots will be filtered with a colored gel to throw a particular color of light on the stage. VRML spotlights are designed to duplicate most of the characteristics of their theatrical cousins. spotlight.wrl
#VRML V2.0 utf8
#spotlight.wrl

#illustrates how spotlights work

#bring in something to look at
Inline {
  url [ "pingPong.wrl" ]
}

DEF theLight Transform {
  children [
    #create a spotlight
    SpotLight {
      beamWidth .3
      location 0 0 10
      direction 0 0 -1
    } # end spotlight
  ] # end children
} # end transform

#turn off the headlight.
NavigationInfo {
  headlight FALSE
}

Transform {
  translation 0 -3 0
  children [

    Shape {
      appearance Appearance {
        material Material {
          diffuseColor 0 0 1
        } # end material
      } # end appearance
      geometry Box {
        size 10 .1 .1
      } # end geometry
    } # end shape

    DEF slider Transform {
      children [
        Shape { 
          appearance Appearance {
            material Material {
              diffuseColor 0 0 1
            } # end material
          } # end appearance
          geometry Box {
            size .3 .3 .3
          } # end geometry
        } # end shape
    
        DEF hsps PlaneSensor {
          minPosition -5 0
          maxPosition 5 0
        } # end hsps
      ] # end children
    } # end slider
  ] # end children
} # end transform

ROUTE hsps.translation_changed TO slider.translation
ROUTE hsps.translation_changed TO theLight.translation
In this example, I have turned off the user's headlight by setting headlight to FALSE in the NavigationInfo node. The basic world is simply a series of white balls. I put a spotlight directly behind the user's point of view, and attached that spotlight to a box that can be used as a horizontal scroll bar.

(Take a careful look at this approach if you need some kind of slider control. It's a pretty easy way to get certain kinds of input. I made a long box for the scroll bar, and then another box for the 'elevator.' I then attached a planesensor to the elevator, clipped to the x axis, and mapped the results to a transform around the spotlight. The effect speaks for itself.)

When the user moves the scroller, the spotlight moves, brightening the balls directly in its beam, and darkening those outside its beam. The official spec for the spotlight node shows a few interesting fields:
SpotLight { 
  exposedField SFFloat ambientIntensity  0         # [0,1]
  exposedField SFVec3f attenuation       1 0 0     # [0,)
  exposedField SFFloat beamWidth         1.570796  # (0,/2]
  exposedField SFColor color             1 1 1     # [0,1]
  exposedField SFFloat cutOffAngle       0.785398  # (0,/2]
  exposedField SFVec3f direction         0 0 -1    # (-,)
  exposedField SFFloat intensity         1         # [0,1]
  exposedField SFVec3f location          0 0 0     # (-,)
  exposedField SFBool  on                TRUE
  exposedField SFFloat radius            100       # [0,)
}
Of these fields, pay particular attention to location, direction, and color. These are the main controlling fields of the node, and their purpose is relatively obvious from their names. You can use a combination of beamwidth and cutOffAngle to determine the size of the 'spot' of light thrown. The beamWidth field determines the angle of the cone of full - intensity light thrown by the spot. The cutOffAngle determines the total extent of the light's influence. If beamWidth and cutOffAngle are the same value, you'll have a sharp circle of light. If cutOffAngle is larger than beamWidth (as it is by default), the spotlight will gradually fade to black.

pointLight

 

While the spotlight is useful for certain situations, it really isn't the way most lighting appears in ordinary circumstances. More often, you will have one primary source of light (the sun or a grid of lights in the ceiling in indoor scenes) or a series of light sources that cast light in all directions. This second type of lighting is called a pointlight, and it is easy to create in VRMLl: pointlight.wrl
#VRML V2.0 utf8
#spotlight.wrl

#illustrates how point lights work

#bring in something to look at
Inline {
  url [ "pingPong.wrl" ]
}

DEF theLight Transform {
  children [
    #create a spotlight
    PointLight {
      location 0 0 0
    } # end spotlight
  ] # end children
} # end transform

#turn off the headlight.
NavigationInfo {
  headlight FALSE
} # end NavInfo


Transform {
  translation 0 -3 0
  children [

    Shape {
      appearance Appearance {
        material Material {
          diffuseColor 0 0 1
        } # end material
      } # end appearance
      geometry Box {
        size 10 .1 .1
      } # end geometry
    } # end shape

    DEF slider Transform {
      children [
        Shape { 
          appearance Appearance {
            material Material {
              diffuseColor 0 0 1
            } # end material
          } # end appearance
          geometry Box {
            size .3 .3 .3
          } # end geometry
        } # end shape
    
        DEF hsps PlaneSensor {
          minPosition -5 0
          maxPosition 5 0
        } # end hsps
      ] # end children
    } # end slider
  ] # end children
} # end transform

ROUTE hsps.translation_changed TO slider.translation
ROUTE hsps.translation_changed TO theLight.translation
Again, I used the same 'ping-pong' world so you'd have something interesting to light up, and I used the same slider technique to move a light source inside the world. I changed the light source from a spotlight to a pointlight. I also placed the pointlight on the same plane as the ping-pong balls, to get a dramatic effect. As you see when you move the slider, the lighting effect on the balls can be very dramatic. However, you can also see some of the weaknesses of the lighting model from this demonstration. VRML makes no attempt to block light passing through objects, which means you'll get no shadow effects at all. (If you want shadows, you'll have to add them by hand) Also note that the light source itself has no visual representation, but its effects are apparent.

You can use a pointlight to duplicate the effects of a light bulb or candle in a room. For advanced effects, you might want to try adding an interpolator to the intensity field causing the light to flicker. This would be a great way to emulate the flicker of a flame.

Directional Lighting

The most powerful type of lighting in VRML is the directional light node. This node allows you to evenly light a scene with light rays coming from a specified direction. The Directional Lighting node is best used to simulate sunlight or overhead lighting.
DirectionalLight { 
  exposedField SFFloat ambientIntensity  0        # [0,1]
  exposedField SFColor color             1 1 1    # [0,1]
  exposedField SFVec3f direction         0 0 -1   # (-,)
  exposedField SFFloat intensity         1        # [0,1]
  exposedField SFBool  on                TRUE 
}
The ambientIntensity field is useful because it allows you to have the light source contribute to the overall lighting of the world. This replicates the bouncing effect of light in the real world, and allows surfaces that are not directly in the path of a light source to still be lit. The default ambientIntensity of all light sources is zero, which means they add nothing to the overall brightness of the scene. To improve a scene's overall lighting, change the ambientIntensity field of one or more of your light sources.

Also, every lighting Node has a color field, which can be used to light the scene with a particular color.

This model illustrates the potential of directional lighting and color. Note that in order to achieve some of the effects, I employed a script node, which I'll illustrate in the next unit. direcLight.wrl
#VRML V2.0 utf8
#direcLight.wrl

#illustrates how directional light works

#bring in something to look at
Inline {
  url [ "pingPong.wrl" ]
}

DEF theLight DirectionalLight {
  ambientIntensity .8
  direction  0 0 -1 
  color 1 1 1
} # end direc light

#turn off the headlight.
NavigationInfo {
  headlight FALSE
} # end NavInfo

#set up some triggers

Transform {
  translation 0 4 0
  children [
    Shape {
      appearance Appearance {
        material Material {
          diffuseColor 1 0 0
        } # end material
      } # end appearance
      geometry Box {
        size 10 2 .5
      } # end geometry
    } # end shape
    
    DEF topTS TouchSensor { }
  ] # end children
} # end transform      


#bottom
Transform {
  translation 0 -4 0
  children [
    Shape {
      appearance Appearance {
        material Material {
          diffuseColor 0 1 0
        } # end material
      } # end appearance

      geometry Box {
        size 10 2 .5
      } # end geometry
    } # end shape
    
    DEF bottomTS TouchSensor { }
  ] # end children
} # end transform      


#left
Transform {
  translation -6 0 0
  children [
    Shape {
      appearance Appearance {
        material Material {
          diffuseColor 0 0 1
        } # end material
      } # end appearance

      geometry Box {
        size 2 8 .5
      } # end geometry
    } # end shape
    
    DEF leftTS TouchSensor { }
  ] # end children
} # end transform      

#right
Transform {
  translation 6 0 0
  children [
    Shape {
      appearance Appearance {
        material Material {
          diffuseColor 1 1 1
        } # end material
      } # end appearance

      geometry Box {
        size 2 8 .5
      } # end geometry
    } # end shape
    
    DEF rightTS TouchSensor { }
  ] # end children
} # end transform      

#reset
Transform {
  translation -8 0 0
  rotation 1 0 0 1.54
  children [
    Shape {
      appearance Appearance {
        material Material {
          emissiveColor 1 1 1
        } # end material
      } # end appearance

      geometry Cylinder {
        height .2
        radius .5
      } # end geometry
    } # end shape
    
    DEF resetTS TouchSensor { }
  ] # end children
} # end transform      


DEF moveLight Script {
  eventIn SFBool right_isActive
  eventIn SFBool left_isActive
  eventIn SFBool top_isActive
  eventIn SFBool bottom_isActive
  eventIn SFBool reset_isActive
  field SFNode theLight USE theLight
  url "javascript:

    function top_isActive(value, time){
      theLight.direction = new SFVec3f( 0, -1, 0 );
      theLight.color = new SFColor(1, 0, 0);      
    } // end function

    function right_isActive(value, time){
      theLight.direction = new SFVec3f( -1, 0, 0 );
      theLight.color = new SFColor(1, 1, 1);      
    } // end function

    function bottom_isActive(value, time){
      theLight.direction = new SFVec3f( 0, 1, 0 );
      theLight.color = new SFColor(0, 1, 0);      
    } // end function

    function left_isActive(value, time){
      theLight.direction = new SFVec3f( 1, 0, 0 );
      theLight.color = new SFColor(0, 0, 1);      
    } // end function

    function reset_isActive(value, time){
      theLight.direction = new SFVec3f( 0, 0, -1 );
      theLight.color = new SFColor(1, 1, 1);      
    } // end function

  " # end url
} # end script 

ROUTE rightTS.isActive TO moveLight.right_isActive
ROUTE bottomTS.isActive TO moveLight.bottom_isActive
ROUTE leftTS.isActive TO moveLight.left_isActive
ROUTE topTS.isActive TO moveLight.top_isActive
ROUTE resetTS.isActive TO moveLight.reset_isActive

Sound

Real Audio

sound.wrl
#VRML V2.0 utf8

# sound practice...

Group {
  children [
    DEF theBall Shape {
     appearance Appearance {
        material DEF ballColor Material {
          diffuseColor 0 1 0
        } # end material
     } # end appearance
     geometry Sphere {
        radius .5
      }
    } # end shape

    Sound { 
      source AudioClip {
        loop TRUE
        url ["beet9.mid"]
       } # end source
    } #End sound
  ]
}

DEF timer TimeSensor {
  loop TRUE
  cycleInterval 10
}

DEF theColors ColorInterpolator {
  key [
    0.00
    0.33
    0.66
    1.00
  ]
  keyValue [
   1 0 0,
   0 1 0,
   0 0 1,
   1 0 0,
  ]
} # end theColors

ROUTE timer.fraction_changed TO theColors.set_fraction
ROUTE theColors.value_changed TO ballColor.diffuseColor
You add sound to a VRML world with the Sound node. (big surprise, huh?) The sound node contains one major field, called source. The source field expects an AudioClip node. This node has a loop and URL fields, as well as a number of other fields. You can use .wav files and .midi files as the url. Be aware that sound nodes exist in 3D space. As you get closer to a sound object, it will be louder. If a sound node is to the left of the avatar, it will be louder in the left speaker. You can modify the direction and other characteristics of the sound by looking up the characteristics of the Sound and AudioSource nodes.

Backgrounds

Real Audio Backgrounds are used to add color to the background of a world. The background node is especially powerful. bg.wrl
#VRML V2.0 utf8
#bg.wrl
#backgrounds and bound nodes

PROTO TextBox [
  exposedField MFString message ""
  exposedField SFVec3f translation 0 0 0
]

{
  Transform {
    translation IS translation
    children [
      Billboard {
        axisOfRotation 0 0 0
        children [
          Shape {
            appearance Appearance {
              material Material {
                diffuseColor 1 0 0
              } # end material
            } # end appearance
            geometry Text {
              string IS message
            } # end geometry
          } # end shape
        ] # end children
      } # end billboard
    ] # end children
  } # end transform
} # end proto def

#switch 1
Group {
  children [
    TextBox {
      translation -5 0 0
      message [" bg 1: white"]
    } # end TextBox
    DEF ts1 TouchSensor { }
  ] # end children
} # end Group

#switch 2
Group {
  children [
    TextBox {
      translation -5 -2 0
      message [" bg 2: blue sky "]
    } # end TextBox
    DEF ts2 TouchSensor { }
  ] # end children
} # end Group

#switch 3
Group {
  children [
    TextBox {
      translation 0 0 0
      message [" bg 3: gradients "]
    } # end TextBox
    DEF ts3 TouchSensor { }
  ] # end children
} # end Group

#switch 4
Group {
  children [
    TextBox {
      translation 0 -2 0
      message [" bg 3: textures "]
    } # end TextBox
    DEF ts4 TouchSensor { }
  ] # end children
} # end Group


#background 1.  Very simple

DEF bg1 Background {
  skyColor 1 1 1
} #end bg1

#background 2.  Blue Sky, green grass
DEF bg2 Background {
  groundColor 0 1 0
  skyColor 0 0 1
} #end bg1

#background 3.  Introducing gradients
DEF bg3 Background {
  skyAngle [0, 1.54, 3.14]
  skyColor [1 1 1, 1 0 0, 0 1 0, 0 0 1]
} #end bg3

#background 4.  texture
DEF bg4 Background {
  skyAngle [0, 1.54, 3.14]
  skyColor [1 1 1, 1 0 0, 0 1 0, 0 0 1]
  backUrl [ "south.jpg" ]
  frontUrl [ "north.jpg" ]
  leftUrl [ "west.jpg" ]
  rightUrl [ "east.jpg" ]

} #end bg4


DEF bgChanger Script {
  eventIn SFBool set_bg1
  eventIn SFBool set_bg2
  eventIn SFBool set_bg3
  eventIn SFBool set_bg4

  field SFNode bg1 USE bg1
  field SFNode bg2 USE bg2
  field SFNode bg3 USE bg3
  field SFNode bg4 USE bg4
  url "javascript:
    function set_bg1(value, time){
      //sticky behavior
      if (value){
        bg1.set_bind = TRUE;
      } // end if
    } // end function   
   
    function set_bg2(value, time){
      //sticky behavior
      if (value){
        bg2.set_bind = TRUE;
      } // end if
    } // end function

    function set_bg3(value, time){
      //sticky behavior
      if (value){
        bg3.set_bind = TRUE;
      } // end if
    } // end function
   
    function set_bg4(value, time){
      //sticky behavior
      if (value){
        bg4.set_bind = TRUE;
      } // end if
    } // end function
  " # end url

} # end script

ROUTE ts1.isActive TO bgChanger.set_bg1
ROUTE ts2.isActive TO bgChanger.set_bg2
ROUTE ts3.isActive TO bgChanger.set_bg3
ROUTE ts4.isActive TO bgChanger.set_bg4
The Background world consists of four text fields. Each field has a sensor that causes the background to be swapped to a different kind of background. Each of the four backgrounds is described as a background node. (For the purposes of the example, I added a script to this program. If your page has only one background, as most do, you will not need the script functionality. You can skip the script and routs statements for now, but you will want to look at them again after you have read the scripting chapter. For now, the background nodes themselves are the critical features.

The simplest background

Background # 1 produces a completely white background. The script for this background is as follows:
DEF bg1 Background {
  skyColor 1 1 1
} #end bg1
As you can see, the simplest form of the background node contains a field called skyColor which shows what color the sky is. The skyColor is best imagined as a huge sphere which the user views from the center. If the skyColor is set to one color, the entire inside of the sphere is essentially painted with the appropriate sky color.

Adding a ground color

Background # 2 introduces a ground color.
#background 2.  Blue Sky, green grass
DEF bg2 Background {
  groundColor 0 1 0
  skyColor 0 0 1
} #end bg1
If the sky color is a sphere completely surrounding the user, the ground color is painted on a large bowl inside the sphere.

Working with Gradients

You can color parts of the background by applying a gradient to the sky sphere. This is illustrated in background 3.
#background 3.  Introducing gradients
DEF bg3 Background {
  skyAngle [0, 1.54, 3.14]
  skyColor [1 1 1, 1 0 0, 0 1 0, 0 0 1]
} #end bg3
In this background, I began by defining three points in the skyAngle field. The SkyAngle is defined by the angle measured from the user's viewpoint to the sky sphere. 0 radians is straight down (or the North Pole). 1.54 radians is roughly pi/2, which defines the horizon or equator of the sky sphere, and 3.14 is close enough to pi to define the North pole of the sky sphere. By defining these points, I have committed to assigning a color to each of these angles. You can define as many points as you wish, but each must be in the zero to pi range, and you will need to define a color for each angle.

The skyColor field works differently if you have a skyAngle field defined than it does without skyAngle. Without skyAngle, you simply add an SFColor, and the entire skySphere is painted that color. With skyAngle defined, you must add colors to define the various bands of color in your sky sphere. NOTE
For some reason, the first color in the list is always ignored. It doesn't matter which color you place here, but you must have one more color than you do angles in the skyAngle field. I always use <1 1 1> as my first color vector.

Each of the other color vectors maps to one of the angles described in the skyAngle field. So, the sky will be red (color 1 0 0) at the North pole, green( color 0 1 0) at the equator, and blue (color 0 0 1) at the South pole.

You might want to experiment with gradient nodes to get interesting effects such as sunsets, snowy landscapes, and so on. Also, the ground sphere has a corresponding groundAngle field, but its values go from 0 (south pole) to 1.54 (horizon). However, by cleverly designing the gradient for the sky sphere, the ground sphere is unnecessary.

Adding textures to a background

The background node has several fields which allow you to place textures (images) in various parts of the background node. Background # 4 illustrates this approach:
#background 4.  texture
DEF bg4 Background {
  skyAngle [0, 1.54, 3.14]
  skyColor [1 1 1, 1 0 0, 0 1 0, 0 0 1]
  backUrl [ "south.jpg" ]
  frontUrl [ "north.jpg" ]
  leftUrl [ "west.jpg" ]
  rightUrl [ "east.jpg" ]
} #end bg4
The URL fields allow you to specify an image file to use as the background in that direction. If you carefully design these images, you can have some excellent effects. In fact, you can use the background node alone to replicate the effects of Apple's quicktime VR quite easily. You can also use GIF images with transparent segments to combine images with the sky gradients. Wherever the image is transparent, the background gradient will appear. The Background node also has topUrl and bottomUrl fields that I chose not to use in this example.

Creating Fog Effects

Fog can be used to add interesting effects to a scene. You can see the effects of the fog node in the following scene:

Basic Fog

fog.wrl
#VRML V2.0 utf8
#Fog.wrl
#illustrates the fog node

Inline {
  url ["columns.wrl"]
} # end inline

Fog { 
  color 1 1 1
  visibilityRange 10
}

Background {
  groundColor 1 1 1
  skyColor 1 1 1
} # end background

While this fog is effective, it is actually a silly trick. Actual fog is a very complex phenomenon, with millions of small particles obscuring an image. In the fog.wrl, the illusion of fog was maintained because the image appeared to disappear at a range of 10 (the visibilityRange). As the user gets closer to the center of the scene, the visibility improves. However, you'll see in the example below that VRML's approach to fog is actually a shortcut which can cause problems.

Bad Fog

fog2.wrl
#VRML V2.0 utf8
#Fog.wrl
#illustrates the fog node

Inline {
  url ["columns.wrl"]
} # end inline

Fog { 
  color 1 1 1
  visibilityRange 10
}

Background {
  groundColor 0 0 1
  skyColor 0 0 1
} # end background

The only difference between the bad fog world and the previous fog world is the background color. In the bad version, the world's background is set to blue. When the user is close to the columns, they appear normally. However, as the user backs away from the columns, they do not disappear! Instead, the colums simply are no longer shaded, and show up in a purely emissive form of the fog color (which is set to white). If the background color is similar to the fog color, the columns will appear to fade into the background color with a convincing similarity to fog. If the background color and the fog color are different (as in the bad fog model), the illusion will be shattered.

Fog effects can be very useful in a couple of practical ways. If you have a dark background and a dark fog color, you can replicate the effect of limited light in a dark cave. Also, if you find yourself using automatic level of detail (an effect of the cortona browser) some distant objects will appear to disappear abruptly. You can add some fog to the scene to make these disapearances less shocking to the user.
Andy Harris
Indiana University / Purdue University, Indianapolis
email: aharris@cs.iupui.edu
homepage: http://www.cs.iupui.edu/~aharris