🔬

バーチャルキャストのVCIをRubyで解析してみた

2021/02/08に公開

はじめに

VR配信ツール/SNSであるバーチャルキャストではVCI(Virtual Cast Interactive)とアイテムをVR上での小道具として作ることが出来ます。通常、VCIはUnityを使って作るのですが、コンテンツ系----例えばプレゼン用のスライドや電子書籍、あるいはホロポスターやラジカセ/jukeboxのようなプレイヤー系はリソースさえ差し替えてしまえば良いので、Unity無しでもVCIの生成が出来るのでは? というのが今回のチャレンジの動機です。

これが出来ると、UnityをインストールすることもなくWebアプリケーション等にアップロードするだけでVCIが簡単に作れるようになるため、私のようにUnity力がない人間や絵師さんとか漫画家さんとか3Dが得意ではないクリエイターの方もVCIを作ってVR上がもっと賑やかになるのではと思っています。

VCIの用意

まずはVCIを用意します。Unityは結構前からインストール「だけ」はしてのですが、使いかたがさっぱり分からず放置していた因縁のアプリです。(ぉ
今回、以下のチュートリアルを参考にというかそのまま写経して作りました。
https://virtualcast.jp/wiki/vci/beginner

動画チュートリアルめっちゃ助かるね!

VCIのフォーマット

前置き

VCI自体は出来たので解析のために仕様について調べます。残念ながらVCIのフォーマットレベルの仕様書は見つからなかったのですが、なんかの規格に従ったJSONのバイナリ形式らしいという事を教えてもらいました。
というわけでおもむろにテキストエディタで開いた結果が以下の通り。

バイナリっぽいですね。テキストもいくつかそのまま入ってそうです。なので全体がZIP圧縮されてるとかでは無さそう。バイナリエディタ(VSCode + exeditor)で開くと以下のように確かに前の方にJSONがあることが分かります。

で、こいつはglTFと先頭にある通りglTF形式になっていて、これはJSONによって3Dモデルやシーンを表現するフォーマットでで「3DにおけるJPEG」と表現されることもあるそうです。さらに言うとglTFは、同じくバーチャルキャストやPixvのVRoidHub, Cluster, DMM VR Connectをはじめとしてさまざまな用途で利用できるアバター用の汎用フォーマットのVRMでも使わています。自分の中ではVRMはだいぶ違うんだろうけど標準化されたMMDフォーマットみたいなイメージ。

VRMに関しては仕様がきちんと定義されているのでこちらを参照する事が出来ます。
https://github.com/vrm-c/vrm-specification/tree/master/specification/0.0

作ってる人が同じだと思うので、glTFやVRMの仕様を見つつリバースエンジニアリング的にファイルの中身を見てみました。

参考

glTFのサイトと、JSでVRMを解析されてる方がいらっしゃいましたのでめっちゃ参考にさせてもらいました。

https://docs.fileformat.com/3d/glb/
https://zenn.dev/edom18/articles/extract-thumbnail-vrm

フォーマット

glTFのフォーマットは下記のようになっています。

glTFには大きく分けて以下の3つの領域があります。

  • ヘッダー部(先頭12バイト)
  • JSONによるメタ情報(Chunk 0, 長さはChunk 0の先頭4バイト(符号なし32bit整数)に記載)
  • バイナリ形式のBuffer部 (Chunk 1, 長さはChunk 0の先頭4バイト(符号なし32bit整数)に記載)

ヘッダー部分は単にglTFとしての識別子やバージョン番号が入ってるだけなので、通常は無視していい気がします。
重要なのはJOSNによるメタ情報とBuffer部です。JSON部分を取り出したものがこちらです。

例えばVCIの情報に関しては下記のようにVCAST_vci_metaというノードに書かかれています。

"VCAST_vci_meta": {
    "exporterVCIVersion": "UniVCI-0.31",
    "specVersion": "0.31",
    "title": "Suika VCI",
    "version": "0.6",
    "author": "koduki",
    "contactInformation": "",
    "reference": "",
    "description": "",
    "thumbnail": -1,
    "modelDataLicenseType": "redistribution_prohibited",
    "modelDataOtherLicenseUrl": "",
    "scriptLicenseType": "redistribution_prohibited",
    "scriptOtherLicenseUrl": "",
    "scriptWriteProtected": false,
    "scriptEnableDebugging": false,
    "scriptFormat": "luaText"
},

例えば画像であればimages、VCIの振舞いを記述するLuaスクリプトであればVCAST_vci_embedded_script.scriptsに記載があります。

"images": [
    {
        "name": "suikaTex_out",
        "bufferView": 0,
        "mimeType": "image\/png"
    },
    {
        "name": "suikaTex_in",
        "bufferView": 1,
        "mimeType": "image\/png"
    }
],
"VCAST_vci_embedded_script": {
    "scripts": [
        {
            "name": "main",
            "mimeType": "x_application_lua",
            "targetEngine": "moonsharp",
            "source": 19
        }
    ],
    "entryPoint": 0
},

特に重要なのがbufferViewsourceに書かれた数値です。これはBufferのデータの区切り方を示したbufferViews配列のインデックス値となります。画像もソースコードもリソース系の実態は気hン的にBuffer部にあるようです。

下記が、BuffersノードおよびBufferViewsの抜粋です。まずbuffersノードに全体のレコード長さが記載されています。このデータはいわゆる可変長データなのでbyteOffsetbyteLengthで区切ってデータを参照します。

"buffers": [
    {
        "byteLength": 100012
    }
],
"bufferViews": [
    {
        "buffer": 0,
        "byteOffset": 0,
        "byteLength": 21697
    },
    {
        "buffer": 0,
        "byteOffset": 21697,
        "byteLength": 4630
    },

とてもシンプルですね! このJSONから必要なメタ情報を取得したり、取得したいデータのBuffer上での位置を確認して、実際のデータを取得します。割とシンプルで解析が早々に済みそうな仕組みですね。

Rubyによる解析

では仕様が分かったところで、さっそくRubyで解析をしてみます。と言っても、先ほど解説したフォーマットに則ってバイナリ操作をするだけなので特に細かな解説は不要かと思います。
なお、仕様でUINT32(符号無し単精度整数)と記載がありますので数値として取り出したいところはunpackをしています。

# Read glTF

io = open('suika_vci.vci', "rb")

## Header
glb_h_magic = io.read(4)
glb_h_version = io.read(4).unpack("L*")[0]
glb_h_length = io.read(4).unpack("L*")[0]

### Chunk 0 (JSON)
glb_json_length = io.read(4).unpack("L*")[0]
glb_json_type = io.read(4)
glb_json_data = io.read(glb_json_length)

### Chunk 1 (Buffer)
glb_buff_length = io.read(4).unpack("L*")[0]
glb_buff_type = io.read(4)
glb_buff_data = io.read(glb_buff_length)


# Parse VCI
property = JSON.parse(glb_json_data)
generator = property["asset"]["generator"]
vci_meta = property["extensions"]["VCAST_vci_meta"].slice("title", "version", "authoer")

## Get images
img_idx = property["images"].find{|x| x["name"] == "suikaTex_out"}["bufferView"]
bfv = property["bufferViews"][img_idx]
img_data = glb_buff_data[bfv["byteOffset"], bfv["byteLength"]]
open('suikaTex_out.png', 'wb') do |f|
    f.write(img_data.unpack('C*').pack('C*'))
end

img_idx = property["images"].find{|x| x["name"] == "suikaTex_in"}["bufferView"]
bfv = property["bufferViews"][img_idx]
img_data = glb_buff_data[bfv["byteOffset"], bfv["byteLength"]]
open('suikaTex_in.png', 'wb') do |f|
    f.write(img_data.unpack('C*').pack('C*'))
end

## Get Lua Scripts
src_idx = property["extensions"]["VCAST_vci_embedded_script"]["scripts"][0]["source"]

bfv = property["bufferViews"][src_idx]
lua_script = glb_buff_data[bfv["byteOffset"], bfv["byteLength"]]

一応、RubyやJavaのglTFライブラリもありそうなのですが、簡単な内容なら自分で作った方が学習コストの面で楽そうですね。書き換えも特に問題なく出来そうですが、メタ情報側のレコード長等の調整をするコードは必要になりそうです。

まとめ

というわけで思った以上に簡単にVCIの解析をする事が出来ました。Unitiy無しRubyで出来るのでWebアプリケーションにするのも簡単そうです。
さすがに完全にRubyだけでFULLのVCIを作るのは3Dモデルの座標決めとかめんどそうなのですが、Unityでテンプレートをまず作りそれのリソースだけを部分的に変更して再パックするようなコードは十分書けそうなきがしました。

昨日あったMIKULAND 2021 SnowやVキャスのVCIの展示会である冬キャスを見ているとVCI作ってみたい欲がムクムクと湧き上がるものの、Unity力がゼロの人間なので、こういうアプローチで戦えそうなのは嬉しいですね。
とりあえず、自分が良く使うスライドをVCIにしてこの方法でコンテンツが差し替えれるかを次は頑張ってみたいと思います。

それではHappy Hacking!

Discussion