Files

177 lines
7.0 KiB
JavaScript

"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;