* feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持 * chore: 更新 pnpm-lock.yaml * fix: 移除未使用的变量和方法 * fix: 修复 mesh-3d-editor tsconfig 引用路径 * fix: 修复正则表达式 ReDoS 漏洞
200 lines
6.4 KiB
JavaScript
200 lines
6.4 KiB
JavaScript
/**
|
|
* Test FBXLoader Bind Pose
|
|
* 测试 FBXLoader 绑定姿势
|
|
*
|
|
* Verify: worldMatrix * inverseBindMatrix = Identity at bind pose
|
|
*/
|
|
|
|
import { readFileSync } from 'fs';
|
|
|
|
const filePath = process.argv[2] || 'F:\\MyProject4\\assets\\octopus.fbx';
|
|
console.log(`Testing bind pose: ${filePath}\n`);
|
|
|
|
// Matrix math utilities
|
|
function createTransformMatrix(position, rotation, scale) {
|
|
const [qx, qy, qz, qw] = rotation;
|
|
const [sx, sy, sz] = scale;
|
|
const xx = qx * qx, xy = qx * qy, xz = qx * qz, xw = qx * qw;
|
|
const yy = qy * qy, yz = qy * qz, yw = qy * qw;
|
|
const zz = qz * qz, zw = qz * qw;
|
|
|
|
return new Float32Array([
|
|
(1 - 2 * (yy + zz)) * sx, 2 * (xy + zw) * sx, 2 * (xz - yw) * sx, 0,
|
|
2 * (xy - zw) * sy, (1 - 2 * (xx + zz)) * sy, 2 * (yz + xw) * sy, 0,
|
|
2 * (xz + yw) * sz, 2 * (yz - xw) * sz, (1 - 2 * (xx + yy)) * sz, 0,
|
|
position[0], position[1], position[2], 1
|
|
]);
|
|
}
|
|
|
|
function multiplyMatrices(a, b) {
|
|
const result = new Float32Array(16);
|
|
for (let row = 0; row < 4; row++) {
|
|
for (let col = 0; col < 4; col++) {
|
|
let sum = 0;
|
|
for (let k = 0; k < 4; k++) {
|
|
sum += a[row + k * 4] * b[k + col * 4];
|
|
}
|
|
result[row + col * 4] = sum;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function identity() {
|
|
return new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
}
|
|
|
|
function isIdentity(m, tolerance = 0.01) {
|
|
const id = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
for (let i = 0; i < 16; i++) {
|
|
if (Math.abs(m[i] - id[i]) > tolerance) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function maxDiffFromIdentity(m) {
|
|
const id = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
let maxDiff = 0;
|
|
for (let i = 0; i < 16; i++) {
|
|
maxDiff = Math.max(maxDiff, Math.abs(m[i] - id[i]));
|
|
}
|
|
return maxDiff;
|
|
}
|
|
|
|
async function main() {
|
|
const { FBXLoader } = await import('../packages/asset-system/dist/index.js');
|
|
|
|
const binaryData = readFileSync(filePath);
|
|
const loader = new FBXLoader();
|
|
|
|
const context = {
|
|
metadata: {
|
|
path: filePath,
|
|
name: filePath.split(/[\\/]/).pop(),
|
|
type: 'model/fbx',
|
|
guid: '',
|
|
size: binaryData.length,
|
|
hash: '',
|
|
dependencies: [],
|
|
lastModified: Date.now(),
|
|
importerVersion: '1.0.0',
|
|
labels: [],
|
|
tags: [],
|
|
version: 1
|
|
},
|
|
loadDependency: async () => null
|
|
};
|
|
|
|
const content = {
|
|
type: 'binary',
|
|
binary: binaryData.buffer
|
|
};
|
|
|
|
const asset = await loader.parse(content, context);
|
|
|
|
if (!asset.skeleton) {
|
|
console.log('No skeleton data!');
|
|
return;
|
|
}
|
|
|
|
const { joints, rootJointIndex } = asset.skeleton;
|
|
const nodes = asset.nodes;
|
|
|
|
console.log(`Skeleton: ${joints.length} joints, rootJointIndex=${rootJointIndex}`);
|
|
|
|
// Build parent index map (node hierarchy)
|
|
const nodeParentMap = new Map();
|
|
for (const node of nodes) {
|
|
if (node.children) {
|
|
for (const childIdx of node.children) {
|
|
nodeParentMap.set(childIdx, nodes.indexOf(node));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate world matrices for each joint using node.transform hierarchy
|
|
const worldMatrices = new Array(joints.length);
|
|
|
|
// Processing order: root first, then children
|
|
const processed = new Set();
|
|
const processingOrder = [];
|
|
|
|
function addJoint(jointIndex) {
|
|
if (processed.has(jointIndex)) return;
|
|
const joint = joints[jointIndex];
|
|
if (joint.parentIndex >= 0 && !processed.has(joint.parentIndex)) {
|
|
addJoint(joint.parentIndex);
|
|
}
|
|
processingOrder.push(jointIndex);
|
|
processed.add(jointIndex);
|
|
}
|
|
|
|
for (let i = 0; i < joints.length; i++) addJoint(i);
|
|
|
|
for (const jointIndex of processingOrder) {
|
|
const joint = joints[jointIndex];
|
|
const node = nodes[joint.nodeIndex];
|
|
|
|
if (!node) {
|
|
worldMatrices[jointIndex] = identity();
|
|
continue;
|
|
}
|
|
|
|
const { position, rotation, scale } = node.transform;
|
|
const localMatrix = createTransformMatrix(position, rotation, scale);
|
|
|
|
if (joint.parentIndex >= 0) {
|
|
worldMatrices[jointIndex] = multiplyMatrices(
|
|
worldMatrices[joint.parentIndex],
|
|
localMatrix
|
|
);
|
|
} else {
|
|
worldMatrices[jointIndex] = localMatrix;
|
|
}
|
|
}
|
|
|
|
// Calculate skin matrices and check if they are identity
|
|
let identityCount = 0;
|
|
let nonIdentityJoints = [];
|
|
|
|
for (let i = 0; i < joints.length; i++) {
|
|
const joint = joints[i];
|
|
const skinMatrix = multiplyMatrices(worldMatrices[i], joint.inverseBindMatrix);
|
|
|
|
if (isIdentity(skinMatrix)) {
|
|
identityCount++;
|
|
} else {
|
|
const diff = maxDiffFromIdentity(skinMatrix);
|
|
nonIdentityJoints.push({ index: i, name: joint.name, diff, skinMatrix });
|
|
}
|
|
}
|
|
|
|
console.log(`\n=== BIND POSE VERIFICATION ===`);
|
|
console.log(`Identity skin matrices: ${identityCount}/${joints.length}`);
|
|
|
|
if (nonIdentityJoints.length > 0) {
|
|
console.log(`\n❌ NOT at bind pose! ${nonIdentityJoints.length} joints have non-identity skin matrices.`);
|
|
|
|
// Show first 3 problematic joints
|
|
nonIdentityJoints.sort((a, b) => b.diff - a.diff);
|
|
console.log(`\nTop 3 worst joints:`);
|
|
for (let i = 0; i < 3 && i < nonIdentityJoints.length; i++) {
|
|
const { index, name, diff, skinMatrix } = nonIdentityJoints[i];
|
|
console.log(` Joint[${index}] "${name}": maxDiff=${diff.toFixed(4)}`);
|
|
console.log(` skinMatrix diagonal: [${skinMatrix[0].toFixed(2)}, ${skinMatrix[5].toFixed(2)}, ${skinMatrix[10].toFixed(2)}, ${skinMatrix[15].toFixed(2)}]`);
|
|
console.log(` skinMatrix translation: [${skinMatrix[12].toFixed(2)}, ${skinMatrix[13].toFixed(2)}, ${skinMatrix[14].toFixed(2)}]`);
|
|
}
|
|
|
|
console.log(`\n=== ANALYSIS ===`);
|
|
console.log(`The skin matrix should be Identity at bind pose (t=0).`);
|
|
console.log(`This means: worldMatrix * inverseBindMatrix = Identity`);
|
|
console.log(`If not identity, the mesh will appear deformed at rest.`);
|
|
} else {
|
|
console.log(`\n✅ All skin matrices are identity at bind pose!`);
|
|
}
|
|
|
|
console.log('\nDone!');
|
|
}
|
|
|
|
main().catch(console.error);
|