import type { ArmatureBoneIR, MeshSummaryIR, SceneSnapshotIR } from "./scene-ir"; function multiply(left: readonly number[], right: readonly number[]): number[] { const result = new Array(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(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(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; }