備忘録を兼ねたメモです.
面倒くさそうな構造のファイルだなと思って今まであまり関わらないようにしてましたが,ファイル自体は頻繁に目にするので中身を読んでみました.雰囲気から推測したことも多く含まれるので注意.間違っている箇所もあるかもしれないので,ツッコミは歓迎です.
FBXとは
Autodesk製品で使われるファイルフォーマットです.3Dモデルデータをやりとりにデファクトスタンダードに近い形で使われているフォーマットの割には,仕様が不明瞭だったり,ツールごとの対応に差があって困るのは有名な話のようです(要出典)
Wikipedia: https://ja.wikipedia.org/wiki/FBX
参考にした情報へのリンク
断片的な情報はあちこちにありますが,まとまった仕様書は見つかりません.ボーンやブレンドシェイプでモデルを動かすところまで実装するために,色々見たり試行錯誤する必要がありました.
-
https://help.autodesk.com/view/FBX/2017/ENU/
- フォーマット自体の情報は乏しいですが,どんな値をどう扱えばよいかはFBX SDKのドキュメントが参考になります
-
https://www.slideshare.net/L1048576/fbx-1-1
- Nodeのパーサを書く上で一番参考になりました(日本語)
-
https://code.blender.org/2013/08/fbx-binary-file-format-specification/
- Blenderのドキュメントも参考にしました
- 他
- TODO
あと,手元のPCにFBXを出力できるソフトがインストールされてなかったので,ニコニ立体やSketchfabからダウンロードしたFBXファイルを参考にしました.
FBXを他のフォーマットに変換するツールを書いた
理解が正しいか確認するためにGoでパーサを実装してみました.とりあえず,Material, Model, Geometry, Deformerあたりを読み込んで,MQOやglTFに変換したらそれらく動いたので大丈夫そう.
テキスト,バイナリフォーマットの両方とFBX7.5以降のバージョンにも対応しているので世の中のFBXは大体読めると思います.一方,確認のためだけに作ったので,確認に必要ない値は捨てていたり面倒な箇所は端折っています.
FBXファイルの構造
3層に分けて捉えると理解しやすいです.
- シリアライズフォーマット
- ツリー構造を持つNode
- Nodeは名前といくつかのAttributeを持つ
- ASCIIとBinaryフォーマットの二種類ある
- FBX Object
- ID(整数値),種類(Model,Material,Geometryなど),プロパティ等を持つ
- オブジェクト間の参照関係を持つ
- オブジェクトの種類ごとにデフォルト値などを定義したPropertyTemplateがある
- シーングラフ
- Model, Geometry, Material, Deformer等のFBX Objectで構成されるシーン
- Model がユーザが目にするツリーを構成するオブジェクト
- Model が Geometry, Material などを参照する
Nodeとシリアライズフォーマット
Node
は名前といくつかの属性値と子のNodeのリストを持ち,木構造を表現します.
NodeName: Attr1, Attr2,... {
ChildNode1: ...
ChildNode2: ...
...
}
Blenderのドキュメントではノードが持つ値を Property
と呼んでいますが,後で出てくるObject
のプロパティと紛らわしいのと,Autodeskのドキュメントにはattribute
という単語が使われているのでこの説明ではそちらに合わせます.
バージョンごとの差異やASCIIフォーマット時の文字列の扱いなどは,このスライドが参考になりました.
テキストの文字コードに関する情報はファイルには書かれていませんが,通常はUTF-8のようです.しかしShiftJISのFBXも世の中にあったりして簡単に判別する方法は無さそうです.
バイナリフォーマット
構造は単純なのでBlenderのドキュメントなどを読めば悩む箇所は無いと思います.
すべて 0 で埋められた NULL-record
は無視できそうに見えますが,最後のノードとファイルのフッタの境界を検出するために必要になります(ファイル直下のオブジェクト数が最後まで分からない構造になっている理由は不明).
実装: https://github.com/binzume/modelconv/blob/master/fbx/binary_parser.go
ASCIIフォーマット
バイナリフォーマットと全く同じ構造ですが,読み込み時に型が分からないことと配列の格納方法が少し異なります.
実装: https://github.com/binzume/modelconv/blob/master/fbx/text_parser.go
- 数値: intとfloatの区別がつかないので配列をパースするときに少し困る.
1e-10
などの指数表記も許可 - 文字列: HTML等で使われる実体参照(
"
等)に似たエスケープがされています.オブジェクト名の区切りの "\x00\x01" は "::" に置き換えられます - 配列:
*5 {a: 1,2,3,4,5}
のように*配列サイズと "a"というノードに実施の値が入る.複数の配列があった場合に,a,b,c...となるのかは不明. - バイト列: バイナリフォーマットにあるFileId等のバイト列の値は扱わないようです.バイナリデータを持つオブジェクトの場合はbase64でエンコードするなどして格納します.
FBX Object
FBXファイルの直下には以下のようなノードが並んでいます.
- FBXHeaderExtension
- GlobalSettings: 座標系やシーン全体の設定
- Documents
- Definitions: オブジェクトの種類ごとの定義,プロパティのデフォルト値など
- Objects: オブジェクトのリスト
- Connections: オブジェクトの接続関係
- Takes
FBXの構造を知る上で,特に重要なのがObjects
, Connections
, Definitions
です.
Objects
ドキュメント内のオブジェクトがすべて列挙されています.
Objects: {
Geometry: 1576875632, "Geometry::", "Mesh" {
Properties70: {
P: "Color", "ColorRGB", "Color", "",0.690196078431373,0.101960784313725,0.101960784313725
}
Vertices: *1234 { ..... }
PolygonVertexIndex: *18196 { .... }
LayerElementUV: 0 { .... }
}
Model: 255650640, "Model::hello", "Mesh" {
Properties70: {
P: "Lcl Translation", "Lcl Translation", "", "A+",100,0,0
P: "Lcl Rotation", "Lcl Rotation", "", "A+",0,0,-90
......
}
}
....
}
各 Object
はノード名の他に数値のIDとオブジェクト名,用途を表す文字列を Attribute
として持ちます.
Objects内にはフラットにオブジェクトが並んでいるだけで,これだけでは関連や木構造はわかりません.オブジェクト間の参照関係や親子関係は後述するConnectionsに格納されます.
プロパティ
各オブジェクトは Properties70
というノードを持ち,その下に含まれる名前付きのプロパティは頻繁に使います.ここで見つからない場合は下記の Definitions
にある値をデフォルト値として使います.(70
はFBX7.0を意味しているようです)
各プロパティは P: "Name", "Type", "Label", "Flags", Values...
という構造です.ノード名のP
は特に意味がなく何でも良いようです.
Type にはプロパティの型が入っています.int
, bool
, Vector3D
, KString
, KTime
あたりは良いとして,Lcl Translation
とか Visibility
みたいなのもあってフリーダムな感じです.雰囲気的にはデータ型ではなくモデリングソフトのUI上にどのように表示するかを表すもののようです.
Connections
オブジェクト間の接続関係が書かれています.オブジェクト同士の接続は有向グラフになっていて,複数のオブジェクトから同じオブジェクトが参照されます.循環参照が許可されるかどうかは不明です.
Connections: {
C: "OO",1576875632,0
C: "OO",255650640,1576875632
}
この例では,上の Objects にある,Model(255650640) がシーン直下に存在し,Geometry(1576875632)を持っていることが分かります.
ID=0のオブジェクトは上記のObjects
内には存在しませんが,シーンのルートノードを意味します.
"OO"はオブジェクト同士の接続,"OP"はプロパティに接続されます.オブジェクトはプロパティの値自体と同一構造というわけではないので,名前付きの参照として理解するのが良さそうです.
Definitions
オブジェクトに対象のプロパティがない場合は,Definitions内のPropertyTemplateを参照する必要があります.
以下の例では,Modelオブジェクトの場合は Translation = [0,0,0], Rotation = [0,0,0], Scaling = [1,1,1], Visibility = 1 が初期値として使われることが分かります.
Definitions: {
Version: 100
Count: 3040
ObjectType: "Model" {
Count: 194
PropertyTemplate: "KFbxNode" {
Properties70: {
P: "Lcl Translation", "Lcl Translation", "", "A+",0,0,0
P: "Lcl Rotation", "Lcl Rotation", "", "A+",0,0,0
P: "Lcl Scaling", "Lcl Scaling", "", "A+",1,1,1
P: "Visibility", "Visibility", "", "A+",1
......
}
}
}
}
GlobalSettings
色々な座標系をサポートするために,それぞれの軸をどう扱うかが格納されています.
UpAxis
, FrontAxis
, CoordAxis
と UpAxisSign
, FrontAxisSign
, CoordAxisSign
を見て座標系を決定します.
今回はプログラム内での座標系を特に決めてないので,GlobalSettingsから作った行列を一番最後(グローバル側)から掛けることにしました.配列等からVectorに変換する時点で入れ替えたほうが扱いやすいかもしれません.
GlobalSettingsはIDも持たないしシーンにも含まれませんが,Definitionsには含まれる少し特殊なオブジェクトのようです.
シーングラフ
ここからが本番.ID=0の架空のオブジェクトをシーンのルートとみなしてオブジェクトを辿っていきます.
- Scene(ID=0)
- Model1
- Model2 (Mesh)
- Geometry
- Material1
- Material2
- Model3
- Model4
- Model5
Model
シーン直下にいくつかのModelが存在して木構造になっています.
メッシュを表すModelであれば,GeometryとMaterialを参照しています.このあたりまでは,FBX以外でもよく見る構造ですね.
Model: 255650640, "Model::hello", "Mesh" {
Properties70: {
P: "Lcl Translation", "Lcl Translation", "", "A+",100,0,0
P: "Lcl Rotation", "Lcl Rotation", "", "A+",0,0,-90
......
}
}
....
Modelの座標系
Modelはスケール,回転,移動のいわゆるTRSを持っています.メッシュをレンダリングする時などにGeometryなどが持つ頂点座標などにこれが適用されます.
ツールを実装したりするときは,変換行列そのものが入ってるとありがたいのですが,残念ながらModelは変換行列を持ってないのでTRSそれぞれのプロパティから計算する必要があります.(DeformerとかPoseNodeは普通にMatrixで持ってるのに...)
オブジェクトのプロパティを眺めるとLcl Translation
,Lcl Rotation
,Lcl Scaling
とかが並んでいるのですぐわかると思います.(Lcl = Local.たぶん)
TranslateMatrix(translation) * EulerToRotationMatrix(rotation, rotationOrder?) * ScaleMatrix(scaling)
みたいにすると良さそう.プロパティは省略可能なので見つからなければ,PropertyTemplateからデフォルト値を探します.Rotationはオイラー角のようですが,回転順序も設定によって変わります(ZYXがデフォルト?).SphericXYZとか単なるオイラー角じゃなさそうなのもありますが,多くのツールは無視してるっぽいので今回も無視します.
FBXファイルを見ながら計算してみると,それっぽい行列が得られました.
……ここまでなら,世界は平和だったのだけど現実はもっと厳しいようです.モデリングソフト上でのpivotやoffset周りの操作を知らないと計算できないし,いまや同じ会社の製品なのに3ds MaxとMayaでも違うあたりが闇の深さを感じさせます.
PreRotation
は比較的よく使われてるようなので,今回は気休めとして Translation * PreRotation * Rotation * Scaling
としました.
Geometry
頂点・面・法線などのジオメトリデータを保持します.BlendShapeに使うShape
もGeometry
内で持ちます.
Geometry: 1576875632, "Geometry::", "Mesh" {
Vertices: *1234 { ..... }
PolygonVertexIndex: *18196 { .... }
LayerElementUV: 0 { .... }
Shape: "morph_a" { ... }
}
一番基本となるオブジェクトの形状を表す配列がGeometry
オブジェクト直下に格納されています.
- Vertices 頂点配列.頂点数 * 3個 の数値の配列
- PolygonVertexIndex 面 頂点インデックスを表す整数値の配列
- Edges エッジ.頂点のペアの配列
FBXは任意の頂点数の面が扱えるので,PolygonVertexIndex
は負の値を使って面の境界を表します.負の値は2の補数になっているので,ビットを反転するか,(N*-1)-1
して面の最後の頂点とします.
LayerElement
頂点座標と面以外の情報は,LayerElement
という構造で格納されています.例えば,LayerElementNormal
, LayerElementUV
, LayerElementMaterial
に法線やUVやマテリアルが格納されます.
LayerElementは実データやインデックスの配列とそれをどのように扱うかを持ちます.配列の名前や中身は種類によりまちまちなので,個別に扱う必要がありあそうです.
MappingInformationType:
- ByPolygonVertex 面の頂点ごと (PolygonVertexIndex)
- ByControlPoint 頂点ごと (Vertices)
- ByPolygon 面ごと
- ByEdge エッジごと
- AllSame すべて同じ値
実装が面倒そうですが,実際に利用可能なタイプの選択肢は少ないです.たとえば,ほとんどの場合,Materialは面ごとにしか設定できないと思うので,ByPolygon
以外は考慮する意味がありません.ただ,すべて同じMaterialの場合に AllSame
として格納されている場合があるので,最低限 ByPolygon
+ AllSame
の場合について実装する必要があります.
ReferenceInformationType:
- Direct: 実際のデータが配列で格納されている
- Index 現状のFBXでは使われない
- IndexToDirect: 実データ+インデックスの配列として格納されている
実際にはインデックスが指す配列が要素内に無い場合も意味的にインデックスとして使われるなら,IndexToDirect
になるようです.例えば,LayerElementMaterial
がIndexToDirect
の場合,Materialsにマテリアル番号の配列が入っていますが,これはModelに紐付けられたMaterialの暗黙的な配列へのインデックスとして解釈するのだと思います(たぶん)
Shape
Geometry
は複数の Shape
を持つことが可能でBlendShape等に使えます.独立したオブジェクトとすることも,(Connectionsで参照されるのではなく)Geometry内に持たせることも可能なようです.
Shape: "morph_a" {
Indexes: *111 { ... }
Vertices: *333 { ... }
Normals: *333 { ... }
}
通常のGeometry
内では LayerElement
として扱われていたはずの Normals
がここでは何の属性も伴わずに出現するのが少し不気味です.
フォーマット上は Normals
を ByPolygon
とかに設定できますが,その場合は Vertices
と一対一対応しなくなるので,何が起きるのか謎です.
Material
マテリアルの情報です.
- DiffuseColor + DiffuseFactor のように色 + 係数で表現しているようです
-
ShadingModel
にPhong, Lambertなどのシェーディングの種類が入っていますが大文字小文字は区別されていなさそう - テクスチャは
Texture
オブジェクトを参照しています
Deformer
ボーンによるスキニングなどのGeometry
を変形させるための頂点ごとのウエイトを管理します.
Skinning
Geometry
→ Deformer(Skin)
→ SubDeformer(Cluster)
→ Skeleton内のModelノード
という参照関係です.Skinを表すDeformerにボーンごとのSubDeformerがぶら下がります.
Deformer: 1878370368, "Deformer::", "Skin" {
Version: 100
Link_DeformAcuracy: 50
}
Deformer: 813549504, "SubDeformer::", "Cluster" {
Version: 100
UserData: "", ""
Indexes: *1234 { .... }
Weights: *1234 { .... }
Transform: *16 { .... }
TransformLink: *16 { .... }
}
SubDeformerの Indexes
と Weights
に頂点インデックスとボーンのウエイトの配列が格納されます.
関節の初期位置は,SubDeformerが指すModelの座標ではなく,TransformLink
から計算する必要があります.
Skeleton内のModel
の座標は初期位置ではなく,現在の変形状態を適用した状態で保存されています(おそらく).
Skeletonを構成するModelノードは他のModelと区別できない気がするので,Skeletonのルートノードなどの概念は無いように見えます(要確認)
BlendShape
BlendShapeの場合は以下のような参照関係になり,BlendShapeChannelにShapeの頂点ごとの重みが格納されます.
Geometry
→ Deformer(BlendShape)
→ SubDeformer(BlendShapeChannel)
→ Shapeノード
Deformer: 813549500, "::SubDeformer", "BlendShapeChannel" {
Version: 100
DeformPercent: 0
FullWeights: *123 { ... }
}
感想
FBXは3Dモデルをやりとりするのに使われてはいるけど,モデルデータを表現する以上にモデリングソフト上での状態を保持する目的で作られてる感じがつらい.アニメーション周りは,機会があれば.