見出し画像

3Dモデル編その④FBXをSDKなしで読み込もう!

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

今回はFBXデータを変換して描画するために、FBXを読み込んで格納していこうと思います。

今回参考にする資料はこちらの

blenderが2013年に公開したドキュメントです。
大分古いですが、載っているのはFBXフォーマットの根幹的な部分の情報なので現在でも全然参考になります。
それとこの


Autodeskが公開しているドキュメントも参考にしましたが、大したことは書いてなかったです。


まず読み込む前の前提として、Autodeskが上のドキュメントで

The FBX file format is not documented. Applications should use the FBX SDK to export and import scene data to and from FBX files (and other file formats supported by the FBX SDK).

としているように本来FBXファイルはFBX SDKを使用して読み込むものであり、「正しい」読み方、解釈の仕方は神とAutodeskのみぞ知るところです。
なのでこれから私がここに書くものは非公式であり、必ずしも正しい解説とは限りません。
それを踏まえた上で、以下に続きます。

◆FBXの実データ、ノードの構造


終了オフセット(unsigned int)
プロパティ数(unsigned int)
プロパティ全て合わせてのバイト数(unsigned int)
ノードの名前のバイト数(unsigned char)
ノード名(ノードの名前のバイト数文の文字列)
プロパティ{
一バイトのプロパティ決定文字
プロパティ決定文字に応じたデータ
}*プロパティ数
子ノード(再帰構造)*0~無限

FBXデータはjsonやXMLファイルのように無限に入れ子を含めることができる再帰構造、ノードで構成されています。
ただし、json、XMLと違って要素の閉じる箇所を示す文字や記述子がなく、自分の終了オフセットに到達するまでにあるノードが全て子ノード、という形で親子関係ができていきます。
一つのノードが持てる子ノードとプロパティの数、プロパティの種類の組み合わせに制限はありません。
また、終了オフセット、プロパティ数、プロパティ全て合わせてのバイト数ノードの名前のバイト数のすべてが0のNULLノードなるものがあり、何の意味も持たないものなので読み込んでも保持せず捨てましょう。


◆プロパティの読み取り方
プロパティ決定文字から種類を取得し、それぞれ以下のように要素を読み込みます。

・単一プロパティ'C','Y','I','L','F','D'
これらはそれぞれ左からbool(1バイト真偽値)、short(2バイト整数)、int(4バイト整数)、long(8バイト整数)、float(単精度浮動小数)、double(倍精度浮動小数)
がひとつだけ含まれているプロパティです。
よって、プロパティ決定文字から判断した後はそれぞれのバイト分読み込めばOKです。

・配列プロパティ'b','i','l','f','d'
これらはそれぞれ左からbool(1バイト真偽値)、int(4バイト整数)、long(8バイト整数)、float(単精度浮動小数)、double(倍精度浮動小数)
が複数個格納された配列が含まれるプロパティです。

内容は

配列のサイズ(UINT)//いくつ要素があるか
エンコードするかしないか(UINT)//0でエンコード無し、1でエンコード有り
圧縮後のバイト数(UINT)
圧縮後のバイト数分のデータ

です。
圧縮にはZlib形式のdeflate(zipやpngに使われている圧縮方法)が使われているので、展開もZlib形式deflateを使います。

・特殊プロパティ'S','R'
左から文字列、生データが含まれるプロパティです。

内容は


読み込みバイト数(int)
読み込みバイト数分のデータ

◆FBXファイル全体の構造

ヘッダー{
20文字のファイルマジック("Kaydara FBX Binary ")
一バイト分のNULL
2バイトの正体不明のマジック(26)
バージョン(int)
}

実データ{
ノード*0~無限
}

フッター

ヘッダー部分の27バイト分すっ飛ばして実データであるノードを読み始めます。
どれだけの数のノードがあるかは取得できない上、ノードは再帰的な構造であるためどこがフッターかはわかりません。
なのでノードの読み込みをひたすら続けてプロパティ判別文字が定義してあるもの以外になった時に読み込みを終了します。

以上がFBXフォーマットの構成です。

これだけ仕様が判明すれば、FBXファイルを「読む」ことができるようになると思います。
そして実際に「読み込ん」で使用するために、FBXフォーマットの各要素をc++で構造体として定義して保持します。
値一種類を保持するプロパティ、プロパティと子ノードを無制限に保持できるノード、全てのノードを保持するシーンを作ります。

まずはプロパティ
親クラスとなるプロパティ

struct FBXProperty :IObject {//IObjectクラスはstd::shared_ptr<>で保持するオブジェクト全てに継承させている自作クラスで、いろいろ機能があるが今回は全く使わない
		void Initialize()override {};//IObjectクラスの純粋仮想関数をオーバーライド
		void PreInitialize()override {};//上と同じ
};

メンバ変数はなく、実質インターフェースです。

実データを入れるプロパティはこんな感じに定義していきます。

/////前略
struct FBXNode_BoolProperty :public FBXProperty {
	bool nodeProperty = 0;
	static FBXPropertyDataType GetType() //後述
	{
		return FBXPropertyDataType::Bool;
	}
};
////中略
struct FBXNode_DoubleArrayProperty :public FBXProperty {
	std::vector< double> nodeProperty;
	std::vector< Vector4> CreateVector4();//double型の配列をVector4の配列にして返す関数
	std::vector< Vector3> CreateVector3();//double型の配列をVector3の配列にして返す関数
	std::vector< Vector2> CreateVector2();//double型の配列をVector2の配列にして返す関数
	static FBXPropertyDataType GetType() //後述
	{
		return FBXPropertyDataType::DoubleArray;
	}
};
////後略

プロパティとして保持する値をメンバ変数として持ち、そしてその値を実際に使用する際のために加工するメンバ関数も適宜定義します。

好きなプロパティを取り出したい時やプロパティ決定文字を保持、switch文で使うときなどのためにプロパティのタイプを列挙型として定義します。

enum class FBXPropertyDataType {
	Bool = 'C',Short = 'Y',  Int = 'I', Long='L',Float = 'F', Double = 'D',
	BoolArray = 'b',IntArray = 'i', LongArray = 'l',FloatArray = 'f', DoubleArray = 'd',  
	String = 'S', RawData='R',
};

各プロパティがこのプロパティタイプを返す静的関数、GetType()を持っているのは後でテンプレート関数を使って検索するのに使うためです。


次にノード

struct FBXNode :public IObject//IObjectクラスはstd::shared_ptr<>で保持するオブジェクト全てに継承させている自作クラスで、いろいろ機能があるが今回は全く使わない
{
		void Initialize()override {}//IObjectクラスの純粋仮想関数をオーバーライド
		void PreInitialize()override{}//上と同じ
		std::string recordName;//ノード名
		UINT endOffset;//終了オフセット
		UINT propertyListLen;//プロパティを保持していたバイトの長さ
		UINT propertyCount;//プロパティ数
		bool isEmpty = false;//プロパティの有無
		bool isParent=false;//子を持つか
		bool NullCheck();//NULLノードかどうかのチェック
		std::weak_ptr< FBXNodeStructure>parent;//親ノードを手繰る際に使うweak_ptr
		std::unordered_multimap<std::string, std::shared_ptr< FBXNodeStructure>> multimap_childNodes;//子ノードをstringで検索できるソートなしmultimap
		std::multimap<FBXPropertyDataType,std::shared_ptr<FBXProperty>> vec_properties;//プロパティをFBXPropertyDataTypeで検索できるソートなしmultimap
		std::vector < std::shared_ptr<FBXNodeStructure>> SerchChildNode(const std::string& arg_nodeName);//arg_nodeNameと一致するノード名の子ノードを全てvector<>に入れて返す
		std::shared_ptr<FBXNodeStructure> GetChildNode(const std::string& arg_nodeName, const UINT& arg_index = 0);//arg_nodeNameと一致するノード名の子ノードの中から、arg_index番目の子ノードを返す
		template <typename T>
		inline std::shared_ptr<T> GetProperty(const UINT& arg_index=0) //Tと一致するプロパティの中から、arg_index番目のプロパティをTとして返す
		{
			FBXPropertyDataType type = T::GetType();
			auto itrs= vec_properties.find(type);
		
			for (int i = 0; i < arg_index; i++) {
				itrs++;
			}
			return (itrs)->second->GetThis<T>();
		}
}; 

親のweak_ptrと子のshared_ptrを保持して親子関係のある木構造を実現します。
どうしてプロパティと子ノードをvectorではなくunordered_multimapで保持しているかというと、ノード、プロパティは検索することは星の数ほどあれど、走査することはまずないからです。
あとはプロパティ検索関数や子ノードを検索する関数、ノード名やエンドオフセット等の基本情報を持っています。

最後はシーンです。

struct FBXScene:public IObject//IObjectクラスはstd::shared_ptr<>で保持するオブジェクト全てに継承させている自作クラスで、いろいろ機能があるが今回は全く使わない
{
	public:
		void Initialize()override {}//IObjectクラスの純粋仮想関数をオーバーライド
		void PreInitialize()override{}//上と同じ
		std::unordered_multimap<std::string, std::shared_ptr< FBXNodeStructure>>& GetVec_NodeRecords() //保持しているノードのGetter
		{
			return multimap_nodeRecords;
		}
		std::vector < std::shared_ptr< FBXNodeStructure>> SerchNode(const std::string& arg_serchNodeName);//arg_nodeNameと一致するノード名の子ノードを全てvector<>に入れて返す
	private:
		std::unordered_multimap<std::string, std::shared_ptr< FBXNodeStructure>> multimap_nodeRecords;//ノードをstringで検索できるソートなしmultimap
};

保持しているのはノードのunordered_multimapとそのGet関数、検索用の関数のみです。
ノード同士が連結している木構造だけでは検索が不便なため、親子関係に関係なく読み込んだ順に全てのノードを格納させ、線形構造を作っています。
シーンと親ノードが子ノードをshared_ptrで持っても大丈夫なのか、という点ですがノードはシーンを一切保持せず、循環参照は生まれないため大丈夫です。

読み込み関数はやや複雑な処理が続くことと上で説明したことと大幅に被ること等から載せませんが、
読み方と格納用の構造体を公開したということで、今回の目標「FBXを読み込んで格納」は達成しました。
次回はいよいよ、読み込んだFBXデータを独自フォーマットに変換したいと思います。

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

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