feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持 (#315)
* feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持 * chore: 更新 pnpm-lock.yaml * fix: 移除未使用的变量和方法 * fix: 修复 mesh-3d-editor tsconfig 引用路径 * fix: 修复正则表达式 ReDoS 漏洞
This commit is contained in:
309
scripts/trace-fbxloader-output.mjs
Normal file
309
scripts/trace-fbxloader-output.mjs
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* 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!');
|
||||
Reference in New Issue
Block a user