Generic model posing and animation API¶
The runtime interface does not require the Model to come from any particular format, and ModelData / BoneData may be null.
For the principles see model subsystem boundaries; for IK, inheritance, fixed axes and physics definitions see
Skeleton Dynamics.
Model and format integration¶
| Interface | Package | Behavior |
|---|---|---|
AnimationLibrary Model.getAnimations() |
lib.kasuga.rendering.models.uml.structure |
Named clip library shared by Model |
ModelPosing ModelInstance.getPosing() |
lib.kasuga.rendering.models.uml.dynamic |
Lazily creates this instance's generic pose controller; obtaining it does not itself install a driver |
void ModelData.configureSkeleton(Skeleton) |
lib.kasuga.rendering.models.uml.structure.data |
Converts skeleton capabilities when the Model is constructed; default is an empty implementation |
void ModelData.configureModel(Model) |
same as above | Integrates the generic subsystems after the Model's public fields are initialized; default is an empty implementation |
The glTF Converter registers the animations it loads into the clip library; tracks that were not loaded are not in the library.
glTF's weights track shares this entry point with TRS, and supports STEP, LINEAR and CUBIC_SPLINE.
POSITION / NORMAL morph targets are registered per node instance as GltfAsset.MorphId(nodeIndex, targetIndex);
different nodes of the same mesh are controlled independently. The default weight is taken in order from node.weights, mesh.weights, or all zeros,
and is baked into the static geometry; the sampler outputs offsets relative to the default value, and the default shape is restored when playback stops.
Weights may be negative or greater than 1, and assembly and cross-fade preserve their linear meaning. Custom samplers can write finite signed weights with
Pose.Builder.morphWeight(id, weight); morph(id, value, factor)
keeps its original normalized activation semantics.
The Blockbench Loader registers the animations inside the file into the same library. Unnamed clips in both formats use animation_<file index>.
VMD/VPD, generic AnimationClip or custom formats are integrated through sampler/data; there is no need to create a new model type.
Bone names, morph identities and material references must still match the target model; this interface does not automatically retarget skeletons with a different topology.
AnimationLibrary¶
Package: lib.kasuga.rendering.models.uml.dynamic.animation.
new AnimationLibrary();
<T> AnimationLibrary.Clip<T> register(String name, AnimationSampler<T> sampler, T data);
AnimationLibrary.Clip<?> get(String name);
boolean contains(String name);
Map<String, AnimationLibrary.Clip<?>> clips();
new AnimationLibrary.Clip<T>(String name, AnimationSampler<T> sampler, T data);
float AnimationLibrary.Clip.duration();
Pose AnimationLibrary.Clip.sample(float seconds);
Pose AnimationLibrary.Clip.sample(float seconds, Namespace namespace);
AnimationLibrary.Clip<Pose> AnimationLibrary.pose(Pose pose);
The name passed to register must be non-empty and must not be all whitespace; registering the same name replaces the existing binding; get returns null on a miss.
clips() returns an unmodifiable view. Register at build/load time, and create players only after registration is complete; registration is not a thread-safe operation.
sampler/data must be non-null; the integrating party guarantees the immutability of external data. The library does not copy the original animation, nor does it rewrite its interpolation mode.
Clip times and durations are in seconds, and the shared player checks whether the duration is finite when playback actually starts.
AnimationLibrary.SAMPLER is the shared clip adapter used by AnimationPlayer/FSM.
The FSM's formula Namespace is passed through to the original sampler; a named binding does not lose the formula context.
pose produces an unnamed, zero-duration constant clip.
import lib.kasuga.rendering.models.uml.dynamic.ModelInstance;
import lib.kasuga.rendering.models.uml.dynamic.fsm.Pose;
import lib.kasuga.rendering.models.uml.math.Transform;
import lib.kasuga.rendering.models.uml.typo.miku_miku_dance.vmd.VmdSampler;
import lib.kasuga.rendering.models.uml.typo.miku_miku_dance.vpd.VpdSampler;
// model is a Model from any source; motion / vpd are data already read by the corresponding Reader.
model.getAnimations().register("dance", new VmdSampler(), motion);
model.getAnimations().register("stance", VpdSampler.INSTANCE, vpd);
ModelInstance instance = new ModelInstance(model, null, null, null, null, null);
instance.getPosing().play("dance", true);
// A static pose can also use the generic Pose directly.
instance.getPosing().pose(new Pose.Builder()
.bone("head", new Transform(), lib.kasuga.rendering.models.uml.dynamic.fsm.ApplyMode.REPLACE)
.ikEnabled("left-arm", false).build());
ModelPosing¶
Package: lib.kasuga.rendering.models.uml.dynamic; implements PoseDriver.
| Method | Behavior |
|---|---|
new ModelPosing(ModelInstance instance) |
Usually use instance.getPosing(); does not check the model format |
boolean hasClip(String name) |
Queries the model clip library |
String currentClip() |
Current clip name; null for a static pose or after stopping |
boolean play(String name, boolean loop) |
Plays from the start and installs as the instance driver; returns false for a missing name and keeps the current driver |
boolean follow(String name, AnimationTimeline timeline) |
Follows an external timeline and installs a driver; does not advance the shared clock a second time |
void pose(Pose pose) |
Installs a driver and keeps sampling a constant pose; pose must not be null |
void stop() |
The next render sample restores the bone/morph/IK channels written by that driver to their defaults; material frames are kept |
void pause(), void resume() |
Pauses/resumes the playback clock |
void seek(float seconds) |
Seeks to a finite, non-negative number of seconds |
void setSpeed(float speed) |
Finite, non-negative multiplier; 0 freezes the clock |
boolean isPlaying() |
Whether the clock keeps advancing; a static pose is still sampled continuously |
float currentTime() |
Current clock seconds; not taken modulo when looping |
AnimationTimeline timeline() |
Current timeline, for other targets to follow |
void tick(float dt) |
Usually advanced by ModelInstance.animate(dt) |
void sample(float partialTick) |
Usually called by the instance's render sampling entry point |
Each instance holds its own player, timeline and PoseSink; a static pose and a named clip switch reuse the same sink,
so channels that disappear from the previous pose are cleared automatically. Playback control and tick run serially, and the render sample consumes the clock snapshot
published by the shared player. stop's reset runs during the sampling phase and cannot replace sampling or instance updates.
The current PoseDriver is still a single slot; installing a ModelPosing replaces the current driver.
The shared drivers implement RebindablePoseDriver; for resource switching and clock preservation see input migration.
When animation layering is needed, use the FSM's State.clip(AnimationLibrary.SAMPLER, clip, loop) and State.pose(Pose).
Pose IK channels¶
Package: lib.kasuga.rendering.models.uml.dynamic.fsm.
new Pose(Map<Object, Pose.Morph> morphs, Map<String, Pose.Bone> bones,
Map<Object, Pose.Frame> frames, Map<String, Boolean> ikEnabled);
Map<String, Boolean> Pose.ikEnabled();
Pose.Builder Pose.Builder.ikEnabled(String chain, boolean enabled);
State<Owner> State.ikEnabled(String chain, boolean enabled);
boolean SkeletonInstance.clearIkEnabled(String chain);
The original three-parameter Pose constructor is kept, and the IK map defaults to empty. The keys of the map are IK chain names, not model format or file bone indices.
VmdSampler automatically writes the IK state of the property track into the Pose, so there is no need to write the skeleton separately after playback.
pose.ik_enabled in FSM JSON is an optional object from names to booleans, defaulting to {}.
A switch takes the earlier end while the cross-fade alpha is less than 0.5 and the later end otherwise; a one-sided channel keeps that side's value.
As with material frames, BASE/ADDITIVE use the last value and OVERRIDE takes precedence; BoneMask applies only to bone transforms and
does not mask IK chain switches. PoseSink ignores unknown chains; when a channel disappears it clears only the chains it wrote itself, and other manual switches are kept.
clearIkEnabled restores the chain's default enabled state and returns true when an explicit state was removed.
Name migration¶
| Original interface | Current interface |
|---|---|
GltfAnimationPoseDriver |
ModelInstance.getPosing() / generic ModelPosing |
MmdRagdoll |
lib.kasuga.rendering.models.uml.dynamic.physics.SkeletonRagdoll |
MmdPhysicsScene |
ModelPhysicsScene in the same package |
MmdPhysicsClusterManager |
ModelPhysicsClusterManager in the same package |
SkeletonInstance.getPmxBones() |
Format Readers maintain their own file index mapping; the instance exposes only the generic Skeleton/Bone |
These format-specific runtime entry points have been removed. The parameters and capabilities of the physics interfaces are preserved; callers referencing the old types need to update imports and recompile. glTF's STEP/LINEAR/CUBIC_SPLINE continues to be sampled exactly by GltfSampler.