// 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 }