頂点と法線でFBXを描画する

概要

最終更新日:2020/03/18
FBXSDKバージョン:FBX SDK 2020.0.1 VS2017

頂点と法線だけ使用してFBXファイルを描画することについて書いた記事です。
主に次の項目に該当する方に向けて書いています。
  • FBXをDirectXで描画したい
  • 左手系の対応方法が知りたい
  • 頂点情報の取得方法が知りたい
  • 法線情報の取得方法が知りたい
  • インデックスバッファの取得方法が知りたい

サンプル

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

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


省略内容

以下の内容については別記事で説明を完了しているのでこの記事では省略します。
  • FBX SDKのインストールと環境設定
  • FbxManagerの作成からFbxファイルのインポート
  • ルートノードから子ノードを辿る方法
  • Attributeについて
記事はこちらで書いてあります。

FBX SDKのインストールと環境設定の方法 => こちら

FbxManagerの作成からFbxファイルのインポートまで => こちら

ルートノードから子ノードを辿る方法とAttributeについて => こちら

ポリゴンを三角形で作り直す

ここからはFbxファイルをインポートした後の処理です。
DirectXではポリゴンの描画は三角形一つで一ポリゴンとして行われます。
そのため四角形などの多角形の場合、三角形にするための対応が必要です。
しかし、Fbxの場合はFBX SDKが変換関数を用意してくれているので、
そちらを使用するだけでポリゴンの対応ができます。
変換はFbxGeometryConverterの「Triangulate」を使用します。

model_render_0071
FbxGeometryConverter converter(fbx_manager);
// ポリゴンを三角形にする
converter.Triangulate(fbx_scene, true);

この関数はポリゴンの作り直しが発生するのでコストの高い処理です。
そのため、グラフィック側でポリゴンの対応が可能なら済ませておいてください。

Meshを探す

この記事では頂点と法線で描画を行うので、必要なノードはMeshのみです。
ノードの取得は様々な方法がありますが、今回はルートから辿る方法を使用します。

// メッシュNodeを探す
CollectMeshNode(fbx_scene->GetRootNode(), mesh_node_list);

void ObjFile::CollectMeshNode(FbxNode* node, std::map<std::string, FbxNode*>& list)
{
	for (int i = 0; i < node->GetNodeAttributeCount(); i++)
	{
		FbxNodeAttribute* attribute = node->GetNodeAttributeByIndex(i);

		// Attributeがメッシュなら追加
		if (attribute->GetAttributeType() == FbxNodeAttribute::EType::eMesh)
		{
			list[node->GetName()] = node;
			break;
		}
	}

	for (int i = 0; i < node->GetChildCount(); i++)
	{
		CollectMeshNode(node->GetChild(i), list);
	}
}

上のコードではCollectMeshNodeにルートノードを渡して再帰で
Meshの検索と保存を繰り返します。
追加方法はどのノードに所属しているメッシュかを分かりやすくするために
mapのキーにノードの名前を取得して保存しています。

インデックスバッファを作り直す

インデックスバッファにはポリゴンを構成する頂点バッファの番号が保存されています。
この情報は頂点バッファを作成するときに使用しますが、描画の際は新しく作り直した
インデックスバッファを使用します。
作り直す内容は単純で「ポリゴンの数 * 一つのポリゴンの頂点数」分の連番を作ります。

// ポリゴンの数だけ連番として保存する
for (int i = 0; i < mesh->GetPolygonCount(); i++)
{
	// 右手系の描画ならこれで問題ない
	m_Indices[node_name].push_back(i * 3);
	m_Indices[node_name].push_back(i * 3 + 1);
	m_Indices[node_name].push_back(i * 3 + 2);
}

ただ、FBXは右手系で作られているのでポリゴンの作成が左周りになっています。
DirectXは右周りなので、少し順番を変更します。

// ポリゴンの数だけ連番として保存する
for (int i = 0; i < mesh->GetPolygonCount(); i++)
{
	// 2 => 1 => 0にしてるのは左手系対策
	m_Indices[node_name].push_back(i * 3 + 2);
	m_Indices[node_name].push_back(i * 3 + 1);
	m_Indices[node_name].push_back(i * 3);
}

各ポリゴンの最初と最後を入れ替えるだけですが、これで右周りの順番になります。

頂点バッファ用のデータを用意する

集めたMeshノード一つにつき、頂点バッファ用のデータを一つ作成します。
頂点バッファの頂点座標と法線は別々に取得します。

頂点座標の取得

頂点座標の取得はインデックスバッファの情報を使用して行います。
取得方法は以下の手順です。
  1. インデックスバッファから要素を取得する
  2. 取得した要素を頂点座標リストの要素番号として使う
  3. 頂点座標リストから座標を取得して保存する
コードで表すと以下のようになります。

// 頂点バッファの取得
FbxVector4* vertices = mesh->GetControlPoints();
// インデックスバッファの取得
int* indices = mesh->GetPolygonVertices();
// 頂点座標の数の取得
int polygon_vertex_count = mesh->GetPolygonVertexCount();

// GetPolygonVertexCount => 頂点数
for (int i = 0; i < polygon_vertex_count; i++)
{
	CustomVertex vertex;
	// インデックスバッファから頂点番号を取得
	int index = indices[i];

	// 頂点座標リストから座標を取得する
	vertex.Posision.X = -vertices[index][0];
	vertex.Posision.Y = vertices[index][1];
	vertex.Posision.Z = vertices[index][2];

	// 追加
	m_Vertices[node_name].push_back(vertex);
}

更に図で表すと以下の通りです。

model_render_0072

ポリゴンを構成する頂点の配列番号情報を持っているインデックスバッファから、
順番に頂点番号を取得し、その番号をもとに頂点座標リストの座標を取得しています。

コードで使用しているFbxMeshの「GetControlPoints」と
「GetPolygonVertices」「GetPolygonVertexCount」の内容は以下の通りです。

model_render_0074
model_render_0075
model_render_0076

法線の取得

法線の取得は「GetPolygonVertexNormals」を使用して行います。

model_render_0077
FbxArray normals;
// 法線リストの取得
mesh->GetPolygonVertexNormals(normals);

// 法線設定
for (int i = 0; i < normals.Size(); i++)
{
	m_Vertices[node_name][i].Normal.X = -normals[i][0];
	m_Vertices[node_name][i].Normal.Y = normals[i][1];
	m_Vertices[node_name][i].Normal.Z = normals[i][2];
}

法線は頂点とは違い、インデックスバッファの順番でリストが作られているので
変換を行わずにそのまま保存できます。

X軸を反転させている理由

頂点座標と法線を保存する際にX軸を反転させていますが、
これはFBXの右手系座標軸を、DirectXの左手系に変換するために行っています。

頂点とインデックスバッファの作成

頂点とインデックスバッファはメッシュ単位で作成します。
まずは頂点バッファの作成コードです。

for (auto vertex_buffer : m_Vertices)
{
	//頂点バッファ作成
	D3D11_BUFFER_DESC buffer_desc;
	buffer_desc.ByteWidth = sizeof(CustomVertex) * vertex_buffer.second.size();	// バッファのサイズ
	buffer_desc.Usage = D3D11_USAGE_DEFAULT;					// 使用方法
	buffer_desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;				// BIND設定
	buffer_desc.CPUAccessFlags = 0;							// リソースへのCPUのアクセス権限についての設定
	buffer_desc.MiscFlags = 0;							// リソースオプションのフラグ
	buffer_desc.StructureByteStride = 0;						// 構造体のサイズ

	D3D11_SUBRESOURCE_DATA sub_resource;
	sub_resource.pSysMem = &vertex_buffer.second[0];	// バッファの中身の設定
	sub_resource.SysMemPitch = 0;				// textureデータを使用する際に使用するメンバ
	sub_resource.SysMemSlicePitch = 0;			// textureデータを使用する際に使用するメンバ

	// バッファ作成
	if (FAILED(device->CreateBuffer(
		&buffer_desc,					// バッファ情報
		&sub_resource,					// リソース情報
		&m_VertexBuffers[vertex_buffer.first])))	// 作成されたバッファの格納先
	{
		return false;
	}
}

次はインデックスバッファの作成コードです。

for (auto index : m_Indices)
{
	//頂点バッファ作成
	D3D11_BUFFER_DESC buffer_desc;
	buffer_desc.ByteWidth = (UINT)sizeof(UINT) * index.second.size();	// バッファのサイズ
	buffer_desc.Usage = D3D11_USAGE_DEFAULT;				// 使用方法
	buffer_desc.BindFlags = D3D11_BIND_INDEX_BUFFER;			// BIND設定
	buffer_desc.CPUAccessFlags = 0;						// リソースへのCPUのアクセス権限についての設定
	buffer_desc.MiscFlags = 0;						// リソースオプションのフラグ
	buffer_desc.StructureByteStride = 0;					// 構造体のサイズ

	D3D11_SUBRESOURCE_DATA sub_resource;
	sub_resource.pSysMem = &index.second[0];				// バッファの中身の設定
	sub_resource.SysMemPitch = 0;						// textureデータを使用する際に使用するメンバ
	sub_resource.SysMemSlicePitch = 0;					// textureデータを使用する際に使用するメンバ

	// バッファ作成
	if (FAILED(device->CreateBuffer(
		&buffer_desc,				// バッファ情報
		&sub_resource,				// リソース情報
		&m_IndexBuffers[index.first])))		// 作成されたバッファの格納先
	{
		return false;
	}
}

どちらもmeshの数だけforでまわしているだけで難しいことはしていません。

描画

描画も作成と同様にMesh単位で行います。

for (auto index : m_Indices)
{
	// インデックスバッファの数 = マテリアルの数だけメッシュを描画する
	// IA(InputAssemblerStage)に入力レイアウトを設定する
	graphics->GetContext()->IASetInputLayout(m_InputLayout);
	// IAに設定する頂点バッファの指定
	graphics->GetContext()->IASetVertexBuffers(
		0,						// バッファ送信のスロット番号
		1,						// バッファの数
		&m_VertexBuffers[index.first],		// 頂点バッファ
		&strides,				// バッファに使用している構造体のサイズ
		&offsets);				// 開始オフセット

	graphics->GetContext()->IASetIndexBuffer(
		m_IndexBuffers[index.first],
		DXGI_FORMAT_R32_UINT,
		0);

	// ワールドマトリクス設定
	DirectX::XMMATRIX world_matrix;
	DirectX::XMMATRIX translate = DirectX::XMMatrixTranslation(pos.X, pos.Y, pos.Z);
	DirectX::XMMATRIX rotate_x = DirectX::XMMatrixRotationX(DirectX::XMConvertToRadians(degree.X));
	DirectX::XMMATRIX rotate_y = DirectX::XMMatrixRotationY(DirectX::XMConvertToRadians(degree.Y));
	DirectX::XMMATRIX rotate_z = DirectX::XMMatrixRotationZ(DirectX::XMConvertToRadians(degree.Z));
	DirectX::XMMATRIX scale_mat = DirectX::XMMatrixScaling(scale.X, scale.Y, scale.Z);
	world_matrix = scale_mat * rotate_x * rotate_y * rotate_z * translate;

	// ワールドマトリクスをコンスタントバッファに設定
	XMStoreFloat4x4(&graphics->GetConstantBufferData()->World, XMMatrixTranspose(world_matrix));

	// コンスタントバッファ更新
	graphics->GetContext()->UpdateSubresource(graphics->GetConstantBuffer(), 0, NULL, graphics->GetConstantBufferData(), 0, 0);

	ID3D11Buffer* constant_buffer = graphics->GetConstantBuffer();
	// コンテキストにコンスタントバッファを設定
	graphics->GetContext()->VSSetConstantBuffers(0, 1, &constant_buffer);
	graphics->GetContext()->PSSetConstantBuffers(0, 1, &constant_buffer);

	// 描画
	graphics->GetContext()->DrawIndexed(
		index.second.size(),		// 頂点数
		0,				// オフセット
		0);				// 開始頂点のインデックス

}

こちらもバッファ作成と同様にmeshの数だけforでまわしているだけです。


※サンプルにunitychanは含まれていません。
 別途ダウンロードしてください。