未来能用
js
编写的软件,都会用js
编写
前言
MikuMikuDance,简称MMD,如果是b站用户的话,应该都看到过一个专门的mmd分区,里面是各类舞蹈视频,这个是早期(真的很久了)给v家角色制作舞蹈动画的一个软件,当然除了v家6人以外,崩坏3,元神各类角色相关的宅舞投稿都有
mmd
其实本质上还是一款三维动画制作软件,包含 模型 动作 关键帧等一系列相关的概念,模型的话,TGA式,大妈式,很多都有相关的配布,模型和镜头也有相关的发布,其实相关产业已经很完善了,但是这一次,我打算对mmd
中保存动作,镜头,表情和关键帧的VMD(Vocaload Mation Data)
文件下手了,当然主要还是因为。。。
mmd
不支持mac
!我不想装windows
啊,作为前端工程师的我,如果想实现类似的渲染,那么,一方面是需要搞定模型的渲染**(Tree.js永远的神)**,另一方面就是能够编辑解析其中的vmd
文件了
当然,为了解决动作的问题, 一方面,mmd
提供了kinect
进行捕捉,另一方面,我也想试试用posenet
来解析动作,不过在这之前,先得吧vmd
搞定
顺便,练习一下用js
读取二进制流嘛
几个概念
二进制流
文件其实就是一堆 0,1 组成的二进制码流,在javascript
的世界中,这种文件叫做ArrayBuffer
, 虽然我们不能直接操作,但是做相应的读取还是没用问题的
你不能直接操作 ArrayBuffer 的内容,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
用js
获取二进制流有几种办法,最常用的就是从file
直接转过来,这里我们通过FileReader即可
FileReader 接口提供的 readAsArrayBuffer() 方法用于启动读取指定的 Blob 或 File 内容。当读取操作完成时,readyState 变成 DONE(已完成),并触发 loadend 事件,同时 result 属性中将包含一个 ArrayBuffer 对象以表示所读取文件的数据。
当然啦,还有很多种办法,比如把请求头设置成arrayBuffer
等等,这里请查阅相关的文档吧
TypedArray
在真实的使用场景(比如其他语言体系,例如c)其实对bytes
有很多种不同的编码方案,用来表示不同的类型,int
型一般2
个字节这种,在js中也有相对应的TypedArrays
只要参考对照表,其实读取方面就比较简单了
VMD文件的格式
光有文件流,还需要一套相应的读取方案才行,感谢国内两位大佬的相关文章,解惑了
文档
- [【MMD】用python解析VMD格式读取
](https://www.jianshu.com/p/ae312fb53fc3?from=groupmessage&isappinstalled=0)
- [MMD中的VMD文件格式详解
](https://blog.csdn.net/haseetxwd/article/details/82821533)
可以看到的,不同的字节区域,一般代表了不同的数据,只要读取指定长度的数据,根据之前对照表上的对照关系,就可以成功读取了
编码格式
这里科普一下,计算机世界是包含不同的编码格式的,网页现在基本上都是utf-8
编码,在欧美因为是英语环境,主要的还是unicode
,国内类似香港地区用的是big5
,大陆用的是gb2312
,编码主要是为了解决字符集的问题,不同的编码格式转换的时候,就会造成乱码这个现象,不知道这个编码对应显示哪个字符,作为一个日本软件,还是一个早年的日本软件,vmd
文件采用了
SHIFT_JIS
进行编码,这个我尝试过了,通过浏览器自带的TextDecoder
可以解码成文字,但是TextEncoder
却不行
Properties
The TextEncoder interface doesn’t inherit any property.
TextEncoder.prototype.encodingRead only
Always returns “utf-8”.
瞬间扎心了,不过后来在网上看到有人做好了转换库,万能的js
开工
读取文件
因为我是本地文件,所以起了一个http
服务通过fetch
方法直接返回了
fetch('/models/mmd/vmds/test.vmd')
.then(res => res.blob())
.then(blob => blob.arrayBuffer())
.then((arrayBuffer) => {}
开始读取数据
一开始我想的是,通过arrayBuffer
的切割功能,每次都返回一个子序列,之后的读取读子序列,这样其实每次都是从头读取一定长度了,然而理想很丰满,现实很残酷,因为每次引用对象对象都进行变更,导致一旦上了循环,速率立马从ms
级别掉到min
级别,为此,我只能特别开了一个index
标记上一次的开头,然后游离这个指针获取数据,速度还不错,不过指针这个东西。。。很容易没操作好类型的话,就是一个 out of bounds
,不过这个是后话了
读取字符串的问题
记得解码器要用SHIFT_JIS
格式!我一开始用的是浏览器自带的TextDecoder
, 后来就直接上了shiftjis
这个js
库来解析和编码了
版本信息
最开头版本信息**(VersionInformation)**长度为30,有两种可能
Vocaloid Motion Data file
Vocaloid Motion Data 0002
当然话是那么说没错啦,直接读取前30个bytes就可以了,但是这两串字符其实不是百分百填满的,通过
new TextDecode('shift_jis').decode
进行解码之后发现其实还带了乱码,更可恶的是,是空字符,只有复制下来自己粘贴之后才能看得到!为什么这个很重要呢?因为接下来的模型名称是要根据版本信息判断长度的!
如果同时把对应的ArrayBuffer
输出出来之后,会发现,后面跟了一串0
所以读取字符串之后把对应的0
先抹掉,然后用抹掉之后的ArrayBuffer
解码,完美的字符串
模型名称
根据version
判断长度之后一样读取,不过查看原始ArrayBuffer
之后发现,填充的不是全是0
了,而是0,253,253,253...
,把过滤乱码策略改了一下,0
开头之后的都不要了
读取关键帧
根据vmd
格式提到的,vmd
格式存储是分区块的,一般是一类的关键帧放在一起,所以读取之前,需要先读取这一类有多少帧,然后循环读取
获取数字的方法
之前提到过,读取的都是ArrayBuffer
但是要转换为我们具体的数字的话,就得靠解码器了,首先得知道要解码的是什么类型的数字,好在文件配置里都有,例如数量就是一个uint32_t
,之后我们查询上表之后可以知道,对应的是Uint32Array
, 那读取的长度一方面可以看配置表,另一方面,Uint32Array
自带一个BYTES_PER_ELEMENT
属性,告诉你需要至少读取几位的,所以我这边通过DateView
获取数字
const buffer = this.readBytes(Type.BYTES_PER_ELEMENT)
const view = new DataView(buffer, 0)
return view.getUint32(0, true)
解释一下,读取一定的bytes
然后扔进dataView
里,然后通过dataView
的getUint32
方法获取到真实数字
特别注意 参数里0
和true
是必须的,编码其实有大端序和小端序两种,vmd
是小端序,但是默认是大端序
准备完成了,开始读取关键帧就好了
之后就比较枯燥了,根据配置去设置每个关键帧怎么读取,读取方案又是什么,然后开始跑循环,之后就能收获啦
唯一的问题,我不知道为什么有4个bytes没用上,不过看了一下都是0,就算了
写入
光读取不写入的话,是没办法生成vmd
文件的,好在写入的方法和读取是一个反操作,基本就是
字符串
根据给定的长度,先生成arrayBuffer
之后填入,主要version
的填充用0,其他用253,不过还是要注意编码,因为TextDecoder
不行所以我换了shiftjis
的encode
writeString (text = '', length = 0) {
const textBuffer = shiftjis.encode(text)
const buffer = new Uint8Array(length)
buffer.fill(253, textBuffer.length + 1)
buffer.set(textBuffer)
// 只有version是靠0填充的
if (text === VERSION.V1 || text === VERSION.V2) {
buffer.fill(0, textBuffer.length)
}
return buffer.buffer
}
因为uint8
是单子节,而且可以用fill
,set
等方法(生成指定长度的时候会自动填充0),.buffer
可以获取到原始的ArrayBuffer
数字
用DataView
的setUint32
等方法即可
拼接ArrayBuffer
和之前字符串的方案类似,设定总长度之后不停的set
就行了,我事先保存成了一个ArrayBuffer
的数组,最后对它拼接
const totalBytes = this.bufferList.reduce((_totalBytes, buffer) => {
return _totalBytes + new Uint8Array(buffer).length
}, 0)
const result = new Uint8Array(totalBytes)
let offset = 0
for (const buffer of this.bufferList) {
result.set(new Uint8Array(buffer), offset)
offset += buffer.byteLength
}
const buffer = result.buffer
return buffer
至此位置,读取和生成的操作就可以了,之后可以通过new Blob([arrayBuffer])
来实现导出
总结
其实核心原理还是对二进制的操作,在看得到文档的情况下并不是很难,更加重要的是细心和耐心,不过浏览器js对多语言编码其实还是有很大的不足的
总而言之,这个vmd
读取和编写,我做了一个vmd.js
的npm
包,相关代码也可以在对应的 GITHUB 上直接查看, 欢迎使用啦
之后会尝试将posenet
给整好,用posenet
生成动作文件x