acron.anim¶
AnimationTree replacement – blend spaces, a transition SM, and root motion.
Godot’s AnimationTree (blend-trees of BlendSpace2D nodes, additive layers,
one-shots, a Transition state machine, root-motion extraction) has no Acron
equivalent. These helpers drive a direct.actor.Actor in blend mode
(enable_blend + set_control_effect) with weights computed in Python.
They are approximations: there is no true additive layer (approximate additive
aim by weight-mixing the aim poses in) and no root-motion API (the
RootMotionSampler below derives it from the root joint).
- Pieces:
BlendSpace2D– N animations positioned in a 2D space; given a sample point it returns per-animation weights (inverse-distance, normalised), reproducing Godot’s blend-space interpolation closely enough for movement.TransitionSM– named states each owning a set of (anim, weight) contributions; on a state change it cross-fades group weight from the old state to the new overxfade_time(Godot’s Transition xfade default 0.2s).apply_weights()– pushes a{anim: weight}map onto a blend-mode Actor, starting/stopping clips as weights come and go.RootMotionSampler– Godotget_root_motion_position/rotation: drives the dominant clip deterministically and samples the root joint’s per-step delta.authored_cycle_motion()– a clip’s per-cycle root motion, read off theAnimBundle’s own keys. This is the number a character should MOVE at; the sampler’s per-step delta is a differenced blend and is unreliable whenever more than one clip is playing.detach_root_motion()– the other half of a root-motion track: pin the root joint to rest so the clip stops applying the motion the game is already moving the character by.AudioTrack– Godot’sAnimationPlayer“audio” track: streams keyed on a clip’s OWN clock, which is the only clock they can be keyed on (footsteps belong to the walk cycle, not to the wall).OneShot– Godot’sAnimationNodeOneShot: one clip faded over whatever is underneath it, withlayered()as the mix.
The biggest gotcha is the one detach_root_motion() exists for. Godot’s
root_motion_track names a track that is captured instead of applied: the
bone stays at its rest transform and the motion reaches the game only through
get_root_motion_position(). Acron has no such switch – every channel poses
its joint – so a port that reads the motion AND moves the character by it moves
it twice, and the pose half of that snaps back to the start of the cycle on
every loop. Detaching is not optional dressing; it is what makes the track a
track.
Other root-motion gotchas baked in: the exposed joint only updates after
actor.update(force=True); animation time only advances on the global clock
(unreliable headless), so the sampler keeps its own time and pose()``s the
clip; on a loop wrap the root joint snaps back a full cycle and that backward
delta must not count as movement; and a skeleton split into ``Actor SUBPARTS
holds one AnimControl per part, each with its own clock, so advance
takes mirrors to keep them on one frame. Consumers should move along the facing
direction at the root-motion speed (delta.length()/dt) – transforming the
raw noisy root vector through the model each frame makes the movement direction
oscillate.
|
Godot's |
|
One animation clock per clip -- Godot's |
|
Godot's |
|
Godot's |
|
Drives an Actor's clips and reports the root motion they carry. |
|
A cross-fading state machine over named animation states. |
Global Functions
- apply_weights(actor, weights: dict, playing: set) None¶
Apply a
{anim: weight}map to a blend-mode Actor.playingis a set the caller maintains of anims currently loop()’d, so we only start each clip once and stop those that drop to zero.
- authored_cycle_motion(anim, root_joint: str)¶
(net_translation, net_heading_degrees)of one cycle ofanim.animis anAnimBundleandroot_jointthe name of the joint carrying the root motion. The answer is read straight off that joint’s own channel in the bundle – the authored keys – at frame 0 and the last frame.This is deliberately NOT measured by posing the character and reading the exposed joint, which is what it used to be. Three things go wrong that way, and all three have cost a day:
on a blend-mode
Actorthe exposed joint reports the WEIGHTED AVERAGE of every playing clip, so a locomotion clip sampled underneath a layered aim pose reports motion that shrinks with the layer’s weight and, as that weight moves, reverses sign;soloing the clip first (zeroing every other control effect) only fixes that while every control hangs off
lodRoot.Actor.get_anim_blends()does not enumerate SUBPARTS, so once the skeleton is split into parts the solo silently isolates nothing and the reading comes back diluted by whatever else is playing – measured at exactly half on a two-part split;it is a measurement of a constant. The authored per-cycle motion of a clip cannot depend on what else the character happens to be doing, and anything that reads it through the posed skeleton makes it look as though it can.
A clip with no channel for
root_jointdoes not move it, so the answer is zero – the same pose Acron would put the joint in.
- detach_root_motion(actor, root_joint: str = 'root') bool¶
Take the root-motion track OUT of the applied pose, Godot-style.
Godot’s
AnimationMixer.root_motion_tracknames one track that is captured instead of applied: the bone holds its rest transform and the motion is handed to the game throughget_root_motion_position(). Acron has no such switch, so unless something does this the clip poses the root joint AND the game moves the body by the same motion – twice the travel, and the pose half of it yanks back to the start of the cycle on every loop.Measured on the TPS player’s
running_gun-cycle: the model slid 2.9 m forward over the cycle’s 0.83 s and then jumped back, on top of the body’s own authored 4.25 m/s. That is what “the character jitters / gets pulled back while walking” is, and no test that watches the body’s position can see it – the body was always right; the model drawn on top of it was not.Actor.freeze_jointis the mechanism: the joint is pinned to its REST transform, so every clip’s root channel stops reaching the skeleton while the rest of the skeleton animates normally. Rest, not each clip’s own first frame, is what Godot leaves an unapplied track’s bone at – and clips disagree about where the root sits (the player’s jump clips park it 1.29 m above rest, the leans 0.16 m to one side), so a per-clip constant would pop the whole model the moment one faded in.Returns whether a root joint was found to detach.
This freezes the ACTOR’s own joint and does not touch the
AnimBundle. That distinction is load-bearing:Actorcopies the character but SHARES the animation bundles with every other Actor built from the same file, so flattening the channels there (the first version of this) reached back through the model pool – the second robot spawned fromred_robot.glbread every authored cycle as zero and stood still. The authored keys stay exactly whereauthored_cycle_motion()reads them.
- layered(base: dict, layer: dict, amount: float) dict¶
Godot’s filtered blend:
lerp(base, layer, amount)over weight maps.Both arguments are
{anim: weight}maps that each sum to about 1 – the thing underneath and the thing over it – and the result is the mix atamount. At 0 it is exactlybase, which is what anAnimationNodeBlend2at 0 and anAnimationNodeOneShotthat is not running both are.Weights that fall to nothing are dropped rather than kept as zeros, so a full-strength layer returns just the layer: a caller that walks the result to start and stop clips would otherwise keep every faded-out clip alive.