Keyframe Animation Functions
GPU-optimized keyframe animation system for skeletal animation.
Asset Configuration
Configure animations in nether.toml:
Explicit Import (Single Animation)
[[assets.animations]]
id = "walk"
path = "models/character.glb"
animation_name = "WalkCycle"
skin_name = "Armature"
Bulk Import (All Animations from GLB)
Import all animations from a single GLB file with one entry:
[[assets.animations]]
path = "models/character.glb"
skin_name = "Armature"
id_prefix = "char_" # Optional: creates char_Walk, char_Run, etc.
When both id and animation_name are omitted, ALL animations from the GLB are imported using their original Blender names as asset IDs. Use id_prefix to prevent collisions when importing from multiple GLB files.
Loading Keyframes
keyframes_load
Loads keyframes from WASM memory.
Signature:
#![allow(unused)]
fn main() {
fn keyframes_load(data_ptr: *const u8, byte_size: u32) -> u32
}
Parameters:
| Name | Type | Description |
|---|---|---|
| data_ptr | *const u8 | Pointer to keyframe data |
| byte_size | u32 | Size of data in bytes |
Returns: Keyframe collection handle (non-zero on success)
Constraints: Init-only.
Example:
#![allow(unused)]
fn main() {
static WALK_DATA: &[u8] = include_bytes!("walk.nczxanim");
static mut WALK_ANIM: u32 = 0;
fn init() {
unsafe {
WALK_ANIM = keyframes_load(WALK_DATA.as_ptr(), WALK_DATA.len() as u32);
}
}
}
rom_keyframes
Loads keyframes from ROM data pack.
Signature:
#![allow(unused)]
fn main() {
fn rom_keyframes(id_ptr: *const u8, id_len: u32) -> u32
}
Parameters:
| Name | Type | Description |
|---|---|---|
| id_ptr | *const u8 | Pointer to asset ID string |
| id_len | u32 | Length of asset ID |
Returns: Keyframe collection handle (non-zero on success)
Constraints: Init-only.
Example:
#![allow(unused)]
fn main() {
static mut WALK_ANIM: u32 = 0;
static mut IDLE_ANIM: u32 = 0;
static mut ATTACK_ANIM: u32 = 0;
fn init() {
unsafe {
WALK_ANIM = rom_keyframes(b"walk".as_ptr(), 4);
IDLE_ANIM = rom_keyframes(b"idle".as_ptr(), 4);
ATTACK_ANIM = rom_keyframes(b"attack".as_ptr(), 6);
}
}
}
Querying Keyframes
keyframes_bone_count
Gets the bone count for a keyframe collection.
Signature:
#![allow(unused)]
fn main() {
fn keyframes_bone_count(handle: u32) -> u32
}
Returns: Number of bones in the animation
Example:
#![allow(unused)]
fn main() {
fn init() {
unsafe {
WALK_ANIM = rom_keyframes(b"walk".as_ptr(), 4);
let bones = keyframes_bone_count(WALK_ANIM);
log_fmt(b"Walk animation has {} bones", bones);
}
}
}
keyframes_frame_count
Gets the frame count for a keyframe collection.
Signature:
#![allow(unused)]
fn main() {
fn keyframes_frame_count(handle: u32) -> u32
}
Returns: Number of frames in the animation
Example:
#![allow(unused)]
fn main() {
fn render() {
unsafe {
let frame_count = keyframes_frame_count(WALK_ANIM);
let current_frame = (ANIM_TIME as u32) % frame_count;
keyframe_bind(WALK_ANIM, current_frame);
}
}
}
Using Keyframes
keyframe_bind
Binds a keyframe directly from GPU buffer (zero CPU overhead).
Signature:
#![allow(unused)]
fn main() {
fn keyframe_bind(handle: u32, index: u32)
}
Parameters:
| Name | Type | Description |
|---|---|---|
| handle | u32 | Keyframe collection handle |
| index | u32 | Frame index (0 to frame_count-1) |
Example:
#![allow(unused)]
fn main() {
static mut ANIM_FRAME: f32 = 0.0;
fn update() {
unsafe {
ANIM_FRAME += delta_time() * 30.0; // 30 FPS animation
}
}
fn render() {
unsafe {
let frame_count = keyframes_frame_count(WALK_ANIM);
let frame = (ANIM_FRAME as u32) % frame_count;
// Bind frame - GPU reads directly, no CPU decode!
keyframe_bind(WALK_ANIM, frame);
draw_mesh(CHARACTER_MESH);
}
}
}
keyframe_read
Reads a keyframe to WASM memory for CPU-side blending.
Signature:
#![allow(unused)]
fn main() {
fn keyframe_read(handle: u32, index: u32, out_ptr: *mut u8)
}
Parameters:
| Name | Type | Description |
|---|---|---|
| handle | u32 | Keyframe collection handle |
| index | u32 | Frame index |
| out_ptr | *mut u8 | Destination buffer (must be bone_count × 40 bytes) |
Output Format (TRS, 40 bytes per bone):
Per-bone layout:
[0..16] rotation: [f32; 4] (quaternion x, y, z, w)
[16..28] position: [f32; 3] (translation x, y, z)
[28..40] scale: [f32; 3] (scale x, y, z)
Note: This returns TRS (Translation-Rotation-Scale) format, NOT matrices. Use this for CPU-side interpolation/blending. For stamp animation without blending, use
keyframe_bind()which uses pre-converted matrices on GPU.
Example:
#![allow(unused)]
fn main() {
const BONE_COUNT: usize = 64;
const TRS_SIZE: usize = 40; // 10 floats per bone (quat[4] + pos[3] + scale[3])
fn render() {
unsafe {
let frame_count = keyframes_frame_count(WALK_ANIM);
let frame_a = (ANIM_TIME as u32) % frame_count;
let frame_b = (frame_a + 1) % frame_count;
let blend = ANIM_TIME.fract();
// Read TRS frames for interpolation
let mut buf_a = [0u8; BONE_COUNT * TRS_SIZE];
let mut buf_b = [0u8; BONE_COUNT * TRS_SIZE];
keyframe_read(WALK_ANIM, frame_a, buf_a.as_mut_ptr());
keyframe_read(WALK_ANIM, frame_b, buf_b.as_mut_ptr());
// Interpolate TRS on CPU (SLERP quaternions, LERP position/scale)
let blended_trs = interpolate_trs(&buf_a, &buf_b, blend);
// Convert TRS to matrices and upload
let matrices = trs_to_matrices(&blended_trs, BONE_COUNT);
set_bones_4x4(matrices.as_ptr(), BONE_COUNT as u32);
draw_mesh(CHARACTER_MESH);
}
}
}
Animation Paths
| Path | Function | Use Case | Performance |
|---|---|---|---|
| Static | keyframe_bind() | Pre-baked ROM animations | Zero CPU work |
| Immediate | set_bones() | Procedural, IK, blended | Minimal overhead |
Static keyframes: Data uploaded to GPU once in init(). keyframe_bind() just sets buffer offset.
Immediate bones: Matrices appended to per-frame buffer, uploaded before rendering.
Complete Example
#![allow(unused)]
fn main() {
static mut SKELETON: u32 = 0;
static mut CHARACTER: u32 = 0;
static mut WALK_ANIM: u32 = 0;
static mut IDLE_ANIM: u32 = 0;
static mut ANIM_TIME: f32 = 0.0;
static mut IS_WALKING: bool = false;
fn init() {
unsafe {
SKELETON = rom_skeleton(b"player_rig".as_ptr(), 10);
CHARACTER = rom_mesh(b"player".as_ptr(), 6);
WALK_ANIM = rom_keyframes(b"walk".as_ptr(), 4);
IDLE_ANIM = rom_keyframes(b"idle".as_ptr(), 4);
}
}
fn update() {
unsafe {
// Check movement input
let stick_x = left_stick_x(0);
let stick_y = left_stick_y(0);
IS_WALKING = stick_x.abs() > 0.1 || stick_y.abs() > 0.1;
// Advance animation
let anim_speed = if IS_WALKING { 30.0 } else { 15.0 };
ANIM_TIME += delta_time() * anim_speed;
}
}
fn render() {
unsafe {
skeleton_bind(SKELETON);
// Choose animation
let anim = if IS_WALKING { WALK_ANIM } else { IDLE_ANIM };
let frame_count = keyframes_frame_count(anim);
let frame = (ANIM_TIME as u32) % frame_count;
// Bind keyframe (GPU-side, no CPU decode)
keyframe_bind(anim, frame);
// Draw character
texture_bind(player_texture);
push_identity();
push_translate(player_x, player_y, player_z);
draw_mesh(CHARACTER);
skeleton_bind(0);
}
}
}
See Also: Skinning Functions, rom_keyframes