Animation API ReferenceThe animation module provides a complete system for keyframe, skeletal, and morph target animations.

Animation API Reference

The animation module provides a complete system for keyframe, skeletal, and morph target animations.

Overview

import io.materia.animation.*

AnimationMixer

Controls playback of animations on a scene graph.

Constructor

class AnimationMixer(root: Object3D)

Properties

PropertyTypeDescription
timeFloatGlobal mixer time (seconds)
timeScaleFloatPlayback speed multiplier

Methods

// Create an action from a clip
fun clipAction(
    clip: AnimationClip,
    root: Object3D = this.root,
    blendMode: BlendMode = BlendMode.NORMAL
): AnimationAction

// Get existing action for clip
fun existingAction(clip: AnimationClip, root: Object3D = this.root): AnimationAction?

// Stop all actions
fun stopAllAction(): AnimationMixer

// Update animations (call each frame)
fun update(deltaTime: Float): AnimationMixer

// Set global time
fun setTime(time: Float): AnimationMixer

// Get root object
fun getRoot(): Object3D

// Uncache clip/action/root
fun uncacheClip(clip: AnimationClip)
fun uncacheAction(clip: AnimationClip, root: Object3D? = null)
fun uncacheRoot(root: Object3D)

Events

// Animation events
mixer.addEventListener("loop") { event ->
    val action = event.action
    val loopDelta = event.loopDelta
}

mixer.addEventListener("finished") { event ->
    val action = event.action
    val direction = event.direction
}

Example

// Load model with animations
gltfLoader.load("models/character.glb") { gltf ->
    val model = gltf.scene
    scene.add(model)
    
    // Create mixer
    val mixer = AnimationMixer(model)
    
    // Play first animation
    val action = mixer.clipAction(gltf.animations[0])
    action.play()
    
    // In render loop
    fun animate(deltaTime: Float) {
        mixer.update(deltaTime)
        renderer.render(scene, camera)
    }
}

AnimationAction

Controls a single animation clip on a mixer.

Properties

PropertyTypeDefaultDescription
enabledBooleantrueAction is active
loopLoopModeREPEATLoop behavior
repetitionsIntInfinityNumber of loops
pausedBooleanfalsePause playback
timeFloat0Current local time
timeScaleFloat1Playback speed
weightFloat1Blend weight
clampWhenFinishedBooleanfalseHold last frame
zeroSlopeAtStartBooleantrueSmooth start
zeroSlopeAtEndBooleantrueSmooth end

Loop Modes

enum class LoopMode {
    ONCE,        // Play once, then stop
    REPEAT,      // Loop continuously
    PING_PONG    // Play forward, then backward
}

Methods

// Playback control
fun play(): AnimationAction
fun stop(): AnimationAction
fun reset(): AnimationAction

// Pause control
fun pause(): AnimationAction
fun unpause(): AnimationAction

// Time control
fun setTime(time: Float): AnimationAction
fun setDuration(duration: Float): AnimationAction

// Weight/blending
fun setEffectiveWeight(weight: Float): AnimationAction
fun getEffectiveWeight(): Float
fun fadeIn(duration: Float): AnimationAction
fun fadeOut(duration: Float): AnimationAction
fun crossFadeFrom(fadeOutAction: AnimationAction, duration: Float, warp: Boolean = false): AnimationAction
fun crossFadeTo(fadeInAction: AnimationAction, duration: Float, warp: Boolean = false): AnimationAction

// Time scale
fun setEffectiveTimeScale(scale: Float): AnimationAction
fun getEffectiveTimeScale(): Float
fun warp(startScale: Float, endScale: Float, duration: Float): AnimationAction

// Sync
fun syncWith(action: AnimationAction): AnimationAction
fun halt(duration: Float): AnimationAction

// Status
fun isRunning(): Boolean
fun isScheduled(): Boolean

// Get clip
fun getClip(): AnimationClip
fun getMixer(): AnimationMixer
fun getRoot(): Object3D

Example

// Single animation
val idleAction = mixer.clipAction(idleClip)
idleAction.play()

// Crossfade between animations
val runAction = mixer.clipAction(runClip)
idleAction.crossFadeTo(runAction, 0.5f)
runAction.play()

// One-shot animation
val jumpAction = mixer.clipAction(jumpClip)
jumpAction.loop = LoopMode.ONCE
jumpAction.clampWhenFinished = true
jumpAction.play()

AnimationClip

Container for keyframe tracks that define an animation.

Constructor

class AnimationClip(
    name: String = "",
    duration: Float = -1f,
    tracks: List<KeyframeTrack> = emptyList(),
    blendMode: BlendMode = BlendMode.NORMAL
)

Properties

PropertyTypeDescription
nameStringClip name
durationFloatDuration in seconds (-1 = auto)
tracksList<KeyframeTrack>Keyframe tracks
uuidStringUnique identifier
blendModeBlendModeBlending behavior

Static Methods

// Find clip by name in array
fun findByName(clips: List<AnimationClip>, name: String): AnimationClip?

// Create clips from morph target sequence
fun CreateFromMorphTargetSequence(
    name: String,
    morphTargets: List<MorphTarget>,
    fps: Float,
    noLoop: Boolean = false
): AnimationClip

// Create clips from skeleton data
fun CreateFromBoneAnimation(
    name: String,
    skeleton: Skeleton,
    boneAnimation: BoneAnimation
): AnimationClip

// Parse JSON animation data
fun parse(json: JsonObject): AnimationClip

// Parse array of clips
fun parseAnimation(json: JsonObject, bones: List<Bone>): List<AnimationClip>

Methods

// Reset clip duration from tracks
fun resetDuration(): AnimationClip

// Trim/extend clip
fun trim(): AnimationClip

// Validate tracks
fun validate(): Boolean

// Optimize tracks (remove redundant keyframes)
fun optimize(): AnimationClip

// Clone clip
fun clone(): AnimationClip

KeyframeTrack

Base class for animation tracks. Interpolates values over time.

Subclasses

// Number tracks
class NumberKeyframeTrack(
    name: String,
    times: FloatArray,
    values: FloatArray,
    interpolation: Interpolation = Interpolation.LINEAR
)

// Vector tracks
class VectorKeyframeTrack(
    name: String,
    times: FloatArray,
    values: FloatArray,  // Flattened vector components
    interpolation: Interpolation = Interpolation.LINEAR
)

// Quaternion tracks (spherical interpolation)
class QuaternionKeyframeTrack(
    name: String,
    times: FloatArray,
    values: FloatArray  // x, y, z, w per keyframe
)

// Color tracks
class ColorKeyframeTrack(
    name: String,
    times: FloatArray,
    values: FloatArray  // r, g, b per keyframe
)

// Boolean tracks (step interpolation)
class BooleanKeyframeTrack(
    name: String,
    times: FloatArray,
    values: BooleanArray
)

// String tracks (step interpolation)
class StringKeyframeTrack(
    name: String,
    times: FloatArray,
    values: Array<String>
)

Track Name Format

Track names follow the pattern: objectName.propertyName[.subProperty]

// Position track
"mesh.position"

// Single axis
"mesh.position[x]"

// Rotation
"mesh.quaternion"

// Material property
"mesh.material.opacity"

// Morph target
"mesh.morphTargetInfluences[smile]"

// Bone transform
"skeleton.bones[LeftArm].quaternion"

Interpolation Modes

enum class Interpolation {
    DISCRETE,   // Step/hold
    LINEAR,     // Linear interpolation
    SMOOTH      // Smooth (cubic) interpolation
}

Example

// Create position animation
val positionTrack = VectorKeyframeTrack(
    name = "mesh.position",
    times = floatArrayOf(0f, 1f, 2f),
    values = floatArrayOf(
        0f, 0f, 0f,    // t=0
        0f, 2f, 0f,    // t=1
        0f, 0f, 0f     // t=2
    )
)

// Create rotation animation
val rotationTrack = QuaternionKeyframeTrack(
    name = "mesh.quaternion",
    times = floatArrayOf(0f, 2f),
    values = floatArrayOf(
        0f, 0f, 0f, 1f,                    // t=0 (identity)
        0f, 0.707f, 0f, 0.707f             // t=2 (90° around Y)
    )
)

// Create opacity animation
val opacityTrack = NumberKeyframeTrack(
    name = "mesh.material.opacity",
    times = floatArrayOf(0f, 0.5f, 1f),
    values = floatArrayOf(1f, 0f, 1f)
)

// Combine into clip
val clip = AnimationClip(
    name = "bounce",
    duration = 2f,
    tracks = listOf(positionTrack, rotationTrack, opacityTrack)
)

PropertyBinding

Binds a track to a property on an object.

Constructor

class PropertyBinding(
    rootNode: Object3D,
    path: String,
    parsedPath: ParsedPath? = null
)

Static Methods

// Parse track path
fun parseTrackName(trackName: String): ParsedPath

// Find node by name
fun findNode(root: Object3D, nodeName: String): Object3D?

AnimationObjectGroup

Groups objects for synchronized animation.

Constructor

class AnimationObjectGroup(vararg objects: Object3D)

Methods

// Add objects to group
fun add(vararg objects: Object3D)

// Remove objects from group
fun remove(vararg objects: Object3D)

// Uncache objects
fun uncache(vararg objects: Object3D)

Skeletal Animation

Skeleton

Manages a hierarchy of bones.

class Skeleton(
    bones: List<Bone> = emptyList(),
    boneInverses: List<Matrix4>? = null
)

Properties

PropertyTypeDescription
bonesList<Bone>Bone hierarchy
boneInversesList<Matrix4>Inverse bind matrices
boneMatricesFloatArrayFlattened bone matrices
boneTextureDataTexture?Bone data for GPU

Methods

// Calculate bone matrices from current pose
fun update()

// Pose skeleton from clip
fun pose()

// Calculate inverse matrices
fun calculateInverses()

// Get bone by name
fun getBoneByName(name: String): Bone?

// Clone skeleton
fun clone(): Skeleton

// Dispose resources
fun dispose()

Bone

A single bone in the skeleton hierarchy.

class Bone : Object3D() {
    val type: String = "Bone"
}

SkinnedMesh

Mesh with skeletal animation support.

class SkinnedMesh(
    geometry: BufferGeometry,
    material: Material
) : Mesh(geometry, material) {
    
    var bindMode: BindMode = BindMode.ATTACHED
    var bindMatrix: Matrix4 = Matrix4()
    var bindMatrixInverse: Matrix4 = Matrix4()
    var skeleton: Skeleton? = null
    
    fun bind(skeleton: Skeleton, bindMatrix: Matrix4? = null)
    fun pose()
    fun normalizeSkinWeights()
}

Example

// Load skinned model
gltfLoader.load("models/character.glb") { gltf ->
    val model = gltf.scene
    scene.add(model)
    
    // Find skinned mesh
    var skinnedMesh: SkinnedMesh? = null
    model.traverse { obj ->
        if (obj is SkinnedMesh) {
            skinnedMesh = obj
        }
    }
    
    // Get skeleton
    val skeleton = skinnedMesh?.skeleton
    
    // Visualize skeleton
    val helper = SkeletonHelper(model)
    scene.add(helper)
    
    // Play animation
    val mixer = AnimationMixer(model)
    val action = mixer.clipAction(gltf.animations[0])
    action.play()
}

Morph Targets

MorphTarget

Defines a blend shape deformation.

Usage on Geometry

// Geometry with morph targets
val geometry = BoxGeometry(1f, 1f, 1f)

// Create morph target (flattened positions)
val smilePositions = floatArrayOf(
    // Modified vertex positions for "smile" shape
    // ...
)

geometry.morphAttributes["position"] = listOf(
    Float32BufferAttribute(smilePositions, 3)
)

// Create mesh
val mesh = Mesh(geometry, material)
mesh.morphTargetInfluences[0] = 0.5f  // 50% smile

Animation with Morph Targets

// Create morph animation
val morphTrack = NumberKeyframeTrack(
    name = "mesh.morphTargetInfluences[smile]",
    times = floatArrayOf(0f, 1f, 2f),
    values = floatArrayOf(0f, 1f, 0f)
)

val clip = AnimationClip("smile", 2f, listOf(morphTrack))
val action = mixer.clipAction(clip)
action.play()

Animation Utilities

AnimationUtils

Static utility functions.

object AnimationUtils {
    // Convert seconds to frames
    fun convertSecondsToFrames(seconds: Float, fps: Float): Int
    
    // Get all keyframe times
    fun getKeyframeTimes(clip: AnimationClip): FloatArray
    
    // Merge clips
    fun mergeClips(clips: List<AnimationClip>): AnimationClip
    
    // Retarget clip to different skeleton
    fun retargetClip(
        clip: AnimationClip,
        sourceRoot: Object3D,
        targetRoot: Object3D
    ): AnimationClip
    
    // Create pose clip (single frame)
    fun createPoseClip(
        name: String,
        root: Object3D
    ): AnimationClip
}

Complete Animation Example

class AnimatedCharacter(private val scene: Scene) {
    
    private lateinit var model: Object3D
    private lateinit var mixer: AnimationMixer
    private val actions = mutableMapOf<String, AnimationAction>()
    private var currentAction: AnimationAction? = null
    
    fun load(onLoaded: () -> Unit) {
        GLTFLoader().load("models/character.glb") { gltf ->
            model = gltf.scene
            scene.add(model)
            
            // Setup mixer
            mixer = AnimationMixer(model)
            
            // Create actions for all clips
            for (clip in gltf.animations) {
                val action = mixer.clipAction(clip)
                actions[clip.name] = action
                
                // Prepare all actions
                action.enabled = true
                action.setEffectiveWeight(0f)
            }
            
            // Start with idle
            playAnimation("idle")
            
            onLoaded()
        }
    }
    
    fun playAnimation(name: String, fadeTime: Float = 0.3f) {
        val newAction = actions[name] ?: return
        
        if (currentAction == newAction) return
        
        newAction.reset()
        newAction.setEffectiveWeight(1f)
        newAction.play()
        
        currentAction?.let { old ->
            old.crossFadeTo(newAction, fadeTime)
        }
        
        currentAction = newAction
    }
    
    fun update(deltaTime: Float) {
        mixer.update(deltaTime)
    }
}

// Usage
val character = AnimatedCharacter(scene)
character.load {
    println("Character loaded!")
}

// On input
fun onKeyDown(key: String) {
    when (key) {
        "w" -> character.playAnimation("walk")
        "shift+w" -> character.playAnimation("run")
        else -> character.playAnimation("idle")
    }
}

// In render loop
fun animate(deltaTime: Float) {
    character.update(deltaTime)
    renderer.render(scene, camera)
}

See Also

Architected in Kotlin. Rendered with Materia. Powered by Aether.
© 2026 Yousef.