未来能用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里,然后通过dataViewgetUint32方法获取到真实数字

特别注意 参数里0true是必须的,编码其实有大端序和小端序两种,vmd是小端序,但是默认是大端序

准备完成了,开始读取关键帧就好了

之后就比较枯燥了,根据配置去设置每个关键帧怎么读取,读取方案又是什么,然后开始跑循环,之后就能收获啦

唯一的问题,我不知道为什么有4个bytes没用上,不过看了一下都是0,就算了

写入

光读取不写入的话,是没办法生成vmd文件的,好在写入的方法和读取是一个反操作,基本就是

字符串

根据给定的长度,先生成arrayBuffer之后填入,主要version的填充用0,其他用253,不过还是要注意编码,因为TextDecoder不行所以我换了shiftjisencode


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是单子节,而且可以用fillset等方法(生成指定长度的时候会自动填充0),.buffer可以获取到原始的ArrayBuffer

数字

DataViewsetUint32等方法即可

拼接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.jsnpm包,相关代码也可以在对应的 GITHUB 上直接查看, 欢迎使用啦

之后会尝试将posenet给整好,用posenet生成动作文件x