// 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, }; })();