mjolnir/animation)The Animation module provides skeletal animation, IK, procedural animation modifiers, and spline utilities.
Splines provide smooth curves through control points, useful for paths, camera movement, and procedural animation.
import "../../mjolnir/animation"
// Create a spline with type and capacity
spline := animation.spline_create([3]f32, capacity = 10)
defer animation.spline_destroy(&spline)
// Add control points
animation.spline_add_point(&spline, {0, 0, 0})
animation.spline_add_point(&spline, {5, 2, 0})
animation.spline_add_point(&spline, {10, 0, 5})
// Validate and build arc-length table for uniform sampling
if animation.spline_validate(spline) {
animation.spline_build_arc_table(&spline, subdivisions = 200)
// Sample along spline uniformly by arc length
s := 0.5 // 0.0 to 1.0
position := animation.spline_sample_uniform(spline, s)
// Or sample by parametric t
position := animation.spline_sample(spline, t = 0.5)
}
Animation layers allow blending multiple animations together. See World Module for layer management functions.
// REPLACE mode: Standard blending (lerp between animations)
// Use for normal animation clips
world.add_animation_layer(
&engine.world,
node,
"Walk",
weight = 1.0,
blend_mode = .REPLACE,
)
// ADD mode: Additive blending (add animation on top)
// Only use for animations specifically authored as additive deltas
world.add_animation_layer(
&engine.world,
node,
"Breathing",
weight = 0.5,
blend_mode = .ADD,
)
// Blend between walk and run by adjusting weights
walk_weight := 0.7
run_weight := 0.3
world.set_animation_layer_weight(&engine.world, node, 0, walk_weight)
world.set_animation_layer_weight(&engine.world, node, 1, run_weight)
// Animate weight smoothly
target := enable_layer ? 1.0 : 0.0
current_weight = math.lerp(current_weight, target, delta_time * blend_speed)
IK solves bone chains to reach target positions, useful for foot placement, hand reaching, and look-at.
// Define bone chain from root to tip
bone_chain := []string{"Hips", "Spine", "Neck", "Head"}
// Set target and pole position
target_pos := [3]f32{0, 2, 5} // Where the chain should reach
pole_pos := [3]f32{0, 3, 2} // Hints the bending direction
// Add IK layer
world.add_ik_layer(
&engine.world,
node,
bone_chain,
target_pos,
pole_pos,
weight = 1.0,
layer_index = -1, // -1 = append new layer
)
// Update target each frame
world.set_ik_layer_target(&engine.world, node, layer_idx, new_target, new_pole)
Creates natural follow-through motion for tails, hair, antennas, etc. Bones react to parent movement with physics-like drag.
// Add tail modifier
success := world.add_tail_modifier_layer(
&engine.world,
node,
root_bone_name = "tail_root",
tail_length = 10, // Number of bones
propagation_speed = 0.85, // Reaction strength (0-1)
damping = 0.1, // Return speed (0-1, higher = slower)
weight = 1.0,
reverse_chain = false, // True if bones are ordered tip->root
)
// Parameters guide:
// - propagation_speed: How strongly bones counter-rotate parent motion
// Higher = more immediate reaction, creates visible drag
// - damping: How quickly bones return to rest pose
// Higher = slower return, longer wave propagation
Directly control one bone’s rotation, useful for root motion that drives other modifiers.
// Add modifier and get pointer
modifier := world.add_single_bone_rotation_modifier_layer(
&engine.world,
node,
bone_name = "root",
weight = 1.0,
layer_index = -1,
) or_else nil
// Update rotation each frame
if modifier != nil {
angle := math.sin(time) * math.PI * 0.3
modifier.rotation = linalg.quaternion_angle_axis_f32(angle, {0, 1, 0})
}
For procedural animation, you can create custom animation clips:
// Create animation clip
clip_handle, clip_ptr := cont.alloc(&engine.world.animation_clips, world.ClipHandle)
clip_ptr.name = "MyAnimation"
clip_ptr.duration = 2.0
clip_ptr.channels = make([]animation.Channel, bone_count)
// Initialize channel with procedural keyframes
mjolnir.init_animation_channel(
engine,
clip_handle,
channel_idx = 0,
position_count = 10,
rotation_count = 10,
position_fn = proc(i: int) -> [3]f32 {
t := f32(i) / 9.0
return {t * 5.0, math.sin(t * math.PI) * 2.0, 0}
},
rotation_fn = proc(i: int) -> quaternion128 {
t := f32(i) / 9.0
return linalg.quaternion_angle_axis_f32(t * math.TAU, {0, 1, 0})
},
)
Animation channels support different interpolation:
animation.InterpolationMode:
.LINEAR // Linear interpolation (smooth)
.STEP // Step interpolation (no smoothing)
.CUBIC_SPLINE // Cubic spline (smoothest, with tangents)
Simulates procedural leg movement for creatures with multiple limbs. Each leg has an offset from the body root, and automatically lifts and plants based on root movement.
import "../../mjolnir/animation"
// One SpiderLeg per limb
leg: animation.SpiderLeg
animation.spider_leg_init(
&leg,
initial_offset = {1.0, 0, 0.5}, // Resting position relative to root
lift_height = 0.4, // Peak arc height during a step
lift_frequency = 0.5, // Step cycle period (seconds)
lift_duration = 0.2, // Time a single step takes (seconds)
time_offset = 0.0, // Phase offset to stagger legs
)
// In update loop — drive from body root position
animation.spider_leg_update_with_root(&leg, delta_time, root_position)
// Current foot world position (use as IK target)
foot_pos := leg.feet_position
If you manage the target yourself (e.g. raycast to ground):
// Set desired foot target
leg.feet_target = ground_hit_position
// Advance the lift animation
animation.spider_leg_update(&leg, delta_time)
// Read current position
foot_pos := leg.feet_position
Use time_offset to prevent all legs lifting
simultaneously:
legs := [4]animation.SpiderLeg{}
offsets := [][3]f32{{1, 0, 1}, {-1, 0, 1}, {1, 0, -1}, {-1, 0, -1}}
for i in 0..<4 {
animation.spider_leg_init(
&legs[i],
initial_offset = offsets[i],
lift_frequency = 0.5,
time_offset = f32(i) * 0.125, // quarter-phase stagger per leg
)
}
lift_height: Arc peak during a step.
Higher = more exaggerated stepping.lift_frequency: Full cycle duration in
seconds. Lower = faster stepping cadence.lift_duration: How long one step
takes. Must be < lift_frequency.time_offset: Phase shift to
desynchronize legs. Use lift_frequency / num_legs
spacing.initial_offset: Rest position relative
to root. Used as the target when stationary.