FBX SDKメモ

書きかけ

初回更新2022/07/15
最終更新2022/07/25

FBX SDKを触ってみて学習したことを書き連ねます。

大局

ふわっとした話から入ります。

Scene

FBX SDKは、Sceneと呼ばれる3Dシーンデータをメモリ上に展開します。FBX SDKを使う人のほとんどは、FBXファイルを自作アプリで読み書きしたい人だと思いますが、このSDKの主役はFBXファイルではなく、どちらかというとSceneです。
なので、FBX SDKの機能のほとんどは、このSceneに対する操作です。FBXファイルのインポートとエクスポートは、Sceneに対する操作の一部になります。(実は他の形式のファイルにも対応しています。)
FBXファイルの内容を自分のアプリに読み込みたい場合、次のような手順を踏むことになります。
1. 空のSceneを作る。
2. FBXファイルをインポートする。
3. Get何々関数で情報を引き出す。

ObjectとProperty

SceneはFbxObject派生クラスの集合でできてます。例えば、メッシュ情報を格納するFbxMeshとか、アニメーションを取りまとめるFbxAnimStackとかそういう感じの派生クラスがあります。
これらは互いに接続することができて、グラフを構成します。2つ以上の親を持つことができて、オブジェクトが他のオブジェクトから共有されるので、いわゆるツリーではありません。ただし、ループを作るのは禁止してます。こういうのを有向非巡回グラフ(DAG)と呼ぶらしいです。
FBX SDKでは、子をSrcObject、親をDstObjectと呼んでいます。矢印の向きで考えると逆な気がしますが、子(Source)の集合で親を構成する、と捉えるとしっくりきます。

矢印の先がSrcObject

でも上の図はちょっと嘘です。実はObjectはPropertyを持つことができます。FbxPropertyクラスです。これに情報を格納することができます。例えば整数とかベクトルとかです。(じゃあFBXのデータは全部Propertyに入っているのかというと、そうではないです。Objectの付加情報、または些細な本文を格納する場所、くらいの立ち位置です。)
で、このPropertyもまた、Object内部でProperty同士のDAGを構成できます。ObjectにSrcObjectとDestObjectがあるように、PropertyにはSrcPropertyとDstPropertyがあるのです。そしてさらに、PropertyはSrcObjectも持つこともできます。つまりPropertyからもObjectに繋げてしまえるのです。

なので、Property経由の接続も含めた、Object同士の接続の図は、次のようになります。また、このProperty含めた接続関係においても、ループは依然として禁止です。

青線含めてもループにはなってない

FBXファイルの内容を走査するようなときには、この接続関係を意識して処理する必要があります。片っ端から情報をたどっていくと、わけわからんくなります。
あと、実はFbxObjectにはSrcPropertyを追加・取得する関数があります。FbxPropertyにも、DstObjectを追加・取得する関数があります。でもこれらを使う機会はありません。普通のFBXファイルでは、FbxObject :: GetSrcPropertyCount()は常にゼロです。(紛らわしいですが、Object内部のPropertyはSrcPropertyではないです。接続しているものではなく、所持している感覚です。GetFirst/NextPropertyで取得します。)これらの機能は、ゲームメーカーさんが自社専用の特殊なFBXファイルを作ったりとか、そういう用途のものでしょう。

Node

FbxNodeというFbxObject継承クラスがあります。これは、3D空間上に配置されているオブジェクトを表しています。内部にPropertyとしてトランスフォーム三点セット、Lcl Translation、Lcl Rotation、Lcl Scalingを持っています。LclはLocalの略でしょう。そして、SrcObject、DstObjectの仕組みを使って、Node同士が繋がります。

そうなんです。NodeはBlenderのオブジェクトのようなものです。UnityならGameObjectです。Unreal EngineならSceneComponent(?)です。実際、BlenderでFBXをエクスポートすると、Blenderのオブジェクトとボーンがほとんどそのまま、Nodeとして再現されます。
このような用途のものなので、Nodeは2つの親を持ちません。ツリーを構成します。Node同士の関係は、FBXのScene下における網の目状のObject DAGの中にありながら、Nodeだけに着目するとその構造はツリーなのです。

Nodeだけ見るとツリー

RootNode

Sceneは様々なSrcObjectを持っています。その中でもたった1つだけ、RootNodeと呼ばれる特別なObject(Node)を持っています。SrcObjectの1つですが、専用の関数GetRootNodeですぐ取得できます。これは名前通り、Nodeが構成するツリーのルートです。
Nodeのツリーはその途中や末端で、非Node系のObject、例えばメッシュ情報を持つFbxMesh、マテリアル情報を持つFbxMaterialに繋がっています。なので、RootNodeから順番に子(SrcObjet)をたどっていけば、FBXファイル内の主要な情報のほとんどにアクセスできます。

逆を言うと、それだけではアクセスできないObjectも結構あります。例えば、肝心のアニメーション情報、FbxAnimStackがその1つです。AnimStackはBlenderの1アクション(Walk、Attack、等)単位のデータを取りまとめる情報です。(RooNode下のDAGとして繋がっていないわけではないですが、GetDstObjectで適切に親をさかのぼっていく必要があり、結構やりにくいです。)

RootからAnimStackにたどるのは大変

SceneとObject

ぶっちゃけると、SceneはFBXファイル内のすべてのObjectをSrcObjectとして持ってます。なので、前述のFbxAnimStackのように、単純にRootNodeから繋がっていないObjectはSceneから直接取っていくことになります。

じゃあObjectは全部SceneのSrcObjectとして取ればいいんじゃないの? いや、そうするとNodeツリーとその周辺の構造を把握できないのです。
なので、FBXファイルの中身を効率的にアプリに取り込むには、戦略としては次のような二段階の手順を踏むことになります。
1.
RoodNodeからSrcObjectとPropertyを順にたどっていって、Node周辺のObjectの接続状況を把握する。「採れる」データはこのとき採っておく。
2.
1.で取得できないようなObject、または1.で深追いするのを保留したObjectをSceneから直接参照して、さらにそれらのSrcObjectをたどっていく。1.で取得できないObjectのクラスはだいたい決まっていて、具体的にはFbxAnimStack(アニメーション)とFbxPose(バインドポーズ)です。

導入

FBX SDKの導入方法です。他のWebサイトのほうが詳しいと思います。

インストール

AutodeskさんのWebからダウンロードしてインストールします。<C:\Program Files\Autodesk\FBX\FBX SDK\2020.3.1>にインストールされます。全体で1.8GBあります。大きい! 容量のほとんどがlibファイルですが、用途別にたくさん用意されていて、実際に使うのはほんの一部です。知らなかったのですが、libファイルはそのままexeにくっ付くわけではないのですね。必要なプログラムのみが抽出されるので、100MBのlibをリンクしてもexeは10MB以下になりました。

libの選択

使うlibは3つのセットから選択します。
A. libfbxsdk.libのみ
B. libfbxsdk-mt.libとlibxml2-mt.libとzlib-mt.lib
C. libfbxsdk-md.libとlibxml2-md.libとzlib-md.lib

A.はFBXSDK部分にDLLを使うのでその分サイズが小さくなります。その代わりfbxsdk-<version>.dllが必要です。でもfbxsdk-<version>.dllがどこにあるのかわかりませんでした。Mayaをインストールしている前提か、それともSDKに同梱されているlibfbxsdk.dllのことなのか・・・。Autodeskさん的には再配布していいDLLっぽいけど、よくわかりません。

B.はFBXSDK部分を静的リンクします。さらにCライブラリにマルチスレッド対応の静的ランタイムライブラリ(LIBCMT.lib)を使います。これを使う場合、ランタイムライブラリオプション/MTを指定すること。

C.もFBXSDK部分を静的リンクします。さらにCライブラリにマルチスレッド対応のDLL(MSVCRT.lib経由でMSVCR100.DLL)を使います。ランタイムライブラリオプション/MDを指定すること。

特定DLLが無いと動かないのは嫌だけど、Windows付属のDLLは活用してほしいので、C.が手堅い気がします。

プロジェクト作成

Visual Studio 2019を使った例です。libは前述のC.構成を採用しています。

1. 「空のプロジェクト」を作る。
2. プロジェクトのプロパティを開く。
3. アクティブプラットフォームをx64にする。
4. DebugとRelease両方の構成に対して、次を設定する。
4-1. VC++ディレクトリ>インクルードディレクトリ
 <C:\Program Files\Autodesk\FBX\FBX SDK\2020.3.1\include>
4-2. VC++ディレクトリ>ライブラリディレクトリ
 Debug: <C:\略\2020.3.1\lib\vs2019\x64\debug>
 Release: <C:\略\2020.3.1\lib\vs2019\x64\release>
4-3. リンカー>入力>追加の依存ファイル
 libfbxsdk-md.lib(改行)libxml2-md.lib(改行)zlib-md.lib
4-4. リンカー>コマンドライン>追加のオプション
 /ignore:4099
4-5. C/C++※>コード生成>ランタイムライブラリ
 Debug: マルチスレッドデバッグDLL(/MDd)
 Relase: マルチスレッドDLL(/MD)
 ※事前に適当なcppファイルを追加しないとこの項目が出てきません

コード

後は普通に使うだけです。VS2019のデフォルトだとワーニングがたくさん出るので、こういう風にヘッダをpragmaでサンドイッチして回避しました。

#pragma warning(disable:26812) // enum Class
#pragma warning(disable:26451) // 演算オーバーフロー
#pragma warning(disable:26495) // メンバ初期化
#include <fbxsdk.h>
#pragma warning(default:26812)
#pragma warning(default:26451)
#pragma warning(default:26495)

クラス

各クラスについては公式のドキュメントに説明が書いてあります。

FbxClassID

前述のFbxObject派生クラスが、実際に何のクラスなのかを特定するためのクラスです。FbxObject派生クラスは皆、static領域にこれを持っています。実装は不明ですが、マクロを使ってクラス名からIDを生成してるんじゃないでしょうか。
こういう風にクラスをチェックして、素性が分かれば専用関数でキャストします。

fbxsdk::FbxClassId classID = srcObject->GetClassId();
if (classID.Is(fbxsdk::FbxAnimStack::ClassId) == true)
{
  const fbxsdk::FbxAnimStack* const stack
    = fbxsdk::FbxCast<fbxsdk::FbxAnimStack>(srcObject);
}

FbxObject

FBXにおけるほとんどのデータオブジェクトの親クラスです。
名前(文字列)を持っています。これはObject間で重複します。
さらに、UniqueIDを持っています。整数です。メッシュやマテリアルがオブジェに共有されているかどうか確かめる、すでにパースしたかどうか判断する、などに使えます。
他のFbxObjectとつながっており、SrcObject(子)とDstObject(親)を0個以上持っています。GetSrcObject/GetDstObject関数で取得できます。

// クラス指定なし
const int objCount = obj->GetSrcObjectCount();
for (int i = 0; i < objCount; i++)
{
  const fbxsdk::FbxObject* const srcObj = obj->GetSrcObject(i);
}

// クラス指定有り(その1)
const int objCount = obj->GetSrcObjectCount<fbxsdk::FbxMesh>();
for (int i = 0; i < objCount; i++)
{
  const fbxsdk::FbxMesh* const srcObj = obj->GetSrcObject<fbxsdk::FbxMesh>(i);
}

// クラス指定有り(その2)
fbxsdk::FbxCriteria criteria = 
  fbxsdk::FbxCriteria::ObjectType(fbxsdk::FbxMesh::ClassId);
const int objCount = obj->GetSrcObjectCount(criteria);
for (int i = 0; i < objCount; i++)
{
  const fbxsdk::FbxObject* const srcObj = obj->GetSrcObject(criteria, i);
}

普通(?)の情報は、サブクラスのメンバ変数に普通に入ってます。例えば頂点情報はサブクラスであるFbxMeshのメンバ変数です。でも一部の情報は、Propertyとして持っています。前述の通り、Propertyにはdoubleとかベクトルとか入れることが出来て、さらにPropertyのSrcObjectを持つことができます。FBX SDKにおいて、Propertyの扱いは非常にあいまいなように感じます。
Object内のPropertyの取得方法は2種類あります。
1. 名前を直接指定する。Propertyは名前を持ってます。
2. GetFirstPropertyとGetNextPropertyの組み合わせで順次取得する。
前述の通り、GetSrcPropertyでは何も取れません。

// 1.
bool caseSensitive = true;
fbxsdk::FbxProperty fbxProperty = 
  fbxObject->FindProperty("Lcl Translation", caseSensitive);
if (fbxProperty.IsValid() == true)
{
  ...
}

// 2.
fbxsdk::FbxProperty nextProperty = fbxObject->GetFirstProperty();
while (nextProperty.IsValid() == true)
{
  ...
  nextProperty = fbxObject->GetNextProperty(nextProperty);
}

FbxProperty

FbxObject内部に存在する、付加(?)情報を構成するクラスです。テンプレートクラスでできたコンテナのように振舞いますが、厳密にはテンプレートクラスではありません。入れられる型は、文字列、Float、整数、ベクトルなどです。
型とは別に名前を持ってます。この名前は、セマンティクスでもあります。たとえばFbxNodeは、<fbxsdk::FbxDouble3>型で"Lcl Translation"という名前のPropertyを持っていて、これはこのNodeのローカルの位置を示します。
GetPropertyDataType関数で型情報が取得できるので、それで適切なGetテンプレートクラスを呼んで、情報を取得します。

const fbxsdk::FbxDataType dataType = fbxProperty->GetPropertyDataType();
const fbxsdk::EFbxType fbxType = dataType.GetType();
if (fbxType == fbxsdk::EFbxType::eFbxDouble2)
{
  fbxsdk::FbxDouble2 dat = fbxProperty->Get<fbxsdk::FbxDouble2>();
}

※ただし、EFbxTypeがeFbxReferenceのときはGetしてはいけません。これは名前の通り、FbxObjectへのポインタを表すらしいのですが、単に初期化されてないデタラメなポインタが返ってきます。

if (fbxType == fbxsdk::EFbxType::eFbxReference)
{
  // FbxReferenceのGetは失敗する
  // fbxsdk::FbxReference dat = fbxProperty->Get<fbxsdk::FbxReference>();
}

Property同士は接続することができます。SrcProperty(子)とDstProperty(親)を0個以上持っています。GetSrc/DstProperty関数で取得できます。ただ実際のところ、PropertyがSrcPropertyを持つFBXファイルは今のところ見たことがありません。

// Count1以上を見たことがない
const int srcPropertyCount = fbxProperty->GetSrcPropertyCount();
for (int i = 0; i < srcPropertyCount; i++)
{
  fbxsdk::FbxProperty srcProperty = fbxProperty->GetSrcProperty(i);
  if (srcProperty.IsValid() == true)
  {
    ...
  }
}


さらに、PropertyはSrcObjectを0個以上持っています。例えば"Lcl Translation" Propertyは、FbxAnimCurveNodeというObjectを複数持っていることがあります。これは、Translationの時間変化を表すObjectで、アニメーションを構成しているデータ、AnimStackの孫SrcObjectでもあります。

// SrcObjectの取得方法はFbxObjectと同じ
// これはクラス指定なしのパターン
const int srcObjectCount = fbxProperty->GetSrcObjectCount();
for (int i = 0; i < srcObjectCount; i++)
{
  fbxsdk::FbxObject* const srcObject = fbxProperty->GetSrcObject(i);
}





まだまだつづく

FbxAnimCurveNodeの共有(FbxNode視点)

FbxAnimCurveNodeの共有(FbxAnimLayer視点)

FbxLayerの構造

FbxLayerの関数で取得できるFbxLayerElementの違い

注意点

参照をreturnする関数

FBX SDKの一部のGet系の関数は、Managerが管理しているはずの重要なオブジェクトの参照をreturnします。
これをA a = obj->GetA()のように変数で受けると、コピーが発生してManager破棄時に例外が出ます。(SDKのソースコードが、コピー代入演算子の自動生成を抑制しておらず、意図せぬ作法のコピーが存在してしまっているように思います。)
A& a = obj->GetA()のように参照で受けてやる。もしくは、後腐れ無いように、double val = obj->GetA().GetVal();のように一続きで書く必要があります。
具体的には、FbxLayerElementArrayTemplate<>を返す関数がこれに該当します。他にもあるかも。気を付けて!


その他

次作ではUnreal Engineを使おうと思ってます。なのでUE4を触って勉強していたのですが、FBXがよくわからなくて手が止まってしまいました。
ポテトはUnityで作ったのでBlenderのblendファイルを直接インポートできたのですが、UEはFBXファイルを経由する必要があります。(Blenderと連携するサードパーティー製のプラグインがあるのは知ってますが、まずは基本だけでやりたいのです。)Smile Game Builderを使ってるときにも思ったのですが、FBXは中身が目に見えないのが辛いのです。Windows10は標準で3Dビューワーが付いてますが、「よろしく」表示するだけで、中身の詳細はわかりません。そこで、FBX SDKを使って、FBXの構造を把握する、プロファイルするプログラムを作ろうと思ったのです。
やりはじめたところ、思ったよりずっと大変です。FBX SDKわからん。そこで、せっかくなので、勉強して理解したことをnoteに書き留めることにしました。ほんとはこういうのは、Qiita(読み方がわからない)とかに書くのがいいのでしょうけど、IT系の文化になじみが無いので、この場を借りてひっそりと書いていきます。

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