Add Chromium-only Blender WebEngine parity work
This commit is contained in:
102
web/protocol/deformation.ts
Normal file
102
web/protocol/deformation.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user