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