見出し画像

3Dモデル編その⑤SDK無しでFBXを解析しよう!

皆さんこんにちは。
フルスクラッチでゲームエンジンを作ろう!第十回です。

前回読み込んだFBXの情報を、今回は独自形式に変換して描画するところまで行きたいと思います。

今回参考にした文献はこちらのAutodeskのSDKのリファレンスです。

もちろん、SDKで加工されたデータの扱いやSDKの機能などを見ても何の役にも立ちませんが、用語の解説などが載っているので、SDK無しでやろうとする場合でも最低限必要な知識はここから得ることができます。

今回例として
Blender2.82
で出力したFBXファイルを使います。

こんなん

画像1

からダウンロードしたものにテクスチャを貼り付け、三角ポリゴン化(後述)したものです。

メッシュ(座標、UVなどから作った頂点とその頂点をどう結ぶのかの情報)とマテリアル(光学的特性)、そしてテクスチャ。
これらを取得して前々回紹介した独自形式に変換していきます。
ですが、変換の工程に関しては僕の役にしかたたないので省略、前回紹介したFBXの複雑怪奇なデータ構造から如何に取り出し結び付けていくかを紹介していきます。

0,GlobalSettingsの取得
一番最初に、GlobalSettingsを取得してこのFBXデータにおけるX、Y、Z軸の向きを決定します。
3D空間の表し方には右手系、左手系の二種類があり、扱うソフトによってどちらで表すかが違うのです。
FBXではこれらのソフトウェア同士でも同じフォーマットを使いまわすので、このノードにどの軸がどの向きなのかの情報を託しています。

GlobalSettingsノードはFBXファイルに一つある親の無いノードで、軸の向きだけでなくアニメーションのフレームレートなどのデータ全体に関わる設定が含まれています。
子ノードのProperties70に、具体的な情報を持ったPノードがたくさん子ノードとしてくっついています。

P{
2:IntProperty
"UPAxis":StringProperty
"Integr":StringProperty
...

こんな感じのノードが15個ぐらい続きます。
これはUpAxis、つまり上向きの軸はfloat3での2番目の値(FBX内で数えるときは配列と同じく0スタート)を参照してね、というノードです。
次の

P{
1:IntProperty
"UPAxisSign":StringProperty
"Integr":StringProperty
...

というノードは上向きの軸は値が正の方向に大きくなれば上に行くよ、という意味です。
この二つを併せて(DirectXにおいて上向きの軸となる)Y軸にはfloat3での2番目の値に1をかけたものを代入すればいいのね!という情報を取得するわけです。

これらGlobalSettingsは後でたくさん使うので構造体に入れて保持しておきます。

1,Meshの取得
形状を表すデータを探し出して座標、UV、法線を含んだ頂点とポリゴンの頂点インデックス配列を書き出します。
これらの情報はGeometryというノード名かつ、"Mesh"という文字列プロパティを持つノードが持っています。
便宜上このノードをGeometryMeshと呼ぶことにします。
今回使う椅子のモデルは背もたれ、座る部分、前の足二本と後ろの足二本のメッシュに分かれているため、GeometryMeshは4つです。

GeometryMeshは子ノードを9個持っており、頂点座標がVertices、頂点インデックスに関する情報がPolygonVertexIndex、UVに関する情報がLayerElementUV、法線に関する情報がLayerElementNormalに格納されています。
これらを取り出したら、以下のように解析します

◆座標の解析、取得
まずはVerticesから頂点の座標を配列として取得します。
VerticesのDoubleArrayPropertyが持っているDoubleの配列を先ほどのGlobalSettingsに従いつつ端からVector3に格納していけば完成です。
GlobalSettingsのUpAxisが2、FrontAxisが1、CoordAxisが0の場合、

画像2

のように格納するわけですね。

◆頂点インデックスの解析、取得
頂点をどの順番で結んでポリゴンを表現するかという情報をPolygonVertexIndexノードから取得します。
PolygonVertexIndexが直接保持しているIntArrayPropertyからintの配列を取り出して使用します。
0,1,3,5,2,4と続く配列だったら、上述の頂点座標の配列の0、1、3番目を結んだ三角形と、5、2、4番目の座標を結んだ三角形を描画してね、ということになります。

が、実際にこの配列を見てみるとおかしなことがあります。
0,1,-4,5,2,-5...と負の値が時折出現するのです。これには以下のような理由があります。

・通常、コンピュータで図形を描画するときは全て三角形の集まりと定義するが、3DCGを制作するソフトウェアでは、三角形以外でもポリゴンを表現する必要がある。
・FBXフォーマットはさまざまなソフトウェア同士でも情報を受け渡しできる必要があるため、例えばMayaで出力した四角ポリゴンの使われたFBXが3DsMaxだと三角ポリゴンになっていて思ったような編集が出来ない、ということはあってはならない。
・よってFBXは様々な頂点数のポリゴンをサポートする必要があり、ポリゴンの一番最後となる値を(本来の値+1)*-1して書き込んで、表したいポリゴンが何角形なのかという情報も同時に与えている

つまり、負の値が出てきたらそこがポリゴンの終端なので-1をかけて1を引いて正常な値に戻し、前回負の値が出てきてからいくつ目なのかを数えて何角形なのかを取得、加工するわけです。
0,1,-4,5,2,-5の例ならば0,1,3を結んだ三角形と5,2,4を結んだ三角形になり、
0,1,6,-4,のような四角形ポリゴンが出てきた場合は、0,1,6と0,6,3の三角形に分割します。
どんな多角形でも三角形に分割できますが、大量の多角形ポリゴンを全て三角形ポリゴンに変換するのは処理が重くなるので、できることなら3DCGのソフトでFBXを出力する際に全てのポリゴンを三角形に分割した方がいいです。(これが上で言っていた三角化)

以上の処理を経て、頂点インデックスを全て取得します。

◆UVの取得
UV情報はLayerElementUVの子ノードであるUV、UVIndex、MappingInformationType、ReferenceInformationTypeから取得できます。

MappingInformationTypeにはUVが頂点そのものに貼られているのか(ByControllPoint)、ポリゴンに貼られているのか(ByPolygonVertex)が、
ReferenceInformationTypeはUVを参照するときに配列から直接参照するか(Direct)UVIndexの配列を利用するか(IndexToDirect)がそれぞれStringPropertyとして入っています。

UVにはDoubleArrayPropertyとしてUVの情報が入っているのでVector2の配列として取得します。
UVIndexのIntArrayPropertyから取得できるintの配列にはUVを参照する際のインデックス番号が入っています。

・MappingInformationTypeがByControllPointかつReferenceInformationTypeがDirectならn番目の頂点はUV配列[n]のVector2をUVとして取得
・MappingInformationTypeがByControllPointかつReferenceInformationTypeがIndexToDirectならn番目の頂点はUV配列[UVIndex配列[n]]のVector2をUVとして取得
・MappingInformationTypeがByPolygonVertexかつReferenceInformationTypeがDirectならn番目のポリゴン頂点はUV配列[n]のVector2をUVとして取得
・MappingInformationTypeがByPolygonVertexかつReferenceInformationTypeがIndexToDirectならn番目のポリゴン頂点はUV配列[UVIndex配列[n]]のVector2をUVとして取得

大抵の場合はMappingInformationTypeがByPolygonVertexかつReferenceInformationTypeがIndexToDirectです。

これで頂点に対してUVを結びつけることができます。

◆法線の取得
法線情報はLayerElementNoramlの子ノードであるNormal、NormalIndex、MappingInformationType、ReferenceInformationTypeから取得できます。

MappingInformationTypeには法線が頂点そのものに貼られているのか(ByControllPoint)、ポリゴンに貼られているのか(ByPolygonVertex)が、
ReferenceInformationTypeは法線を参照するときに配列から直接参照するか(Direct)法線Indexの配列を利用するか(IndexToDirect)がそれぞれStringPropertyとして入っています。

NormalにはDoubleArrayPropertyとして法線の情報が入っているのでVector3の配列として取得します。
NormalIndexのIntArrayPropertyから取得できるintの配列には法線を参照する際のインデックス番号が入っています。

法線と頂点の結び付け方はUVを結びつけるときと完全に同じ方法です。
LayerElementNormalは基本的に、MappingInformationTypeがByPolygonVertexかつReferenceInformationTypeがDirectです。

以上で、Meshに関する情報を全て抜き出せたといえるでしょう。

2,Modelの取得
1でメッシュの情報を取得できましたが、そのまま描画しようとするとおかしなことになります。


全てのパーツが重なっていますね。(真っ黒なのはシェーダで塗りつぶしているからです)
上で取得できるMeshの頂点座標はパーツ単位で決まっている原点からの距離なので、そのままだとうまくいきません。
それぞれのパーツが原点からどれだけ離れ回転しスケール調整されているかの情報はModelノードが持っています。
Modelノードは描画するオブジェクトの数だけ存在するので、今回はGeometryMeshと同じく四つです。

Modelノードの子ノードのProperties70に、子ノードとしていくつかモデル情報が格納されています。

P{
-0.3:DoubleProperty
5.4:DoubleProperty
47:DoubleProperty
"Lcl Translation":StringProperty
"Lcl Translation":StringProperty
...

こんな感じのノードがいくつか続いているので、GlobalSettingsに従ってVector3を取得していきます。
注意点として

P{
-0.3:DoubleProperty
178.999:DoubleProperty
30:DoubleProperty
"Lcl Rotation":StringProperty
"Lcl Rotation":StringProperty
...

には回転情報が入っているのですが、この回転はラジアンではなく度数法の値です。

こうしてModelから得た情報から座標変換行列を作り、メッシュにかけてあげると

しっかりと組み立てられた椅子になります。
なお、ModelとGeometryMeshの対応をどうやってとるかは大分下の「5.それぞれの要素の結び付け」に書いてあります。

3,Materialの取得
マテリアルはMaterialという名前のノードに情報が格納されています。
今回は背もたれ、座る部分、脚でマテリアルを作っているので該当するノードは3つです。

マテリアル名はMaterialのStringPropertyに格納されています。
マテリアルのタイプ(Phong等)はMaterialの子ノードShadingModelのStringPropertyに、
DiffuseやAmbient等の各パラメータはMaterialの子ノードのProperties70の子ノード、Pとしてぶら下がっています。
各パラメータのノードは

P{
0.0012:DoubleProperty
0.0012:DoubleProperty
0.0012:DoubleProperty
"DiffuseColor":StringProperty
"Color":StringProperty
...

のような構造です。
一つ目のStringPropertyがどのパラメータの値なのか、二つ目のStringPropertyで先頭のDoublePropertyの羅列をどう解釈するかが取得でき、
このノードの場合はマテリアルのDiffuseにあたる色情報、(0.0012,0.0012,0.0012)が入っているのがわかります。
また

P{
0.05:DoubleProperty
0.05:DoubleProperty
0.05:DoubleProperty
"AmbientColor":StringProperty
"Color":StringProperty
...

P{
0.000:DoubleProperty
"AmbientFactor":StringProperty
"Number":StringProperty
...

のように二つのノードを使って一つのパラメータを表す場合もあります。(これは(0.05,0.05,0.05,0.00)のAmbientですね。)

自分に必要なマテリアルの情報分だけ取得、使用すればMaterialからの情報抜き出しは終了です。

4,Textureの取得
テクスチャの情報はTextureというノードにあります。
このモデルは一つのマテリアルにつき一枚のテクスチャを貼り付けているので、三つのTextureノードが存在します。
今回テクスチャに関する情報で欲しいのはファイルのパスのみです、
Textureノードの子ノードの一つ、RelativeFilenameノードのStringPropertyに、現在解析しているFBXからの相対ファイルパスがあるので、これを利用します。

5,それぞれの要素の結び付け
これまでの工程でメッシュ情報、マテリアル情報、テクスチャ情報、モデル情報を対応するノードから取得できましたが、
どのテクスチャがどのマテリアルに使用されているのか、どのマテリアルにどのメッシュが結びついているのかがまだわかりません。
Connectionというノード群から情報を得て各要素の結び付けをします。

参照の方向は
Geometry→Model
Model→Material
Material→Texture
です。

◆Connectionノードの取得
ノード同士の結びつきの情報は繋がれているノードが持っているのではなく、
Connectionというノード群が「どのノードとどのノードが接続されている」というデータを保持することで表現されています。

少しややこしくなりますがConnectionという名前のノードの子ノード、CをこれよりConnectionとします。(「Connectionという名前のノード」はCを大量に保持しているだけなので)
Connectionは以下のような構成になっています。

C{
9152201562:LongProperty
6555898623:LongProperty
"OO":StringProperty
}

これは9152201562というノード番号のノードが、6555898623というノード番号のノードと結合してる、という情報として読み取れます。
急にノード番号とはなんだ、と思われるかもしれませんが聞いてください。
今まで挙げてきた大きな分類のノード、GeometryMesh、Model、Material、Textureは全てLongPropertyとして謎の数値を持っているのです。
この数値こそ、プロパティとプロパティをつなぐ際のカギ、ノード番号です。私が勝手につけた名前です。正式名称はNodeIndexとかそんな感じじゃないですかね。知りませんが。
Connectionの一つ目のLongPropertyが情報を送る側、二つ目が情報を受け取る側のノードを表します。

これでModelとGeometryMesh、ModelとMaterialの繋がりを取得できましたがMaterialとTextureをつなぐConnectionノードだけ中身が今までと違います。

C{
9152201562:LongProperty
6555898623:LongProperty
"OP":StringProperty
"DiffuseColor":StringProperty
}

今迄"OO"だったStringPropertyが"OP"に変わり、"DiffuseColor"なる二つ目のStringPropertyが現れています。
OOとはObject-Object結合、OPはObject-Property結合という意味です。
つまり今まではこの(一つ目のLongPropertyで表されている)ノードにこの(二つ目の)ノードを結び付けるよ、という情報だけだったのが
OPとなった今はこの(一つ目の)ノードのDiffuseColorというプロパティにこの(二つ目の)ノードを結びつけるよ、という情報になったのです。
ModelとGeometryMeshは一対一の繋がりしかありませんが、MaterialにはDiffuseに使うTexture、Opaqueに使うTexture、等たくさんのテクスチャ情報が付与されるケースがあるからですね。

これでMaterialとTexture、ModelとMaterial、ModelとGeometryMeshの要素をつなげることができました。

これまでの手順を踏んでFBXファイルをB3Mに変換、Dx12で描画したものがこちら。

見事に椅子ですね。

今回の内容、正直言ってかなりしんどかったです。
地道にノードの中身を書き読み、アブダクションによってどのデータが何を表しているのか推察する作業は、もはや勉強とか習得ではなく
「推理」でした。
ただおかげで、c++とDirectXでFBXをSDK無しで読み込み、解析して描画した(かなりあっさり調べた範囲ですが)初の日本語記事となれました。(多分)

Rust(ポストc++とか言われてる新しめの言語)でFBXの読み書きをするライブラリを作った方(サークル?)がいらっしゃいますが、あくまでもFBXの木構造の取り出しをサポートするのみで、(前回私がやってたやつをライブラリ化したもの)
描画までは記事、ライブラリになっていませんでした。(できたという記述はありました)


FBXのアップデートに振り回されたとかも書いてあったので、大変な思いをたくさんされただろうな…と勝手に思っています。

FBXからはまだまだたくさんの情報が含まれていますが、それを取得しても表現するだけの描画レベルがないと意味がありません。
きっとできることが増えるたびにFBXとにらめっこする毎日に突入するでしょうが、ひとまず3Dモデル編はここで終わり、これからは自分の描画のレベルを上げていこうと思います。

次回からは3Dモデルのモーションか、マルチライティングをやっていきます。

最後まで読んで頂きありがとうございました。それでは。

この記事が気に入ったらサポートをしてみませんか?