Custom Timeline
A Timeline is the per-bone (or global) keyframe container inside a BlueprintAnimation. Each timeline holds a set
of KeyframeInterpolators — one per KeyframeType (position, rotation, scale, script) — and is what the animation
system samples when a model is playing an animation. You normally never build a Timeline by hand; the model parser
creates them while reading a .bbmodel. This page shows what a Timeline is, how it is constructed, and how an
animation made of timelines is played on a model.
Anatomy of a Timeline
A Timeline is constructed with the owning animation and a flag for global rotation:
// animation: the BlueprintAnimation this timeline belongs to
// globalRotation: whether rotation keyframes are in global (model) space
Timeline timeline = new Timeline(blueprintAnimation, false);
It exposes three operations, all keyed by a KeyframeType:
// Whether an interpolator for this type already exists
boolean has = timeline.hasInterpolator(KeyframeTypes.POSITION);
// Get (lazily create) the interpolator for a keyframe type
KeyframeInterpolator<VectorKeyframe, Vector3f> interp = timeline.getInterpolator(KeyframeTypes.POSITION);
// Get (lazily create) the keyframe at a given time, in seconds
VectorKeyframe frame = timeline.getKeyframe(0.0f, KeyframeTypes.POSITION);
getInterpolator and getKeyframe both create-if-absent, so calling getKeyframe is how you add a keyframe to a
timeline: it returns the existing frame at that time, or a freshly created one, which you then populate.
Keyframe types
The built-in types live in KeyframeTypes:
KeyframeTypes.POSITION—VectorKeyframe/Vector3fKeyframeTypes.ROTATION—VectorKeyframe/Vector3fKeyframeTypes.SCALE—VectorKeyframe/Vector3fKeyframeTypes.SCRIPT—ScriptKeyframe(global only)
POSITION, ROTATION and SCALE are stored on a bone's timeline; SCRIPT lives on the animation's
globalTimeline.
Filling a Timeline
A VectorKeyframe value is supplied through IKeyframeData (use DoubleData for a constant value). The per-axis
setters are fluent, and a per-axis scale factor lets the parser convert Blockbench units into Model Engine units:
VectorKeyframe frame = timeline.getKeyframe(time, KeyframeTypes.POSITION);
frame.setXFactor(0.0625f)
.setYFactor(0.0625f)
.setZFactor(-0.0625f)
.setX(new DoubleData(x))
.setY(new DoubleData(y))
.setZ(new DoubleData(z));
frame.setInterpolation("linear"); // "linear", "step", "catmullrom", "bezier"
Script keyframes are added to the animation's global timeline:
ScriptKeyframe script = blueprintAnimation.getGlobalTimeline().getKeyframe(time, KeyframeTypes.SCRIPT);
script.getScript().add(ScriptKeyframe.Script.from("playsound:entity.zombie.ambient"));
Building an animation from timelines
A bone timeline is registered on its BlueprintAnimation under the bone's UUID; the script timeline is the
animation's built-in globalTimeline. This mirrors what the Blockbench parser does:
BlueprintAnimation animation = new BlueprintAnimation(modelBlueprint, "my_animation");
Timeline timeline = new Timeline(animation, false);
// ... fill timeline via getKeyframe(...) as shown above ...
animation.getTimelines().put(boneUuid, timeline); // boneUuid is the BlueprintBone UUID
animation.setLength(2.0); // length in seconds
animation.setLoopMode(BlueprintAnimation.LoopMode.LOOP);
animation.setOverride(false);
LoopMode is one of ONCE, HOLD, or LOOP.
Driving a model
You do not sample a Timeline directly. Once a BlueprintAnimation exists on a model's blueprint (registered under
its name), you play it by ID through the model's AnimationHandler, and the handler walks the timelines for you each
tick:
ActiveModel model = ...;
model.getAnimationHandler().playAnimation(
"my_animation", // animation ID (BlueprintAnimation name)
0.1, // lerp in, seconds
0.1, // lerp out, seconds
1.0, // speed multiplier
true // force restart if already playing
);
To stop it again, use stopAnimation(String) (graceful lerp-out) or forceStopAnimation(String) (immediate).