This component defines nodes to interpolate between a given set
of values. Interpolators are often used for animation,
receiving time values from TimeSensor
and sending interpolated
values to visible nodes.
Contents:
Animation in X3D usually requires connecting 3 nodes, as described below.
TimeSensor
node generates output events as time passes.
Most importantly, it generates fraction_changed
output event,
that represents progress within the animation, in the [0..1] range.
Note: the actual duration of this animation, in seconds, is specified
in the field TimeSensor.cycleInterval
. Whatever it is,
the TimeSensor.fraction_changed
is still generated in
the [0..1] range. This way you can easily make animation faster/slower
by only changing the TimeSensor.cycleInterval
value.
How to start the animation?
In Castle Game Engine, it's easiest to just use the
TCastleSceneCore.PlayAnimation
method. It internally does everything necessary to reliably start the
indicated TimeSensor
node.
Just be aware that there are other ways to start an animation,
X3D standard allows to make TimeSensor
node active at open,
or activate it through various other means. But using
TCastleSceneCore.PlayAnimation
is almost always simpler, and you get some cool extra options that
we will mention later.
Consider this X3D file (in classic encoding -- you can save it as
test.x3dv
file and open with view3dscene
or any engine tool):
#X3D V3.2 utf8 PROFILE Interchange DEF MyAnimationName TimeSensor { cycleInterval 5.0 }
This animation does not do anything (MyAnimationName
output events are not connected to anything). But it can already be started
using Scene.PlayAnimation('MyAnimationName', true)
in the engine. The Scene.AnimationDuration('MyAnimationName')
will return 5.0.
You can also play it in view3dscene:
Open the created test.x3dv
file an choose menu item
Animation -> Named Animations -> MyAnimationName.
Note that we have named the TimeSensor
node,
by prefixing it with DEF MyAnimationName
statement.
This is the standard way in X3D to name nodes.
The PlayAnimation
simply takes this name as a parameter.
Next we need an interpolator node, like PositionInterpolator.
Every interpolator processes keys in [0..1] range are produces
key values. All interpolators have an input event
called set_fraction
that, as it's name suggests,
can be connected with the TimeSensor.fraction_changed
output event. In response to receiving the set_fraction
input,
an interpolator generates an output event called value_changed
.
The value_changed
is calculated by looking where
is the received fraction inside key
field,
and calculating appropriate value by picking a range from keyValue
field.
The type of values (there are placed in keyValue
,
and generated by value_changed
) depends on the interpolator
type. There are many interpolator nodes, most of them are part of the "Interpolation"
X3D component and are listed lower on this page
(and some more interpolators are added by the NURBS component
and our extensions).
For example, in case of PositionInterpolator
,
each "key value" is a 3D vector (which is called SFVec3f
in X3D, "SFVec3f" = "Single Field with a Vector of 3 floats").
This is how we would connect PositionInterpolator
to a TimeSensor
to create a movement from (0,0,0)
to (10,0,0) in 3 seconds, and then a movement from (10,0,0) to (10,10,0)
in 1 second:
#X3D V3.2 utf8 PROFILE Interchange DEF MyAnimationName TimeSensor { cycleInterval 4.0 } DEF MyInterpolator PositionInterpolator { key [ 0 , 0.75 , 1 ] keyValue [ 0 0 0, 10 0 0, 10 10 0 ] } ROUTE MyAnimationName.fraction_changed TO MyInterpolator.set_fraction
Note that nothing actually moves yet, as there is nothing visible in the scene yet.
As a last step, we need to connect the output event of the interpolator,
like PositionInterpolator.value_changed
to... something.
This is where the power of the X3D animation system is most prominent, as you can connect
"anything to anything" as long as the type matches.
So PositionInterpolator.value_changed
can be connected
to any input-output field that holds 3D vectors (this is marked like
SFVec3f [in,out]
in the X3D specification)
or any input event that can receive 3D vectors (this is marked like
SFVec3f [in]
in the X3D specification).
For example, note that the translation
field of the
Transform
node looks suitable. This way you can animate movement of anything visible.
Since Transform
node may contain other nodes, including visible Shape nodes,
or a nested Transform
(that can also be animated),
you can build a lot of complicated animations with this approach.
Here's a simple animation of a moving ball:
#X3D V3.2 utf8 PROFILE Interchange DEF MyAnimationName TimeSensor { cycleInterval 4.0 } DEF MyInterpolator PositionInterpolator { key [ 0 , 0.75 , 1 ] keyValue [ 0 0 0, 10 0 0, 10 10 0 ] } DEF MyTransform Transform { children Shape { geometry Sphere { } appearance Appearance { material Material { diffuseColor 1 1 0 # yellow } } } } ROUTE MyAnimationName.fraction_changed TO MyInterpolator.set_fraction ROUTE MyInterpolator.value_changed TO MyTransform.translation
Everything described above can be loaded from an X3D file, or it can be constructed by code. You can create X3D nodes and routes completely programmatically, using Object Pascal.
The example program below creates a sphere animation,
by programmatically creating the TimeSensor
and all the other
X3D nodes we discussed above.
It is done completely in Pascal (instead of loading the scene from X3D file,
like Scene.Load('example.x3dv')
), which allows you to extend
this example to do something much cooler (e.g. add it to a procedurally-generated
model).
{ Copyright 2018-2024 Michalis Kamburelis. No warranty. This example is under a permissive Apache 2.0 license, https://www.apache.org/licenses/LICENSE-2.0 . Feel free to modify and reuse. ---------------------------------------------------------------------------- } { Example program that builds a scene with an animated sphere, using Castle Game Engine and Object Pascal. See https://castle-engine.io/x3d_implementation_interpolation.php for description what the nodes used here (like TimeSensor) do. } uses CastleWindow, CastleViewport, X3DNodes, CastleCameras, CastleColors, CastleVectors, CastleScene; function BuildScene: TX3DRootNode; var Shape: TShapeNode; Material: TMaterialNode; //Sphere: TSphereNode; // unused Transform: TTransformNode; TimeSensor: TTimeSensorNode; PositionInterpolator: TPositionInterpolatorNode; Appearance: TAppearanceNode; begin Result := TX3DRootNode.Create; Material := TMaterialNode.Create; Material.DiffuseColor := YellowRGB; Appearance := TAppearanceNode.Create; Appearance.Material := Material; {Sphere := }TSphereNode.CreateWithTransform(Shape, Transform); Shape.Appearance := Appearance; Result.AddChildren(Transform); TimeSensor := TTimeSensorNode.Create('MyAnimationName'); TimeSensor.CycleInterval := 4; Result.AddChildren(TimeSensor); PositionInterpolator := TPositionInterpolatorNode.Create; PositionInterpolator.SetKey([0, 0.75, 1]); PositionInterpolator.SetKeyValue([Vector3(0, 0, 0), Vector3(10, 0, 0), Vector3(10, 10, 0)]); Result.AddChildren(PositionInterpolator); Result.AddRoute(TimeSensor.EventFraction_Changed, PositionInterpolator.EventSet_Fraction); Result.AddRoute(PositionInterpolator.EventValue_Changed, Transform.FdTranslation.EventIn); end; var Window: TCastleWindow; Viewport: TCastleViewport; Scene: TCastleScene; begin Window := TCastleWindow.Create(Application); Window.Open; Viewport := TCastleViewport.Create(Application); Viewport.FullSize := true; Viewport.InsertFront(TCastleExamineNavigation.Create(Application)); Window.Controls.InsertFront(Viewport); // add headlight Viewport.Camera.Add(TCastleDirectionalLight.Create(Application)); Scene := TCastleScene.Create(Application); Scene.Load(BuildScene, true); Scene.PlayAnimation('MyAnimationName', true); Viewport.Items.Add(Scene); // move camera, to better see the animation Viewport.Camera.Translation := Vector3(0, 0, 30); Application.Run; end.
To make the animation behave nicely when looping, you will usually want
to make the first item on the keyValue
list equal to the last.
For example change this:
DEF MyInterpolator PositionInterpolator { key [ 0 , 0.75 , 1 ] keyValue [ 0 0 0, 10 0 0, 10 10 0 ] }
into this:
DEF MyInterpolator PositionInterpolator { key [ 0 , 0.675 , 0.9 , 1 ] keyValue [ 0 0 0, 10 0 0, 10 10 0, 0 0 0 ] }
To actually run the animation as looping in Castle Game Engine
just call Scene.PlayAnimation('MyAnimationName', true)
.
Note: if you read the X3D specification, you may notice a field
called TimeSensor.loop
. Do not touch this field,
it will be automatically changed each time you use
the PlayAnimation
method. If you generate the X3D files yourself,
leave TimeSensor.loop
initially FALSE
(this is the default),
otherwise the animation will be already playing when you load the file.
The TCastleSceneCore.PlayAnimation
method is very powerful.
For example you can optionally
play the animation backward, or with blending,
or get a notification when animation stops.
Let us look at various popular animation methods, and how to do them in X3D:
If you have a skeleton with rigid parts attached to bones,
then you simply create a hierarchy of Transform
nodes
(TTransformNode
in Pascal).
Then you animate them as described above.
You can animate Transform.translation
and
Transform.scale
with
PositionInterpolator
(Pascal API: TPositionInterpolatorNode
).
And you can animate Transform.rotation
with
OrientationInterpolator
(Pascal API: TOrientationInterpolatorNode
).
This is also how we animate Spine models (2D skeletons). You can check this by loading a Spine model, e.g. dragon.json from here, into view3dscene, and saving it as X3D.
Optimization hint: if your models have a deep hierarchy of transformations, and a lot of these transformations simultaneously change, it's often beneficial to set global OptimizeExtensiveTransformations
to true
. See the manual about optimization. This is only temporary, of course — in the future we hope to make this optimization automatic. But for now, it sometimes helps (a lot), but sometimes can also cause a slowdown, so it's optional and should be enabled only after testing.
Another animation method is to deform meshes by interpolating between a couple of mesh versions. To do this, you use CoordinateInterpolator
(Pascal API: TCoordinateInterpolatorNode
) node in X3D. It works consistently with other interpolators. It generates a set of 3D vectors (MFVec3f
field in X3D terms) that can be connected e.g. to Coordinate.point
field. The Coordinate
node may be in turn be placed inside the IndexedFaceSet.coord
. See the Coordinate and IndexedFaceSet nodes specifications.
This is similar to how "blend shapes" in Blender work. We interpolate between some sets of coordinates. It's suitable e.g. for facial animations.
Another animation method is the skinned mesh animation, where we deform meshes by animating bones, and then calculating how these bones pull the mesh. This is different from using CoordinateInterpolator
(Pascal API: TCoordinateInterpolatorNode
): now the animation engine (Castle Game Engine code) must be aware of bones, of how do they map onto the vertexes: which bone affects which vertex and with what strength.
The skinned mesh animation is part of the "H-Anim" X3D component. The name of the component ("H-Anim", short for "humanoid animation") is a little misleading, as it actually alllows to animate any meshes, not only humanoids. It allows to animate using the "skinned mesh animation" approach. We describe the relevant fields in the "H-Anim" documentation.
Right now, our engine also implements another animation method as part of animating castle-anim-frames files. In this case, we use a special node interpolator that performs a linear interpolation between whole graphs of X3D nodes. So it's not using PositionInterpolator
or CoordinateInterpolator
or H-Anim skinned mesh animation, for now.
The approach of node interpolator is extremely flexible (able to animate anything that you can create in Blender, whether it's "structurally equal" or not). It is also extremely fast (as the frames are precalculated in memory, so you're actually just rendering static models). However, it may also be slow to load, and it can eat a significant amount of memory.
We expect to improve it at some point, and then loading castle-anim-frames
will just result in an X3D graph using interpolators like PositionInterpolator
and CoordinateInterpolator
inside. It will be optional, though (the current method has some advantages, so it will remain available too).
Note that these animations techniques are not mutually exclusive. You can, to some extent, use them within a single scene:
A single TimeSensor
node can be connected to multiple interpolators, it can e.g. connect to many PositionInterpolator
and CoordinateInterpolator
nodes.
Running one TimeSensor
node can also run other TimeSensor
nodes. To do this, you would route a couple of fields from one TimeSensor
to another: startTime
, stopTime
. Once this is set up in X3D, from Pascal code, you only need to start the "main" TimeSensor
by Scene.PlayAnimation('MainTimeSensor', ...)
.
Also note that the castle-anim-frames
file can be inserted into another model using the X3D Inline
node. The Inline
may be even under a transformation. You can still play the animations from the inlined castle-anim-frames
(because internally we use X3D EXPORT
mechanism). Here's an example how an X3D file uses Inline to insert castle-anim-frames inside. So you can "compose" your files, e.g. store the human head as castle-anim-frames
, and add it on top of a human body with Inline
.
The important advice is that, no matter how complicated is your animation inside X3D graph, it's worth to control each animation through a central TimeSensor
, such that it can be controlled easily as a single animation. This makes the TCastleSceneCore.PlayAnimation
method useful for you to control your animations. This way the complexity of the animation system can be hidden by the engine. Even if the X3D graph is complicated, you just run a trivial TCastleSceneCore.PlayAnimation
method.
Note that some other higher-level engine routines have the same "concept" of animations as TCastleSceneCore.PlayAnimation
. These include
TCastleSceneCore.AnimationDuration
,
TCastleSceneCore.ForceAnimationPose
,
TCastleSceneCore.HasAnimation
.
All these engine methods are capable of handling all the animation types
described on this page.
The supported X3D nodes from the "Interpolation" component are listed below. Moreover, see also Castle Game Engine (and view3dscene) extensions related to the interpolation.
ColorInterpolator
(Pascal API: TColorInterpolatorNode
) - Animate color change.
The colors between keyframes are calculated by interpolation in the HSV space.
PositionInterpolator
(Pascal API: TPositionInterpolatorNode
) - Animate 3D vector (like position) change.
PositionInterpolator2D
(Pascal API: TPositionInterpolator2DNode
) - Animate 2D vector (like 2D position) change.
ScalarInterpolator
(Pascal API: TScalarInterpolatorNode
) - Animate changing a single floating-point value.
OrientationInterpolator
(Pascal API: TOrientationInterpolatorNode
) - Animate a rotation.
The rotations between keyframes go through the shortest path on a conceptual unit sphere, with constant velocity.
Warning: Never define two consecutive key frames such that the model would point in exactly the opposite directions. In this case it is undefined how exactly the model with rotate, since there are many possible rotations that achieve the necessary transition. The X3D specification explicitly says that this is undefined ("""The results are undefined if the two orientations are diagonally opposite.""").This is an often mistake when defining a rotation for model to spin in a loop. It is tempting to define it using 3 keyframes:
initial rotation,
rotation by 180 degrees (by pi, i.e. 3.14),
rotation by 360 degrees (2 * pi, 6.28, that brings model back to original orientation)
Like this:
# THIS IS AN INCORRECT EXAMPLE, RESULTS ARE UNDEFINED! OrientationInterpolator { key [ 0, 0.5, 1, ] keyValue [ 0 1 0 0, 0 1 0 3.14, 0 1 0 6.28, ] }
The above is not correct, i.e. it is not precisely defined. Instead of a spinning model, you may see a model that spins by 180 degrees in one direction, then spins by 180 degrees back (since this also satisifies the given key frames).
The solution is to use more key frames. Instead of 1 intermediate key frame ([0, pi, 2*pi]
), use at least 2 intermediate key frames ([0, 2/3*pi, 4/3*pi, 2*pi]
). Often 3 intermediate keyframes ([0, pi/2, pi, 1.5*pi, 2*pi]
) are most comfortable to define.
CoordinateInterpolator
(Pascal API: TCoordinateInterpolatorNode
) - Animate a set of 3D vectors (like coordinates of a mesh).
CoordinateInterpolator2D
(Pascal API: TCoordinateInterpolator2DNode
) - Animate a set of 2D vectors.
NormalInterpolator
(Pascal API: TNormalInterpolatorNode
) - Animate a set of 3D directions.
TODO: Interpolation of NormalInterpolator
simply interpolates
3D vectors (and normalizes afterwards), instead of
a nice interpolation on the unit sphere.
TODO: Nodes from the X3D standard not implemented yet: EaseInEaseOut, Spline*, SquadOrientationInterpolator.