Appearance
VMD 的回写
背景:联邦一直只「读」VMD(见《动作的语法》)——把二进制密函翻译成骨骼关键帧。现在要「存盘」:把编辑后的动作写回
.vmd文件。 过程:buildVmd/buildBoneFrame/buildMorphFrame的字节布局(111 / 23)、canEncodeName的 Shift-JIS 无损回环校验、帧号排序约束。
一、密函的反面
外交官是被一个保存按钮唤醒的。
这次的任务和《动作的语法》里那次恰好相反。上一次,它读一份 .vmd,把字节翻译成骨骼的姿势;这一次,它要读一份骨骼的姿势,把姿势翻译回字节。
「写出去。」桌面壳说。
外交官愣了一下。它能读。读是它最擅长的事——VMD 二进制格式、Shift-JIS 骨骼名、四元数旋转,它了如指掌。但「读」和「写」之间,隔着一条它从没跨过的河。
读是理解。写是表达。
它打开了 vmd-writer.ts。
二、111 字节的契约
buildBoneFrame 是写回的最小单位——一帧骨骼动作,精确 111 字节。
外交官数了一遍:
- 骨骼名,15 字节。Shift-JIS 编码,不足补零,多余截断。
- 帧号,4 字节。无符号 32 位整数,小端。
- 位置,12 字节。三个
float32,x / y / z。 - 旋转,16 字节。四个
float32,四元数 w / x / y / z。 - 插值,64 字节。16 组,每组
[x1, y1, x2, y2],每组 4 字节。
15 + 4 + 12 + 16 + 64 = 111。没有 padding,没有对齐,没有预留位。
VMD 是个刻薄的格式——它不宽容任何多余的字节,也不原谅任何缺失的字节。少一个,解析器在下一帧就会把位置读成旋转;多一个,整条骨骼曲线错位。
morph 帧更严苛。buildMorphFrame 是 23 字节:15 字节 morph 名 + 4 字节帧号 + 4 字节权重。没有旋转,没有插值——口型只有「张到什么程度」,没有「怎么张」。
buildVmd 把这些帧拼成一个完整的文件:30 字节签名 + 20 字节模型名 + 4 字节骨骼帧数 + 骨骼帧 + 4 字节 morph 帧数 + morph 帧 + 后面跟着四个计数都为 0 的尾巴(相机、灯光、自阴影、IK)。
外交官突然懂了:VMD 不是一个「数据格式」。它是一个契约——每一字节的位置都和历史绑定,和 2000 年代某个 MMD 编辑器作者的决定绑定。它不演进,它只是被遵守。
三、canEncodeName 的否决权
但真正让外交官停下来的,是 canEncodeName。
读的时候,外交官见过 Shift-JIS / UTF-8 / GBK 三态共存的骨骼名。解码器有评分择优——即使编码三态混乱,它也能猜出「センター」是什么。读是包容的。
写的时候,canEncodeName 做了一件读绝不会做的事:
typescript
const sjisArr = Encoding.convert(name, { to: 'SJIS', from: 'UNICODE', type: 'array' });
const roundTrip = Encoding.convert(sjisArr, { to: 'UNICODE', from: 'SJIS', type: 'string' });
return roundTrip === name;它把骨骼名编码成 Shift-JIS,再编码回 Unicode,然后问:回来的还是原来的名字吗?
不平等。如果某个骨骼名里混进了 emoji、混进了特殊符号、混进了 Shift-JIS 字符集之外的任何东西,编码回去就会失真——roundTrip 不等于 name,canEncodeName 返回 false,这一帧被拒。
读能容忍模糊。写不能容忍失真。
外交官想起 01 章那个 frame 312。那时它读一份 VMD,左腕偏离 2.3σ——它能读,因为它不在乎「意义」,只在乎字节。现在它要写,它才发现:能读动的字节,不一定能写回去。一个含特殊字符的骨骼名,读的时候被正确解码,写的时候被 canEncodeName 一票否决。
联邦能「理解」一个城邦,不代表能「复现」一个城邦。
四、排序的纪律
最后一个约束藏在 buildVmd 的注释里,只有一行:
typescript
// 确保帧按帧号排序,防止解析器插值异常骨骼帧必须按 frame 升序排列。如果外交官把 frame 480 放在 frame 0 前面,写出去的文件在别的解析器里会被插值算法当成「从结尾跳回开头」——角色的动作会在最后一帧突然瞬移回第一帧。
这不是 vmd-writer.ts 的 bug。这是 VMD 契约的另一条铁律:顺序即意义。帧的顺序不是存储优化,是动作本身。乱序的字节,翻译回姿势时,就是一支被打乱的舞。
外交官在写回前排了一次序。然后它看着 buildVmd 吐出的 ArrayBuffer——30 + 20 + 4 + 111×N + 4 + 23×M + 0 + 0 + 0 + 0 字节,严丝合缝。
它把这份密函交还给桌面壳。
「写出去了。」
桌面壳接过文件,加载,播放。骨骼按它写回的姿势动了起来。
读是理解。写是表达。理解一个动作,和让一个动作被别人理解,是同一枚硬币的两面——但硬币的背面,多了一条 canEncodeName 的否决权。
聚合悖论的新面孔:联邦能收编、能理解、能渲染一个城邦,但要把它「存盘」交出去时,却受限于最底层的编码契约——Shift-JIS 不认识的名字,读得懂,写不出。
教训:读包容失真,写拒绝失真。能理解不代表能复现。