DirectX9によるXFile描画


概要

DirectX9を使用したXFileの描画に必要な内容の説明をします。
XFileとはMicrosoftが開発した3Dモデルのファイルフォーマットの一つです。
階層モデルやスキンメッシュによるアニメーションにも対応しています。

XFileの2019年の状況

XFileがよく利用されていたのはDirectX9の時代までで、
2019年では3DモデルはFBXやObjファイルが利用されており、
XFileは使われていません。

XFileを取り扱った理由

今回XFileの描画の説明を行う理由はUnityやUE4を使わずに3Dゲーム開発をしている
初心者の方に対して、3Dモデル描画や3Dのノウハウを学ぶための
最初のステップになればと思い書いています。

3Dゲーム開発を行う上でまず最初にオブジェクトを描画して、
移動や、回転、拡縮を行ったり、オブジェクトが見えるように
カメラを動かしたりすることで3Dゲームの理解が深まって、
より高度内容にステップアップしていきます。
それなのにオブジェクトを描画するという段階で手間取ると
移動など処理を実装したとしても結果を確認できないので、
やる気がそがれて足踏みしてしまいます。

そんなもったいないことにならないようにするために
まずはDirectX9を使用してXFileの描画の方法を理解します。
DirectX9はXFileの読み込みや描画のサポートが充実しているため、
FBXやObjファイルを描画するよりも圧倒的に簡単です。
UnityやUE4を使用せずに学ぼうとしている方は
XFileを描画してカメラや行列による挙動制御などの実験をしてください。

もちろん3Dモデルの理解力に関しても、メッシュとマテリアルが分かれていることや
テクスチャは別途読み込む必要があること、描画がマテリアル単位で行われていること等、
モデルデータに対する知識も深まるので時代遅れのフォーマットの描画ではありますが
デメリットしかないわけではありません。

必要知識

XFile描画について知っておいたほうがいいと思われる内容がいくつかありますので
関連するリンクを以下に書いておくのでよければ参考にしてください。

	・ポリゴン3Dモデル(基本)

サンプル

サンプルはここからダウンロードでき、環境については以下の内容となっています。

開発環境
VSのバージョン VisualStudio 2019
DirectXのバージョン DirectX9

描画に必要な処理

XFileの描画を行うために必要な処理は「読み込み」「描画」「解放」です。
これらの処理を以降の項目で順番に説明していきます。

読み込み

XFileの読み込みは、DirectXが読み込むための関数やデータ型を用意してくれているので、
それらを使用することで比較的簡単にファイルを読み込むことができます。
ただ、気を付けて欲しいのはXFileの読み込みのサポートはDirectX9までしかやっていません。
DirectX10以降は自力で読み込み処理を作る必要があります。
この読み込みで行うことは「読み込み実行」と「マテリアル解析」です。

読み込み実行

まずは「.x」拡張子のファイルを「D3DXLoadMeshFromX」関数を使用して
読み込むところから始まります。

directx_0061
// マテリアルを一時的に格納する仮バッファ
LPD3DXBUFFER p_material_buffer = NULL;

// XFileの読み込み start
if (FAILED(D3DXLoadMeshFromXA(
	file_name,
	D3DXMESH_SYSTEMMEM,
	g_D3DDevice,
	nullptr,
	&xfile_data->Materials,		// マテリアルデータ
	nullptr,
	&xfile_data->MaterialNum,	// マテリアルの数
	&xfile_data->Meshes		// メッシュデータ
)))
{
	return false;
}

XFileの読み込みが無事完了すると第五引数に「マテリアルデータ」
第七引数に「マテリアルの数」第八引数に「メッシュデータ」が格納されます。

マテリアル解析

読み込みが無事完了したら次はマテリアル解析を行います。
解析を行う理由はテクスチャの読み込みが必要だからです。
D3DXLoadMeshFromXはメッシュデータやマテリアルを作成してくれますが、
XFileに使用されているテクスチャの読み込みは行われません。
この処理は開発側で行う必要があります。
解析の実装自体は「GetBufferPointer関数」を使用してマテリアル情報を取得、
そのマテリアルにテクスチャが使われていないかを調べるだけです。

directx_0062

わざわざGetBufferPointerを使用する理由はD3DXLoadMeshFromXを使用して取得した
マテリアルデータはマテリアル型ではなくLPD3DXBUFFERのバッファ型なので、
そのままでは使えません。
そこでGetBufferPointerを使用してバッファの先頭アドレスを取得して、
それのポインタをキャストしてマテリアル型として扱います。

// メッシュに使用されているテクスチャ用の配列を用意する
xfile_data->Textures = new LPDIRECT3DTEXTURE9[xfile_data->MaterialNum];

// バッファの先頭ポインタをD3DXMATERIALにキャストして取得
D3DXMATERIAL* materials = (D3DXMATERIAL*)xfile_data->Materials->GetBufferPointer();

// 各メッシュのマテリアル情報を取得する
for (DWORD i = 0; i < xfile_data->MaterialNum; i++)
{
	// マテリアルで設定されているテクスチャ読み込み
	if (materials[i].pTextureFilename != NULL)
	{
		char* file_name = materials[i].pTextureFilename;
		LPDIRECT3DTEXTURE9 texture = NULL;
		D3DXCreateTextureFromFileA(g_D3DDevice,
			file_name,
			&xfile_data->Textures[i]);
	}
	else
	{
		xfile_data->Textures[i] = nullptr;
	}
}

テクスチャの読み込みは特別な関数ではなくD3DXCreateTextureFromFileを使用します。
これでメッシュ、マテリアル、テクスチャのデータがそろったので読み込みは終了です。

描画

DirectX9で用意されているXFileの描画処理はフレームやメッシュ単位ではなく、
マテリアル単位で行いますdirectx_0063

上の絵のようにポリゴンはマテリアル毎でグループ分けされています。
このグループをサブセットと呼んでいて、
サブセットを描画する関数が「DrawSubset関数」です。

directx_0064
// XFile描画
for (DWORD i = 0; i < xfile->MaterialNum; i++)
{
	// メッシュを描画
	xfile->Meshes->DrawSubset(i);
}

これでメッシュの描画は行われますが、このままではマテリアルの反映はされません。
マテリアルの反映はDrawSubSetとは別に「SetMaterial関数」や「SetTexture関数」を
実行する必要があります。

directx_0065
directx_0057
// XFile描画(マテリアル込み)
D3DXMATERIAL* materials = (D3DXMATERIAL*)xfile->Materials->GetBufferPointer();

for (DWORD i = 0; i < xfile->MaterialNum; i++)
{
	// マテリアルの設定
	g_D3DDevice->SetMaterial(&materials[i].MatD3D);
	// テクスチャの設定
	g_D3DDevice->SetTexture(0, xfile->Textures[i]);
	// メッシュを描画
	xfile->Meshes->DrawSubset(i);
}

これでXFileの描画処理は完成です。
directx_0066

解放

ゲーム上で不要になったXFileのデータは解放を行わないと
メモリリークが発生してしまうのできちんと解放を行ってください。

解放の順番

解放は以下の順番で行います。

番号 内容
テクスチャの解放
マテリアルデータ
メッシュデータ

①.テクスチャの解放

まず最初に解放するのはテクスチャです。
XFileを解放するということは、その時に一緒に読み込んだテクスチャも
解放する必要があります。
解放はIDirect3DTexture9の「Release関数」を使用します。
また、テクスチャ情報を保存するために配列などを動的に確保しているなら
そちらも合わせて解放します。
directx_0042
// テクスチャの解放
if (g_XFileList[i].Textures != nullptr)
{
	for (int j = 0; j < g_XFileList[i].MaterialNum; j++)
	{
		// テクスチャの解放
		if (g_XFileList[i].Textures[j] != nullptr)
		{
			g_XFileList[i].Textures[j]->Release();
			g_XFileList[i].Textures[j] = nullptr;
		}
	}
	delete[](g_XFileList[i].Textures);
	g_XFileList[i].Textures = nullptr;
}

これでテクスチャの解放が完了したの②に進みます。

②.マテリアルデータの解放

次はマテリアルデータを解放します。
マテリアルデータの解放はID3DXBufferの「Release関数」を使用します。
※関数の内容自体はテクスチャのReleaseと同じなので省略します。
// マテリアルの解放
if (g_XFileList[i].Materials != nullptr)
{
	g_XFileList[i].Materials->Release();
	g_XFileList[i].Materials = nullptr;
}

マテリアル解放も完了したので③に進みます。

③.メッシュデータの解放

最後の解放はメッシュデータです。
こちらもID3DXMeshの「Release関数」を使用して解放します。
※関数の内容自体はテクスチャやマテリアルのReleaseと同じなので省略します。
// メッシュの解放
if (g_XFileList[i].Meshes != nullptr)
{
	g_XFileList[i].Meshes->Release();
	g_XFileList[i].Meshes = nullptr;
}

これでXFileの解放処理が全て完了しました。
解放処理を忘れるとメモリリークが発生しますので、
実装漏れがないように気を付けてください。