muse-engine-32 / engine /anim /root-motion.js
axonis22's picture
Muse Engine 32: your FBX/GLB scene as the level, skinned characters, Model check, save/load setup
6c78aa4 verified
Raw History Blame Contribute Delete
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;
})();