Files
esengine/scripts/trace-fbxloader-output.mjs
YHH 828ff969e1 feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持 (#315)
* feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持

* chore: 更新 pnpm-lock.yaml

* fix: 移除未使用的变量和方法

* fix: 修复 mesh-3d-editor tsconfig 引用路径

* fix: 修复正则表达式 ReDoS 漏洞
2025-12-23 15:34:01 +08:00

310 lines
12 KiB
JavaScript

/**
* Trace FBXLoader Output
* 追踪 FBXLoader 输出
*
* Load the FBX with actual FBXLoader and compare with expected values
*/
import { readFileSync } from 'fs';
import { FBXLoader } from '../packages/asset-system/dist/index.js';
const filePath = process.argv[2] || 'F:\\MyProject4\\assets\\octopus.fbx';
// Suppress console.log temporarily to hide FBXLoader debug output
const originalLog = console.log;
let suppressLogs = true;
console.log = (...args) => {
if (!suppressLogs) originalLog(...args);
};
originalLog(`=== Trace FBXLoader Output: ${filePath} ===\n`);
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
};
try {
const asset = await loader.parse(content, context);
console.log(`Meshes: ${asset.meshes?.length || 0}`);
console.log(`Nodes: ${asset.nodes?.length || 0}`);
console.log(`Animations: ${asset.animations?.length || 0}`);
if (asset.skeleton) {
console.log(`Skeleton joints: ${asset.skeleton.joints.length}`);
console.log(`Root joint index: ${asset.skeleton.rootJointIndex}`);
// Check first few joints
console.log(`\nFirst 3 skeleton joints:`);
for (let i = 0; i < 3 && i < asset.skeleton.joints.length; i++) {
const joint = asset.skeleton.joints[i];
console.log(` Joint[${i}] "${joint.name}":`);
console.log(` nodeIndex: ${joint.nodeIndex}`);
console.log(` parentIndex: ${joint.parentIndex}`);
// Check inverseBindMatrix
const ibm = joint.inverseBindMatrix;
if (ibm) {
console.log(` inverseBindMatrix diagonal: [${ibm[0].toFixed(4)}, ${ibm[5].toFixed(4)}, ${ibm[10].toFixed(4)}, ${ibm[15].toFixed(4)}]`);
console.log(` inverseBindMatrix last row: [${ibm[12].toFixed(4)}, ${ibm[13].toFixed(4)}, ${ibm[14].toFixed(4)}, ${ibm[15].toFixed(4)}]`);
}
}
// Check corresponding nodes
console.log(`\nCorresponding nodes:`);
for (let i = 0; i < 3 && i < asset.skeleton.joints.length; i++) {
const joint = asset.skeleton.joints[i];
const node = asset.nodes?.[joint.nodeIndex];
if (node) {
console.log(` Node[${joint.nodeIndex}] "${node.name}":`);
console.log(` position: [${node.transform.position.map(v => v.toFixed(4)).join(', ')}]`);
console.log(` rotation: [${node.transform.rotation.map(v => v.toFixed(4)).join(', ')}]`);
console.log(` scale: [${node.transform.scale.map(v => v.toFixed(4)).join(', ')}]`);
}
}
} else {
console.log(`No skeleton data!`);
}
// Check animation channels
if (asset.animations && asset.animations.length > 0) {
const clip = asset.animations[0];
console.log(`\nAnimation "${clip.name}":`);
console.log(` Duration: ${clip.duration}s`);
console.log(` Channels: ${clip.channels.length}`);
console.log(` Samplers: ${clip.samplers.length}`);
// Find channels targeting first few skeleton joints
if (asset.skeleton) {
console.log(`\nChannels for first 3 joints:`);
for (let i = 0; i < 3 && i < asset.skeleton.joints.length; i++) {
const joint = asset.skeleton.joints[i];
const channels = clip.channels.filter(c => c.target.nodeIndex === joint.nodeIndex);
console.log(` Joint[${i}] nodeIndex=${joint.nodeIndex}: ${channels.length} channels`);
channels.forEach(c => {
const sampler = clip.samplers[c.samplerIndex];
console.log(` - ${c.target.path}: ${sampler.input.length} keyframes, first value at t=0:`);
if (c.target.path === 'rotation') {
const q = [sampler.output[0], sampler.output[1], sampler.output[2], sampler.output[3]];
console.log(` quaternion: [${q.map(v => v.toFixed(4)).join(', ')}]`);
} else {
const v = [sampler.output[0], sampler.output[1], sampler.output[2]];
console.log(` vec3: [${v.map(v => v.toFixed(4)).join(', ')}]`);
}
});
}
}
}
// Now test bone matrix calculation
if (asset.skeleton && asset.animations && asset.animations.length > 0) {
console.log(`\n=== TESTING BONE MATRIX CALCULATION ===`);
const skeleton = asset.skeleton;
const nodes = asset.nodes;
const clip = asset.animations[0];
// Sample animation at t=0
function sampleAnimation(clip, time) {
const nodeTransforms = new Map();
for (const channel of clip.channels) {
const sampler = clip.samplers[channel.samplerIndex];
if (!sampler) continue;
const nodeIndex = channel.target.nodeIndex;
const path = channel.target.path;
// Get first keyframe value (t=0)
let value;
if (path === 'rotation') {
value = [sampler.output[0], sampler.output[1], sampler.output[2], sampler.output[3]];
} else {
value = [sampler.output[0], sampler.output[1], sampler.output[2]];
}
let transform = nodeTransforms.get(nodeIndex);
if (!transform) {
transform = {};
nodeTransforms.set(nodeIndex, transform);
}
if (path === 'translation') transform.position = value;
else if (path === 'rotation') transform.rotation = value;
else if (path === 'scale') transform.scale = value;
}
return nodeTransforms;
}
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]);
}
const animTransforms = sampleAnimation(clip, 0);
console.log(`Sampled ${animTransforms.size} node transforms at t=0`);
// Calculate bone matrices
const { joints } = skeleton;
const boneCount = joints.length;
const localMatrices = new Array(boneCount);
const worldMatrices = new Array(boneCount);
const skinMatrices = new Array(boneCount);
// Build processing order
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 < boneCount; i++) addJoint(i);
for (const jointIndex of processingOrder) {
const joint = joints[jointIndex];
const node = nodes[joint.nodeIndex];
if (!node) {
localMatrices[jointIndex] = identity();
worldMatrices[jointIndex] = identity();
skinMatrices[jointIndex] = identity();
continue;
}
// Get animated or default transform
const animTransform = animTransforms.get(joint.nodeIndex);
const pos = animTransform?.position || node.transform.position;
const rot = animTransform?.rotation || node.transform.rotation;
const scl = animTransform?.scale || node.transform.scale;
localMatrices[jointIndex] = createTransformMatrix(pos, rot, scl);
if (joint.parentIndex >= 0) {
worldMatrices[jointIndex] = multiplyMatrices(
worldMatrices[joint.parentIndex],
localMatrices[jointIndex]
);
} else {
worldMatrices[jointIndex] = localMatrices[jointIndex];
}
skinMatrices[jointIndex] = multiplyMatrices(
worldMatrices[jointIndex],
joint.inverseBindMatrix
);
}
// Count identity matrices
let identityCount = 0;
let maxDiff = 0;
const id = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
for (let i = 0; i < boneCount; i++) {
const sm = skinMatrices[i];
let diff = 0;
for (let j = 0; j < 16; j++) {
diff = Math.max(diff, Math.abs(sm[j] - id[j]));
}
if (diff < 0.001) identityCount++;
if (diff > maxDiff) maxDiff = diff;
}
console.log(`\nAt t=0 with animation data:`);
console.log(` Identity matrices: ${identityCount}/${boneCount}`);
console.log(` Max diff from identity: ${maxDiff.toFixed(4)}`);
if (identityCount !== boneCount) {
console.log(`\n⚠️ NOT all skin matrices are identity at bind pose!`);
// Show first problematic joint
for (let i = 0; i < boneCount; i++) {
const sm = skinMatrices[i];
let diff = 0;
for (let j = 0; j < 16; j++) {
diff = Math.max(diff, Math.abs(sm[j] - id[j]));
}
if (diff >= 0.001) {
const joint = joints[i];
const node = nodes[joint.nodeIndex];
const animT = animTransforms.get(joint.nodeIndex);
console.log(`\n First non-identity: Joint[${i}] "${joint.name}"`);
console.log(` nodeIndex: ${joint.nodeIndex}`);
console.log(` parentIndex: ${joint.parentIndex}`);
console.log(` animTransform exists: ${!!animT}`);
if (animT) {
console.log(` animTransform.rotation: [${animT.rotation?.map(v => v.toFixed(4)).join(', ') || 'null'}]`);
}
console.log(` node.transform.rotation: [${node.transform.rotation.map(v => v.toFixed(4)).join(', ')}]`);
break;
}
}
} else {
console.log(`\n✅ All skin matrices are identity at bind pose!`);
}
}
} catch (error) {
console.error('Error:', error);
}
console.log('\nDone!');