需求 1:管理端端口与软件 API 端口分离 - 软件 API 端口 PORT(默认 3000)只提供 /api、/health、/static、/contracts, 不提供管理页面(未命中路径 404);管理端端口 ADMIN_PORT(默认 3001) 提供管理页面与全部接口;ADMIN_HOST 可单独限制管理端来源 - main.ts 同进程监听两个端口,用 req.socket.localPort 区分端口行为; ADMIN_PORT=PORT 为单端口兼容模式(只创建一个 server,无 EADDRINUSE) - 端口非法在生产环境中止启动;ADMIN_PORT=PORT 只 warn - 启动日志分别打印两个端口的访问地址 需求 2:动作数据支持 VMD(MikuMikuDance) - vmd-parser.ts:二进制解析(30 字节头 + Shift-JIS 模型名 + 111 字节/条骨骼 关键帧 + morph 段)、MMD 贝塞尔插值、GBK 模型名回退、固定 30fps - mmd-skeleton.ts:标准 MMD 骨架;静止比例从参考 PMX 骨骼段实测得到 (足首 1.43 / 膝 6.57 / 髋 11.50 / 首 16.21 / 頭 17.16,腿长 10.09) - mmd-pose.ts:FK + 两骨骼解析腿部 IK(膝盖朝正面,不可达收敛为伸直) + 左手系→右手系 Z 镜像(由真实数据对照实验确定:94.1% vs 5.9%) - retarget.ts:抽出 worldFramesToKeypointFrames(),BVH 与 VMD 共用重定向 - MotionFormat 增 vmd、扩展名映射增 .vmd;管理端上传支持 .vmd - scripts/inspect-vmd.mjs:上传前自查工具 契约:rest-api / data-models / model-formats → 0.2.0(另两库已同步逐字节一致) 验证:85 项单测通过(VMD 31 项);契约与 29 条路由一致;typecheck/build 通过; 真实样本经 HTTP 端到端:霜雪千年 2901 帧(IK 腿)、恋爱循环 4604 帧(FK 腿) 均 preprocessState=done、0 NaN、0 越界(测试数据已从数据库清理)。
137 lines
3.8 KiB
TypeScript
137 lines
3.8 KiB
TypeScript
export type Vec3 = [number, number, number];
|
||
|
||
/** 3×3 行主序矩阵 */
|
||
export type Mat3 = [number, number, number, number, number, number, number, number, number];
|
||
|
||
export const IDENTITY3: Mat3 = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||
|
||
export function vAdd(a: Vec3, b: Vec3): Vec3 {
|
||
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
||
}
|
||
|
||
export function vSub(a: Vec3, b: Vec3): Vec3 {
|
||
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
||
}
|
||
|
||
export function vScale(a: Vec3, s: number): Vec3 {
|
||
return [a[0] * s, a[1] * s, a[2] * s];
|
||
}
|
||
|
||
export function vMid(a: Vec3, b: Vec3): Vec3 {
|
||
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
|
||
}
|
||
|
||
export function vLen(a: Vec3): number {
|
||
return Math.hypot(a[0], a[1], a[2]);
|
||
}
|
||
|
||
export function vNorm(a: Vec3): Vec3 {
|
||
const len = vLen(a);
|
||
if (len < 1e-9) return [0, 0, 0];
|
||
return [a[0] / len, a[1] / len, a[2] / len];
|
||
}
|
||
|
||
export function vDot(a: Vec3, b: Vec3): number {
|
||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||
}
|
||
|
||
/** 向量 ba 与 bc 夹角的余弦,落在 [-1, 1] —— 契约 skeleton-topology.md §5 */
|
||
export function angleCos(a: Vec3, b: Vec3, c: Vec3): number {
|
||
const ba = vSub(a, b);
|
||
const bc = vSub(c, b);
|
||
const denom = vLen(ba) * vLen(bc);
|
||
if (denom < 1e-9) return 0;
|
||
return Math.max(-1, Math.min(1, vDot(ba, bc) / denom));
|
||
}
|
||
|
||
export function matMul(a: Mat3, b: Mat3): Mat3 {
|
||
const out = new Array<number>(9);
|
||
for (let r = 0; r < 3; r += 1) {
|
||
for (let c = 0; c < 3; c += 1) {
|
||
out[r * 3 + c] = a[r * 3] * b[c] + a[r * 3 + 1] * b[3 + c] + a[r * 3 + 2] * b[6 + c];
|
||
}
|
||
}
|
||
return out as Mat3;
|
||
}
|
||
|
||
/**
|
||
* 四元数 (x, y, z, w) → 3×3 行主序旋转矩阵。
|
||
* 输入会自动归一化;零四元数视为单位四元数。
|
||
*/
|
||
export function quatToMat3(q: readonly [number, number, number, number]): Mat3 {
|
||
let [x, y, z, w] = q;
|
||
const len = Math.hypot(x, y, z, w);
|
||
if (len < 1e-9) return IDENTITY3;
|
||
x /= len;
|
||
y /= len;
|
||
z /= len;
|
||
w /= len;
|
||
return [
|
||
1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w),
|
||
2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w),
|
||
2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y),
|
||
];
|
||
}
|
||
|
||
export function matTranspose(m: Mat3): Mat3 {
|
||
return [m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]];
|
||
}
|
||
|
||
export function vCross(a: Vec3, b: Vec3): Vec3 {
|
||
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
||
}
|
||
|
||
export function matVec(m: Mat3, v: Vec3): Vec3 {
|
||
return [
|
||
m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
|
||
m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
|
||
m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
|
||
];
|
||
}
|
||
|
||
const DEG2RAD = Math.PI / 180;
|
||
|
||
/** 绕单轴旋转矩阵,axis ∈ {X, Y, Z} */
|
||
export function rotationMatrix(axis: string, degrees: number): Mat3 {
|
||
const rad = degrees * DEG2RAD;
|
||
const c = Math.cos(rad);
|
||
const s = Math.sin(rad);
|
||
switch (axis.toUpperCase()) {
|
||
case 'X':
|
||
return [1, 0, 0, 0, c, -s, 0, s, c];
|
||
case 'Y':
|
||
return [c, 0, s, 0, 1, 0, -s, 0, c];
|
||
case 'Z':
|
||
return [c, -s, 0, s, c, 0, 0, 0, 1];
|
||
default:
|
||
return IDENTITY3;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 由通道序列合成局部旋转矩阵。
|
||
* BVH 约定:按通道声明顺序依次左乘,即 R = R1 × R2 × R3(内旋)。
|
||
*/
|
||
export function composeRotation(channels: ReadonlyArray<{ axis: string; value: number }>): Mat3 {
|
||
let out: Mat3 = IDENTITY3;
|
||
for (const { axis, value } of channels) {
|
||
out = matMul(out, rotationMatrix(axis, value));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** 世界变换:旋转 + 平移 */
|
||
export interface Transform {
|
||
r: Mat3;
|
||
t: Vec3;
|
||
}
|
||
|
||
export const IDENTITY_TRANSFORM: Transform = { r: IDENTITY3, t: [0, 0, 0] };
|
||
|
||
export function applyTransform(parent: Transform, offset: Vec3, localR: Mat3, extraT?: Vec3): Transform {
|
||
return {
|
||
r: matMul(parent.r, localR),
|
||
t: vAdd(parent.t, matVec(parent.r, vAdd(offset, extraT ?? [0, 0, 0]))),
|
||
};
|
||
}
|