flutter3d
Showcase Changelog 38 packages API reference

A pose with no scene

since 0.7.0 Animation

Most of this set drives a clip onto scene nodes: AnimationPlayer needs targets, and a target is normally a node in a running scene. Pose is the other half. It is a hierarchy's local transforms as three flat arrays, sampled straight from a clip with no scene node anywhere in the arithmetic — the shape a bake, a retarget pass or an offline tool over a thousand frames wants, where building a scene just to throw it away would be real allocation for a question that is only arithmetic.

Step 1: The hierarchy, as arrays #

Two joints, a root and a tip, each with a parent index and a rest local transform. Nothing here is a SceneNode.

_pose = Pose(
  parents: <int>[-1, 0],
  restTranslations: Float32List.fromList(<double>[
    0.0,
    0.0,
    0.0,
    0.0,
    -1.2,
    0.0,
  ]),
  restRotations: Float32List.fromList(<double>[
    0.0,
    0.0,
    0.0,
    1.0,
    0.0,
    0.0,
    0.0,
    1.0,
  ]),
  restScales: Float32List.fromList(<double>[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]),
);

Step 2: A clip to sample #

An ordinary swing on the root's rotation. Rotating a joint never moves the joint itself, only whatever hangs off it — the same rule a real skeleton follows — so this is what carries the tip through an arc rather than leaving it in place. Pose reads the track exactly the way AnimationPlayer would, through the same AnimationTrack.sample.

final Float32List times = Float32List.fromList(<double>[0.0, 0.75, 1.5]);
final List<double> angles = <double>[-0.6, 0.6, -0.6];
final Float32List values = Float32List(angles.length * 4);
for (var i = 0; i < angles.length; i++) {
  final Quaternion q = Quaternion.axisAngle(
    Vector3(1.0, 0.0, 0.0),
    angles[i],
  );
  values[i * 4] = q.x;
  values[i * 4 + 1] = q.y;
  values[i * 4 + 2] = q.z;
  values[i * 4 + 3] = q.w;
}
_swing = AnimationClip(
  name: 'swing',
  tracks: <AnimationTrack>[
    AnimationTrack(
      nodeIndex: 0,
      path: AnimationPath.rotation,
      interpolation: AnimationInterpolation.linear,
      times: times,
      values: values,
      componentCount: 4,
    ),
  ],
);

Step 3: Sample it, then look at the result #

sampleClip resets the pose to rest and writes every track's value onto it. worldMatrices composes the whole hierarchy from those local transforms. Only after both of those does this page read a translation out to place a marker — the pose itself never heard of the markers.

_time = (_time + dt) % _swing.duration;
_pose.sampleClip(_swing, _time);
final List<Matrix4> world = _pose.worldMatrices();
_rootMarker.setPositionFrom(world[0].getTranslation());
_tipMarker.setPositionFrom(world[1].getTranslation());

What to look at #

The orange marker swings under the grey one, which never moves: the pose holds exactly what the clip says and nothing more, whether or not anything is drawing it.