Spaces:
Running
Running
Download engine/anim/root-motion.js from axonis22/muse-engine-32: direct link, hf CLI and curl.
- Browser
- Download file 6.82 kB
-
https://huggingface.co/spaces/axonis22/muse-engine-32/resolve/main/engine/anim/root-motion.js
- Command line
-
hf download hf://spaces/axonis22/muse-engine-32/engine/anim/root-motion.js
-
curl -L -o root-motion.js https://huggingface.co/spaces/axonis22/muse-engine-32/resolve/main/engine/anim/root-motion.js
6.82 kB
| // engine/anim/root-motion.js - written by Muse Glimmer (Phase 32, part A, 27 Sep 2026; raw output in game/muse-authored/phase32/raw/), | |
| // reviewed by Grok Bot (coach) with game/muse-authored/phase32/harness/. Coach fixes are marked [coach fix P32-n], see FIXES-P32.md. | |
| (function () { | |
| function dot(a, b) { | |
| return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; | |
| } | |
| function sub(a, b) { | |
| return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; | |
| } | |
| function add(a, b) { | |
| return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; | |
| } | |
| function scale(a, s) { | |
| return [a[0] * s, a[1] * s, a[2] * s]; | |
| } | |
| function len(v) { | |
| return Math.sqrt(dot(v, v)); | |
| } | |
| function hVec(v, up) { | |
| var d = dot(v, up); | |
| return [v[0] - d * up[0], v[1] - d * up[1], v[2] - d * up[2]]; | |
| } | |
| function measure(clip, node, up) { | |
| var tracks = clip.tracks || []; | |
| var tr = null; | |
| for (var i = 0; i < tracks.length; i++) { | |
| var t = tracks[i]; | |
| if (t.node === node && t.path === 'translation') { | |
| tr = t; | |
| break; | |
| } | |
| } | |
| if (!tr) return null; | |
| var times = tr.times; | |
| var values = tr.values; | |
| var n = times.length; | |
| if (n === 0) return null; | |
| var first = [values[0], values[1], values[2]]; | |
| var lastIdx = (n - 1) * 3; | |
| var last = [values[lastIdx], values[lastIdx + 1], values[lastIdx + 2]]; | |
| var net = sub(last, first); | |
| var hnet = hVec(net, up); | |
| var horiz = len(hnet); | |
| var vert = Math.abs(dot(net, up)); | |
| var maxHoriz = 0; | |
| for (var i = 0; i < n; i++) { | |
| var idx = i * 3; | |
| var key = [values[idx], values[idx + 1], values[idx + 2]]; | |
| var diff = sub(key, first); | |
| var hd = hVec(diff, up); | |
| var l = len(hd); | |
| if (l > maxHoriz) maxHoriz = l; | |
| } | |
| var duration = clip.duration || 0; | |
| var speed = duration > 0 ? horiz / duration : 0; | |
| return { keys: n, first: first, last: last, net: net, horiz: horiz, vert: vert, maxHoriz: maxHoriz, speed: speed }; | |
| } | |
| function strip(clip, node, up, opts) { | |
| opts = opts || {}; | |
| var mode = opts.mode === 'lock' ? 'lock' : 'drift'; | |
| var tracks = clip.tracks || []; | |
| var found = false; | |
| var newTracks = []; | |
| var removed = [0, 0, 0]; | |
| for (var i = 0; i < tracks.length; i++) { | |
| var t = tracks[i]; | |
| if (t.node === node && t.path === 'translation' && t.times.length > 0) { // [coach fix P32-3] an empty track made NaN keys; now treated as no track | |
| found = true; | |
| var times = t.times; | |
| var values = t.values; | |
| var n = times.length; | |
| var first = [values[0], values[1], values[2]]; | |
| var lastIdx = (n - 1) * 3; | |
| var last = [values[lastIdx], values[lastIdx + 1], values[lastIdx + 2]]; | |
| var net = sub(last, first); | |
| var hnet = hVec(net, up); | |
| removed = hnet; | |
| var newValues = new Float32Array(values.length); | |
| newValues.set(values); | |
| var t0 = times[0]; | |
| var tLast = times[n - 1]; | |
| var denom = tLast - t0; | |
| for (var k = 0; k < n; k++) { | |
| var idx = k * 3; | |
| var key = [values[idx], values[idx + 1], values[idx + 2]]; | |
| var newKey; | |
| if (mode === 'drift') { | |
| var u = denom !== 0 ? (times[k] - t0) / denom : 0; | |
| var subVec = scale(hnet, u); | |
| newKey = sub(key, subVec); | |
| } else { | |
| var diff = sub(key, first); | |
| var hdiff = hVec(diff, up); | |
| newKey = sub(key, hdiff); | |
| } | |
| newValues[idx] = newKey[0]; | |
| newValues[idx + 1] = newKey[1]; | |
| newValues[idx + 2] = newKey[2]; | |
| } | |
| var newTrack = Object.assign({}, t, { times: times, values: newValues }); // [coach fix P32-3] keep any other track fields (was a fixed field list) | |
| newTracks.push(newTrack); | |
| } else { | |
| newTracks.push(t); | |
| } | |
| } | |
| if (!found) { | |
| return { clip: clip, removed: [0, 0, 0], mode: mode }; | |
| } | |
| var newClip = { name: clip.name, duration: clip.duration, tracks: newTracks }; | |
| return { clip: newClip, removed: removed, mode: mode }; | |
| } | |
| function pickRoot(skeleton, clips, hipsNode) { | |
| var nodes = (skeleton && skeleton.nodes) || []; | |
| var candidates = new Set(); | |
| for (var i = 0; i < clips.length; i++) { | |
| var trs = clips[i].tracks || []; | |
| for (var j = 0; j < trs.length; j++) { | |
| var t = trs[j]; | |
| if (t.path === 'translation') { | |
| candidates.add(t.node); | |
| } | |
| } | |
| } | |
| function moves(nodeIdx) { | |
| for (var i = 0; i < clips.length; i++) { | |
| var trs = clips[i].tracks || []; | |
| for (var j = 0; j < trs.length; j++) { | |
| var t = trs[j]; | |
| if (t.node === nodeIdx && t.path === 'translation') { | |
| var times = t.times; | |
| var values = t.values; | |
| var n = times.length; | |
| if (n < 2) continue; | |
| var first = [values[0], values[1], values[2]]; | |
| var maxd = 0; | |
| for (var k = 1; k < n; k++) { | |
| var idx = k * 3; | |
| var key = [values[idx], values[idx + 1], values[idx + 2]]; | |
| var d = Math.sqrt( | |
| (key[0] - first[0]) * (key[0] - first[0]) + (key[1] - first[1]) * (key[1] - first[1]) + (key[2] - first[2]) * (key[2] - first[2]), | |
| ); | |
| if (d > maxd) maxd = d; | |
| } | |
| if (maxd > 1e-6) return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| if (hipsNode >= 0 && hipsNode < nodes.length) { | |
| var chain = []; | |
| var cur = hipsNode; | |
| while (cur >= 0 && cur < nodes.length && chain.length <= nodes.length) { // [coach fix P32-3] stop on bad / cyclic parent indices (was an endless loop or a TypeError) | |
| chain.push(cur); | |
| var p = nodes[cur].parent; | |
| if (p == null || p < 0) break; | |
| cur = p; | |
| } | |
| for (var i = 0; i < chain.length; i++) { | |
| var nIdx = chain[i]; | |
| if (candidates.has(nIdx) && moves(nIdx)) { | |
| return nIdx; | |
| } | |
| } | |
| } | |
| var bestIdx = -1; | |
| var bestDepth = Infinity; | |
| function depth(idx) { | |
| var d = 0; | |
| var cur = idx; | |
| while (cur >= 0 && cur < nodes.length && d <= nodes.length) { // [coach fix P32-3] cycle guard | |
| var p = nodes[cur].parent; | |
| if (p == null || p < 0) break; | |
| d++; | |
| cur = p; | |
| } | |
| return d; | |
| } | |
| candidates.forEach(function (nIdx) { | |
| if (!moves(nIdx)) return; | |
| var d = depth(nIdx); | |
| if (d < bestDepth || (d === bestDepth && nIdx < bestIdx)) { | |
| bestDepth = d; | |
| bestIdx = nIdx; | |
| } | |
| }); | |
| return bestIdx; | |
| } | |
| var MuseRootMotion = { measure: measure, strip: strip, pickRoot: pickRoot }; | |
| if (typeof window !== 'undefined') window.MuseRootMotion = MuseRootMotion; | |
| if (typeof module !== 'undefined') module.exports = MuseRootMotion; | |
| })(); | |