Interpolation component

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.

See also X3D specification of the Interpolation component.

Contents:

1. How to animate things using X3D

1.1. Basic animation concepts: connect 3 nodes

Animation in X3D usually requires connecting 3 nodes, as described below.

1.1.1. TimeSensor node

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.

1.1.2. Interpolator node, e.g. PositionInterpolator

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.

1.1.3. Node with the thing you want to animate, e.g. Transform

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

1.2. Building an animation in Object Pascal code

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.

Download and compile build_scene_interpolator.lpr

This example program 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).

1.3. Notes about looping animations

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.

1.4. How to implement skeletons, mesh deformations, skinned mesh animation...

Let us look at various popular animation methods, and how to do them in X3D:

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

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

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

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

2. Supported nodes

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:

    1. initial rotation,

    2. rotation by 180 degrees (by pi, i.e. 3.14),

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