Spaces:
Running
Running
Download engine/anim/skeleton-pose.js from axonis22/muse-engine-32: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/spaces/axonis22/muse-engine-32/resolve/main/engine/anim/skeleton-pose.js
- Command line
-
hf download hf://spaces/axonis22/muse-engine-32/engine/anim/skeleton-pose.js
-
curl -L -o skeleton-pose.js https://huggingface.co/spaces/axonis22/muse-engine-32/resolve/main/engine/anim/skeleton-pose.js
12.7 kB
| // engine/anim/skeleton-pose.js - written by Muse Glimmer (Phase 31, part B, 27 Sep 2026; raw output in game/muse-authored/phase31/raw/), | |
| // reviewed by Grok Bot (coach) with game/muse-authored/phase31/harness/. Coach fixes are marked [coach fix P31-n], see FIXES-P31.md. | |
| window.MuseSkeletonPose = (function () { | |
| function createPose(N) { | |
| var t = new Float32Array(3 * N); | |
| var r = new Float32Array(4 * N); | |
| var s = new Float32Array(3 * N); | |
| for (var i = 0; i < N; i++) { | |
| r[4 * i + 3] = 1; | |
| s[3 * i] = 1; | |
| s[3 * i + 1] = 1; | |
| s[3 * i + 2] = 1; | |
| } | |
| return { t: t, r: r, s: s }; | |
| } | |
| function restPose(skeleton, out) { | |
| var n = skeleton.nodes; | |
| var N = n.length; | |
| var t = out.t, | |
| r = out.r, | |
| s = out.s; | |
| for (var i = 0; i < N; i++) { | |
| var nd = n[i]; | |
| t[3 * i] = nd.t[0]; | |
| t[3 * i + 1] = nd.t[1]; | |
| t[3 * i + 2] = nd.t[2]; | |
| r[4 * i] = nd.r[0]; | |
| r[4 * i + 1] = nd.r[1]; | |
| r[4 * i + 2] = nd.r[2]; | |
| r[4 * i + 3] = nd.r[3]; | |
| s[3 * i] = nd.s[0]; | |
| s[3 * i + 1] = nd.s[1]; | |
| s[3 * i + 2] = nd.s[2]; | |
| } | |
| } | |
| function lerp(a, b, u) { | |
| return a + (b - a) * u; | |
| } | |
| function slerp(qa, qb, u) { | |
| var x1 = qa[0], | |
| y1 = qa[1], | |
| z1 = qa[2], | |
| w1 = qa[3]; | |
| var x2 = qb[0], | |
| y2 = qb[1], | |
| z2 = qb[2], | |
| w2 = qb[3]; | |
| var dot = x1 * x2 + y1 * y2 + z1 * z2 + w1 * w2; | |
| if (dot < 0) { | |
| x2 = -x2; | |
| y2 = -y2; | |
| z2 = -z2; | |
| w2 = -w2; | |
| dot = -dot; | |
| } | |
| if (dot > 0.9995) { | |
| var ix = x1 + (x2 - x1) * u, | |
| iy = y1 + (y2 - y1) * u, | |
| iz = z1 + (z2 - z1) * u, | |
| iw = w1 + (w2 - w1) * u; | |
| var len = Math.sqrt(ix * ix + iy * iy + iz * iz + iw * iw) || 1; | |
| return [ix / len, iy / len, iz / len, iw / len]; | |
| } | |
| var theta = Math.acos(Math.min(1, Math.max(-1, dot))); | |
| var sinTheta = Math.sin(theta); | |
| var s0 = Math.sin((1 - u) * theta) / sinTheta; | |
| var s1 = Math.sin(u * theta) / sinTheta; | |
| return [x1 * s0 + x2 * s1, y1 * s0 + y2 * s1, z1 * s0 + z2 * s1, w1 * s0 + w2 * s1]; | |
| } | |
| function binarySearch(times, t) { | |
| var lo = 0, | |
| hi = times.length - 1; | |
| while (lo <= hi) { | |
| var mid = (lo + hi) >> 1; | |
| if (times[mid] <= t) lo = mid + 1; | |
| else hi = mid - 1; | |
| } | |
| return hi; | |
| } | |
| function sampleClip(skeleton, clip, time, loop, out) { | |
| restPose(skeleton, out); | |
| var dur = clip.duration; | |
| if (loop) { | |
| time = ((time % dur) + dur) % dur; | |
| } else { | |
| if (time < 0) time = 0; | |
| if (time > dur) time = dur; | |
| } | |
| var N = skeleton.nodes.length; | |
| var t = out.t, | |
| r = out.r, | |
| s = out.s; | |
| for (var i = 0; i < clip.tracks.length; i++) { | |
| var tr = clip.tracks[i]; | |
| var node = tr.node; | |
| if (node < 0 || node >= N) continue; | |
| var times = tr.times; | |
| var vals = tr.values; | |
| var path = tr.path; | |
| var interp = tr.interp; | |
| var stride = path === "rotation" ? 4 : 3; | |
| var k = times.length; | |
| if (k === 0) continue; | |
| var idx = binarySearch(times, time); | |
| if (idx < 0) { | |
| idx = 0; | |
| } | |
| if (idx >= k - 1) { | |
| idx = k - 1; | |
| } | |
| var t0 = times[idx]; | |
| var t1 = times[Math.min(idx + 1, k - 1)]; | |
| var u = 0; | |
| if (t1 > t0) u = (time - t0) / (t1 - t0); | |
| if (u < 0) u = 0; // [coach fix P31-7] before the first key use the first key (was: extrapolated from keys 0-1) | |
| if (u > 1) u = 1; | |
| var base = idx * stride; | |
| var base1 = Math.min(idx + 1, k - 1) * stride; | |
| var off = 3 * node; | |
| if (path === "translation") { | |
| if (interp === "STEP") { | |
| t[off] = vals[base]; | |
| t[off + 1] = vals[base + 1]; | |
| t[off + 2] = vals[base + 2]; | |
| } else { | |
| t[off] = lerp(vals[base], vals[base1], u); | |
| t[off + 1] = lerp(vals[base + 1], vals[base1 + 1], u); | |
| t[off + 2] = lerp(vals[base + 2], vals[base1 + 2], u); | |
| } | |
| } else if (path === "scale") { | |
| if (interp === "STEP") { | |
| s[off] = vals[base]; | |
| s[off + 1] = vals[base + 1]; | |
| s[off + 2] = vals[base + 2]; | |
| } else { | |
| s[off] = lerp(vals[base], vals[base1], u); | |
| s[off + 1] = lerp(vals[base + 1], vals[base1 + 1], u); | |
| s[off + 2] = lerp(vals[base + 2], vals[base1 + 2], u); | |
| } | |
| } else { | |
| var qa = [vals[base], vals[base + 1], vals[base + 2], vals[base + 3]]; | |
| var qb = [vals[base1], vals[base1 + 1], vals[base1 + 2], vals[base1 + 3]]; | |
| if (interp === "STEP") { | |
| r[4 * node] = qa[0]; | |
| r[4 * node + 1] = qa[1]; | |
| r[4 * node + 2] = qa[2]; | |
| r[4 * node + 3] = qa[3]; | |
| } else { | |
| var q = slerp(qa, qb, u); | |
| r[4 * node] = q[0]; | |
| r[4 * node + 1] = q[1]; | |
| r[4 * node + 2] = q[2]; | |
| r[4 * node + 3] = q[3]; | |
| } | |
| } | |
| } | |
| } | |
| function blendPoses(a, b, w, out) { | |
| var N = a.t.length / 3; | |
| var ta = a.t, | |
| tb = b.t, | |
| oa = out.t; | |
| var sa = a.s, | |
| sb = b.s, | |
| os = out.s; | |
| var ra = a.r, | |
| rb = b.r, | |
| or = out.r; | |
| var iw = 1 - w; | |
| for (var i = 0; i < N; i++) { | |
| var i3 = i * 3, | |
| i4 = i * 4; | |
| oa[i3] = ta[i3] * iw + tb[i3] * w; | |
| oa[i3 + 1] = ta[i3 + 1] * iw + tb[i3 + 1] * w; | |
| oa[i3 + 2] = ta[i3 + 2] * iw + tb[i3 + 2] * w; | |
| os[i3] = sa[i3] * iw + sb[i3] * w; | |
| os[i3 + 1] = sa[i3 + 1] * iw + sb[i3 + 1] * w; | |
| os[i3 + 2] = sa[i3 + 2] * iw + sb[i3 + 2] * w; | |
| var qa = [ra[i4], ra[i4 + 1], ra[i4 + 2], ra[i4 + 3]]; | |
| var qb = [rb[i4], rb[i4 + 1], rb[i4 + 2], rb[i4 + 3]]; | |
| var q = slerp(qa, qb, w); | |
| or[i4] = q[0]; | |
| or[i4 + 1] = q[1]; | |
| or[i4 + 2] = q[2]; | |
| or[i4 + 3] = q[3]; | |
| } | |
| } | |
| function quatToMat3(q, s) { | |
| var x = q[0], | |
| y = q[1], | |
| z = q[2], | |
| w = q[3]; | |
| var xx = x * x, | |
| yy = y * y, | |
| zz = z * z; | |
| var xy = x * y, | |
| xz = x * z, | |
| yz = y * z; | |
| var wx = w * x, | |
| wy = w * y, | |
| wz = w * z; | |
| var sx = s[0], | |
| sy = s[1], | |
| sz = s[2]; | |
| return [ | |
| (1 - 2 * (yy + zz)) * sx, | |
| 2 * (xy - wz) * sy, | |
| 2 * (xz + wy) * sz, | |
| 2 * (xy + wz) * sx, | |
| (1 - 2 * (xx + zz)) * sy, | |
| 2 * (yz - wx) * sz, | |
| 2 * (xz - wy) * sx, | |
| 2 * (yz + wx) * sy, | |
| (1 - 2 * (xx + yy)) * sz, | |
| ]; | |
| } | |
| var _m = new Float32Array(16), _res = new Float32Array(16); // [coach fix P31-8] scratch (was 2 new Float32Array per node per frame) | |
| function worldMatrices(skeleton, pose, rootMatrix, out) { | |
| var N = skeleton.nodes.length; | |
| var nodes = skeleton.nodes; | |
| var t = pose.t, | |
| r = pose.r, | |
| s = pose.s; | |
| for (var i = 0; i < N; i++) { | |
| var parent = nodes[i].parent; | |
| var m = _m; | |
| var ti = t[3 * i], | |
| tj = t[3 * i + 1], | |
| tk = t[3 * i + 2]; | |
| var si = s[3 * i], | |
| sj = s[3 * i + 1], | |
| sk = s[3 * i + 2]; | |
| var q = [r[4 * i], r[4 * i + 1], r[4 * i + 2], r[4 * i + 3]]; | |
| var m3 = quatToMat3(q, [si, sj, sk]); | |
| m[0] = m3[0]; | |
| m[1] = m3[3]; | |
| m[2] = m3[6]; | |
| m[3] = 0; | |
| m[4] = m3[1]; | |
| m[5] = m3[4]; | |
| m[6] = m3[7]; | |
| m[7] = 0; | |
| m[8] = m3[2]; | |
| m[9] = m3[5]; | |
| m[10] = m3[8]; | |
| m[11] = 0; | |
| m[12] = ti; | |
| m[13] = tj; | |
| m[14] = tk; | |
| m[15] = 1; | |
| if (parent === -1) { | |
| var rm = rootMatrix; | |
| var res = _res; | |
| for (var row = 0; row < 4; row++) | |
| for (var col = 0; col < 4; col++) { | |
| var sum = 0; | |
| for (var k = 0; k < 4; k++) sum += rm[k * 4 + row] * m[col * 4 + k]; | |
| res[col * 4 + row] = sum; | |
| } | |
| out.set(res, i * 16); | |
| } else { | |
| var p = out.subarray(parent * 16, parent * 16 + 16); | |
| var res = _res; | |
| for (var row = 0; row < 4; row++) | |
| for (var col = 0; col < 4; col++) { | |
| var sum = 0; | |
| for (var k = 0; k < 4; k++) sum += p[k * 4 + row] * m[col * 4 + k]; | |
| res[col * 4 + row] = sum; | |
| } | |
| out.set(res, i * 16); | |
| } | |
| } | |
| } | |
| function skinPalette(world, boneNodes, inverseBind, bindShape, out) { | |
| var J = boneNodes.length; | |
| var tmp = new Float32Array(16); | |
| for (var j = 0; j < J; j++) { | |
| var node = boneNodes[j]; | |
| var w = world.subarray(node * 16, node * 16 + 16); | |
| var ib = inverseBind.subarray(j * 16, j * 16 + 16); | |
| for (var row = 0; row < 4; row++) { | |
| for (var col = 0; col < 4; col++) { | |
| var sum = 0; | |
| for (var k = 0; k < 4; k++) sum += w[k * 4 + row] * ib[col * 4 + k]; | |
| tmp[col * 4 + row] = sum; | |
| } | |
| } | |
| var off = 12 * j; | |
| out[off] = tmp[0]; | |
| out[off + 1] = tmp[4]; | |
| out[off + 2] = tmp[8]; | |
| out[off + 3] = tmp[12]; | |
| out[off + 4] = tmp[1]; | |
| out[off + 5] = tmp[5]; | |
| out[off + 6] = tmp[9]; | |
| out[off + 7] = tmp[13]; | |
| out[off + 8] = tmp[2]; | |
| out[off + 9] = tmp[6]; | |
| out[off + 10] = tmp[10]; | |
| out[off + 11] = tmp[14]; | |
| } | |
| } | |
| // [coach fix P31-9] Fader rewritten around Muse's two-layer design. Muse's version: (a) at the end of a fade did | |
| // `cur = nxt; nxt.idx = -1`, aliasing both layers so the character froze after every crossfade; (b) play() with the | |
| // clip already playing started a fade to itself; (c) a new play() during a fade dropped the blended pose (a visible | |
| // pop); (d) state().done read a non-existent field and was never true; (e) allocated two poses per update. | |
| // Now: layer `to` (target) and `from` (a live clip, or a frozen snapshot when a play interrupts a fade). | |
| function Fader(skeleton) { | |
| var N = skeleton.nodes.length; | |
| this.skeleton = skeleton; | |
| this.to = null; // { idx, time, speed, loop, dur } | |
| this.from = null; // { idx, time, speed, loop, dur } or { frozen: true } | |
| this.fadeDur = 0; | |
| this.fadeEl = 0; | |
| this.pa = createPose(N); | |
| this.pb = createPose(N); | |
| this.snap = createPose(N); | |
| this.last = createPose(N); | |
| this.hasLast = false; | |
| } | |
| Fader.prototype.play = function (clipIndex, fadeSeconds, opts) { | |
| opts = opts || {}; | |
| if (this.to && this.to.idx === clipIndex) return; // already the target: keep its time | |
| var layer = { idx: clipIndex, time: 0, speed: opts.speed !== undefined ? opts.speed : 1, loop: opts.loop !== undefined ? opts.loop : true, dur: 0 }; | |
| var fade = fadeSeconds > 0 ? fadeSeconds : 0; | |
| if (fade > 0 && this.to) { | |
| if (this.from && this.fadeEl < this.fadeDur && this.hasLast) { | |
| // interrupting a fade: the current blended result becomes a frozen source | |
| this.snap.t.set(this.last.t); | |
| this.snap.r.set(this.last.r); | |
| this.snap.s.set(this.last.s); | |
| this.from = { frozen: true }; | |
| } else this.from = this.to; | |
| } else this.from = null; | |
| this.to = layer; | |
| this.fadeDur = fade; | |
| this.fadeEl = 0; | |
| }; | |
| function advance(l, clips) { | |
| var c = clips[l.idx]; | |
| l.dur = c.duration; | |
| l.time += l.dt; | |
| if (!l.loop && l.time > c.duration) l.time = c.duration; | |
| } | |
| Fader.prototype.update = function (dt, clips, outPose) { | |
| if (!this.to) { | |
| restPose(this.skeleton, outPose); | |
| return; | |
| } | |
| this.to.dt = dt * this.to.speed; | |
| advance(this.to, clips); | |
| sampleClip(this.skeleton, clips[this.to.idx], this.to.time, this.to.loop, this.pb); | |
| var w = 1; | |
| if (this.from && this.fadeDur > 0) { | |
| this.fadeEl += dt; | |
| w = Math.min(1, this.fadeEl / this.fadeDur); | |
| } | |
| if (this.from && w < 1) { | |
| var src = this.snap; | |
| if (!this.from.frozen) { | |
| this.from.dt = dt * this.from.speed; | |
| advance(this.from, clips); | |
| sampleClip(this.skeleton, clips[this.from.idx], this.from.time, this.from.loop, this.pa); | |
| src = this.pa; | |
| } | |
| blendPoses(src, this.pb, w, outPose); | |
| } else { | |
| this.from = null; | |
| outPose.t.set(this.pb.t); | |
| outPose.r.set(this.pb.r); | |
| outPose.s.set(this.pb.s); | |
| } | |
| this.last.t.set(outPose.t); | |
| this.last.r.set(outPose.r); | |
| this.last.s.set(outPose.s); | |
| this.hasLast = true; | |
| this.w = w; | |
| }; | |
| Fader.prototype.state = function () { | |
| var l = this.to; | |
| return { | |
| clip: l ? l.idx : -1, | |
| time: l ? l.time : 0, | |
| weight: this.from && this.fadeDur > 0 ? Math.min(1, this.fadeEl / this.fadeDur) : 1, | |
| fading: !!(this.from && this.fadeDur > 0 && this.fadeEl < this.fadeDur), | |
| done: !!(l && !l.loop && l.dur > 0 && l.time >= l.dur), | |
| }; | |
| }; | |
| return { | |
| createPose: createPose, | |
| restPose: restPose, | |
| sampleClip: sampleClip, | |
| blendPoses: blendPoses, | |
| worldMatrices: worldMatrices, | |
| skinPalette: skinPalette, | |
| Fader: Fader, | |
| }; | |
| })(); | |