6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Elixir(Livebook) で VRM をパースしてみる

Last updated at Posted at 2023-01-09

はじめに

VRM を扱うライブラリとして、UniVRMVRM4Uthree-vrmなどがありますが、
Elixir で VRM を扱うライブラリがなかったため、今回は Elixir で VRM のパースして中身を見ていこうと思います。

VRM とは

VRM は glTF-2.0 のバイナリ形式である glb をベースとして作られています。この glb をヒューマノイドとして扱うのに必要なものを定義したフォーマットになります。

VRM は glb を拡張して作られているため、拡張子を .vrm から .glb に変えるとglTF対応のソフトで読み込むことも可能です(但し、VRM で拡張された情報は読み込まれません)。

glb とは

KhronosGroup が使用策定をしてる JSON 形式で 3D モデルのを表現するフォーマットとして glTF があります。

glTF はメッシュやテスクチャなどのバイナリデータは別ファイルに分かれており、URL やパスで参照する形になっています。一方、glb ではメッシュやテスクチャなどの情報をひとつのファイルにまとめたバイナリ形式のフォーマットです。
glTF では読み込み時に外部ファイルの参照が必要になりますが、glb では外部ファイルの参照をせずにバイナリファイル内の参照で済みます。

glb 形式

glb は Header 部 + JSON Chunk 部 + BINARY Chunk 部 の3つからできています。また glb の値は Little Endian で保存されています。

glTF仕様書から引用

Header 部

Header 部は合計12バイトの長さがあります。
Magic は ASCII で glTF、Version は glTF バージョンである2固定です。Length は Header部を含むファイル全体のデータサイズを表しています。

バイト数 内容
4 magic ascii "glTF"
4 version int32 2
4 length int32

Chunk 部

Chunk 部の chunk size は chunk data のバイトサイズを表しています。また、chunk type でこの Chunk が Json Chunk と Binary Chunk のどちらかを示しています。

バイト数 内容
4 chunk size int32
4 chunk type int32 “JSON” or “BIN\x00”
4 chunk data バイト列

LiveBook で VRM をパースしてみる

iex でもできますが、今回は LiveBook を使って VRM のパースしてみようと思います。

セットアップ

今回、Json のデコードに Jason、画像の表示に Kino を使うので、ライブラリ追加を追加します。

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.8"}
])

VRM のパース

今回はサンプルとして AIcia Solid の VRM モデルを使います。
https://3d.nicovideo.jp/alicia/

alicia_solid = File.read!("alicia_solid.vrm")

glb 形式 で紹介した VRM のバイナリの構造をパターンマッチを使ってパースしていきます。

binary = IO.iodata_to_binary(alicia_solid)

<<"glTF"::binary, 2::32-little-integer, length::32-little-integer,
  rest::size((length - 12) * 8)-bits>> = binary
<<length::32-little-integer, "JSON"::binary, json_chunk_data::size(length)-bytes, rest::bits>> =
  rest
<<length::32-little-integer, "BIN\0"::binary, binary_chunk_data::size(length)-bytes>> = rest

Header、Json Chunk、Binary Chunk の順にパターンマッチでパースしていきます。このとき、glb は Little Endian のため、int の修飾子は 32-little-integer を指定する必要があります。また、size(length)-bytes で chunk size で指定された長さ分 chunk data を取得します。

UniVRM のドキュメントに Python を使ったパース例がのってますが、Elixir のバイナリのパターンマッチを利用するとかなり短く書けます。
https://vrm-c.github.io/UniVRM/en/implementation/format.html#python3

Json Chunk のデータはエンコードされた状態なので Jason でデコードをする必要があります。

alicia_data = Jason.decode!(json_chunk_data)

VRM の画像一覧

VRM のパースができたので VRM の中に保存されている画像一覧を取得してみようと思います。

画像に関する情報がどこに入っているかというと、"images" の中に入っています。

alicia_data["images"]

image.png
ファイル名に加えて MIME Type と bufferView というカラムが入っています。 glb には bufferViews というカラムがあり、bufferViewはこの index の値になります。

bufferViewsですが、対象のデータが Buffer 領域のどこからどこまでかを示す値になります。

alicia_data["bufferViews"]

image.png

画像一覧を取得して Kino.Image を使って表示させると下のようになります。

%{"images" => images, "bufferViews" => buffer_views} = alicia_data

images
|> Enum.map(fn image ->
  %{"bufferView" => index, "mimeType" => mime_type} = image
  %{"byteOffset" => offset, "byteLength" => length} = Enum.at(buffer_views, index)

  <<_::size(offset)-bytes, content::size(length)-bytes, _::bits>> = binary_chunk_data
  %{mime_type: mime_type, content: content}
end)
|> Enum.map(& struct!(Kino.Image, &1))
|> Kino.Layout.grid(columns: 3)

image.png

UniVRM で VRM を読み込んだ場合、Textures は以下で、今回読み込んだ VRM のテスクチャ画像と一致してそうです。
image.png

おわりに

今回は Elixir で VRM をパースして画像一覧を取得してみました。

AIcia Solid の VRM モデルを使いましたが、VRoid Studio のサンプルキャラも同様にテクスチャ一覧が出せます。
image.png

今回出力した画像一覧の中にキャラクタのサムネイル画像が含まれていますが、
次回は VRM の Meta ファイルを読んでサムネイルを取得しようと思います。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?