Sensors and user input

Pondering interactivity

As we experiment with VRML, it becomes clear that this is a powerful way to generate interesting 3-dimensional representations of things, but there are other ways to do the same thing. Rendering tools, and even physical modeling environments can do the same thing. In order for our work to truly approach the illusion of reality, it MUST respond to user interaction in interesting ways. By this definition, VRML 1.0 was barely a VR tool. The user could look at a world from a user-controlled perspective, might have moved the object through something like the EXAMINE mode, and could move around inside a 'virtual world', but there was very little interactivity with it. So far, we have used VRML as a modelling tool, but we have not yet explored the factors that allow VRML 2.0 to be an interactive and dynamic environment. In this unit, we will begin to do so.

Sensing user input

Real Audio

The first thing we will consider is how we might sense input from the user. The user should be considered part of the world, and ought to have some ability to interact with components in the world. There should be some way to tell when the user has done certain things, like moved to a certain area, touched an object, dragged an object, or other actions.

VRML uses special nodes called sensors to gather exactly this kind of information. We will examine a number of different kinds of sensors, and experiment with how they work.

Let's start with an example of a world that does something interesting, and examine how it works...
ts.wrl
#VRML V2.0 utf8
#ts.wrl
#demonstrates touch sensor

Group{ 
  children [
    Shape {
      appearance Appearance {
        material Material {
          diffuseColor 1 1 1
        } # end material
      } # end appearance
    
      geometry Sphere { }
    } # end shape
  
    DEF ts TouchSensor {}
  ] # end children
} # end ball group

DEF blue DirectionalLight {
  ambientIntensity 0
  color 0 0 1
  direction 0 0 -1
  intensity 1
  on FALSE
} # end light

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

#add some ambient light (to compensate for no headlight)
DirectionalLight {
  ambientIntensity .5
  color 1 1 1
  direction 0 0 -1
  intensity .2
  on TRUE
} # end light

ROUTE ts.isActive TO blue.on

How it works

The user will see a dimly lit sphere. When the user clicks on the sphere, it will appear to turn blue (oooooh!) This world has one primary object, the sphere itself. The sphere is pretty normal. It's white, even though it appears gray or blue. The sphere belongs in a group, which it shares with a touch sensor. TouchSensors are one example of a special kind of node called sensors. Sensors are not visible to the user. They are intended to capture user events, and send out signals. The TouchSensor in particular is designed to interpret whether the elements in its group were touched. Whenever any element in the same group as the TouchSensor is touched by the user, a number of signals are emitted by the sensor. Here is the spec of the TouchSensor node:
TouchSensor {
  exposedField SFBool  enabled TRUE
  eventOut     SFVec3f hitNormal_changed
  eventOut     SFVec3f hitPoint_changed
  eventOut     SFVec2f hitTexCoord_changed
  eventOut     SFBool  isActive
  eventOut     SFBool  isOver
  eventOut     SFTime  touchTime
}
As you can see, it has a bunch of eventOuts (whatever those are). The one we are interested in is isActive. This thing sends out an SFBool value (TRUE or FALSE) when the user has clicked on the group. Our TouchSensor is named ts (get it? ts for 'T'ouch'S'ensor.) The touch sensor allows any object in its group to respond to "touches" with the mouse. Since the sphere and the touch sensor are in the same group, clicking on the sphere will trigger the 'isActive' action.

The ball will never actually change color. Instead, I'll turn off the normal lighting and add a small ambient light to the scene so you can see the ball dimly lit. (The sphere is white, but it appears gray because there is very little light in the scene. You'll learn much more about lighting in chapter 10). To make the ball seem blue when it is clicked on, I pointed a bright blue spotlight on the ball, which will be turned on when the user clicks on the sphere.

The spotlight is a new object, but you can probably guess at its purpose. We don't actually see the spotlight, but we will see its effects. As a default, the spotlight is between the viewer and the sphere, and is pointed directly at the sphere. We currently have the spotlight turned off (the on field is set to FALSE), and when turned on, it will shine a blue light (set by the color field.) The spotlight is named blueLight. Here is the official spec of the SpotLight node:
SpotLight {
  exposedField SFFloat ambientIntensity  0 
  exposedField SFVec3f attenuation       1 0 0
  exposedField SFFloat beamWidth         1.570796
  exposedField SFColor color             1 1 1 
  exposedField SFFloat cutOffAngle       0.785398
  exposedField SFVec3f direction         0 0 -1
  exposedField SFFloat intensity         1  
  exposedField SFVec3f location          0 0 0  
  exposedField SFBool  on                TRUE
  exposedField SFFloat radius            100 
}
As usual, we will not need all the fields. For this example, we'll just deal with on, direction, and color. The meaning of all these fields is reasonably obvious. Note that the on field is of SFBOOL type. That will be important in a minute or two.

Routing

The natural question is "OK, the sensor is emitting this information, but who or what is hearing it?" So far, nothing. In order to do anything interesting, we need to channel the information somewhere. The last line of the code is the following:
ROUTE theButton.isActive TO blueLight.on
What the ROUTE statement does is channel these events that are generated by the sensor to appropriate receptacles. In other words, the value of the first thing will be transferred to the second. theButton.isActive is FALSE whenever the user is NOT clicking on the button. The value FALSE is transferred over to the blueLight.on field, so our sphere is it's default white color. When the user clicks on the button, the value of theButton.isActive becomes TRUE. The ROUTE statement sends that TRUE value to blueLight.on, turning on the spotlight. The ball then appears to turn blue. (whew!) Now that you know what's going on, look at the world again, and make sure you can visualize the process.

Plane Sensor

Real Audio

The above example was pretty easy to understand, but boolean data is not all that interesting. Often we are interested in values that would be more complicated, such as locations (3Dvec3fs) or colors. There are a variety of sensors, and they each specialize in receiving certain kinds of messages. Each also sends out specific kinds of data. Take a look at the following world for an interesting example:
planeSense.wrl
#VRML V2.0 utf8

Group{
  children [
    DEF cone Transform {
      children [
        Shape {
          appearance Appearance{
            material Material {
              diffuseColor 1 .5 .5
            } # end material
          } # end appearance
    
          geometry Cone{
            height 2
            bottomRadius 1
          } # end geometry
        } # end Shape
      ] # end children
    } # end cone

    DEF ps PlaneSensor{
      enabled TRUE
    } # end planeSensor

  ] # end children


} # end group

ROUTE ps.translation_changed TO cone.translation

How it works

As you can see, this is a very simple world. However, when you click on the cone and drag (using the mouse, wand, or whatever pointing device your system provides), you will see that the cone moves!! Of course, we needed to make a few modifications to the basic cone to allow all this to happen, but it is worth the effort. First, we built a rather ordinary cone shape, then placed it into a Transform. I named that Transform node 'cone' (lower case c cone is not the same as the Cone geometry type, so this is legal. In a moment, you will see why we named the Transform node rather than the Cone geometry. The Transform is inside a Group, which also contains a PlaneSensor. The PlaneSensor is named ps. So, we have a basic geometry with a sensor attached.

The ROUTE statement is where the real magic happens:
ROUTE ps.translation_changed TO cone.translation
The PlaneSensor exposes a special output field called translation_changed. This field sends out a 3Dvec3f corresponding to how much the mouse has moved since it was clicked over the group containing the sensor. We route that coordinate to the translation field of the cone. As you can see, the cone SHAPE does not have any translation field, but the transform that contains the shape does have translation, because that is an exposed field of the Transform node. The net effect is that when the user clicks and drags on an object, she will be able to move it around on the screen. Note that the PlaneSensor will move the object in the XY plane only. We will talk later about how to make it appear to move things in other planes.

The cylinder sensor

Take a look at the following world:
cylSense.wrl
#VRML V2.0 utf8

Group{
  children [
    DEF cone Transform {
      children [
        Shape {
          appearance Appearance{
            material Material {
              diffuseColor 1 .5 .5
            } # end material
          } # end appearance
    
          geometry Box{

          } # end geometry
        } # end Shape
      ] # end children
    } # end cone

    DEF cs CylinderSensor{
      enabled TRUE
    } # end Sensor
  ] # end children
} # end group

ROUTE cs.rotation_changed TO cone.rotation
As you can see, it appears that the user can 'spin' the object. As usual, there is a sensor attached to a group. The sensor sends out a rotation, which can be fed to the rotation field of a Transform. Of course, that is exactly what happens. The Cylinder sensor always rotates around the Y axis. There's a lot of hidden math going on to translate the 2-dimensional motions of the mouse into a rotation, but all you need worry about most of the time is that it does pretty much what you would expect. Again, we'll describe later how to deal with it if we want to rotate along a different axis.

The sphere sensor

The last of the 'shape' sensors is the sphere sensor. You can see its behaviour more easily than I can describe it:

sphereSense.wrl
#VRML V2.0 utf8

Group{
  children [
    DEF cone Transform {
      children [
        Shape {
          appearance Appearance{
            material Material {
              diffuseColor 1 .5 .5
            } # end material
          } # end appearance
    
          geometry Cone{
            height 2
            bottomRadius 1
          } # end geometry
        } # end Shape
      ] # end children
    } # end cone

    DEF ss SphereSensor{
      enabled TRUE
    } # end planeSensor
  ] # end children
} # end group

ROUTE ss.rotation_changed TO cone.rotation
Like the cylinder sensor, the sphere sensor has a rotation_changed out value, but this one is much more liberal. The translation will go along any axis, and is very much like the feel we get when using the examine mode.

The proximity sensor

Real Audio

Another very interesting sensor keeps track of whether the user's avatar (virtual prescence) is close to a certain spot. The proximity sensor has a size value and a center value. If the user is within the virtual box described by the size field and centered on the center field, the sensor will emit an SFBool called isActive. Here's an example. Note that this is a familiar world, but the button is gone. Now, to turn on the light, just move close to the sphere. Here is an example:
ps.wrl
#VRML V2.0 utf8
#ps.wrl
#demonstrates proximity sensor

Group{ 
  children [
    Shape {
      appearance Appearance {
        material Material {
          diffuseColor 1 1 1
        } # end material
      } # end appearance
    
      geometry Sphere { }
    } # end shape
  
    DEF ps ProximitySensor {
      size 7 7 7
    } # end 
  ] # end children
} # end ball group

DEF blue DirectionalLight {
  ambientIntensity 0
  color 0 0 1
  direction 0 0 -1
  intensity 1
  on FALSE
} # end light

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

#add some ambient light (to compensate for no headlight)
DirectionalLight {
  ambientIntensity .5
  color 1 1 1
  direction 0 0 -1
  intensity .2
  on TRUE
} # end light

ROUTE ps.isActive TO blue.on

Some more involved examples:

multiple sensors

Real Audio

You can connect more than one sensor to a shape, and a sensor can connect to more than one shape. Here's a very simple example: twoSense.wrl
#VRML V2.0 utf8

Group{
  children [
    DEF cone Transform {
      children [
        Shape {
          appearance Appearance{
            material Material {
              diffuseColor 1 .5 .5
            } # end material
          } # end appearance
    
          geometry Cone{
            height 2
            bottomRadius 1
          } # end geometry
        } # end Shape
      ] # end children
    } # end cone

    DEF ps PlaneSensor{
      enabled TRUE
    } # end planeSensor

    DEF ss SphereSensor {
      enabled TRUE
    } # end sphereSensor
  ] # end children


} # end group

ROUTE ps.translation_changed TO cone.translation
ROUTE ss.rotation_changed TO cone.rotation

The block world

Here's an example of a repetitive file:

senseLeg.wrl

I will not show you the wrl file, because it is over 700 lines long, and quite repetitive. It was written in just under an hour, by using a javascript application to generate most of the code. Basically, here were the steps I used: Here's the javascript used to generate the code: makeLeg.html
<html>
<head>
<title>Make a leg wrl file</title>

<script>
function doIt(){
  var counter = 0;
  var code = "";
   for (counter = 0; counter < 15; counter++){ 
    //set x and y values for block placement  
    var xOffset = (counter * .25) ;
    var yOffset = -3.5;
  
    //write the code
    code += "#yellow" + counter + "\n";
    code += "Group {\n";
    code += "  children [\n";
    code += "    DEF yellow" + counter +" Leg4 {\n";
    code += "      color 1 1 0 \n";
    code += "      translation ";
    code += xOffset + " ";
    code += yOffset + " 0\n";
    code += "    } \n";
    code += "    DEF yellow";
    code += counter + "PS PlaneSensor { }\n";
    code += "  ] # end children \n";
    code += "} # end group \n";
    code += "ROUTE yellow";
    code += counter;
    code += "PS.translation_changed TO yellow";
    code += counter;
    code += ".translation \n";
    code += "\n";
  
  } // end for loop
  
  window.document.theForm.txtOutput.value = code;

} // end function
</script>



</head>

<body bgcolor = "white">
<center>
<h1>Make a leg wrl file<hr></h1>
</center>

<form name = theForm>
<textarea name = txtOutput
          rows = 30
          cols = 60>
</textarea>
<br>
<input type = button
       value = "do it!!"
       onClick = doIt()>

</form>
<hr>
</body>
</html>
The lesson you might learn here is that sometimes you can use your programming skills to great effect to automate the construction of repetitive pages.

Rotating sensors

The plane and cylinder sensors are great, but they are limited in nature. The plane sensor will only translate along the XY plane, and the cylinder sensor will only rotate around the Y axis. What if you want some other behavior? We will see a much cleaner answer to this problem when we work with scripts, but for now, here is a solution that will work without any scripting. Examine the following

planeSense2.wrl
#VRML V2.0 utf8

Transform{
  rotation 1 0 0 1.57
  children [
    DEF cone Transform {
      rotation 1 0 0 -1.57
      children [
        Shape {
          appearance Appearance{
            material Material {
              diffuseColor 1 .5 .5
            } # end material
          } # end appearance
    
          geometry Cone{
            height 2
            bottomRadius 1
          } # end geometry
        } # end Shape
      ] # end children
    } # end cone

    DEF ps PlaneSensor{
      enabled TRUE
    } # end planeSensor


  ] # end children


} # end group

ROUTE ps.translation_changed TO cone.translation
The main thing to notice here is that we have a PAIR of nested transforms. The first Transform is rotated 'back' 1/4 circle around the X axis. The second (named) transform is rotated 'forward' 1/4 circle. The planeSensor is tipped back, and the cone inside it is tipped forward. The cone is the part that receives the input. This is quite a sneaky trick.

The robot

Real Audio

Here is a more complex world that demonstrates the same kind of trick with rotations:

robot.wrl
#VRML V2.0 utf8
# robot.wrl
# illustrates use of proximity sensor
# to keep a control panel close.
# also illustrates cylinderSensor for motion


#robot
DEF base Transform {
  translation 0 -2 0
  children [

    #base
    Shape {
      appearance DEF robColor Appearance {
        material Material {
          diffuseColor .6 .6 .9
        } # end material
      } # end appearance
      geometry Cylinder {
        height .2
        radius 1.5
      } # end geometry
    } # end shape

    #lower arm
    Transform  {
      translation 0 0 1
      #tip back so sensor will work
      rotation 1 0 0 1.57
      children [
        #rotate this 'Y' which is really 'Z'
        DEF la Transform {
          children [
            Transform {
              #tip shape back up
              rotation 1 0 0 -1.57
              center 0 -1 0
              children [
                Shape {
                  appearance USE robColor
                  geometry Cylinder {
                    height 2
                    radius .3
                  } # end geometry
                } # end shape
 
                # upper arm
                DEF ua Transform {
                  translation 1 1 0
                  center -1 0 0
                  children [
                    Shape {
                      appearance USE robColor
                      geometry Box {
                        size 2 .1 .1
                      } # end geometry
                    } # end shape
                  ] # end children
                } # end transform
              ] # end children
            } # end transform
          ] # end children
        } # end tranform
      ] # end children
    } # end transform
  ] # end children
} # end lowerArm

#control panel
Transform {
  translation 0 -2. 4
  children [

    #base
    Shape {
      appearance DEF ctlColor Appearance {
        material Material {
          diffuseColor .6 .6 .6
        } # end material
      } # end ctlColor
      geometry Box {
        size 6 .1 1
      } # end geometry
    } # end shape

    #rotator
    DEF rotator Transform {
      translation -2.5 .125 0
      children [
        Shape {
          appearance USE ctlColor
          geometry Cylinder {
            height .25
            radius .3
          } # end geometry
        } # end shape
        DEF rotCS CylinderSensor { }
      ] # end children
    } # end transform

    #lower arm control
    Transform {
      translation 0 0 .5
      #rotate whole shape down
      rotation 1 0 0 1.57
      children [

        #this is the transform that will be turned
        DEF laControl Transform {
          children [
            Transform {
              #rotate arm back up
              rotation 1 0 0 -1.57
              center 0 -.5 0
              children [
                Shape {
                  appearance USE ctlColor
                  geometry Cylinder {
                    height 1
                    radius .1
                  } # end geometry
                } # end shape
              ] # end children
            } # end transform
          ] # end children
        } # end transform
        
        DEF laCS CylinderSensor {
          minAngle -1
          maxAngle 1
          offset -.5
        }

      ] # end children
    } # end laControl

    #upper arm control
    DEF uaControl Transform {
      translation 2.5 .125 0
      children [
        Shape {
          appearance USE ctlColor
          geometry Cylinder {
            height .25
            radius .3
          } # end geometry
        } # end shape
        DEF uaCS CylinderSensor { }
      ] # end children
    } # end transform

  ] # end children
} # end transform

Viewpoint {
  position 0 3 10
  orientation 1 0 0 -.5
} # end viewpoint


ROUTE rotCS.rotation_changed TO rotator.rotation
ROUTE rotCS.rotation_changed TO base.rotation
ROUTE laCS.rotation_changed TO laControl.rotation
ROUTE laControl.rotation_changed TO la.rotation
ROUTE uaCS.rotation_changed TO uaControl.rotation
ROUTE uaCS.rotation_changed TO ua.rotation

This one is pretty big, but it is mostly not too bad. There are two main structures: the control panel and the robot. All the movements are rotations. The first knob is very easy, as it is meant to rotate around the Y axis, which is the default behavior. The middle lever causes quite a bit more trouble, as it requires a number of new features. We will tip the assembly back so that it's Y axis is the Z axis of the larger world. We also need a named transform with NO default rotation to receive rotation values, and nested inside that, we need the transform which will tip the named transform back up. In all, we have three nested transformations. Note also that we set values for the minAngle and maxAngle fields of the cylinderSensor, which caused limits to be set in the model's rotation.

You might also note that the upper arm was considerably easier to work with, because it was a box, and very flexible size attribute eliminates the need for an extra rotation, so the box could be done with only two transforms.
© Andy Harris
Indiana University / Purdue University, Indianapolis
email: aharris@.cs.iupui.edu
homepage: www.cs.iupui.edu/~aharris