10
4

More than 1 year has passed since last update.

自前 AR アプリとかで USDZ を直接読むための USDC file format のメモ

Last updated at Posted at 2019-12-27

背景

  • AR アプリとかで USDZ 読みたい. iOS でも C++ で USDZ 読みたい. USDZ を glTF とかに変換したい
    • が, USD(Universal Scene Description)ビルドがつらいぴえん :cry: :cry: :cry: :cry:
    • べつに USD 全部の機能はいらないので, 最低限必要な感じで USDZ のデータが読めればよい

USDZ は usdc(USD binary format)でのシーンデータと, 画像ファイルなどを非圧縮 ZIP でまとめたものです.
したがって usdc バイナリ形式を読むことができれば, USDZ を読めることになります.
(USDA Ascii 形式も格納できるが, Apple のビューアだと USDA はサポートしていないっぽい)

USD 自体が boost を使っているので, usdc のバイナリシリアライズ/deserialize には boost serialization を使っていると思いきや, データを効率的にシリアライズするために, 独自のシリアライザでのフォーマットになっていました.

情報

特報 :tada: Crate format document がリリースされました :smile: https://github.com/PixarAnimationStudios/USD/pull/2086

これと以下の crateFile.cpp ソースコードを見るとよいでしょう.

crateFile(createFile では無いことに注意. crate = 枠箱, 箱に詰めるなどの意味. Rust で言う Crate とだいたい同じかと思います)にだいたいの情報があります.

https://github.com/PixarAnimationStudios/USD/blob/release/pxr/usd/usd/crateFile.h
https://github.com/PixarAnimationStudios/USD/blob/release/pxr/usd/usd/crateFile.cpp

このあたりからたどるとよい.

TinyUSDZ でだいたい feature complete な USDC parser もできたよ

USDZ のフォーマット

USDC や, 画像データ(png)などのデータをひとつの ZIP に uncompressed でまとめたものです.
各データは 64bytes にアラインしておく必要があります.

実際のところは, ZIP のヘッダだけパースできればいいので, zlib などを使う必要はありません.

フォーマット

USDC は以下のような構造になっています. だいたい OpenEXR とか TIFF のデータ構造に似た感じです.

  • BootStrap(header)
    • バージョン情報, TOC へのバイトオフセット
  • [各種バイナリデータ]
  • TOC(table of content)
    • section の array
  • Sections
    • シーンデータを復元するための情報

ファイルの先頭は BootStrap です(crateFile.h のコメントを見るとファイルの最後とあるが, たぶん間違いでしょう)
ファイルの最後は TOC と Sections です(構造的には, 各データのありかはバイトオフセットで扱っているので, TOC/Sections と各種データの配置位置は仕組み上は自由に入れ替え可能)

BootStrap

マジックヘッダー(PXR-USDC), version, toc へのオフセットの情報があります. 88 bytes 固定です.

TOC

Section のアレイです.
predefined(known)な Section と, unknown の Section を持つことができます.

known section は TOKENS, STRINGS, FIELDS, FIELDSETS, PATHS, SPECS になります.

unknown section は拡張用です.
USDZ データには known section しか存在しないと思われます.

Sections

名前, バイト位置, バイトサイズで構成されています.

TOKENS

文字リテラルの配列です. version 0.4.0 からデータは圧縮(LZ4)されています
それぞれの文字リテラルは unique になっています(e.g. float3 は TOKENS には複数存在しない). STRING や PATHS などで使われる文字がここに全部詰まっています.

STRINGS

文字列ですが, 内部データとしてはトークンへのインデックス(StringIndex)で表現されています.
したがって実際に文字列を得るには先に TOKENS をパースする必要があります.

PATHS

シーングラフのパスのリストです.

Maya でいう DAG, unix でのパスのような感じです.

親子関係などの階層構造の情報もここに格納されています. ただ, 絶対パスの文字列自体は token(TokenIndex) としては格納されておらず, 絶対パスは各ノードの elementName と改造構造関係から復元する必要があります(エンコードするときちょっとめんどい)

データ構造自体は glTF のようにノードを index で表現(格納)されており, それぞれのノード(Prim もしくは Property)は elementName(リーフ(terminal. 末端)名のみ. "teapot", "xformOpOrder" など. )が格納されています.
elementToken インデックスは elementName の token へのインデックスです. マイナスの場合は Property であることを示しています(invert して token index を得る).

elementTokenIndexes[0] = 7("")
elementTokenIndexes[1] = 4("root")
elementTokenIndexes[2] = -11("xformOp:transform")                                                       elementTokenIndexes[3] = -12("xformOpOrder") 

のような感じです.

階層構造を表現するのに 子と sibling(兄弟)のオフセットを記録した jump インデックスもあります
(二重連鎖木表現かな? https://ja.wikipedia.org/wiki/%E4%BA%8C%E9%87%8D%E9%80%A3%E9%8E%96%E6%9C%A8 )

teapot.usdc の path を, jumps を使い, elementTokenIndex をつなげていって階層構造を復元したあとにダンプすると以下のようになります.

path[0] = /
path[1] = /teapot
path[2] = /teapot/Looks
path[3] = /teapot/Looks/pxrUsdPreviewSurface1SG
path[4] = /teapot/Looks/pxrUsdPreviewSurface1SG/TeapotMaterial
path[5] = /teapot/Looks/pxrUsdPreviewSurface1SG/file4
path[6] = /teapot/Looks/pxrUsdPreviewSurface1SG/file4/TexCoordReader
path[7] = /teapot/Looks/pxrUsdPreviewSurface1SG/teapot_high_lambert1_BaseColor_1
path[8] = /teapot/Looks/pxrUsdPreviewSurface1SG/teapot_high_lambert1_BaseColor_1/TexCoordReader
path[9] = /teapot/Looks/pxrUsdPreviewSurface1SG/teapot_high_lambert1_Metallic_1
path[10] = /teapot/Looks/pxrUsdPreviewSurface1SG/teapot_high_lambert1_Metallic_1/TexCoordReader
path[11] = /teapot/Looks/pxrUsdPreviewSurface1SG/teapot_high_lambert1_Normal_1
...
path[86] = /teapot/Teapot.subdivisionScheme
path[87] = /teapot/Teapot.xformOp:translate:pivot
path[88] = /teapot/Teapot.xformOpOrder

. までが prim path, . のあとが attribute path(アトリビュート名)になります.
[] で他のノードをリファレンスしたりすることもできます.
@ も使えるようですね.
また, {} で variant を扱うこともできます. ただ, これは USDZ では基本使わないです(たぶん).

: はネームスペースです. /teapot/Teapot.xformOp:translate:pivot などに見られるように, ネームスペースは複数持てます.

primvars, xformOp など決まったネームスペースがあります.
(スキーマのコメントに記述はされているが, スキーマ自体では定義されていないので, 何が predefiend なネームスペースなのか調べるのがちょっとめんどい)

FIELDS

実際のデータ(頂点データなど)のメタ情報です.

Field は

  • Token への Index(フィールド名)
  • ValueRep (実データ情報)

から成ります. Field も Index 管理されます.
FieldIndex は後述の FIELDSETS で使われます.

ValueRep はいくらかややこしい定義になっています.

ValueRep は 8 バイト(uint64_t)のデータで, 2 バイトが型情報やデータが圧縮されているかなど, 6 バイトが実データもしくはオフセット値になります.

6 バイトに収まるような値はこの 6 バイトの中に埋め込み, 配列などデータサイズが大きいものはそのありかへのオフセット値になります(TIFF のファイルフォーマットのような感じですね).

Fields では, index の配列と ValueRep の配列が, それぞれ int 圧縮, LZ4 圧縮で格納されるようになっています.

たとえば以下のような感じになります.

field[0] name = defaultPrim, value = ty: 11, isArray: 0, isInlined: 1, isCompressed: 0, payload: 1
field[1] name = documentation, value = ty: 10, isArray: 0, isInlined: 1, isCompressed: 0, payload: 0
field[2] name = metersPerUnit, value = ty: 9, isArray: 0, isInlined: 0, isCompressed: 0, payload: 88
field[3] name = upAxis, value = ty: 11, isArray: 0, isInlined: 1, isCompressed: 0, payload: 6
field[4] name = primChildren, value = ty: 41, isArray: 0, isInlined: 0, isCompressed: 0, payload: 96
field[5] name = specifier, value = ty: 42, isArray: 0, isInlined: 1, isCompressed: 0, payload: 0
field[6] name = typeName, value = ty: 11, isArray: 0, isInlined: 1, isCompressed: 0, payload: 11
field[7] name = primChildren, value = ty: 41, isArray: 0, isInlined: 0, isCompressed: 0, payload: 108
field[8] name = properties, value = ty: 41, isArray: 0, isInlined: 0, isCompressed: 0, payload: 136
field[9] name = typeName, value = ty: 11, isArray: 0, isInlined: 1, isCompressed: 0, payload: 21
field[10] name = primChildren, value = ty: 41, isArray: 0, isInlined: 0, isCompressed: 0, payload: 152
field[11] name = properties, value = ty: 41, isArray: 0, isInlined: 0, isCompressed: 0, payload: 168
field[12] name = typeName, value = ty: 11, isArray: 0, isInlined: 1, isCompressed: 0, payload: 27
field[13] name = properties, value = ty: 41, isArray: 0, isInlined: 0, isCompressed: 0, payload: 196
field[14] name = primChildren, value = ty: 41, isArray: 0, isInlined: 0, isCompressed: 0, payload: 216
field[15] name = typeName, value = ty: 11, isArray: 0, isInlined: 1, isCompressed: 0, payload: 31
field[16] name = active, value = ty: 1, isArray: 0, isInlined: 1, isCompressed: 0, payload: 1

FIELDSETS

STRINGS と同じように, インデックスの配列が格納されています.
このインデックスの値は, FiledSet(FIELD のグループ) id になります.

FIELD Setとは, field の index を複数まとめたものです.

たとえば

0 番目の field set は field 0, 1, 2
1 番目は field set は field 1, 2, 3

など.

FIELDSETS でシリアライズされている flatten されたインデックスの配列では, invalid(~0) を terminator(区切り)として利用します.

fieldsets[0] = 0
fieldsets[1] = 1
fieldsets[2] = 2
fieldsets[3] = 3
fieldsets[4] = 4
fieldsets[5] = 5
fieldsets[6] = 4294967295
fieldsets[7] = 6
fieldsets[8] = 7
fieldsets[9] = 8
fieldsets[10] = 9
fieldsets[11] = 4294967295
fieldsets[12] = 6
fieldsets[13] = 10
fieldsets[14] = 11
...

fieldset が指すインデックスは, 各グループでの先頭インデックスの頭だけが格納されてるので,
ここからデータを復元するにはすこしややこしいことをする必要があります.

先頭インデックスから, INVALID terminator(もしくは配列の終わりまで)をスキャンすることで, ひとつの FieldSet が使っている field の数がわかります.

文字列がひとつの配列にまとまっていて, null terminated の配列に対して, 文字の先頭のオフセットが格納されている, 各グループでの field の数は strlen で求める, みたいな感じです.

empty scene の場合, FIELD と FIELDSET は空になる場合があります.
ただ, FIELDSET は ~0 terminator があるため,少なくとも要素は 1 になります.
(その場合, 後述の SPEC では fieldset index は 0 になる(zero 個の field をもつ FieldSet を指す))

PrimVar(Property, attribute)ですと, FieldSet の Field にはたとえば以下のようなのが含まれます

  • typeName: データの型(point3f[] など)
  • default に実際のデータ(頂点データなど)
  • variability
  • interpolation

Custom

property(attribute)が custom かどうかの情報は custom field にあります.

Uniform?

variability にあります.

SPECS

FIELDSETS(field のグループ) と PATH のマッピング(+ PATH に対するデータ型)を記述しています.

たとえば

/teapot/Teapot.primvars:st:indices

には fieldset N のデータをアサイン, など.
FIELDSETS で記述したとおり, fieldset のインデックスは flatten されているため, 実際のインデックス値は

specs[0].fieldset_index = 0
specs[1].fieldset_index = 5
specs[2].fieldset_index = 9
specs[3].fieldset_index = 14 

のようなとびとびのインデックス値になります.

fieldset が無い Spec の場合は, invalid(~0)の値が設定されています.

PATH に対する型は SdfSpecType(enum = uint32)で指定します.

シリアライズで使う SpecType は, C++ オブジェクトの SpecType とちょっと異るようです.

  • Root 要素(Stage meta) : SpecTypeRelationship
  • Prim(AbsolutePath) : SpecTypePseudoRoot
  • rel(Relationship) プロパティの場合 : SpecTypeRelationshipTarget
  • アトリビュート(double radius など) : SpecTypeConnection
    • .connect(Connection) の場合は SpecTypeRelationshipTarget

になります.

ValueRep から実際のデータを復元する

FIELD に紐づいているデータは ValueRep 形式なので, ここから実際のデータを展開/復元する必要があります.
ちなみに pxrUSD では, inlined 形式は読み込み時に展開されますが, non-inlined なデータ(頂点データなど)は ValueRep のままになっています.

USD では delayed load などもできますので, 実際にデータが必要になったときにデコードしているようです.

Inlined

48 bits(6 バイト)のペイロードに収まるデータの場合は, inlined でペイロードに収納されています.
Int 変数や String(index, uint32_t)型の変数などですね.

double で inlined の場合は, float で格納されています.
double4 あたりとかや, Matrix データで対角成分だけのもので, 浮動小数点値が整数で int8 配列で表現できる場合(1.0, (1.0, 2.0, 3.0, 4.0), Identity Matrix など)は, この 6 バイト内にパッキングしています(ので復元がちょっとめんどい)

Inline 化可能な型はここにあります.
また, Inline 化されるのは限られたプロパティのようです(xformOp での行列データなど)
たとえば GeomMesh::points などは inline 化されません.

頂点データなどのように要素数多いのが想定されるデータだと, Inline 化できるかのチェックで処理かかってしまうからですかね.

その他

VtValue を ValueRep に変換したもので格納されています.
各型にあわせて復元していきます.

元 USD コードだと, 基本 template で Read<T> としています.
したがってコードを追うのがめんどいです...

VALUE, UNREGISTERED_VALUE

通常は Value の型が指定されていますが, UNREGISTERED_VALUE などもあります.
(スキーマで定義されていないカスタム値用)

VALUE, UNREGISTERED_VALUE は Value 型(再度 ValueRep を読んで実際のデータ型を得る必要がある)で任意のデータを保存できるしくみです.

ただ, UNREGISTERED_VALUE は実際には string 型か dictionary 型になります
(細かいところでは ListOp な UNREGISTERED_VALUE もある)

Map

VtDictionary とかです.

C++ での定義では std::map になります.

シリアライズのレイアウトは以下のようになります. key に対応する文字列は StringIndex で格納されています.

VtValue は ValueRep 形式で格納されていますが, 再帰的なデータを扱うために(Value は, JSON のように, たとえば Dictionary などで入れ子になったデータを持てる), 実際の ValueRep へのバイトオフセット値 が最初に格納されています.

今まではオフセットはファイル(or USDC データ)の先頭からのバイト数でしたが, ここでの offset は string_index からの相対バイト数になりますので注意ください.

[num_keys] : 8byte
[string_index0] : 8byte
[offset0] : 8 byte
[ValueRep data0] : 8byte
[string_index1] : 8byte
...

TimeSamples

アニメーションデータなどの時間軸でのデータです. times(double 配列) と, elements(実データ(ValueRep スカラーの配列))の配列としてシリアライズされています.

[times] : double[]
[num_reps] : 8 bytes
[ValueReps] : ValueRep[]

None(Attribute Block) については SdfValueBlock 型のデータとなっているようです(TODO: 要確認).

データ圧縮

version 0.4.0 から, 配列データなどには LZ4 を使っています.
また, インデックスなど整数値には専用の圧縮用データ構造(これも圧縮自体には LZ4 利用)を使っています.

ただ, 頂点データなど(float3[] など)は圧縮は行われません.

よくある field

specifier: Specifier: SpecifierDef(Prim の `def`, `over` など)
typeName: Token: "Xform", "Shader", "Material", "Scope"(グループ) ...
primChildren: TokenArray: 子ノードの名前(path)のリスト
kind: Token: "component", ...
properties: TokenArray: property(attribute) のリスト(e.g. "info:id", "outputs:surface") 

Field, FieldSet のメモ

targetChildren (PathVector)

reltargetPaths field に付随してつくもの. targetChildren が付いている USDZ もあれば付いていない USDZ もある.
のリレーションの先(target)の Path 情報が保存されているっぽい.

connectionChildren (PathVector)

.connect のアトリビュートで, connectionPaths に付随してつくもの. これも targetChildren と同等のようです.

参考までに, 以下は TinyUSDZ でダンプしたものの情報

...
connectionChildren = [</baked_mesh/Materials/g0/surfaceShader.outputs:surface>]
/home/syoyo/work/tinyusdz/src/usdc-reader.cc:ParseProperty:734  fv name connectionPaths(type = ListOpPath)
/home/syoyo/work/tinyusdz/src/usdc-reader.cc:ParseProperty:799 connectionPaths = ListOp(isExplicit 1) {
  explicit_items = [/baked_mesh/Materials/g0/surfaceShader.outputs:surface]
  added_items = []
  prepended_items = []
  deleted_items = []
  ordered_items = []
}

パスの存在証明のためになんか追加しているっぽい.

rel mybinding みたいなリレーションの定義(?)だけのもの.

FieldSet には typeName はなくて variability だけ(Spec は RelationTarget)

Stage Mea

upAxis: string: "Y"
metersPerUnit: double: 0.01
timeCodesPerSecond: double: 24

復元する

known section をパースできたら, 素材が揃っている状況になります.
ここからあとは基本的には自前の C++ データ構造(もしくは USD schema)に合わせてひらすら復元していけば OK かと思われます.

Schema

パースできた状態では, セマンティックな情報はありません.
ここから実際のシーングラフ表現に戻すためには, schema 定義(usda で定義されている)などを参考に具体的なクラス(オブジェクト)に復元していく必要があります.

Prim, Property

Prim の Spec(FieldSets) 場合は基本 SpecType::Prim, Property は SpecType::Attribute です.

Variant

SpecType::Variant, SpecType::VariantSet になります.

SpecType::VariantSet のノード(PseudoRoot 相当)に, SpecType::Variant が子として存在するような感じになります.

SpecType::Variant の子は SpecType::Prim と同じように Prim or Attribute を持ちます.

#usda 1.0

def "bora"
(
    append variantSets = ["shapeVariant"]
)
{
    variantSet "shapeVariant" = {
        "Capsule" {
            double myval = 2.0
        }
        "Cone" {
            int myval = 3
        }
    }
}

/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5175 spec[2].pathIndex  = 2, fieldset_index = 6, spec_type = SpecTypeVariantSet
/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5177 spec[2] string_repr = [Spec] path: /bora{shapeVariant=}, fieldset id: 6, spec_type: SpecTypeVariantSet
/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5175 spec[3].pathIndex  = 3, fieldset_index = 8, spec_type = SpecTypeVariant
/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5177 spec[3] string_repr = [Spec] path: /bora{shapeVariant=Capsule}, fieldset id: 8, spec_type: SpecTypeVariant
/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5175 spec[4].pathIndex  = 4, fieldset_index = 8, spec_type = SpecTypeVariant
/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5177 spec[4] string_repr = [Spec] path: /bora{shapeVariant=Cone}, fieldset id: 8, spec_type: SpecTypeVariant
/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5175 spec[5].pathIndex  = 5, fieldset_index = 10, spec_type = SpecTypeAttribute
/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5177 spec[5] string_repr = [Spec] path: /bora{shapeVariant=Capsule}.myval, fieldset id: 10, spec_type: SpecTypeAttribute
/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5175 spec[6].pathIndex  = 6, fieldset_index = 15, spec_type = SpecTypeAttribute
/home/syoyo/work/tinyusdz/src/crate-reader.cc:ReadSpecs:5177 spec[6] string_repr = [Spec] path: /bora{shapeVariant=Cone}.myval, fieldset id: 15, spec_type: SpecTypeAttribute

SpecType::VariantAttribute もあるようですが, 今は使われないようです.

Shader

usdImaging にあります.

usdImaging/plugin/usdShaders/shaders/shaderDefs.usda

テクスチャマッピングをするには, 一旦 UsdPrimvarReader(通常は float2(st)版の UsdPrimvarReader_float2)を介してテクスチャ座標を読み込む必要があります.
(通常, 読み出し先テクスチャ座標データは GeomMesh 側の primvar に接続されている)

UsdPrimVarReader は Shader (のアトリビュート(e.g. diffuseColor))ごとに保持できるため, 理論上は異るテクスチャ座標をそれぞれのテクスチャに割り当てることができます.
(OpenGL では描画が面倒になりそうですが)

Prim Kind

Prim kind に sceneLibrary kind が追加されています.

USD で読む.

というわけで USDC(USDZ) を直接読む準備ができました.

とりあえず手っ取り早く Android で USD を使って読みたい方向けに, Android(+aarch64 linux) ビルドご用意しました.

Jetson でもコンパイルできるよ.

Meshula 先生による iOS ビルド. ありがとうございます.

には USD C++ API の最小限のチュートリアルコードもあって便利だよ.

C++ で読む

TinyUSDZ の crate-*** を参照ください.

おまけ

FBX を Autodesk FBX SDK を使わずにパース, シリアライズできるのがあります.

(他にも assimp などいくつかありますが, この fbx-file が一番コンパクトそうでした)

FBX も内部形式は USDC のような感じです(USDC ほどややこしくはないですが).

TODO

  • C++ から USDC(Crate) にシリアライズする
10
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
10
4