"use strict"; // Skeleton poses and CPU skinning, the io3DEngine way (D3D row vectors: v' = v · BipedSpaceTM · // BoneWorld; keys replace the bone's local position / rotation; linked skeletons use the offset TM // and bone-length ratio of the skeleton they link to). Used by the map editor for animated map // objects; the model viewer (model.js) has the same maths inline. const SkinKit = (() => { const M4 = { fromQT(q, p) { const [x, y, z, w] = q; return new Float64Array([ 1 - 2 * (y * y + z * z), 2 * (x * y + z * w), 2 * (x * z - y * w), 0, 2 * (x * y - z * w), 1 - 2 * (x * x + z * z), 2 * (y * z + x * w), 0, 2 * (x * z + y * w), 2 * (y * z - x * w), 1 - 2 * (x * x + y * y), 0, p[0], p[1], p[2], 1, ]); }, mul(a, b) { const o = new Float64Array(16); for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) o[r * 4 + c] = a[r * 4] * b[c] + a[r * 4 + 1] * b[4 + c] + a[r * 4 + 2] * b[8 + c] + a[r * 4 + 3] * b[12 + c]; return o; }, /** Inverse of an affine row-vector matrix (rotation / scale part + translation row). */ invertAffine(m) { const a = m[0], b = m[1], c = m[2], d = m[4], e = m[5], f = m[6], g = m[8], h = m[9], i = m[10]; const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); if (Math.abs(det) < 1e-12) return M4.fromQT([0, 0, 0, 1], [0, 0, 0]); const k = 1 / det; const r = new Float64Array(16); r[0] = (e * i - f * h) * k; r[1] = (c * h - b * i) * k; r[2] = (b * f - c * e) * k; r[4] = (f * g - d * i) * k; r[5] = (a * i - c * g) * k; r[6] = (c * d - a * f) * k; r[8] = (d * h - e * g) * k; r[9] = (b * g - a * h) * k; r[10] = (a * e - b * d) * k; const tx = m[12], ty = m[13], tz = m[14]; r[12] = -(tx * r[0] + ty * r[4] + tz * r[8]); r[13] = -(tx * r[1] + ty * r[5] + tz * r[9]); r[14] = -(tx * r[2] + ty * r[6] + tz * r[10]); r[15] = 1; return r; }, }; const qmul = (a, b) => [ a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1], a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0], a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3], a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2], ]; function qrot(q, v) { const [x, y, z, w] = q; const ux = y * v[2] - z * v[1], uy = z * v[0] - x * v[2], uz = x * v[1] - y * v[0]; const uux = y * uz - z * uy, uuy = z * ux - x * uz, uuz = x * uy - y * ux; return [v[0] + 2 * (w * ux + uux), v[1] + 2 * (w * uy + uuy), v[2] + 2 * (w * uz + uuz)]; } /** A parsed .skl (LSF.parseSkl) → skeleton ready for posing. */ function prepare(data) { const n = data.bones.length; const byName = new Map(); data.bones.forEach((b, i) => byName.has(b.name.text.toLowerCase()) || byName.set(b.name.text.toLowerCase(), i)); const parent = data.bones.map((b) => { const p = b.parent.text.toLowerCase(); return p === "noparent" || !byName.has(p) ? -1 : byName.get(p); }); const order = []; const done = new Uint8Array(n); const visit = (i, depth) => { if (done[i] || depth > n) return; if (parent[i] >= 0) visit(parent[i], depth + 1); if (!done[i]) { done[i] = 1; order.push(i); } }; for (let i = 0; i < n; i++) visit(i, 0); return { data, byName, parent, order, bst: data.bones.map((b) => M4.fromQT(b.invRot, b.invPos)), rate: new Float64Array(n).fill(1), linked: data.linked || "" }; } /** Applies the skeleton it links to (same bone count required, like the client). */ function link(sk, other) { if (!other || other.bones.length !== sk.data.bones.length) return false; const rm = (a) => Float64Array.from(a); sk.data.bones.forEach((b, i) => { const o = other.bones[i]; const offset = M4.mul(M4.invertAffine(rm(o.objectTM)), rm(b.objectTM)); sk.bst[i] = M4.mul(offset, M4.fromQT(b.invRot, b.invPos)); const l = Math.hypot(...o.localPos); sk.rate[i] = l > 1e-6 ? Math.hypot(...b.localPos) / l : 1; }); return true; } const mapTracks = (sk, ani) => Int32Array.from(ani.tracks, (t) => sk.byName.get(t.name.text.toLowerCase()) ?? -1); const palette = (sk, meshData) => (meshData.boneNames ? Int32Array.from(meshData.boneNames, (b) => sk.byName.get(b.text.toLowerCase()) ?? -1) : null); /** Bone world transforms + skin matrices at t (ms). */ function pose(sk, ani, map, t) { const bones = sk.data.bones; const n = bones.length; const lp = bones.map((b) => b.localPos.slice()); const lr = bones.map((b) => b.localRot.slice()); if (ani) { const p = [0, 0, 0]; const q = [0, 0, 0, 1]; ani.tracks.forEach((tr, ti) => { const bi = map[ti]; if (bi < 0 || tr.weight <= 0 || !LSF.sampleTrack(tr, t, p, q)) return; const r = sk.rate[bi]; lp[bi] = [p[0] * r, p[1] * r, p[2] * r]; lr[bi] = q.slice(); }); } const wp = new Array(n); const wr = new Array(n); for (const i of sk.order) { const pi = sk.parent[i]; if (pi < 0) { wr[i] = lr[i]; wp[i] = lp[i]; } else { wr[i] = qmul(wr[pi], lr[i]); const v = qrot(wr[pi], lp[i]); wp[i] = [v[0] + wp[pi][0], v[1] + wp[pi][1], v[2] + wp[pi][2]]; } } const skin = new Array(n); for (let i = 0; i < n; i++) skin[i] = M4.mul(sk.bst[i], M4.fromQT(wr[i] || [0, 0, 0, 1], wp[i] || [0, 0, 0])); return { wp, wr, skin }; } /** Skins a meshkit mesh (m.data, m.skinPos / m.skinNrm) with a palette into its buffers. */ function skin(m, pal, ps) { const d = m.data; const V = d.vertexCount; const P = d.pos; const N = d.normal; const out = m.skinPos; const on = m.skinNrm; for (let v = 0; v < V; v++) { let x = 0, y = 0, z = 0, nx = 0, ny = 0, nz = 0, tw = 0; const px = P[v * 3], py = P[v * 3 + 1], pz = P[v * 3 + 2]; const qx = N ? N[v * 3] : 0, qy = N ? N[v * 3 + 1] : 0, qz = N ? N[v * 3 + 2] : 0; for (let j = 0; j < 4; j++) { const w = d.weights[v * 4 + j]; if (w <= 0) continue; const bi = pal[d.boneIdx[v * 4 + j]]; if (bi === undefined || bi < 0) continue; const M = ps.skin[bi]; x += w * (px * M[0] + py * M[4] + pz * M[8] + M[12]); y += w * (px * M[1] + py * M[5] + pz * M[9] + M[13]); z += w * (px * M[2] + py * M[6] + pz * M[10] + M[14]); if (N) { nx += w * (qx * M[0] + qy * M[4] + qz * M[8]); ny += w * (qx * M[1] + qy * M[5] + qz * M[9]); nz += w * (qx * M[2] + qy * M[6] + qz * M[10]); } tw += w; } if (tw <= 0) { x = px; y = py; z = pz; nx = qx; ny = qy; nz = qz; } else if (Math.abs(tw - 1) > 1e-3) { x /= tw; y /= tw; z /= tw; } out[v * 3] = x; out[v * 3 + 1] = y; out[v * 3 + 2] = z; if (on) { on[v * 3] = nx; on[v * 3 + 1] = ny; on[v * 3 + 2] = nz; } } } return { M4, prepare, link, mapTracks, palette, pose, skin }; })(); window.SkinKit = SkinKit;