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