* feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持 * chore: 更新 pnpm-lock.yaml * fix: 移除未使用的变量和方法 * fix: 修复 mesh-3d-editor tsconfig 引用路径 * fix: 修复正则表达式 ReDoS 漏洞
228 lines
8.2 KiB
JavaScript
228 lines
8.2 KiB
JavaScript
/**
|
|
* Debug Animation Channels Building
|
|
*/
|
|
|
|
import { readFileSync } from 'fs';
|
|
import pako from 'pako';
|
|
const { inflate } = pako;
|
|
|
|
const filePath = process.argv[2] || 'F:\\MyProject4\\assets\\octopus.fbx';
|
|
|
|
const buffer = readFileSync(filePath);
|
|
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
const version = view.getUint32(23, true);
|
|
const is64Bit = version >= 7500;
|
|
|
|
let offset = 27;
|
|
|
|
function readNode() {
|
|
const startOffset = offset;
|
|
let endOffset, numProperties, propertyListLen, nameLen;
|
|
|
|
if (is64Bit) {
|
|
endOffset = Number(view.getBigUint64(offset, true));
|
|
numProperties = Number(view.getBigUint64(offset + 8, true));
|
|
propertyListLen = Number(view.getBigUint64(offset + 16, true));
|
|
nameLen = view.getUint8(offset + 24);
|
|
offset += 25;
|
|
} else {
|
|
endOffset = view.getUint32(offset, true);
|
|
numProperties = view.getUint32(offset + 4, true);
|
|
propertyListLen = view.getUint32(offset + 8, true);
|
|
nameLen = view.getUint8(offset + 12);
|
|
offset += 13;
|
|
}
|
|
|
|
if (endOffset === 0) return null;
|
|
|
|
const name = new TextDecoder().decode(buffer.slice(offset, offset + nameLen));
|
|
offset += nameLen;
|
|
|
|
const properties = [];
|
|
const propsEnd = offset + propertyListLen;
|
|
|
|
while (offset < propsEnd) {
|
|
const typeCode = String.fromCharCode(buffer[offset]);
|
|
offset++;
|
|
|
|
switch (typeCode) {
|
|
case 'Y':
|
|
properties.push(view.getInt16(offset, true));
|
|
offset += 2;
|
|
break;
|
|
case 'C':
|
|
properties.push(buffer[offset] !== 0);
|
|
offset += 1;
|
|
break;
|
|
case 'I':
|
|
properties.push(view.getInt32(offset, true));
|
|
offset += 4;
|
|
break;
|
|
case 'F':
|
|
properties.push(view.getFloat32(offset, true));
|
|
offset += 4;
|
|
break;
|
|
case 'D':
|
|
properties.push(view.getFloat64(offset, true));
|
|
offset += 8;
|
|
break;
|
|
case 'L':
|
|
properties.push(view.getBigInt64(offset, true));
|
|
offset += 8;
|
|
break;
|
|
case 'S':
|
|
case 'R':
|
|
const strLen = view.getUint32(offset, true);
|
|
offset += 4;
|
|
if (typeCode === 'S') {
|
|
properties.push(new TextDecoder().decode(buffer.slice(offset, offset + strLen)));
|
|
} else {
|
|
properties.push(buffer.slice(offset, offset + strLen));
|
|
}
|
|
offset += strLen;
|
|
break;
|
|
case 'f':
|
|
case 'd':
|
|
case 'l':
|
|
case 'i':
|
|
case 'b':
|
|
const arrayLen = view.getUint32(offset, true);
|
|
const encoding = view.getUint32(offset + 4, true);
|
|
const compressedLen = view.getUint32(offset + 8, true);
|
|
offset += 12;
|
|
|
|
if (encoding === 0) {
|
|
const elemSize = typeCode === 'd' || typeCode === 'l' ? 8 : 4;
|
|
const arr = [];
|
|
for (let i = 0; i < arrayLen; i++) {
|
|
if (typeCode === 'd') arr.push(view.getFloat64(offset + i * 8, true));
|
|
else if (typeCode === 'f') arr.push(view.getFloat32(offset + i * 4, true));
|
|
else if (typeCode === 'l') arr.push(view.getBigInt64(offset + i * 8, true));
|
|
else if (typeCode === 'i') arr.push(view.getInt32(offset + i * 4, true));
|
|
}
|
|
properties.push({ type: typeCode, data: arr });
|
|
offset += arrayLen * elemSize;
|
|
} else {
|
|
const compData = buffer.slice(offset, offset + compressedLen);
|
|
try {
|
|
const decompressed = inflate(compData);
|
|
const elemSize = typeCode === 'd' || typeCode === 'l' ? 8 : 4;
|
|
const dataView = new DataView(decompressed.buffer, decompressed.byteOffset, decompressed.byteLength);
|
|
const arr = [];
|
|
for (let i = 0; i < arrayLen; i++) {
|
|
if (typeCode === 'd') arr.push(dataView.getFloat64(i * 8, true));
|
|
else if (typeCode === 'f') arr.push(dataView.getFloat32(i * 4, true));
|
|
else if (typeCode === 'l') arr.push(dataView.getBigInt64(i * 8, true));
|
|
else if (typeCode === 'i') arr.push(dataView.getInt32(i * 4, true));
|
|
}
|
|
properties.push({ type: typeCode, data: arr });
|
|
} catch (e) {
|
|
properties.push({ type: typeCode, compressed: true, len: arrayLen });
|
|
}
|
|
offset += compressedLen;
|
|
}
|
|
break;
|
|
default:
|
|
offset = propsEnd;
|
|
}
|
|
}
|
|
|
|
const children = [];
|
|
while (offset < endOffset) {
|
|
const child = readNode();
|
|
if (child) children.push(child);
|
|
else break;
|
|
}
|
|
|
|
offset = endOffset;
|
|
return { name, properties, children };
|
|
}
|
|
|
|
// Parse root nodes
|
|
const rootNodes = [];
|
|
while (offset < buffer.length - 100) {
|
|
const node = readNode();
|
|
if (node) rootNodes.push(node);
|
|
else break;
|
|
}
|
|
|
|
const objectsNode = rootNodes.find(n => n.name === 'Objects');
|
|
const connectionsNode = rootNodes.find(n => n.name === 'Connections');
|
|
|
|
// Parse AnimationCurveNodes
|
|
const animCurveNodes = objectsNode.children.filter(n => n.name === 'AnimationCurveNode');
|
|
|
|
console.log(`AnimationCurveNodes count: ${animCurveNodes.length}`);
|
|
console.log(`First 3 AnimationCurveNodes:`);
|
|
animCurveNodes.slice(0, 3).forEach((cn, i) => {
|
|
console.log(` [${i}] properties:`, cn.properties);
|
|
console.log(` id type: ${typeof cn.properties[0]}`);
|
|
console.log(` id value: ${cn.properties[0]}`);
|
|
});
|
|
|
|
// Parse connections
|
|
const connections = connectionsNode.children.map(c => ({
|
|
type: c.properties[0].split('\0')[0],
|
|
fromId: c.properties[1],
|
|
toId: c.properties[2],
|
|
property: c.properties[3]?.split?.('\0')[0]
|
|
}));
|
|
|
|
console.log(`\nConnections count: ${connections.length}`);
|
|
|
|
// Find OP connections with Lcl property
|
|
const lclConnections = connections.filter(c => c.type === 'OP' && c.property?.includes('Lcl'));
|
|
console.log(`OP connections with Lcl: ${lclConnections.length}`);
|
|
console.log(`First 3 Lcl connections:`);
|
|
lclConnections.slice(0, 3).forEach((c, i) => {
|
|
console.log(` [${i}] fromId=${c.fromId} (type: ${typeof c.fromId}), toId=${c.toId}, prop=${c.property}`);
|
|
});
|
|
|
|
// Check if any AnimationCurveNode id matches connection fromId
|
|
console.log(`\nChecking AnimationCurveNode ID matches:`);
|
|
const cnIds = new Set(animCurveNodes.map(cn => cn.properties[0]));
|
|
const lclFromIds = lclConnections.map(c => c.fromId);
|
|
|
|
let matchCount = 0;
|
|
for (const fromId of lclFromIds) {
|
|
// Check different ID formats
|
|
const matchesDirect = cnIds.has(fromId);
|
|
const matchesBigInt = cnIds.has(BigInt(fromId));
|
|
|
|
if (matchesDirect || matchesBigInt) {
|
|
matchCount++;
|
|
}
|
|
}
|
|
|
|
console.log(`Matches found: ${matchCount}/${lclConnections.length}`);
|
|
|
|
// The issue might be that animCurveNodes doesn't have an 'id' property
|
|
// Let's check how we should reference them
|
|
console.log(`\nAnimationCurveNode structure check:`);
|
|
const firstCN = animCurveNodes[0];
|
|
if (firstCN) {
|
|
console.log(` Has 'id' property: ${'id' in firstCN}`);
|
|
console.log(` properties[0] type: ${typeof firstCN.properties[0]}`);
|
|
console.log(` properties[0] value: ${firstCN.properties[0]}`);
|
|
}
|
|
|
|
// The fix: we need to use cn.properties[0] as the ID, not cn.id
|
|
// Let's verify by creating a proper map
|
|
const curveNodeMap = new Map();
|
|
for (const cn of animCurveNodes) {
|
|
curveNodeMap.set(cn.properties[0], cn);
|
|
}
|
|
|
|
console.log(`\nBuilt curveNodeMap with ${curveNodeMap.size} entries`);
|
|
|
|
// Now check matches
|
|
let matchCount2 = 0;
|
|
for (const conn of lclConnections) {
|
|
if (curveNodeMap.has(conn.fromId)) {
|
|
matchCount2++;
|
|
}
|
|
}
|
|
console.log(`Matches using proper lookup: ${matchCount2}/${lclConnections.length}`);
|
|
|
|
console.log('\nDone!');
|