LoginSignup
9
3

More than 1 year has passed since last update.

FBXファイルをちょっとだけ理解した

Last updated at Posted at 2021-08-29

備忘録を兼ねたメモです.

面倒くさそうな構造のファイルだなと思って今まであまり関わらないようにしてましたが,ファイル自体は頻繁に目にするので中身を読んでみました.雰囲気から推測したことも多く含まれるので注意.間違っている箇所もあるかもしれないので,ツッコミは歓迎です.

FBXとは

Autodesk製品で使われるファイルフォーマットです.3Dモデルデータをやりとりにデファクトスタンダードに近い形で使われているフォーマットの割には,仕様が不明瞭だったり,ツールごとの対応に差があって困るのは有名な話のようです(要出典)

Wikipedia: https://ja.wikipedia.org/wiki/FBX

参考にした情報へのリンク

断片的な情報はあちこちにありますが,まとまった仕様書は見つかりません.ボーンやブレンドシェイプでモデルを動かすところまで実装するために,色々見たり試行錯誤する必要がありました.

あと,手元の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の構造を知る上で,特に重要なのがObjectsConnectionsDefinitions です.

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, CoordAxisUpAxisSign, 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 TranslationLcl RotationLcl 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に使うShapeGeometry内で持ちます.

    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 になるようです.例えば,LayerElementMaterialIndexToDirectの場合,Materialsにマテリアル番号の配列が入っていますが,これはModelに紐付けられたMaterialの暗黙的な配列へのインデックスとして解釈するのだと思います(たぶん)

Shape

Geometry は複数の Shape を持つことが可能でBlendShape等に使えます.独立したオブジェクトとすることも,(Connectionsで参照されるのではなく)Geometry内に持たせることも可能なようです.

Shape: "morph_a" {
    Indexes: *111 { ... } 
    Vertices: *333 { ... } 
    Normals: *333 { ... }
}

通常のGeometry 内では LayerElement として扱われていたはずの Normals がここでは何の属性も伴わずに出現するのが少し不気味です.
フォーマット上は NormalsByPolygon とかに設定できますが,その場合は Vertices と一対一対応しなくなるので,何が起きるのか謎です.

Material

マテリアルの情報です.

  • DiffuseColor + DiffuseFactor のように色 + 係数で表現しているようです
  • ShadingModel にPhong, Lambertなどのシェーディングの種類が入っていますが大文字小文字は区別されていなさそう
  • テクスチャはTextureオブジェクトを参照しています

Deformer

ボーンによるスキニングなどのGeometry を変形させるための頂点ごとのウエイトを管理します.

Skinning

GeometryDeformer(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の IndexesWeights に頂点インデックスとボーンのウエイトの配列が格納されます.

関節の初期位置は,SubDeformerが指すModelの座標ではなく,TransformLink から計算する必要があります.
Skeleton内のModelの座標は初期位置ではなく,現在の変形状態を適用した状態で保存されています(おそらく).

Skeletonを構成するModelノードは他のModelと区別できない気がするので,Skeletonのルートノードなどの概念は無いように見えます(要確認)

BlendShape

BlendShapeの場合は以下のような参照関係になり,BlendShapeChannelにShapeの頂点ごとの重みが格納されます.

GeometryDeformer(BlendShape)SubDeformer(BlendShapeChannel)Shapeノード

Deformer: 813549500, "::SubDeformer", "BlendShapeChannel" {
  Version: 100
  DeformPercent: 0
  FullWeights: *123 { ... }
}

感想

FBXは3Dモデルをやりとりするのに使われてはいるけど,モデルデータを表現する以上にモデリングソフト上での状態を保持する目的で作られてる感じがつらい.アニメーション周りは,機会があれば.

9
3
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
9
3