177 lines
7.0 KiB
JavaScript
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;
|