feat(web): T-WEB-006 BVH/FBX → MediaPipe33 关键点预处理

This commit is contained in:
2026-09-14 19:00:55 +08:00
parent 73e793318e
commit a2e3282603
12 changed files with 1799 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
/**
* 生成用于测试与验收的 BVH 样例(60 帧挥手 + 深蹲)。
* 用法:node test/fixtures/generate-sample-bvh.mjs [输出路径]
*/
import { writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const outPath = process.argv[2] ?? join(here, 'sample.bvh');
const HIERARCHY = `HIERARCHY
ROOT Hips
{
OFFSET 0.0 0.0 0.0
CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation
JOINT Spine
{
OFFSET 0.0 12.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT Neck
{
OFFSET 0.0 14.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT Head
{
OFFSET 0.0 8.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
End Site
{
OFFSET 0.0 8.0 0.0
}
}
}
JOINT LeftShoulder
{
OFFSET 6.0 10.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT LeftArm
{
OFFSET 12.0 0.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT LeftForeArm
{
OFFSET 22.0 0.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT LeftHand
{
OFFSET 20.0 0.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
End Site
{
OFFSET 8.0 0.0 0.0
}
}
}
}
}
JOINT RightShoulder
{
OFFSET -6.0 10.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT RightArm
{
OFFSET -12.0 0.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT RightForeArm
{
OFFSET -22.0 0.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT RightHand
{
OFFSET -20.0 0.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
End Site
{
OFFSET -8.0 0.0 0.0
}
}
}
}
}
}
JOINT LeftUpLeg
{
OFFSET 5.0 -3.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT LeftLeg
{
OFFSET 0.0 -26.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT LeftFoot
{
OFFSET 0.0 -25.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT LeftToeBase
{
OFFSET 0.0 -4.0 10.0
CHANNELS 3 Zrotation Xrotation Yrotation
End Site
{
OFFSET 0.0 -2.0 6.0
}
}
}
}
}
JOINT RightUpLeg
{
OFFSET -5.0 -3.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT RightLeg
{
OFFSET 0.0 -26.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT RightFoot
{
OFFSET 0.0 -25.0 0.0
CHANNELS 3 Zrotation Xrotation Yrotation
JOINT RightToeBase
{
OFFSET 0.0 -4.0 10.0
CHANNELS 3 Zrotation Xrotation Yrotation
End Site
{
OFFSET 0.0 -2.0 6.0
}
}
}
}
}
}
MOTION
`;
/** 通道顺序(与上面的 HIERARCHY 一致):Hips 6 通道 + 19 个关节各 3 通道 */
const JOINT_ORDER = [
'Hips',
'Spine',
'Neck',
'Head',
'LeftShoulder',
'LeftArm',
'LeftForeArm',
'LeftHand',
'RightShoulder',
'RightArm',
'RightForeArm',
'RightHand',
'LeftUpLeg',
'LeftLeg',
'LeftFoot',
'LeftToeBase',
'RightUpLeg',
'RightLeg',
'RightFoot',
'RightToeBase',
];
const CHANNELS = 6 + 3 * (JOINT_ORDER.length - 1);
const FRAMES = 60;
const FRAME_TIME = 1 / 30;
const lines = [];
for (let f = 0; f < FRAMES; f += 1) {
const t = f / FRAMES;
const phase = Math.sin(t * Math.PI * 2);
const squat = (1 - Math.cos(t * Math.PI * 2)) / 2;
const values = [];
for (const joint of JOINT_ORDER) {
if (joint === 'Hips') {
values.push(0, -squat * 12, 0); // Xposition Yposition Zposition
values.push(phase * 6, 0, 0); // Zrotation Xrotation Yrotation
continue;
}
const rot = { z: 0, x: 0, y: 0 };
if (joint === 'LeftArm') rot.z = 20 + phase * 55;
if (joint === 'RightArm') rot.z = -20 - phase * 55;
if (joint === 'LeftForeArm') rot.x = -25 - phase * 30;
if (joint === 'RightForeArm') rot.x = -25 + phase * 30;
if (joint === 'LeftUpLeg') rot.x = -squat * 35;
if (joint === 'RightUpLeg') rot.x = -squat * 35;
if (joint === 'LeftLeg') rot.x = squat * 60;
if (joint === 'RightLeg') rot.x = squat * 60;
if (joint === 'LeftFoot') rot.x = -squat * 25;
if (joint === 'RightFoot') rot.x = -squat * 25;
if (joint === 'Spine') rot.z = phase * 5;
if (joint === 'Neck') rot.z = -phase * 3;
values.push(rot.z, rot.x, rot.y);
}
if (values.length !== CHANNELS) throw new Error(`帧 ${f} 通道数错误:${values.length}`);
lines.push(values.map((v) => v.toFixed(4)).join(' '));
}
const text = `${HIERARCHY}Frames: ${FRAMES}\nFrame Time: ${FRAME_TIME.toFixed(7)}\n${lines.join('\n')}\n`;
writeFileSync(outPath, text, 'utf8');
console.log(`已生成 ${outPath}(${FRAMES} 帧,${CHANNELS} 通道,${text.length} 字节)`);