Files
retro3d/animation.cm
2026-01-18 11:23:32 -06:00

233 lines
5.9 KiB
Plaintext

// Animation module for retro3d
// Handles animation sampling, playback, and node transform updates
var model_c = use('model')
// Prepare animation data from a loaded model
// Extracts keyframe blobs from accessors for efficient sampling
function prepare_animations(model) {
if (!model._gltf || !model._gltf.animations) return []
var g = model._gltf
var buffer_blob = g.buffers[0] ? g.buffers[0].blob : null
if (!buffer_blob) return []
var prepared = []
for (var ai = 0; ai < length(g.animations); ai++) {
var anim = g.animations[ai]
var channels = []
var duration = 0
for (var ci = 0; ci < length(anim.channels); ci++) {
var chan = anim.channels[ci]
var sampler = anim.samplers[chan.sampler]
if (!sampler) continue
var input_acc = g.accessors[sampler.input]
var output_acc = g.accessors[sampler.output]
if (!input_acc || !output_acc) continue
// Extract times blob
var input_view = g.views[input_acc.view]
var times_blob = model_c.extract_accessor(
buffer_blob,
input_view.byte_offset || 0,
input_view.byte_stride || 0,
input_acc.byte_offset || 0,
input_acc.count,
input_acc.component_type,
input_acc.type
)
// Extract values blob
var output_view = g.views[output_acc.view]
var values_blob = model_c.extract_accessor(
buffer_blob,
output_view.byte_offset || 0,
output_view.byte_stride || 0,
output_acc.byte_offset || 0,
output_acc.count,
output_acc.component_type,
output_acc.type
)
// Track max time for duration
if (input_acc.max && input_acc.max[0] > duration) {
duration = input_acc.max[0]
}
channels.push({
node: chan.target.node,
path: chan.target.path,
interpolation: sampler.interpolation,
times: times_blob,
values: values_blob,
count: input_acc.count
})
}
prepared.push({
name: anim.name,
duration: duration,
channels: channels
})
}
return prepared
}
// Create an animation instance for playback
function create_instance(model, prepared_anims) {
return {
model: model,
animations: prepared_anims || prepare_animations(model),
clip_index: 0,
time: 0,
speed: 1,
loop: true,
playing: false
}
}
// Get clip count
function clip_count(instance) {
return instance.animations ? length(instance.animations) : 0
}
// Get clip duration
function clip_duration(instance, clip_idx) {
if (!instance.animations || clip_idx >= length(instance.animations)) return 0
return instance.animations[clip_idx].duration
}
// Get clip name
function clip_name(instance, clip_idx) {
if (!instance.animations || clip_idx >= length(instance.animations)) return null
return instance.animations[clip_idx].name
}
// Find clip by name
function find_clip(instance, name) {
if (!instance.animations) return -1
for (var i = 0; i < length(instance.animations); i++) {
if (instance.animations[i].name == name) return i
}
return -1
}
// Play a clip
function play(instance, clip_idx, loop_val) {
instance.clip_index = clip_idx
instance.loop = loop_val != null ? loop_val : true
instance.playing = true
instance.time = 0
}
// Stop playback
function stop(instance) {
instance.playing = false
}
// Set time
function set_time(instance, t) {
instance.time = t
}
// Set speed
function set_speed(instance, s) {
instance.speed = s
}
// Update animation time
function update(instance, dt) {
if (!instance.playing) return
instance.time += dt * instance.speed
var duration = clip_duration(instance, instance.clip_index)
if (duration > 0 && instance.time > duration) {
if (instance.loop) {
instance.time = instance.time % duration
} else {
instance.time = duration
instance.playing = false
}
}
}
// Apply animation to model nodes at current time
// This samples all channels and updates node TRS values
function apply(instance) {
if (!instance.animations || instance.clip_index >= length(instance.animations)) return
var anim = instance.animations[instance.clip_index]
var model = instance.model
var t = instance.time
for (var ci = 0; ci < length(anim.channels); ci++) {
var chan = anim.channels[ci]
var node_idx = chan.node
if (node_idx == null || node_idx >= length(model.nodes)) continue
var node = model.nodes[node_idx]
if (chan.path == "translation") {
var v = model_c.sample_vec3(chan.times, chan.values, chan.count, t, chan.interpolation)
node.x = v[0]
node.y = v[1]
node.z = v[2]
node.has_local_mat = false
node.dirty_local = true
node.dirty_world = true
} else if (chan.path == "rotation") {
var q = model_c.sample_quat(chan.times, chan.values, chan.count, t, chan.interpolation)
node.qx = q[0]
node.qy = q[1]
node.qz = q[2]
node.qw = q[3]
node.has_local_mat = false
node.dirty_local = true
node.dirty_world = true
} else if (chan.path == "scale") {
var s = model_c.sample_vec3(chan.times, chan.values, chan.count, t, chan.interpolation)
node.sx = s[0]
node.sy = s[1]
node.sz = s[2]
node.has_local_mat = false
node.dirty_local = true
node.dirty_world = true
}
}
// Mark all children dirty (propagate down hierarchy)
for (var ni = 0; ni < length(model.nodes); ni++) {
var n = model.nodes[ni]
if (n.dirty_world) {
_mark_children_dirty(n)
}
}
}
function _mark_children_dirty(node) {
for (var i = 0; i < length(node.children); i++) {
var child = node.children[i]
child.dirty_world = true
_mark_children_dirty(child)
}
}
return {
prepare_animations: prepare_animations,
create_instance: create_instance,
clip_count: clip_count,
clip_duration: clip_duration,
clip_name: clip_name,
find_clip: find_clip,
play: play,
stop: stop,
set_time: set_time,
set_speed: set_speed,
update: update,
apply: apply
}