Files
workinf_Blender_Wasm/web/protocol/deformation.ts
2026-08-12 04:47:48 -04:00

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