マテリアルを反映させたFBXを描画する

概要

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

マテリアルを反映させたFBXファイルを描画する方法について書いた記事です。
主に次の項目に該当する方に向けて書いています。
  • FBXをDirectXで描画したい
  • FBXでマテリアルを反映して描画したい
  • MeshとMaterialの関係を知りたい
  • Materialの情報の取得方法が知りたい

サンプル

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

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


描画のための準備

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

FBX SDKのインストールと環境設定の方法 => こちら
FbxManagerの作成からFbxファイルのインポートまで => こちら
ルートノードから子ノードを辿る方法とAttributeについて => こちら
頂点と法線、頂点インデックスバッファを使った描画 => こちら
頂点カラーを使った描画 => こちら

MeshとMaterialの関係

Meshにはマテリアルの情報が含まれていますが、この情報は色情報ではありませんmodel_render_0094

上の図ように色情報をもつマテリアルはNode内で別に用意されています。
では、Meshのマテリアル情報は何かというと、
Node内の色情報を持つマテリアルとの繋がりを示す情報を持っています。

model_render_0095

そのため、マテリアルを描画に反映させるには以下の処理が必要です。
  • FbxSurfaceMaterialからマテリアル情報の取得
  • Meshとマテリアルのリンク

複数のマテリアルが割り当てられた際の対応

先ほどの図のようにMeshには複数のマテリアルが割り当てられることがあります。
その状態で正しい描画を行うには各ポリゴン単位でマテリアルの設定が必要です。
しかし、数万ポリゴンの一つ一つに対してマテリアルを切り替える処理を行うのは
明らかに非効率です。
そこで考えられたのは、使用するマテリアル毎にポリゴンをグループ分けすることです。

model_render_0096

グループ分けをすることでマテリアル切り替えの回数を大幅に減らすことが出来ます。
実際にObjファイルなどは、この方法で描画をしています。
そして、この方法をサポートしてくれる機能がFBXSDKにあります。
それがFbxGeometryConverterの「SplitMeshesPerMaterial」と
「SplitMeshPerMaterial」です。
このメソッドは一つのメッシュに複数のマテリアルが割り当てられている場合に、
マテリアル単位でメッシュを分割してくれます。
前者がSceneに含まれる全てのMeshが対象となっており、
後者が指定したFbxMeshだけが分割の対象になります。

model_render_0097
model_render_0098
FbxGeometryConverter converter(fbx_manager);
// 全Mesh分割
converter.SplitMeshesPerMaterial(fbx_scene, true);

// Mesh単体分割
converter.SplitMeshPerMaterial(fbx_mesh, true);

これで複数のマテリアルが一つのMeshに割り当てられていたとしても、
一つのMeshにつき、一つのマテリアルとして分割されているので、
グループ分けを意識した処理を実装する必要がなくなります。

model_render_0099

FbxSurfaceMaterialからマテリアル情報の取得

マテリアル情報を取得するためにFbxSurfaceMaterialを使用しますが、
実質的に色情報を取得するのは「FbxSurfaceLambert」と「FbxSurfacePhong」です。

LambertとPhongの関係

FbxSurfaceLambertとFbxSurfacePhongはFbxSurfaceMaterialと
以下のような継承関係にあります。

model_render_0100

以下の表は各クラスで扱える情報の一例です。

model_render_0101

FbxSurfacePhongは最終的な継承先になるので、FbxSurfaceLambertの値も扱えますが、
FbxSurfaceLambertの場合はFbxSurfacePhongのSpecularなどは扱えません。

クラスの調べ方

GetClassId」と「Is」を使用してFbxSurfaceMaterialが
どちらのクラスなのかを調べて情報を取得する方法を紹介します。

model_render_0102
model_render_0103
// FbxSurfaceLambertクラスかどうかを調べる
if (material->GetClassId().Is(FbxSurfaceLambert::ClassId))
{
}

GetClassIdはFbxObjectを継承しているクラスが持っている
クラス識別子の取得メソッドです。
そして、Isは引数で指定したClassIdとインスタンスのClassIdが
「is - a」の関係かどうかを調べます。
「is - a」の関係とは継承の関係を示す際に使用されている表現です。

例えば、ClassIdがFbxSurfaceMaterialのインスタンスであるmaterialは
アップキャストされており、アップキャスト前のClassIdがFbxSurfaceLambertだとすると
「material is a FbxSurfaceLambert」は成立しますが、
「material is a FbxSurfacePhong」は成立しません。

このようにIsを使うとクラスの関係性がわかるのでキャストができるか、
そのクラスの情報を取得することが出来るのかが分かります。

情報取得の方法

マテリアル情報の取得は「FindProperty」でプロパティを探すことから始まります。
プロパティとは情報や属性、性質という意味で使われています。
例えば、Ambientのプロパティを取得するとAmbientの色情報が取得できます。

model_render_0105
// Ambientのプロパティを見つける
prop = material->FindProperty(FbxSurfaceMaterial::sAmbient);

マテリアルで必要とする基本的な要素は色とFactorなので、
プロパティには色かFactorを指定してください。
色はRGB、Factorは重みの情報になっており、0.0~1.0で設定されています。
※1.Factorが強いほどAmbientやDiffuseの色が強く出ます。
※2.アルファ値などRGBなどになっていないもあります。

model_render_0104

FindPropertyで見つけたプロパティが有効かどうかは
「IsValid」で調べることが出来ます。
IsValidで有効と判断されたら、プロパティから情報を取得します。
取得は「Get」を使用しますが、取得するデータの型は情報によって異なります。
例えば、色情報なら「FbxDouble3」Factorなら「FbxDouble」の型を指定します。

model_render_0106
model_render_0107
// 有効チェック
if (prop.IsValid())
{
	colors[i] = prop.Get<FbxDouble3>();
}
else
{
	colors[i] = FbxDouble3(1.0, 1.0, 1.0);
}

カラー情報を保存出来たらマテリアル情報の取得は完了です。

Meshとマテリアルのリンク

マテリアルの情報を取得したら、次はメッシュ側の設定を行います。
こちらはマテリアルの名前などのメッシュが使用しているマテリアルの情報を
保存することで、マテリアルとの繋がりを作ります。

Meshに設定されているマテリアルの情報は「GetElementMaterialCount」と
「GetElementMaterial」で取得します。

// マテリアルが無ければ終わり
if (mesh->GetElementMaterialCount() == 0)
{
	mesh_data.m_MaterialName = "";
	return;
}

// Mesh側のマテリアル情報を取得
FbxLayerElementMaterial* material = mesh->GetElementMaterial(0);

FbxLayerElementMaterialにはカラー情報は含まれていませんが
どのFbxSurfaceMaterialが使用されているかを調べることはできます。
その方法は、GetIndexArrayでインデックスを取得し、そのインデックスを使って
GetSrcObject<FbxSurfaceMaterial>を行います。

// FbxSurfaceMaterialのインデックスバッファからインデックスを取得
int index = material->GetIndexArray().GetAt(0);
// GetSrcObject<FbxSurfaceMaterial>でマテリアル取得
FbxSurfaceMaterial* surface_material = mesh->GetNode()->GetSrcObject<FbxSurfaceMaterial>(index);

これで、該当するFbxSurfaceMaterialが取得できるので、
名前を取得して保存しておきます。

// マテリアル名の保存
if (surface_material != nullptr)
{
	mesh_data.m_MaterialName = surface_material->GetName();
}
else
{
	mesh_data.m_MaterialName = "";
}

これで、メッシュ側の対応は終わりです。
必要な時に保存した名前を使用してマテリアル本体のデータを取得します。

描画 -DirectX11-

マテリアルを描画で使用するには、ConstantBufferを使用します。
カラー情報をConstantBufferで送信するデータに含めて、更新、設定します。

ConstantBuffer* constant = graphics->GetConstantBufferData();
constant->MaterialDiffuse = DirectX::XMFLOAT4(material.Diffuse[0], material.Diffuse[1], material.Diffuse[2], material.Diffuse[3]);
constant->MaterialAmbient = DirectX::XMFLOAT4(material.Ambient[0], material.Ambient[1], material.Ambient[2], material.Ambient[3]);

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

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

シェーダ側

コンスタントバッファで送られてきたマテリアルをピクセルシェーダで受け取り、
各カラー情報を反映させます。

float ambient_factor = MaterialAmbient[3];
float diffuse_factor = MaterialDiffuse[3];

float4 ambient_color = MaterialAmbient * ambient_factor;
float4 diffuse_color = input.color * (LightColor * MaterialDiffuse) * diffuse_factor;

// アンビエントカラー + ディフューズカラー
return ambient_color + diffuse_color;

これで、マテリアルの反映は終了です。