103 lines
5.3 KiB
TypeScript
103 lines
5.3 KiB
TypeScript
import type { ArmatureBoneIR, MeshSummaryIR, SceneSnapshotIR } from "./scene-ir";
|
|
|
|
function multiply(left: readonly number[], right: readonly number[]): number[] {
|
|
const result = new Array<number>(16).fill(0);
|
|
for (let column = 0; column < 4; column++) for (let row = 0; row < 4; row++) for (let index = 0; index < 4; index++) {
|
|
result[column * 4 + row] += left[index * 4 + row] * right[column * 4 + index];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function inverse(matrix: readonly number[]): number[] {
|
|
const rows = Array.from({ length: 4 }, (_, row) => [
|
|
matrix[row], matrix[4 + row], matrix[8 + row], matrix[12 + row],
|
|
row === 0 ? 1 : 0, row === 1 ? 1 : 0, row === 2 ? 1 : 0, row === 3 ? 1 : 0,
|
|
]);
|
|
for (let column = 0; column < 4; column++) {
|
|
let pivot = column;
|
|
for (let row = column + 1; row < 4; row++) if (Math.abs(rows[row][column]) > Math.abs(rows[pivot][column])) pivot = row;
|
|
if (Math.abs(rows[pivot][column]) < 1e-10) throw new Error("deformation matrix is singular");
|
|
[rows[column], rows[pivot]] = [rows[pivot], rows[column]];
|
|
const scale = rows[column][column];
|
|
for (let index = 0; index < 8; index++) rows[column][index] /= scale;
|
|
for (let row = 0; row < 4; row++) if (row !== column) {
|
|
const factor = rows[row][column];
|
|
for (let index = 0; index < 8; index++) rows[row][index] -= factor * rows[column][index];
|
|
}
|
|
}
|
|
const result = new Array<number>(16);
|
|
for (let column = 0; column < 4; column++) for (let row = 0; row < 4; row++) result[column * 4 + row] = rows[row][4 + column];
|
|
return result;
|
|
}
|
|
|
|
function transform(matrix: readonly number[], point: readonly number[]): [number, number, number] {
|
|
const x = point[0], y = point[1], z = point[2];
|
|
return [
|
|
matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
|
|
matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
|
|
matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14],
|
|
];
|
|
}
|
|
|
|
function baseShapePositions(mesh: MeshSummaryIR): number[] {
|
|
if (!mesh.positions) throw new Error(`mesh ${mesh.id} has no positions`);
|
|
const positions = [...mesh.positions];
|
|
for (const shape of mesh.shapeKeys ?? []) {
|
|
const value = shape.value ?? 0;
|
|
if (Math.abs(value) < 1e-12) continue;
|
|
if (shape.positions.length !== positions.length) throw new Error(`shape key ${shape.name} does not match mesh ${mesh.id}`);
|
|
for (let index = 0; index < positions.length; index++) positions[index] += (shape.positions[index] - mesh.positions[index]) * value;
|
|
}
|
|
return positions;
|
|
}
|
|
|
|
function boneByJoint(snapshot: SceneSnapshotIR, armatureId: string, jointIds: readonly string[]): ArmatureBoneIR[] {
|
|
const armature = snapshot.armatures?.find((candidate) => candidate.id === armatureId);
|
|
if (!armature) throw new Error(`armature ${armatureId} is missing`);
|
|
return jointIds.map((id) => {
|
|
const bone = armature.bones.find((candidate) => candidate.id === id);
|
|
if (!bone) throw new Error(`joint ${id} is missing from armature ${armatureId}`);
|
|
return bone;
|
|
});
|
|
}
|
|
|
|
/** Evaluate Blender's linear armature deformation for a SceneIR mesh at its current pose. */
|
|
export function evaluateDeformedMesh(snapshot: SceneSnapshotIR, meshId: string): number[] {
|
|
const mesh = snapshot.meshes.find((candidate) => candidate.id === meshId);
|
|
if (!mesh?.positions) throw new Error(`mesh ${meshId} has no available geometry`);
|
|
const skin = mesh.skinWeights;
|
|
if (!skin?.armatureId || !skin.jointIds) return baseShapePositions(mesh);
|
|
const meshNode = snapshot.nodes.find((node) => node.dataId === meshId && node.type === "MESH");
|
|
const armature = snapshot.armatures?.find((candidate) => candidate.id === skin.armatureId);
|
|
const armatureNode = armature?.objectId ? snapshot.nodes.find((node) => node.id === armature.objectId) : undefined;
|
|
if (!meshNode || !armatureNode || !armature) throw new Error(`mesh ${meshId} bind objects are missing`);
|
|
const meshWorld = meshNode.worldMatrix;
|
|
const armatureWorld = armatureNode.worldMatrix;
|
|
const preMatrix = multiply(inverse(armatureWorld), meshWorld);
|
|
const postMatrix = multiply(inverse(meshWorld), armatureWorld);
|
|
const bones = boneByJoint(snapshot, skin.armatureId, skin.jointIds);
|
|
const deformMatrices = bones.map((bone) => multiply(bone.poseMatrix ?? bone.restMatrix, inverse(bone.restMatrix)));
|
|
const source = baseShapePositions(mesh);
|
|
const output = new Array<number>(source.length).fill(0);
|
|
for (let vertex = 0; vertex < mesh.vertexCount; vertex++) {
|
|
const local = transform(preMatrix, source.slice(vertex * 3, vertex * 3 + 3));
|
|
let total = 0;
|
|
for (let slot = 0; slot < 4; slot++) total += skin.weights[vertex * 4 + slot] ?? 0;
|
|
const normalized = total > 1e-12 ? 1 / total : 0;
|
|
const deformed: [number, number, number] = [0, 0, 0];
|
|
for (let slot = 0; slot < 4; slot++) {
|
|
const weight = (skin.weights[vertex * 4 + slot] ?? 0) * normalized;
|
|
if (weight <= 0) continue;
|
|
const transformed = transform(deformMatrices[skin.indices[vertex * 4 + slot]], local);
|
|
deformed[0] += transformed[0] * weight;
|
|
deformed[1] += transformed[1] * weight;
|
|
deformed[2] += transformed[2] * weight;
|
|
}
|
|
const result = total > 1e-12 ? transform(postMatrix, deformed) : source.slice(vertex * 3, vertex * 3 + 3) as [number, number, number];
|
|
output[vertex * 3] = result[0];
|
|
output[vertex * 3 + 1] = result[1];
|
|
output[vertex * 3 + 2] = result[2];
|
|
}
|
|
return output;
|
|
}
|