ホーム
ゲーム開発
Unity
UnrealEngine
C++
ゲーム数学
ゲームAI
サウンド
アニメーション
GBDK
制作日記
3DCG
Houdini
Blender
USD
グラフィックス
テクノロジ
生成AI関連
動画制作
ツール開発
フロントエンド関連
サーバサイド関連
ソフトウェア設計
ハードウェア関連
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
その他
ブログについて
都会のエレキベア
ラーメン日記
四コマ漫画
おすすめアイテム
おもしろコラム
お問い合わせ
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • ゲーム数学
    • ゲームAI
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • 3DCG
    • Houdini
    • Blender
    • USD
    • グラフィックス
  • テクノロジ
    • 生成AI関連
    • 動画制作
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • ソフトウェア設計
    • ハードウェア関連
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • その他
JP|EN
  1. ホーム
  2. 20221204_01

【DirextX12】第四回 DirextX12を使ったゲーム開発 〜FBX SDKを使用した3Dモデル描画〜

C++グラフィックスDirectX12
2022-12-07

マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜〜
マイケル
マイケル
今回は引き続きDirectXを触っていきます!
前回はコードで定義した3Dモデルを描画したので、今回は実際にFBXファイルを読み込んで描画するところまでやってみます!

↑前回までの記事

エレキベア
エレキベア
ついにモデル描画まで出来るクマね
長かったクマ〜〜〜
マイケル
マイケル
毎度のごとく、作ったサンプルはGitHubにも上げています!
こちらも一緒にご参照ください!

GitHub/masarito617 – cpp-dx12-sample-3d-fbx
↑今回作ったサンプル

エレキベア
エレキベア
サンプルも溜まってきたクマね
マイケル
マイケル
また、前回作ったところから続きとして進めていきますが、
DirectX12の基本的な使い方が知りたい方は前回までの記事や公式サイト、下記書籍等を一読してみることをおすすめします!

Direct3D 12 プログラミング ガイド – Win32 apps | Microsoft Learn

DirectX 12の魔導書 3Dレンダリングの基礎からMMDモデルを踊らせるまで

エレキベア
エレキベア
日本語書籍はありがたいクマ〜〜〜

FBX SDKの導入

マイケル
マイケル
それでは早速進めていきますが、今回はFBXファイルを読み込むにあたり、FBX SDKというAutoDesk公式のライブラリを使用します!

FBX SDK デベロッパー センター

エレキベア
エレキベア
FBX SDKは前も一度使用したことがあったクマね

↑以前FBXSDKを使用した際の記事

マイケル
マイケル
いや〜〜懐かしいね
基本的に同じように使っていくから、復習のつもりでやってみよう!
マイケル
マイケル
ライブラリのダウンロード、インストールは公式サイトから行います。
インストールしたら下記のように環境変数を定義しておきましょう!
今回は「FBXSDK_DIR」という名称で定義しました。
↑FBXSDKのインストールディレクトリを環境変数として登録
マイケル
マイケル
そして追加のインストールディレクトリ追加のライブラリディレクトリにもそれぞれパスを追加します。
DirectXTexを定義済だと思うので、その後ろに追加しましょう。
エレキベア
エレキベア
この辺りはもう定番クマね
マイケル
マイケル
あとはC++のクラスに下記のようにインクルードを追加、ビルドして
エラーが出ないことが確認できれば導入は完了です!
#include <fbxsdk.h>
#pragma comment(lib, "libfbxsdk-md.lib")
#pragma comment(lib, "libxml2-md.lib")
#pragma comment(lib, "zlib-md.lib")
↑インクルードを追加してみる

FBXモデルのロード処理

FbxLoaderクラスを追加

マイケル
マイケル
FBX SDKの導入が完了したら、実際にFBXファイルのロード処理を記述していきます。
今回は FbxLoader.h、FbxLoader.cppとして新たに作成しました。
#pragma once
#include <fbxsdk.h>
#include <vector>
#include <array>
#include <string>
#pragma comment(lib, "libfbxsdk-md.lib")
#pragma comment(lib, "libxml2-md.lib")
#pragma comment(lib, "zlib-md.lib")
class FbxLoader
{
public:
	FbxLoader();
	struct Vertex
	{
		float pos[3];
		float normal[3];
		float uv[2];
	};
	struct VertexInfo
	{
		std::vector<Vertex> vertices;
		std::vector<unsigned short> indices;
	};
	static bool Load(const std::string& filePath, VertexInfo* vertexInfo);
private:
	static bool IsExistNormalUVInfo(const std::vector<float>& vertexInfo);
	static std::vector<float> CreateVertexInfo(const std::vector<float>& vertex, const FbxVector4& normalVec4, const FbxVector2& uvVec2);
	static int CreateNewVertexIndex(const std::vector<float>& vertexInfo, const FbxVector4& normalVec4, const FbxVector2& uvVec2,
		std::vector<std::vector<float>>& vertexInfoList, int oldIndex, std::vector<std::array<int, 2>>& oldNewIndexPairList);
	static bool IsSetNormalUV(const std::vector<float> vertexInfo, const FbxVector4& normalVec4, const FbxVector2& uvVec2);
};
↑FbxLoader.h
#include "FbxLoader.h"
FbxLoader::FbxLoader()
{
}
bool FbxLoader::Load(const std::string& filePath, VertexInfo* vertexInfo)
{
	// マネージャー初期化
	auto manager = FbxManager::Create();
	// インポーター初期化
	auto importer = FbxImporter::Create(manager, "");
	if (!importer->Initialize(filePath.c_str(), -1, manager->GetIOSettings()))
	{
		return false;
	}
	// シーン作成
	auto scene = FbxScene::Create(manager, "");
	importer->Import(scene);
	importer->Destroy();
	// 三角ポリゴンへのコンバート
	FbxGeometryConverter geometryConverter(manager);
	if (!geometryConverter.Triangulate(scene, true))
	{
		return false;
	}
	// メッシュ取得
	auto mesh = scene->GetSrcObject<FbxMesh>();
	if (!mesh)
	{
		return false;
	}
	// UVセット名の取得
	// * 現在の実装だとは1つのUVセット名にしか対応していない...
	FbxStringList uvSetNameList;
	mesh->GetUVSetNames(uvSetNameList);
	const char* uvSetName = uvSetNameList.GetStringAt(0);
	// 頂点座標情報のリストを生成
	std::vector<std::vector<float>> vertexInfoList;
	for (int i = 0; i < mesh->GetControlPointsCount(); i++)
	{
		// 頂点座標を読み込んで設定
		auto point = mesh->GetControlPointAt(i);
		std::vector<float> vertex;
		vertex.push_back(point[0]);
		vertex.push_back(point[1]);
		vertex.push_back(point[2]);
		vertexInfoList.push_back(vertex);
	}
	// 頂点毎の情報を取得する
	std::vector<unsigned short> indices;
	std::vector<std::array<int, 2>> oldNewIndexPairList;
	for (int polIndex = 0; polIndex < mesh->GetPolygonCount(); polIndex++) // ポリゴン毎のループ
	{
		for (int polVertexIndex = 0; polVertexIndex < mesh->GetPolygonSize(polIndex); polVertexIndex++) // 頂点毎のループ
		{
			// インデックス座標
			auto vertexIndex = mesh->GetPolygonVertex(polIndex, polVertexIndex);
			// 頂点座標
			std::vector<float> vertexInfo = vertexInfoList[vertexIndex];
			// 法線座標
			FbxVector4 normalVec4;
			mesh->GetPolygonVertexNormal(polIndex, polVertexIndex, normalVec4);
			// UV座標
			FbxVector2 uvVec2;
			bool isUnMapped;
			mesh->GetPolygonVertexUV(polIndex, polVertexIndex, uvSetName, uvVec2, isUnMapped);
			// インデックス座標のチェックと再採番
			if (!IsExistNormalUVInfo(vertexInfo))
			{
				// 法線座標とUV座標が未設定の場合、頂点情報に付与して再設定
				vertexInfoList[vertexIndex] = CreateVertexInfo(vertexInfo, normalVec4, uvVec2);
			}
			else if (!IsSetNormalUV(vertexInfo, normalVec4, uvVec2))
			{
				// *同一頂点インデックスの中で法線座標かUV座標が異なる場合、
				// 新たな頂点インデックスとして作成する
				vertexIndex = CreateNewVertexIndex(vertexInfo, normalVec4, uvVec2, vertexInfoList, vertexIndex, oldNewIndexPairList);
			}
			// インデックス座標を設定
			indices.push_back(vertexIndex);
		}
	}
	// 頂点情報を生成
	std::vector<Vertex> vertices;
	for (int i = 0; i < vertexInfoList.size(); i++)
	{
		std::vector<float> vertexInfo = vertexInfoList[i];
		vertices.push_back(Vertex{
			{
				vertexInfo[0], vertexInfo[1], vertexInfo[2]
			},
			{
				vertexInfo[3], vertexInfo[4], vertexInfo[5]
			},
			{
				vertexInfo[6], 1.0f - vertexInfo[7] // Blenderで作成した場合、V値は反転させる
			}
		});
	}
	
	// マネージャー、シーンの破棄
	scene->Destroy();
	manager->Destroy();
	// 返却値に設定
	*vertexInfo = {
		vertices,
		indices
	};
	return true;
}
// 法線、UV情報が存在しているか?
bool FbxLoader::IsExistNormalUVInfo(const std::vector<float>& vertexInfo)
{
	return vertexInfo.size() == 8; // 頂点3 + 法線3 + UV2
}
// 頂点情報を生成
std::vector<float> FbxLoader::CreateVertexInfo(const std::vector<float>& vertexInfo, const FbxVector4& normalVec4, const FbxVector2& uvVec2)
{
	std::vector<float> newVertexInfo;
	// 位置座標
	newVertexInfo.push_back(vertexInfo[0]);
	newVertexInfo.push_back(vertexInfo[1]);
	newVertexInfo.push_back(vertexInfo[2]);
	// 法線座標
	newVertexInfo.push_back(normalVec4[0]);
	newVertexInfo.push_back(normalVec4[1]);
	newVertexInfo.push_back(normalVec4[2]);
	// UV座標
	newVertexInfo.push_back(uvVec2[0]);
	newVertexInfo.push_back(uvVec2[1]);
	return newVertexInfo;
}
↑FbxLoader.cpp
マイケル
マイケル
処理の内容は単純で、マネージャー、インポーターといったロードに必要なオブジェクトを作成した後にファイルパスから頂点情報を読み込んでいます。
今回は頂点座標、インデックス座標、UV座標に加えて法線座標も一緒に読み込むようにしました。
エレキベア
エレキベア
UVセットが一つしか読めなかったり、簡易的な実装になっているのには注意クマね
マイケル
マイケル
それから、UV座標のV値を1から引くことで反転させていることにも注意が必要です。
これは自分がBlenderでモデルを作成したこと影響もあり、(0,0)の原点が左下になっていたためです。
エレキベア
エレキベア
使用するソフトによってはちょっとした違いもあるのクマね
マイケル
マイケル
また下記処理の部分については、UV座標、法線座標が異なる頂点についてもインデックス座標が同一のものが存在していたために、インデックス座標を振り直す処理を行なっています。
こちらは前回の記事で死闘を繰り広げた内容を書いていますので、詳細はそちらをご参照ください!

【C++】第四回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 fbx読込とシェーダ編〜
法線座標、UV座標の読み込み

// 新たな頂点インデックスを生成する
int FbxLoader::CreateNewVertexIndex(const std::vector<float>& vertexInfo, const FbxVector4& normalVec4, const FbxVector2& uvVec2,
	std::vector<std::vector<float>>& vertexInfoList, int oldIndex, std::vector<std::array<int, 2>>& oldNewIndexPairList)
{
	// 作成済の場合、該当のインデックスを返す
	for (int i = 0; i < oldNewIndexPairList.size(); i++)
	{
		int newIndex = oldNewIndexPairList[i][1];
		if (oldIndex == oldNewIndexPairList[i][0]
			&& IsSetNormalUV(vertexInfoList[newIndex], normalVec4, uvVec2))
		{
			return newIndex;
		}
	}
	// 作成済でない場合、新たな頂点インデックスとして作成
	std::vector<float> newVertexInfo = CreateVertexInfo(vertexInfo, normalVec4, uvVec2);
	vertexInfoList.push_back(newVertexInfo);
	// 作成したインデックス情報を設定
	int newIndex = vertexInfoList.size() - 1;
	std::array<int, 2> oldNewIndexPair{ oldIndex , newIndex };
	oldNewIndexPairList.push_back(oldNewIndexPair);
	return newIndex;
}
// vertexInfoに法線、UV座標が設定済かどうか?
bool FbxLoader::IsSetNormalUV(const std::vector<float> vertexInfo, const FbxVector4& normalVec4, const FbxVector2& uvVec2)
{
	// 法線、UV座標が同値なら設定済とみなす
	return fabs(vertexInfo[3] - normalVec4[0]) < FLT_EPSILON
		&& fabs(vertexInfo[4] - normalVec4[1]) < FLT_EPSILON
		&& fabs(vertexInfo[5] - normalVec4[2]) < FLT_EPSILON
		&& fabs(vertexInfo[6] - uvVec2[0]) < FLT_EPSILON
		&& fabs(vertexInfo[7] - uvVec2[1]) < FLT_EPSILON;
}
↑法線、UV座標が同一の頂点についてインデックス座標を振り直している
エレキベア
エレキベア
これはマジでどうにかしたいクマ・・

FBXファイルのロード処理

マイケル
マイケル
ロード処理の実装が完了したため、頂点座標を直接定義していた箇所を置き換えてみましょう。
今回はBlenderで作成したサイコロを読み込んでみます。
	FbxLoader::VertexInfo fbxVertexInfo_; // fbxモデルから読み込んだ頂点情報
	// fbxモデルのロード
	{
		if (!FbxLoader::Load("Assets/saikoro.fbx", &fbxVertexInfo_))
		{
			ThrowMessage("failed load fbx file.");
		}
	}
	// 頂点バッファビューの生成
	{
		// 頂点座標
		std::vector<FbxLoader::Vertex> vertices = fbxVertexInfo_.vertices;
		const UINT vertexBufferSize = sizeof(FbxLoader::Vertex) * vertices.size();
・・・略・・・
	}
	// インデックスバッファビューの生成
	{
		// インデックス座標
		std::vector<unsigned short> indices = fbxVertexInfo_.indices;
		const UINT indexBufferSize = sizeof(unsigned short) * indices.size();
・・・略・・・
	}
↑頂点情報をfbxファイルから読み込む
エレキベア
エレキベア
綺麗に置き換えられたクマね
マイケル
マイケル
あとは法線座標も読み込むようにしたため、頂点レイアウトも下記のように修正しておきましょう。
・・・
		ComPtr<ID3DBlob> vsBlob;
		ComPtr<ID3DBlob> psBlob;
		D3DCompileFromFile(L"BasicShaders.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "VSMain", "vs_5_0", compileFlags, 0, &vsBlob, nullptr);
		D3DCompileFromFile(L"BasicShaders.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "PSMain", "ps_5_0", compileFlags, 0, &psBlob, nullptr);
		// 頂点レイアウトの生成
		D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
		{
			{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
			{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
			{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
		};
・・・
↑入力レイアウトに法線情報を追加
マイケル
マイケル
これを機に3つのファイルに分かれていたシェーダーファイルも一つのファイルに統一しました。
こちらの方が見やすいですね!

cbuffer cbuff0 : register(b0) {
	matrix world;
	matrix viewproj;
}
Texture2D g_texture : register(t0);
SamplerState g_sampler : register(s0);
struct PSInput
{
	float4 position : SV_POSITION;
	float2 uv : TEXCOORD;
};
PSInput VSMain(float4 pos : POSITION, float3 normal : NORMAL, float2 uv : TEXCOORD)
{
	PSInput result;
	result.position = mul(mul(viewproj, world), pos);
	result.uv = uv;
	return result;
}
float4 PSMain(PSInput input) : SV_TARGET
{
	return g_texture.Sample(g_sampler, input.uv);
}
↑シェーダーを一つにまとめた
マイケル
マイケル
この状態で実行すれば、下記のようにモデルが描画されるはずです!
エレキベア
エレキベア
サイコロクマ〜〜〜〜〜
マイケル
マイケル
ついでに他のモデルでも試してみます!
下記はゴロヤンのモデルです。
エレキベア
エレキベア
なぜこれを選んだクマ・・・
ゴロヤン
ゴロヤン
ゴロ〜〜〜〜〜〜

Lumbert拡散反射での描画

マイケル
マイケル
せっかく法線座標も読み込めるようにしたので、Lumbert拡散反射の処理も追加して簡易的な影も付けてみます。
エレキベア
エレキベア
ランバート反射は確か、法線と光源の向きから拡散反射量を求める内容だったクマね
マイケル
マイケル
全ての面に同じ角度で一つの光源が当たっていると考える、擬似的な反射シミュレートだね
詳しくは下記記事で紹介しているので、そちらをご参照ください!

【C++】第四回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 fbx読込とシェーダ編〜
ランバート反射モデル

エレキベア
エレキベア
懐かしいクマ〜〜〜
マイケル
マイケル
ランバート反射のシェーダーは下記のようになります。
内容としてはシンプルなものですね。
cbuffer cbuff0 : register(b0) {
	matrix world;
	matrix viewproj;
}
cbuffer cbuff1 : register(b1) {
	float3 ambientLight;
	float3 lightColor;
	float3 lightDirection;
}
Texture2D g_texture : register(t0);
SamplerState g_sampler : register(s0);
struct PSInput
{
	float4 position : SV_POSITION;
	float3 normal : NORMAL;
	float2 uv : TEXCOORD;
};
PSInput VSMain(float4 pos : POSITION, float3 normal : NORMAL, float2 uv : TEXCOORD)
{
	PSInput result;
	result.position = mul(mul(viewproj, world), pos);
	result.normal = mul(world, normal);
	result.uv = uv;
	return result;
}
float4 PSMain(PSInput input) : SV_TARGET
{
	// ベースとなる環境色
	float3 lambert = ambientLight;
	// 拡散反射色を加える
	float NdotL = dot(normalize(input.normal), -normalize(lightDirection));
	if (NdotL > 0.0f)
	{
		float3 diffuse = lightColor * NdotL;
		lambert += diffuse;
	}
	return g_texture.Sample(g_sampler, input.uv) * float4(lambert, 1.0f);
}
↑Lambert拡散反射シェーダー
マイケル
マイケル
この反射量を求めるために
・環境光
・光のカラー
・光の向き

の情報が必要になるため、こちらも追加でシェーダーに渡すようにしていきます!
また、法線座標もワールド座標で変換する必要があるため、ワールド座標行列とビュー射影行列はそれぞれ独立して渡すようにします。
エレキベア
エレキベア
定数バッファを追加で渡す必要がありそうクマね
マイケル
マイケル
ひとまずは下記のようにMatricesData、LightingDataとして定義してこれらを渡すようにしていこうと思います。
	// 3D座標変換用行列
	DirectX::XMMATRIX worldMatrix_;
	DirectX::XMMATRIX viewMatrix_;
	DirectX::XMMATRIX projMatrix_;
	struct MatricesData
	{
		DirectX::XMMATRIX world;
		DirectX::XMMATRIX viewproj;
	};
	MatricesData* mapMatricesData_;
	// ライティング用
	DirectX::XMVECTOR ambientLight_ = { 0.35f, 0.35f, 0.35f };
	DirectX::XMVECTOR lightColor_ = { 0.8f, 0.8f, 1.0f };
	DirectX::XMVECTOR lightDirection_ = { 0.3f, 0.3f, 0.8f };
	struct LightingData
	{
		DirectX::XMVECTOR ambientLight;
		DirectX::XMVECTOR lightColor;
		DirectX::XMVECTOR lightDirection;
	};
	LightingData* mapLightingData_;
↑座標変換マトリクスの分離とライティング用データの追加
マイケル
マイケル
定数バッファ(CBV)を一つ増やす必要があるため、下記のようにディスクリプタヒープとディスクリプタテーブルの数の指定を修正しておきましょう。
	// ディスクリプタヒープの初期化
	{
		// レンダーターゲットビュー
		D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
		rtvHeapDesc.NumDescriptors = kFrameCount;
		rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
		rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
		ThrowIfFailed(device_->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(rtvHeap_.ReleaseAndGetAddressOf())));
		// 基本情報の受け渡し用
		D3D12_DESCRIPTOR_HEAP_DESC basicHeapDesc = {};
		basicHeapDesc.NumDescriptors = 3; // 1SRV + 2CBV
		basicHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
		basicHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
		ThrowIfFailed(device_->CreateDescriptorHeap(&basicHeapDesc, IID_PPV_ARGS(basicHeap_.ReleaseAndGetAddressOf())));
	}
・・・
		// ルートパラメータの生成
		// ディスクリプタテーブルの実体
		CD3DX12_DESCRIPTOR_RANGE1 discriptorRanges[2];
		discriptorRanges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 2, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC); // CBV
		discriptorRanges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC); // SRV
		CD3DX12_ROOT_PARAMETER1 rootParameters[1];
		rootParameters[0].InitAsDescriptorTable(2, discriptorRanges, D3D12_SHADER_VISIBILITY_ALL); // 同一パラメータで複数指定
・・・
		ComPtr<ID3DBlob> vsBlob;
		ComPtr<ID3DBlob> psBlob;
		D3DCompileFromFile(L"LambertShaders.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "VSMain", "vs_5_0", compileFlags, 0, &vsBlob, nullptr);
		D3DCompileFromFile(L"LambertShaders.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "PSMain", "ps_5_0", compileFlags, 0, &psBlob, nullptr);
		// 頂点レイアウトの生成
		D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
		{
			{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
			{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
			{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
		};
・・・
↑ディスクリプタヒープ、ディスクリプタテーブルの指定を1つ増やす
マイケル
マイケル
そしてCBV生成処理も下記のように修正すれば完了です!
	// 座標変換マトリクス(CBV)の生成
	{
		// 定数バッファーの生成
		auto constHeapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
		auto constDesc = CD3DX12_RESOURCE_DESC::Buffer((sizeof(mapMatricesData_) + 0xff) & ~0xff); // 256アライメントでサイズを指定
		ThrowIfFailed(device_->CreateCommittedResource(
			&constHeapProp,
			D3D12_HEAP_FLAG_NONE,
			&constDesc,
			D3D12_RESOURCE_STATE_GENERIC_READ,
			nullptr,
			IID_PPV_ARGS(constMatricesBuffer_.ReleaseAndGetAddressOf())));
		// 定数バッファービューの生成
		D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
		cbvDesc.BufferLocation = constMatricesBuffer_->GetGPUVirtualAddress();
		cbvDesc.SizeInBytes = static_cast<UINT>(constMatricesBuffer_->GetDesc().Width);
		device_->CreateConstantBufferView(&cbvDesc, basicHeapHandle);
		// 3D座標用の行列を生成
		worldMatrix_ = DirectX::XMMatrixScaling(scale_, scale_, scale_)
			* DirectX::XMMatrixRotationY(angle_ * (DirectX::XM_PI / 180.0f))
			* DirectX::XMMatrixTranslation(translate_.x, translate_.y, translate_.z);
		DirectX::XMFLOAT3 eye(0, 0, -5);
		DirectX::XMFLOAT3 target(0, 0, 0);
		DirectX::XMFLOAT3 up(0, 1, 0);
		viewMatrix_ = DirectX::XMMatrixLookAtLH(DirectX::XMLoadFloat3(&eye), DirectX::XMLoadFloat3(&target), DirectX::XMLoadFloat3(&up));
		projMatrix_ = DirectX::XMMatrixPerspectiveFovLH(
			DirectX::XM_PIDIV2, // 画角: 90度
			static_cast<float>(windowWidth_) / static_cast<float>(windowHeight_), // アスペクト比
			1.0f, // near
			10.0f // far
		);
		// 定数情報のコピー
		constMatricesBuffer_->Map(0, nullptr, (void**)&mapMatricesData_);
		mapMatricesData_->world = worldMatrix_;
		mapMatricesData_->viewproj = viewMatrix_ * projMatrix_;
		constMatricesBuffer_->Unmap(0, nullptr);
	}
	basicHeapHandle.ptr += device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
	// ライティング用データ(CBV)の生成
	{
		// 定数バッファーの生成
		auto constHeapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
		auto constDesc = CD3DX12_RESOURCE_DESC::Buffer((sizeof(mapLightingData_) + 0xff) & ~0xff);
		ThrowIfFailed(device_->CreateCommittedResource(
			&constHeapProp,
			D3D12_HEAP_FLAG_NONE,
			&constDesc,
			D3D12_RESOURCE_STATE_GENERIC_READ,
			nullptr,
			IID_PPV_ARGS(constLightingBuffer_.ReleaseAndGetAddressOf())));
		// 定数バッファービューの生成
		D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
		cbvDesc.BufferLocation = constLightingBuffer_->GetGPUVirtualAddress();
		cbvDesc.SizeInBytes = static_cast<UINT>(constLightingBuffer_->GetDesc().Width);
		device_->CreateConstantBufferView(&cbvDesc, basicHeapHandle);
		// 定数情報のコピー
		constLightingBuffer_->Map(0, nullptr, (void**)&mapLightingData_);
		mapLightingData_->ambientLight = ambientLight_;
		mapLightingData_->lightColor = lightColor_;
		mapLightingData_->lightDirection = lightDirection_;
		constLightingBuffer_->Unmap(0, nullptr);
	}
	basicHeapHandle.ptr += device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
↑CBVの生成処理
エレキベア
エレキベア
バッファービューを作るのも慣れてきたクマね
マイケル
マイケル
この状態で実行すると下記のようになるはずです!
正常にランバート反射できていることが確認できます。
エレキベア
エレキベア
これぞ3Dクマ〜〜〜〜〜

深度バッファの指定

マイケル
マイケル
さあこれで完了!!
・・・といいたいところですが、一点問題が残っています。
マイケル
マイケル
よく見ると、このように後ろにあるはずのメッシュが前面に描画されていることが分かります・・・
エレキベア
エレキベア
気づかなかったクマ・・・
マイケル
マイケル
これは深度バッファーが指定されていないために発生する事象になります。
これはフラグを変えれば済む・・・という話でもなく深度バッファービュー(DSV)を生成して設定しなければならないようなのでこちらも生成しましょう!
エレキベア
エレキベア
さすがDirectXクマ・・・
マイケル
マイケル
とはいえ生成の方法はこれまでとさほど変わりはありません。
下記のようにディスクリプタヒープと深度バッファーの変数を定義して・・・
	// パイプラインオブジェクト
	ComPtr<ID3D12Device> device_;
	ComPtr<ID3D12CommandAllocator> commandAllocator_;
	ComPtr<ID3D12GraphicsCommandList> commandList_;
	ComPtr<ID3D12CommandQueue> commandQueue_;
	ComPtr<IDXGISwapChain4> swapchain_;
	ComPtr<ID3D12DescriptorHeap> rtvHeap_;              // レンダーターゲットヒープ
	ComPtr<ID3D12DescriptorHeap> dsvHeap_;              // 深度バッファーヒープ
	ComPtr<ID3D12DescriptorHeap> basicHeap_;            // 基本情報の受け渡し用(SRV + CBV)
	ComPtr<ID3D12Resource> renderTargets_[kFrameCount]; // バックバッファー
	ComPtr<ID3D12Resource> depthBuffer_;                // 深度バッファー
	ComPtr<ID3D12PipelineState> pipelinestate_;         // パイプラインステート
	ComPtr<ID3D12RootSignature> rootsignature_;         // ルートシグネチャ
↑深度バッファーの定義を追加
マイケル
マイケル
それぞれ初期化します!
	// ディスクリプタヒープの初期化
	{
		// レンダーターゲットビュー
		D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
		rtvHeapDesc.NumDescriptors = kFrameCount;
		rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
		rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
		ThrowIfFailed(device_->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(rtvHeap_.ReleaseAndGetAddressOf())));
		// 深度バッファービュー
		D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
		dsvHeapDesc.NumDescriptors = 1;
		dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
		ID3D12DescriptorHeap* dsvHeap = nullptr;
		ThrowIfFailed(device_->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(dsvHeap_.ReleaseAndGetAddressOf())));
		// 基本情報の受け渡し用
		D3D12_DESCRIPTOR_HEAP_DESC basicHeapDesc = {};
		basicHeapDesc.NumDescriptors = 3; // 1SRV + 2CBV
		basicHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
		basicHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
		ThrowIfFailed(device_->CreateDescriptorHeap(&basicHeapDesc, IID_PPV_ARGS(basicHeap_.ReleaseAndGetAddressOf())));
	}
・・・
	// 深度バッファービュー生成
	{
		// 深度バッファー作成
		D3D12_RESOURCE_DESC depthResDesc = {};
		depthResDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
		depthResDesc.Width = windowWidth_;
		depthResDesc.Height = windowHeight_;
		depthResDesc.DepthOrArraySize = 1;
		depthResDesc.Format = DXGI_FORMAT_D32_FLOAT;
		depthResDesc.SampleDesc.Count = 1;
		depthResDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
		depthResDesc.MipLevels = 1;
		depthResDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
		depthResDesc.Alignment = 0;
		auto depthHeapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
		// クリアバリューの設定
		D3D12_CLEAR_VALUE _depthClearValue = {};
		_depthClearValue.DepthStencil.Depth = 1.0f;      //深さ1(最大値)でクリア
		_depthClearValue.Format = DXGI_FORMAT_D32_FLOAT; //32bit深度値としてクリア
		ThrowIfFailed(device_->CreateCommittedResource(
			&depthHeapProp,
			D3D12_HEAP_FLAG_NONE,
			&depthResDesc,
			D3D12_RESOURCE_STATE_DEPTH_WRITE, //デプス書き込みに使用
			&_depthClearValue,
			IID_PPV_ARGS(depthBuffer_.ReleaseAndGetAddressOf())));
		// 深度バッファービュー作成
		D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = {};
		dsvDesc.Format = DXGI_FORMAT_D32_FLOAT;
		dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
		dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
		device_->CreateDepthStencilView(depthBuffer_.Get(), &dsvDesc, dsvHeap_->GetCPUDescriptorHandleForHeapStart());
	}
↑深度バッファービューの生成
マイケル
マイケル
あとはパイプラインステートとコマンドリストにも深度バッファを使用するよう設定すれば完了です!
		// 深度ステンシル
		psoDesc.DepthStencilState.DepthEnable = true;                             // 深度バッファーを使用するか
		psoDesc.DepthStencilState.StencilEnable = false;                          // ステンシルテストを行うか
		psoDesc.DSVFormat = DXGI_FORMAT_D32_FLOAT;                                // 深度バッファーで使用するフォーマット
		psoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;         // 書き込む
		psoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;    // 小さい方を採用する
↑パイプラインステートへの追加
		// レンダーターゲットの設定
		CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap_->GetCPUDescriptorHandleForHeapStart(), frameIndex, device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV));
		auto dsvHandle = dsvHeap_->GetCPUDescriptorHandleForHeapStart();
		commandList_->OMSetRenderTargets(1, &rtvHandle, true, &dsvHandle);
		float clearColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };  // 黄色
		commandList_->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
		commandList_->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
↑深度ビューのクリア処理の追加
エレキベア
エレキベア
だいぶ雑クマがまあ大体分かるクマ〜〜〜
マイケル
マイケル
この状態で実行すると、下記のように綺麗に描画されることが確認できました!
↑綺麗に描画された!
エレキベア
エレキベア
うおおおやったクマ〜〜〜〜

おわりに

マイケル
マイケル
というわけで今回はFBXモデルの描画でした!
どうだったかな??
エレキベア
エレキベア
FBX SDKは使ったことあったから楽勝だったクマね
マイケル
マイケル
やっぱりモデルを描画できると感動するね〜〜
マイケル
マイケル
当初はとりあえずFBXモデルの描画まで出来ればいいかなと思っていたけど、
せっかくここまで作ったので、ゲームエンジンとして使えるようリファクタリングして簡単なゲームも開発してみようかなと思います!
次回もお楽しみに〜〜〜!!
エレキベア
エレキベア
クマ〜〜〜〜〜

【DirextX12】第四回 DirextX12を使ったゲーム開発 〜FBX SDKを使用した3Dモデル描画〜 〜完〜


C++グラフィックスDirectX12
2022-12-07
記事をSNSで共有する
X
Facebook
LINE
はてなブックマーク
Pocket
LinkedIn
Reddit

著者の各種アカウント
フォローいただけると大変励みになります!

関連記事
【Three.js】カスタムシェーダーでトゥーン+背面法アウトラインを実装する
2026-02-15
【Three.js】Three.js入門 - シーン構築・モデル読み込み・ポストプロセスまで
2026-02-15
【Houdini21.0】3Dビル群っぽいブログヘッダー画像を作成する
2026-01-10
【Houdini21.0】Solaris徹底入門:USD構成を意識した基本的な作業フローについてまとめる
2025-12-31
【ゲーム数学】第十回 p5.js(+α)で学ぶゲーム数学「複素数とフラクタル」
2025-11-02
【プロシージャル】Pythonで学ぶ波動関数崩壊アルゴリズム(Wave Function Collapse)
2025-06-22
【UE5.5】Nanite、Lumen、VSMの概要についてまとめる
2025-05-12
【書籍紹介】「コンピュータグラフィックス」に出てくる用語をまとめる【CGエンジニア検定】
2024-07-13