/** * 生成用于测试与验收的 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} 字节)`);