UnityなしでVCIの作成にチャレンジ!
はじめに
VCIはバーチャルキャストで利用できるアイテムを作成するための仕様です。
通常はUnityで作りますが、ちょっとした変更でRubyを起動したり覚えるのは大変なのでWebアプリから生成できるようにRubyでVCIを書き換えれないかを試してみました。
とりあえず前回読み込みが出来たので書き換えてみよう、という話。プレゼンで使うスライドを今回は作っていきます。
先に結論を書くと、書き換え自体は成功はしたのですが、未解決課題が残ってる状態。
oOさんがデバッグして解決してくれました! サンキューです。サンプルコードもアップデートしました。
今回のソースコードは下記となります。
テンプレートVCI
まずは雛形となるVCIを読み込みます。今回はこちらを使ています。グリップするとページが切り替わるシンプルなものです。これはUnityで作りました。初めてのUnityなどでこれ作るの事態が自体が結構時間かかってしまいましたがw
UniUnlitをOpaqueでレンダリングして、800x450の画像を横になれべ手連結した画像をテクスチャとして設定しています。Tilingの値を1 / ページ数
に設定し、こちらの値を変更していくことでページの切り替えを実現。ほぼほぼVCIチュートリアルのサンプルを参考にさせて頂きました。
テンプレートの読み込みは以下の通り。
require 'json'
template_vci_path = ARGV[0] #'WhiteBoard.vci'
image_path = ARGV[1] #'001.png'
page_size = ARGV[2].to_f
output_path = ARGV[3] #'dist/output.vci'
info = {title:ARGV[4], version:ARGV[5], author:ARGV[6], description:ARGV[7]}
GLB_H_SIZE = 4
GLB_H_MAGIC = "glTF".b
GLB_H_VERSION = [2].pack("L*")
GLB_JSON_TYPE = "JSON".b
GLB_BUFF_TYPE = "BIN\x00".b
FF = "\x00".b
#
# Load Template
#
io = open(template_vci_path)
glb_h_magic = io.read(GLB_H_SIZE)
glb_h_version = io.read(GLB_H_SIZE).unpack("L*")[0]
glb_h_length = io.read(GLB_H_SIZE).unpack("L*")[0]
glb_json_length = io.read(GLB_H_SIZE).unpack("L*")[0]
glb_json_type = io.read(GLB_H_SIZE)
glb_json_data = io.read(glb_json_length)
glb_buff_length = io.read(GLB_H_SIZE).unpack("L*")[0]
glb_buff_type = io.read(GLB_H_SIZE)
glb_buff_data = io.read(glb_buff_length)
property = JSON.parse(glb_json_data)
リソースの読み込み
今回のはスライドの画像を置き換えるので、画像とそれに対応するスクリプトを書き換えます。そのため、まずはそれらのリソースを読み込みます。
まずは画像から。合わせてimg_idxに書き換え対象の画像のbufferViewのインデックス値も入れて置きます。今回利用する画像はこちらです。
image = open(image_path, 'rb').read
img_idx = property["images"].find{|x| x["name"] == "template" }["bufferView"]
つづいてスクリプト。同じくsrc_idxに書き換え対象のスクリプトのbufferViewのインデックス値も入れて置きます。
src = <<"EOS"
GrabCount = 0
UseCount = 0
MAX_SLIDE_PAGE = #{page_size.to_i}
function onGrab(target)
GrabCount = GrabCount + 1
print("Grab : "..GrabCount)
print(target)
end
function onUse(use)
UseCount = UseCount + 1
print("onUse : "..use..UseCount)
local index = UseCount % MAX_SLIDE_PAGE
local offset = Vector2.zero
offset.y = 0
offset.x = (1.0 / MAX_SLIDE_PAGE) * index
vci.assets._ALL_SetMaterialTextureOffsetFromName("ScreenTexture", offset)
print("page: "..(index + 1))
end
EOS
src_idx = property["extensions"]["VCAST_vci_embedded_script"]["scripts"][0]["source"]
スクリプトは今回はヒアドキュメントでそのままRubyコードの中に記載して変数展開でページ数を書き換えています。より本格的にやる場合は別ファイルにしてERB等で変数を代入するのが使いやすい気がします。
データ部の書き換え
次にデータ部(Chunk 1)の変更です。Rubyではバイナリを文字列と同様に処理して基本的には大丈夫です。差し替えるリソースの時以外は元の値をそのまま入れます。
def padding_size(data_size)
if data_size == 0 then
return 0
else
m = data_size % 4
return m > 0 ? 4 - m : 0
end
end
data = ""
property["bufferViews"].each_with_index do |x, i|
case i
when img_idx
data += image + FF * padding_size(image.size)
when src_idx
data += src + FF * padding_size(src.size)
else
len = x["byteLength"]
data += glb_buff_data[x["byteOffset"], len] + FF * padding_size(len)
end
end
ポイントというか注意点がこのdiff
でして、最初この値を設定してなかったのでThe Seed Onlineにアップロード自体は成功するのですがプレビューで失敗するという事象に見舞われていました。
エラーメッセージも出ずエラーの詳細が分からないので試行錯誤を繰り返してたのですが、VCIというか元フォーマットのglTFとして以下のような仕様があるようです。
つまり、Float:5126
とかUNSIGNED_INT:5125
とかのデータサイズ、この場合は4バイトで割って、さらにその境界が4バイト区切りになるようにする必要があるようです。たしかにUnityが生成したVCIのbufferViewsを見てるとオフセット値や実際のバイナリで実際のデータ長以外の値が入ってるのが確認できました。
データ型に関しては詳しくはこちらを参照。
ただ、このデータ型の値が分からない。。。imageがつまり何バイトなのかを見つけることが出来なかったのですよね。なので試行錯誤の末、以下の値で良いことが分かりました。
4 - (image.size % 3)
4から引いてるのは4バイト区切りに合わせるためですが問題は3
。この式で補正出来るのですがなぜここに3が入るのか良く分かりません。なんとなくpngの画像を変換したときRGB的な3要素になってるのかも? と思うんですが、確認はできておらず。。。詳しい人いたらぜひ教えてください!
こちらの値でゼロパディングしてやれば、無事にVCIをアップロードして動作させることが出来ます。
メタ情報の書き換え
ここらで息抜きにメタ情報の変更をします。
vci_meta = property["extensions"]["VCAST_vci_meta"]
vci_meta["title"] = info[:title]
vci_meta["version"] = info[:version]
vci_meta["author"] = info[:author]
vci_meta["description"] = info[:description]
特筆すべきことはありませんが、必要なプロパティを更新できます。今回は試してないですがライセンスやアイコンもここで変えることができるはずです。
続いて、テスクチャのスケールの変更をします。ページ数に合わせてこちらを変更しないと画像が分割されたり複数表示されて意図しない挙動になりますので忘れずに設定しましょう。
material = property["materials"].find{|x| x["name"] == "ScreenTexture"}
material["pbrMetallicRoughness"]["baseColorTexture"]["extensions"]["KHR_texture_transform"]["scale"] = [(1.0 / page_size).floor(5), 1]
Buffers/bufferViewsの書き換え
Buffers/bufferViewsは外部データにあたるデータ部のサイズやオフセット値が書き込まれた部分です。こちらに従ってglTF/VCIではバイナリの読み込みを行います。
まずはbufferViewsの書き換え対象のbyteLength
を先ほど読み込んだリソースのサイズに置き換えます。続いて、offset値
は原則ひとつ前のbyteOffset
とbyteLength
の和になるのでそちらで更新してやります。
ここでもポイントはパディング値の取り扱いです。先ほどゼロ埋めした数だけずらしてやる必要があります。単純にbyteOffset
とbyteLength
の和にならないケースがあるので注意しましょう。偶々なのか出力時に調整してるのか分かりませんが、書き換え対象の画像以外は特に弄らなくてよかったのでそこだけ調整を入れています。
property["bufferViews"][img_idx]["byteLength"] = image.size
property["bufferViews"][src_idx]["byteLength"] = src.size
xs = property["bufferViews"]
(1..xs.size-1).each do |i|
px = xs[i - 1]
len = px["byteLength"]
offset = px["byteOffset"] + len + padding_size(len)
xs[i]["byteOffset"] = offset
end
最後にbuffers
のbyteLength
にdata.size
の値を格納しJSON文字列として書き出します。
property["buffers"][0]["byteLength"] = data.size
json = property.to_json.gsub('/', '\/')
Chunk書き込み時の4バイト境界のパディングの設定
先ほど、bufferViews
の時にもパディングを入れましたが、Chunkの書き込み時にもそれぞれパディングが必要なようです。JSON部はスペース(0x20)パディングで、データ部はゼロ(0x00)パディングです。
なので以下のようなコードを書きました。
paddingValue = json.size % 4
padding = (paddingValue > 0) ? 4 - paddingValue : 0;
json = json + (" " * padding)
paddingValue = data.size % 4
padding = (paddingValue > 0) ? 4 - paddingValue : 0;
data = data + (FF * padding)
これで上手くいきそうなものですが上手く動きません! なぜかデータ部のサイズに応じてJSON部のpaddingに調整が必要 という凄まじく謎な状態になっています。
上記のコードでJSON側のサイズを変えても無事対応できるのですが、データ部のサイズを変えたときに上手くいかないのですよね。。。 なので、以下のようにしました。
paddingValue = json.size % 4
padding = (paddingValue > 0) ? 4 - paddingValue : 0;
padding += 3 # 謎の微調整
json = json + (" " * padding)
paddingValue = data.size % 4
padding = (paddingValue > 0) ? 4 - paddingValue : 0;
data = data + (FF * padding)
これで正常に動作するのですが、何故3なのかは全くの謎です. 今のところ計算式も不明なので画像を切り替えたときに0から3の値を入れてみてTSOにアップロードしてチェック とい方法でしか確認できてないです。パッとチェックした限りでは単純にdata.sizeの4バイトの余りとかではないので謎過ぎる。そもそも何故データ部の値に依存を。。。
下記のVキャス公式のコードを見てもそんな謎の微調整は無いので、基本的な所をミスってる可能性が高いのですがどなたかお気づきになりましたらご指摘いただけると非常に助かります。
oOさんがデバッグして解決してくれました! 原因として私がデータのアライメントを中途半端にやっていたので補正値で偶然値が合ったりズレたりしていたようです。下記のように適切にかけることで対応できました。ありがとうございます。
# Padding for 4 byte boundary
json_padding = padding_size(json.size)
json = json + (" " * json_padding)
data_padding = padding_size(data.size)
data = data + (FF * data_padding)
GLBに出力
データが準備できたのでGLBに出力します。読み込みと逆の手順なのでこれは簡単ですね。
glb = GLB_H_MAGIC
glb += GLB_H_VERSION
glb += [(GLB_H_SIZE * 3) + (GLB_H_SIZE * 2) + json.size + (GLB_H_SIZE * 2) + data.size].pack("L*")
glb += [json.size].pack("L*")
glb += GLB_JSON_TYPE
glb += json
glb += [data.size].pack("L*")
glb += GLB_BUFF_TYPE
glb += data
open(output_path, 'wb') do |f|
f.write(glb)
end
最終的に出力したものはこちらに登録してあります。
まとめ
さて、無事にVCIをRubyで出力する事が出来きました。理論的にはUnity一切なしでも出来るのですが、それはツラすぎるのでやはり今回のようにテンプレートになるスクリプトをUnityで作って差し替える方式が無難だと思います。
読み込みが意外に簡単だったので、書き込みも楽勝でしょ! と思ってたらパディング問題に躓きだいぶ難航してしまいました。今も完了したとは言い難いし。
これをベースにWebサイトにPDFをアップロードしたら自動でVCIに変換してくれるサービスを作りたいのですが、まずはパディング問題の原因分析と解決ですかねぇ。
無事に解決して頂いたので、とりあえず早速でもアプリを作ってみたいと思います
現状Roomではスライド機能が使えないから、このVCI生成ツールが個人的に重宝しそうなので何とか完成させたいですね! 将来的にはTSOと連携できると良いなー。
それではHappy Hacking!
Discussion