頂点と法線でFBXを描画する
概要
最終更新日:2020/10/8
FBXSDKバージョン:FBX SDK 2020.0.1 VS2017
頂点と法線だけ使用してFBXファイルを描画することについて書いた記事です。
主に次の項目に該当する方に向けて書いています。
- FBXをDirectXで描画したい
- 左手系の対応方法が知りたい
- 頂点情報の取得方法が知りたい
- 法線情報の取得方法が知りたい
- インデックスバッファの取得方法が知りたい
サンプル
サンプルはDirectX9と11のバージョンを用意しています。
環境については以下の内容となっています。
開発環境
VSのバージョン |
VisualStudio 2019 |
省略内容
以下の内容については別記事で説明を完了しているのでこの記事では省略します。
- FBX SDKのインストールと環境設定
- FbxManagerの作成からFbxファイルのインポート
- ルートノードから子ノードを辿る方法
- Attributeについて
記事はこちらで書いてあります。
FBX SDKのインストールと環境設定の方法 => こちら
FbxManagerの作成からFbxファイルのインポートまで => こちら
ルートノードから子ノードを辿る方法とAttributeについて => こちら
ポリゴンを三角形で作り直す
ここからはFbxファイルをインポートした後の処理です。
DirectXではポリゴンの描画は三角形一つで一ポリゴンとして行われます。
そのため四角形などの多角形の場合、三角形にするための対応が必要です。
しかし、Fbxの場合はFBX SDKが変換関数を用意してくれているので、
そちらを使用するだけでポリゴンの対応ができます。
変換はFbxGeometryConverterの「Triangulate」を使用します。
FbxGeometryConverter converter(fbx_manager);
converter.Triangulate(fbx_scene, true);
この関数はポリゴンの作り直しが発生するのでコストの高い処理です。
そのため、グラフィック側でポリゴンの対応が可能なら済ませておいてください。
Meshを探す
この記事では頂点と法線で描画を行うので、必要なノードはMeshのみです。
ノードの取得は様々な方法がありますが、今回はルートから辿る方法を使用します。
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);
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++)
{
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ノード一つにつき、頂点バッファ用のデータを一つ作成します。
頂点バッファの頂点座標と法線は別々に取得します。
頂点座標の取得
頂点座標の取得はインデックスバッファの情報を使用して行います。
取得方法は以下の手順です。
- インデックスバッファから要素を取得する
- 取得した要素を頂点座標リストの要素番号として使う
- 頂点座標リストから座標を取得して保存する
コードで表すと以下のようになります。
FbxVector4* vertices = mesh->GetControlPoints();
int* indices = mesh->GetPolygonVertices();
int polygon_vertex_count = mesh->GetPolygonVertexCount();
for (int i = 0; i < polygon_vertex_count; i++)
{
CustomVertex vertex;
int index = indices[i];
vertex.Position.X = -vertices[index][0];
vertex.Position.Y = vertices[index][1];
vertex.Position.Z = vertices[index][2];
m_Vertices[node_name].push_back(vertex);
}
更に図で表すと以下の通りです。
ポリゴンを構成する頂点の配列番号情報を持っているインデックスバッファから、
順番に頂点番号を取得し、その番号をもとに頂点座標リストの座標を取得しています。
コードで使用しているFbxMeshの「GetControlPoints」と
「GetPolygonVertices」「GetPolygonVertexCount」の内容は以下の通りです。
法線の取得
法線の取得は「GetPolygonVertexNormals」を使用して行います。
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;
buffer_desc.CPUAccessFlags = 0;
buffer_desc.MiscFlags = 0;
buffer_desc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA sub_resource;
sub_resource.pSysMem = &vertex_buffer.second[0];
sub_resource.SysMemPitch = 0;
sub_resource.SysMemSlicePitch = 0;
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;
buffer_desc.CPUAccessFlags = 0;
buffer_desc.MiscFlags = 0;
buffer_desc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA sub_resource;
sub_resource.pSysMem = &index.second[0];
sub_resource.SysMemPitch = 0;
sub_resource.SysMemSlicePitch = 0;
if (FAILED(device->CreateBuffer(
&buffer_desc,
&sub_resource,
&m_IndexBuffers[index.first])))
{
return false;
}
}
どちらもmeshの数だけforでまわしているだけで難しいことはしていません。
描画
描画も作成と同様にMesh単位で行います。
for (auto index : m_Indices)
{
graphics->GetContext()->IASetInputLayout(m_InputLayout);
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は含まれていません。
別途ダウンロードしてください。