Step 1: Building the Static Scene

The scene includes a red platform, build using a Box. The platform is placed
such that the top surface is in the XZ plane. The code to build the platform
is:
Example:

Transform {

translation 0 -0.2 0

children Shape {

appearance Appearance { material Material { diffuseColor 1 0 0 }}

geometry Box {size 22 0.4 2}

}

}

A textured sphere is also present in the scene. The sphere is placed on
the top at one end of the platform. The code for the sphere is:
Example:

Transform {

translation -10 1 0

children

Shape {

appearance

Appearance { material Material { }

texture ImageTexture { url "cone.jpg"}

}

geometry Sphere {}

}

}

This scene is displayed differently on different viewers. Some viewers
show the whole scene and you can see the whole platform and the ball on
top of it. Other viewers only show a part of the platform and the ball
isn't visible. These later viewers are the ones which are according to
the official specification. The specification defines an initial position
for the viewer by default. The former viewers do not use this default viewer's
position, instead they compute a position where the whole scene is visible.

In here it is assumed that you're using a compliant viewer and therefore
using the default viewer's position you're not able to see the whole scene.
This is because you're too close to the scene. The default position for
the viewer is 0 0 10, looking in the negative Z direction. The direction
is good, so we only have to move ourselves a little further from the origin
to view the whole scene. Try 0 0 20 and you'll be able to see the whole
scene. In order to change the position a Viewpoint
node is added to the VRML file. The code for this node is:
Example:

Step 2: Animation Part I

The animation for this section moves the sphere from one side of the platform,
its original position, to the other side.

In the general case animations require a timer to control the pace. In
VRML timers are implemented with TimeSensors. Suppose we want the ball
to take eight seconds to move from one extreme of the platform to the opposite
extreme and then to start all over again. The following timer could be
used:
Example:

DEF timer TimeSensor {

cycleInterval 8

loop TRUE

}

Note that the node is given a name using DEF,
this is because this name will be needed afterwards. The field loop
is set to TRUE to repeat the animation.

The TimeSensor node can be placed anywhere in the file.

Because we want to change the ball's position we also need a PositionInterpolator.
The interpolator will move the ball from its original position, -10 1 0,
to the opposite extreme of the platform, 10 1 0. The node is defined as:
Example:

DEF pi PositionInterpolator {

key [ 0 1 ]

keyValue [-10 1 0, 10 1 0]

}

Note that the node is given a name using DEF,
this is because this name will be needed afterwards.

The PositionInterpolator node can be placed anywhere in the file.

It is time to look at the events the above nodes generate to see how the
animation is performed.

The main idea is to have the PositionInterpolator outputting events with
3D values which will be used to change the translation of the Transform
node in which the ball is defined. However the PositionInterpolator can't
output events without receiving events. This is where the TimeSensor comes
into play. The TimeSensor node generates events as time goes by, we can
send these events to the PositionInterpolator which in turn will output
events to be feed into the Transform node to change its translation
field.

In order for a node to receive events it must be given a name using DEF,
so we'll have to add this to the Transform node where the ball is. The
new version is:
Example:

DEF tr Transform {

translation -10 1 0

children

Shape {

appearance Appearance {

material Material { }

texture ImageTexture { url "cone.jpg"}

}

geometry Sphere {}

}

}

A TimeSensor node outputs an event called fraction_changed, this
event has a value between 0 and 1, telling the fractional amount of time
from the cycle interval elapsed. We can feed this value into the interpolator's,
sending the event set_fraction to it. This is achieved using ROUTES:
Example:

ROUTE timer.fraction_changed TO pi.set_fraction

The event set_fraction inputs a value which is used has a key
for the interpolator. The interpolator then computed the associated
keyValue, and outputs the computed value with the event value_changed.
This value is then fed into the Transform using the event set_translation.
This is achieved by:
Example:

Step 3: Animation Part II - Adding More Realism

The movement of the ball is not very realistic is it? A rolling ball would
have been better. That's exactly what we're going to do in here, roll the
ball as it moves.

To roll the ball we need to change its orientation as it moves, so we'll
add a OrientationInterpolator to our world. The source code for this interpolator
is:
Example:

DEF oi OrientationInterpolator {

key [0 0.157 0.314 0.471 0.628 0.785 0.942 1]

keyValue [ 0 0 1 0, 0 0 1 -3.14,

0 0 1 -6.28, 0 0 1 -9.42, 0 0 1 -12.56,

0 0 1 -15.7, 0 0 1 -18.84, 0 0 1 -20.0 ]

}

Where did we get all those strange looking values for both key and
keyValue ?

First look at the keyValue list, it is a list of rotations. All rotations
are done in the Z axis, this is because this is the axis of rotation for
a rolling ball moving along the X axis, in the XZ plane.

Now focus on the rotation values, i.e. the angles and the keys. The first
key specifies a fraction of 0.157. We know that the ball will move 20 VRML
units from the start of the animation till the end. So in 0.157 of the
total time the ball will be translated 3.14 VRML units. Now look at the
first angle, -3.14. It is the same absolute value, the minus sign comes
in because we want to rotate the ball in a clockwise direction and that
requires negative values.

The reasoning behind this is that if you roll a ball until the top of the
ball reaches the floor, the distance covered is 3.14 * the radius of the
ball, or PI times the radius. In our case the radius is 1 so the distance
is 3.14. Measuring an angle in radians actually provides the fractional
perimeter for the angle.

If you apply the same computations for the remaining keyValues you'll
see that this ratio is maintained.

So why not just specify two rotations in the keyValues, namely,
0 0 1 0 and 0 0 1 -20 ? Because VRML normalizes rotations, i.e. it computes
the module 2 * PI of the rotation, therefore -20 = -1.16 - (6 * PI). In
VRML -20 is equal to -1.16 which is a little bit under -90 degrees.

When interpolating rotations VRML takes the shortest path, this is why
we must specify rotations which differ at most 3.14 radians between two
consecutive keyValues.

As for the positionInterpolator we need to receive events from the TimeSensor
and send them to the Transform, but this time to the rotation field.
The routes to achieve this are:
Example:

ROUTE timer.fraction_changed TO oi.set_fraction

ROUTE oi.value_changed TO ball_tr.set_rotation

Step 4: Animation Part IV - Making the ball move forward and backwards

Up till now we have a ball moving from left to right, end the ball reaches
the right extreme of the platform over and over again. In this section
we will make the ball move backwards as well.

The animation built so far moves the ball from left to right. Now imagine
what would happen if time went backwards: we would see the ball moving
from right to left.

How do we reverse time? All interpolators in this example receive an event
set_fraction. This event has a value which goes from 0.0 to 1.0.
What we want is an event which goes from 0.0 to 1.0 and then back again
to 0.0. The ball starts at the left (0.0), moves to the right (1.0) and
then moves back to the left (0.0 again).

So basically what we want is something which converts the sequence 0.0
-> 1.0 into the sequence 0.0->1.0->0.0. This can be achieved by
a ScalarInterpolator as follows:
Example:

DEF si ScalarInterpolator {

key [ 0 0.5 1]

keyValue [0 1 0]

}

The TimeSensor should be routed to the ScalarInterpolator to complete the
sequence conversion. This is achieved by:
Example:

ROUTE timer.fraction_changed TO si.set_fraction

The value_changed event from the ScalarInterpolator will move from
0.0 to 1.0 and back to 0.0. All there is left to do now is to change all
the routes to the remaining interpolators, they no longer receive events
from the TimeSensor, instead they will receive events from the ScalarInterpolator.
Example:

ROUTE si.value_changed TO pi.set_fraction

ROUTE si.value_changed TO oi.set_fraction

Note: we haven't changed the cycleInterval field of the TimeSensor, so
the ball will move twice as fast because the the ball is running the double
of the initial distance, i.e. is moving from the left to the right and
back again. IF you want to keep the previous pace then you should set the
cycleInterval to twice the original value.

Step 5: Starting the Movement with a Mouse Click

In the previous sections the ball was moving constantly, now were going
to start the movement with a mouse click and have the movement repeated
only once.

First, one must set the loop field of the TimeSensor to FALSE. The resulting
TimeSensor is as follows:

Example:

DEF timer TimeSensor {

cycleInterval 8

loop FALSE

}

Because the loop field is false, and we're using the default startTime
and stopTime fields for the TimeSensor the ball won't move.

Next we need to add a TouchSensor to the same group as the Sphere. The
Transform node will be:
Example:

DEF ball_tr Transform {

translation -10 1 0

children [

Shape { appearance Appearance {

material Material { }

texture ImageTexture { url "cone.jpg"}

}

geometry Sphere {}

}

DEF ball_sensor TouchSensor {}

]

}

The TouchSensor will generate the event touchTime when the user clicks
the mouse over a shape that within the same group as the sensor. This event
shall be used to set the startTime of the TimeSensor therefore starting
the animation. Because the loop field of the TimeSensor is FALSE the animation
shall perform only once.

The required routing is:
Example:

ROUTE ball_sensor.touchTime TO timer.set_startTime

That's all folks !!!

Site designed and maintained by António Ramires Fernandes
Your comments, suggestions and references to further material are welcome!