0
0

DirectX12でミクさんを躍らせてみよう16-Assimpを使ってFBXを読み込んで描画してみよう

Last updated at Posted at 2024-10-01

前回

Assimpを使ってFBXを読み込んで描画してみよう

Assimp

Assimpは、様々な3Dモデルファイル形式を読み込むことができるオープンソースライブラリです。

これを使ってFBXを読み込んでみましょう。

Assimpをプログラムに追加する方法については、この記事では触れません。記事が長くなってしまうことと、すでに良くまとめられた記事が多くあるため、各自で方法を調べてプロジェクトに追加してください。

Assimpの準備が完了した前提で、さっそくFBXモデルを描画できるようにしてみましょう。

FBXActorクラス

struct FBXVertex
{
	DirectX::XMFLOAT3 position;
	DirectX::XMFLOAT3 normal;
	DirectX::XMFLOAT2 uv;
};

struct FBXMesh
{
	unsigned int startVertex;
	unsigned int vertexCount;
	unsigned int startIndex;
	unsigned int indexCount;
};

class FBXActor 
{
public:
	FBXActor();

	bool Initialize(const std::string& path, Dx12Wrapper& dx);
	void SetMaterialName(const std::vector<std::string> materialNameList);
	void Draw(Dx12Wrapper& dx, bool isShadow) const;
	void Update();

	std::string GetName() const override;
	void SetName(std::string name) override;

	const std::vector<std::string> GetMaterialNameList() const;

private:
  DirectX::XMFLOAT3 AiVector3ToXMFLOAT3(const aiVector3D& vec);
	DirectX::XMFLOAT2 AiVector2ToXMFLOAT2(const aiVector2D& vec);
	void ReadNode(aiNode* node, const aiScene* scene);
	void ReadMesh(aiMesh* mesh, const aiScene* scene);

	HRESULT CreateVertexBufferAndIndexBuffer(Dx12Wrapper& dx);
	HRESULT CreateTransformView(Dx12Wrapper& dx);

private:
	template<typename T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;

	std::string mModelPath;

	std::vector<FBXVertex> mVertices;
	std::vector<unsigned int> mIndices;
	std::vector<FBXMesh> mMeshes;
	std::vector<std::string> mMeshMaterialNameList;
	unsigned int mVertexCount = 0;
	unsigned int mIndexCount = 0;

	ComPtr<ID3D12Resource> mVertexBuffer = nullptr;
	ComPtr<ID3D12Resource> mIndexBuffer = nullptr;
	D3D12_VERTEX_BUFFER_VIEW mVertexBufferView = {};
	D3D12_INDEX_BUFFER_VIEW mIndexBufferView = {};
	FBXVertex* mMappedVertex;
	unsigned int* mMappedIndex;

	ComPtr<ID3D12Resource> mTransformBuffer = nullptr;
	ComPtr<ID3D12DescriptorHeap> mTransformHeap = nullptr;
	DirectX::XMMATRIX* mMappedWorldTransform;
	Transform mTransform;
};

FBXActorで使用する頂点をFBXVertexとして定義しました。

順番に主要なメソッドを見ていきましょう。

bool FBXActor::Initialize(const std::string& path, Dx12Wrapper& dx)
{
	mModelPath = path;

	Assimp::Importer importer;

	unsigned int flag;
	flag = aiProcess_Triangulate |
		aiProcess_JoinIdenticalVertices|
		aiProcess_CalcTangentSpace |
		aiProcess_GenNormals |
		aiProcess_MakeLeftHanded |
		aiProcess_FlipWindingOrder |
		aiProcess_FlipUVs;

	const aiScene* scene = importer.ReadFile(path, flag);

	if (scene == nullptr || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || scene->mRootNode == nullptr)
	{
		return false;
	}

	ReadNode(scene->mRootNode, scene);
	mMeshMaterialNameList.resize(mMeshes.size());

	auto hResult = CreateVertexBufferAndIndexBuffer(dx);
	if (FAILED(hResult))
	{
		return false;
	}

	hResult = CreateTransformView(dx);
	if (FAILED(hResult))
	{
		return false;
	}

	return true;
}

Initializeメソッドは、FBXファイルの名前を受け取り、Assimpライブラリを使用してそれを読み込み、その情報を使用して頂点バッファとインデックスバッファを生成します。

Assimpライブラリのインポーターオブジェクトを使用してファイルを読み込むことができます。

ファイルを読み込む際に、複数のフラグを設定して読み込みます。

	unsigned int flag;
	flag = aiProcess_Triangulate |
		aiProcess_JoinIdenticalVertices|
		aiProcess_CalcTangentSpace |
		aiProcess_GenNormals |
		aiProcess_MakeLeftHanded |
		aiProcess_FlipWindingOrder |
		aiProcess_FlipUVs;

インポーターでファイルを読み込む際、データをどのように変換して読み込むかを指定することができます。

上記のフラグは、DirectX環境に合わせて情報を読み込むように設定しています。

const aiScene* scene = importer.ReadFile(path, flag);

if (scene == nullptr || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || scene->mRootNode == nullptr)
{
	return false;
}

ImporterオブジェクトのReadFileメソッドでファイルを読み込みます。

読み込みに失敗したか、読み込まれたシーンが不完全かどうかをチェックします。

読み込んだ結果はaiSceneというオブジェクトのポインタが返されます。

これをこれからシーンと呼ぶことにします。

ReadNodeメソッドで実際の頂点とインデックスデータを初期化します。

どのようになっているか見てみましょう。

void FBXActor::ReadNode(aiNode* node, const aiScene* scene)
{
	for (unsigned int i = 0; i < node->mNumMeshes; i++)
	{
		aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
		ReadMesh(mesh, scene);
	}

	for (unsigned int i = 0; i < node->mNumChildren; i++) 
	{
		ReadNode(node->mChildren[i], scene);
	}
}

このメソッドの内容を見ると、Assimpで読み込んだシーンはツーリ構造になっていることがわかります。

そして、頂点とインデックスはメッシュ単位で読み込みます。

そのため、FBXMesh構造体を定義しました。

ツーリ構造であるため、現在のノードの情報をすべて読み込んだ後、再帰的に子ノードも処理します。

void FBXActor::ReadMesh(aiMesh* mesh, const aiScene* scene)
{
	unsigned int vertexCount = 0;
	for (unsigned int i = 0; i < mesh->mNumVertices; i++)
	{
		FBXVertex vertex;
		vertex.position = AiVector3ToXMFLOAT3(mesh->mVertices[i]);
		vertex.normal = AiVector3ToXMFLOAT3(mesh->mNormals[i]);

		if (mesh->mTextureCoords[0] != nullptr)
		{
			vertex.uv.x = mesh->mTextureCoords[0][i].x;
			vertex.uv.y = mesh->mTextureCoords[0][i].y;
		}
		else
		{
			vertex.uv = DirectX::XMFLOAT2(0.f, 0.f);
		}

		mVertices.push_back(vertex);
		vertexCount++;
	}

	unsigned int indexCount = 0;
	for (unsigned int i = 0; i < mesh->mNumFaces; i++)
	{
		aiFace face = mesh->mFaces[i];
		for (unsigned int j = 0; j < face.mNumIndices; j++)
		{
			mIndices.push_back(face.mIndices[j] + mVertexCount);
			indexCount++;
		}
	}

	auto meshInfo = FBXMesh();
	meshInfo.startVertex = mVertexCount;
	meshInfo.vertexCount = vertexCount;
	meshInfo.startIndex = mIndexCount;
	meshInfo.indexCount = indexCount;
	mMeshes.push_back(meshInfo);

	mVertexCount += vertexCount;
	mIndexCount += indexCount;
}

DirectX::XMFLOAT3 FBXActor::AiVector3ToXMFLOAT3(const aiVector3D& vec)
{
	return DirectX::XMFLOAT3(vec.x, vec.y, vec.z);
}

DirectX::XMFLOAT2 FBXActor::AiVector2ToXMFLOAT2(const aiVector2D& vec)
{
	return DirectX::XMFLOAT2(vec.x, vec.y);
}

頂点情報とインデックス情報を初期化するメソッドです。

unsigned int vertexCount = 0;
for (unsigned int i = 0; i < mesh->mNumVertices; i++)
{
	FBXVertex vertex;
	vertex.position = AiVector3ToXMFLOAT3(mesh->mVertices[i]);
	vertex.normal = AiVector3ToXMFLOAT3(mesh->mNormals[i]);

	if (mesh->mTextureCoords[0] != nullptr)
	{
		vertex.uv.x = mesh->mTextureCoords[0][i].x;
		vertex.uv.y = mesh->mTextureCoords[0][i].y;
	}
  else
	{
		vertex.uv = DirectX::XMFLOAT2(0.f, 0.f);
	}

	mVertices.push_back(vertex);
	vertexCount++;
}

頂点情報を順に巡回しながら、FBXVertexに変換してmVerticesに追加します。

Assimpのベクトルデータ型はaiVector3D、aiVector2Dなどを使用しているため、AiVector3ToXMFLOAT3のようなメソッドを作成してXMFLOAT3、XMFLOAT2に変換しています。

unsigned int indexCount = 0;
for (unsigned int i = 0; i < mesh->mNumFaces; i++)
{
	aiFace face = mesh->mFaces[i];
	for (unsigned int j = 0; j < face.mNumIndices; j++)
	{
		mIndices.push_back(face.mIndices[j] + mVertexCount);
		indexCount++;
	}
}

メッシュのポリゴンを巡回しながらインデックスデータを読み込みます。

face.mIndices[j]は該当ポリゴンの頂点インデックスを指し、mIndicesに保存します。

mVertexCountは以前に処理したメッシュの頂点数を加えることで、現在のメッシュの頂点インデックスが全体の頂点リストで正しく参照されるようにします。

auto meshInfo = FBXMesh();
meshInfo.startVertex = mVertexCount;
meshInfo.vertexCount = vertexCount;
meshInfo.startIndex = mIndexCount;
meshInfo.indexCount = indexCount;
mMeshes.push_back(meshInfo);

mVertexCount += vertexCount;
mIndexCount += indexCount;

FBXMesh構造体に現在のメッシュの開始頂点、頂点数、開始インデックス、インデックス数を保存します。

この情報は描画する時にメッシュを正しく参照するために使用されます。

メッシュ情報をmMeshesリストに追加します。

頂点とインデックスのデータをすべて読み込んだので、頂点バッファビューとインデックスバッファビューを生成します。

CreateVertexBufferAndIndexBufferメソッドを見てみましょう。

以前にPMXActorを作成したときを覚えていれば、大きな違いはないので、今回は難しくないはずです。

HRESULT FBXActor::CreateVertexBufferAndIndexBuffer(Dx12Wrapper& dx)
{
	D3D12_HEAP_PROPERTIES heapProperties = {};
	heapProperties.Type = D3D12_HEAP_TYPE_UPLOAD;
	heapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
	heapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;

	size_t vertexSize = sizeof(FBXVertex);
	D3D12_RESOURCE_DESC resourceDesc = {};
	resourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
	resourceDesc.Width = mVertexCount * vertexSize;
	resourceDesc.Height = 1;
	resourceDesc.DepthOrArraySize = 1;
	resourceDesc.MipLevels = 1;
	resourceDesc.Format = DXGI_FORMAT_UNKNOWN;
	resourceDesc.SampleDesc.Count = 1;
	resourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
	resourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;

	auto result = dx.Device()->CreateCommittedResource(&heapProperties, 
		D3D12_HEAP_FLAG_NONE, 
		&resourceDesc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(mVertexBuffer.ReleaseAndGetAddressOf()));

	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	result = mVertexBuffer->Map(0, nullptr, (void**)&mMappedVertex);
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	std::copy(std::begin(mVertices), std::end(mVertices), mMappedVertex);
	mVertexBuffer->Unmap(0, nullptr);

	mVertexBufferView.BufferLocation = mVertexBuffer->GetGPUVirtualAddress();
	mVertexBufferView.SizeInBytes = vertexSize * mVertexCount;
	mVertexBufferView.StrideInBytes = vertexSize;

	resourceDesc.Width = sizeof(unsigned int) * mIndexCount;

	result = dx.Device()->CreateCommittedResource(
		&heapProperties,
		D3D12_HEAP_FLAG_NONE,
		&resourceDesc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(mIndexBuffer.ReleaseAndGetAddressOf()));

	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	result = mIndexBuffer->Map(0, nullptr, (void**)&mMappedIndex);
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	std::copy(std::begin(mIndices), std::end(mIndices), mMappedIndex);
	mIndexBuffer->Unmap(0, nullptr);

	mIndexBufferView.BufferLocation = mIndexBuffer->GetGPUVirtualAddress();
	mIndexBufferView.Format = DXGI_FORMAT_R32_UINT;
	mIndexBufferView.SizeInBytes = sizeof(unsigned int) * mIndexCount;

	return S_OK;
}

説明は省略します。

今度はFBXモデルのワールドトランスフォームを渡すコンスタントバッファを生成しましょう。

HRESULT FBXActor::CreateTransformView(Dx12Wrapper& dx)
{
	auto buffSize = sizeof(DirectX::XMMATRIX);
	buffSize = (buffSize + 0xff) & ~0xff;

	auto result = dx.Device()->CreateCommittedResource
	(
		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
		D3D12_HEAP_FLAG_NONE,
		&CD3DX12_RESOURCE_DESC::Buffer(buffSize),
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(mTransformBuffer.ReleaseAndGetAddressOf())
	);

	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	D3D12_DESCRIPTOR_HEAP_DESC transformDescHeapDesc = {};
	transformDescHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	transformDescHeapDesc.NodeMask = 0;
	transformDescHeapDesc.NumDescriptors = 1;
	transformDescHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;

	result = dx.Device()->CreateDescriptorHeap(
		&transformDescHeapDesc,
		IID_PPV_ARGS(mTransformHeap.ReleaseAndGetAddressOf()));

	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
	cbvDesc.BufferLocation = mTransformBuffer->GetGPUVirtualAddress();
	cbvDesc.SizeInBytes = buffSize;

	dx.Device()->CreateConstantBufferView(&cbvDesc, mTransformHeap->GetCPUDescriptorHandleForHeapStart());

	return S_OK;
}
 

以前の記事で何度もコンスタントバッファを作成し、ディスクリプタヒープも生成してきたので、説明は省略します。

ここまでで基本的なものはすべて準備できました。

頂点バッファビュー、インデックスバッファビュー、ワールドトランスフォームコンスタントバッファビュー

このように準備しました。

今度は更新と描画命令を作成しましょう。

void FBXActor::Update()
{
	mMappedWorldTransform[0] = mTransform.GetTransformMatrix();
}

毎フレームワールドトランスフォームを更新するようにします。

void FBXActor::Draw(Dx12Wrapper& dx, bool isShadow) const
{
	dx.CommandList()->IASetVertexBuffers(0, 1, &mVertexBufferView);
	dx.CommandList()->IASetIndexBuffer(&mIndexBufferView);
	dx.CommandList()->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	ID3D12DescriptorHeap* transformHeap[] = { mTransformHeap.Get() };
	dx.CommandList()->SetDescriptorHeaps(1, transformHeap);
	dx.CommandList()->SetGraphicsRootDescriptorTable(1, mTransformHeap->GetGPUDescriptorHandleForHeapStart());

	MaterialManager::Instance().SetMaterialDescriptorHeaps(dx);

	unsigned int indexOffset = 0;

	for (int i = 0; i < mMeshes.size(); i++)
	{
		MaterialManager::Instance().SetGraphicsRootDescriptorTableMaterial(dx, 2, mMeshMaterialNameList[i]);

		unsigned int indexCount = mMeshes[i].indexCount;

		dx.CommandList()->DrawIndexedInstanced(indexCount, 1, indexOffset, 0, 0);

		indexOffset += indexCount;
	}
}

Draw メソッドです。

dx.CommandList()->IASetVertexBuffers(0, 1, &mVertexBufferView);
dx.CommandList()->IASetIndexBuffer(&mIndexBufferView);
dx.CommandList()->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

ID3D12DescriptorHeap* transformHeap[] = { mTransformHeap.Get() };
dx.CommandList()->SetDescriptorHeaps(1, transformHeap);
dx.CommandList()->SetGraphicsRootDescriptorTable(1, mTransformHeap->GetGPUDescriptorHandleForHeapStart());

頂点バッファビュー、インデックスバッファビュー、ワールドトランスフォームコンスタントバッファを設定します。

unsigned int indexOffset = 0;

for (int i = 0; i < mMeshes.size(); i++)
{
	unsigned int indexCount = mMeshes[i].indexCount;
	dx.CommandList()->DrawIndexedInstanced(indexCount, 1, indexOffset, 0, 0);

	indexOffset += indexCount;
}

マテリアルを設定していますが、MaterialManagerを使用して別途管理しています。

MaterialManagerはまだ作成していませんが、後で作成する予定です。

マテリアルを設定した後、メッシュ単位で描画命令を行っています。

FBXRendererを作成しましょう。

FBXRendererクラス

class FBXRenderer
{
public:
	FBXRenderer(Dx12Wrapper& dx);
	~FBXRenderer();

	void Update();

	void BeforeDrawAtForwardPipeline();

	void Draw();

	ID3D12PipelineState* GetPipelineState() const;
	ID3D12RootSignature* GetRootSignature() const;
	void AddActor(std::shared_ptr<FBXActor> actor);
	std::vector<std::shared_ptr<FBXActor>>& GetActor();

private:
	HRESULT CreateRootSignature();
	HRESULT CreateGraphicsPipeline();
	bool CheckShaderCompileResult(HRESULT result, ID3DBlob* error = nullptr);

private:
	Dx12Wrapper& mDx;
	template<typename T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;

	ComPtr<ID3D12RootSignature> mRootSignature = nullptr;
	ComPtr<ID3D12PipelineState> mForwardPipeline = nullptr;
	ComPtr<ID3D12PipelineState> mStencilWritePipeline = nullptr;
};

宣言部です。

では、すぐにルートシグネチャとパイプラインステートを生成するメソッドを見てみましょう。

HRESULT FBXRenderer::CreateRootSignature()
{
	D3D12_DESCRIPTOR_RANGE descriptorRange[2] = {};

	//Scene Buffer
	descriptorRange[0].NumDescriptors = 1;
	descriptorRange[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	descriptorRange[0].BaseShaderRegister = 0;
	descriptorRange[0].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	//World Transform Buffer
	descriptorRange[1].NumDescriptors = 1;
	descriptorRange[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	descriptorRange[1].BaseShaderRegister = 1;
	descriptorRange[1].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
	
	CD3DX12_ROOT_PARAMETER rootParam[2] = {};

	rootParam[0].InitAsDescriptorTable(1, &descriptorRange[0], D3D12_SHADER_VISIBILITY_ALL);
	rootParam[1].InitAsDescriptorTable(1, &descriptorRange[1], D3D12_SHADER_VISIBILITY_ALL);

	CD3DX12_STATIC_SAMPLER_DESC samplerDesc[1] = {};
	samplerDesc[0].AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
	samplerDesc[0].AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
	samplerDesc[0].AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
	samplerDesc[0].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
	samplerDesc[0].Filter = D3D12_FILTER_MAXIMUM_MIN_MAG_MIP_LINEAR;
	samplerDesc[0].MaxLOD = D3D12_FLOAT32_MAX;
	samplerDesc[0].MinLOD = 0.0f;
	samplerDesc[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
	samplerDesc[0].ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
	samplerDesc[0].ShaderRegister = 0;

	CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc = {};
	rootSignatureDesc.Init(_countof(rootParam), 
		rootParam, 
		_countof(samplerDesc),
		samplerDesc,
		D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

	ComPtr<ID3DBlob> rootSignatureBlob = nullptr;
	ComPtr<ID3DBlob> errorBlob = nullptr;

	auto result = D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &rootSignatureBlob, &errorBlob);
	if (FAILED(result) == true)
	{
		assert(SUCCEEDED(result));
		return result;
	}

	result = mDx.Device()->CreateRootSignature(0, rootSignatureBlob->GetBufferPointer(), rootSignatureBlob->GetBufferSize(), IID_PPV_ARGS(mRootSignature.ReleaseAndGetAddressOf()));
	if (FAILED(result) == true)
	{
		assert(SUCCEEDED(result));
		return result;
	}

	return S_OK;
}

シーン情報コンスタントバッファ、ワールドトランスフォームコンスタントバッファを使用できるようにルートパラメータを作成します。

CD3DX12_ROOT_PARAMETERを使用すると、簡単にルートパラメータを作成できます。

ルートパラメータの作成が完了したら、これを使ってルートシグネチャを生成します。

HRESULT FBXRenderer::CreateGraphicsPipeline()
{
	UINT flags = 0;
#if defined( DEBUG ) || defined( _DEBUG )
	flags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif

	D3D12_INPUT_ELEMENT_DESC inputLayout[] =
	{
		{
			"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
		}
	};

	D3D12_GRAPHICS_PIPELINE_STATE_DESC graphicsPipelineDesc = {};

	graphicsPipelineDesc.pRootSignature = mRootSignature.Get();
	graphicsPipelineDesc.SampleMask = D3D12_DEFAULT_SAMPLE_MASK;
	graphicsPipelineDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
	graphicsPipelineDesc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
	graphicsPipelineDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
	graphicsPipelineDesc.InputLayout.pInputElementDescs = inputLayout;
	graphicsPipelineDesc.InputLayout.NumElements = _countof(inputLayout);
	graphicsPipelineDesc.IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED;
	graphicsPipelineDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
	graphicsPipelineDesc.SampleDesc.Count = 1;
	graphicsPipelineDesc.SampleDesc.Quality = 0;
	graphicsPipelineDesc.DepthStencilState.DepthEnable = true;
	graphicsPipelineDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
	graphicsPipelineDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
	graphicsPipelineDesc.DepthStencilState.StencilEnable = false;
	graphicsPipelineDesc.DSVFormat = DXGI_FORMAT_D32_FLOAT;

	graphicsPipelineDesc.RTVFormats[0] = DXGI_FORMAT_B8G8R8A8_UNORM;
	graphicsPipelineDesc.RTVFormats[1] = DXGI_FORMAT_B8G8R8A8_UNORM;
	graphicsPipelineDesc.RTVFormats[2] = DXGI_FORMAT_B8G8R8A8_UNORM;
	graphicsPipelineDesc.NumRenderTargets = 3;

	ComPtr<ID3DBlob> vs = nullptr;
	ComPtr<ID3DBlob> ps = nullptr;
	ComPtr<ID3DBlob> errorBlob = nullptr;

	auto result = D3DCompileFromFile(L"FBXVertexShader.hlsl",
		nullptr,
		D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"VS",
		"vs_5_0",
		flags,
		0,
		&vs,
		&errorBlob);

	if (!CheckShaderCompileResult(result, errorBlob.Get()))
	{
		assert(0);
		return result;
	}

	result = D3DCompileFromFile(L"FBXPixelShader.hlsl",
		nullptr,
		D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"PS",
		"ps_5_0",
		flags,
		0,
		&ps,
		&errorBlob);

	if (!CheckShaderCompileResult(result, errorBlob.Get()))
	{
		assert(0);
		return result;
	}

	graphicsPipelineDesc.VS = CD3DX12_SHADER_BYTECODE(vs.Get());
	graphicsPipelineDesc.PS = CD3DX12_SHADER_BYTECODE(ps.Get());

	result = mDx.Device()->CreateGraphicsPipelineState(&graphicsPipelineDesc, IID_PPV_ARGS(mForwardPipeline.ReleaseAndGetAddressOf()));
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	return S_OK;
}

パイプラインステートを生成するメソッドです。

この部分もこれまでやってきたことと大きく変わらないので、簡単に説明します。

頂点構造体に合わせて入力レイアウトを作成します。

前のチャプターで行ったように、今はマルチレンダーターゲットで描画するため、レンダーターゲットを3に設定します。

これからFBXモデルを描画するためのシェーダーを作成します。

シェーダー

まずヘッダーから作成しましょう。

struct Output
{
	float4 svpos : SV_POSITION;
	float4 pos : POSITION;
	float4 normal : NORMAL;
};

struct PixelOutput
{
	float4 color : SV_TARGET0;
	float4 normal : SV_TARGET1;
	float4 highLum : SV_TARGET2;
};

SamplerState smp : register(s0);

cbuffer SceneBuffer : register(b0)
{
	matrix view;
	matrix proj;
	matrix invproj;
	matrix lightCamera;
	matrix shadow;
	float4 lightVec;
	float3 eye;
};

cbuffer Transform : register(b1)
{
	matrix world;
};

Outputには特別なものはありません。

レンダーターゲットを3つ使用するため、ここでもPixelOutputを作成します。

#include "FBXShaderHeader.hlsli"

Output VS(
	float4 pos : POSITION,
	float4 normal : NORMAL,
	float2 uv : TEXCOORD) 
{
	Output output;

	pos = mul(world, pos);

	output.pos = pos;
	output.svpos = mul(mul(proj, view), output.pos);
	output.normal = mul(world, normal);

	return output;
}

頂点シェーダーです。

PixelOutput PS(Output input) : SV_TARGET
{
	float3 normal = normalize(input.normal);
	float3 light = normalize(lightVec);

	float nDotL = saturate(dot(normal, -light));  

	PixelOutput output;
	output.color.rgb = float3(0.5f, 0.5f, 0.5f) * nDotL;
	output.color.a = 1.0f;
	output.highLum = 0.0f;
	output.normal.rgb = float3((input.normal.xyz + 1.0f) / 2.0f);
	output.normal.a = 0.0f;

	return output;
}

ピクセルシェーダーです。

まだマテリアルの処理を行っていないので、拡散反射値だけをグレーに掛けて出力するようにします。

FBXモデルを描画してみましょう。

描画

std::shared_ptr<FBXActor> newActor = std::make_shared<FBXActor>();
newActor->Initialize(fbxFileName, *mDx12)

mRender->AddFBXActor(newActor);

FBXファイルを読み込んでFBXActorを初期化し、FBXRendererに追加します。

auto cmdList = mDx.CommandList();
cmdList->SetPipelineState(mForwardPipeline.Get());
cmdList->SetGraphicsRootSignature(mRootSignature.Get());

描画コマンドを行う前にパイプラインステートを設定します。

auto wsize = Application::Instance().GetWindowSize();

ID3D12DescriptorHeap* sceneheaps[] = { mSceneDescHeap.Get() };
mCmdList->SetDescriptorHeaps(1, sceneheaps);
mCmdList->SetGraphicsRootDescriptorTable(0, mSceneDescHeap->GetGPUDescriptorHandleForHeapStart());

D3D12_VIEWPORT vp = CD3DX12_VIEWPORT(0.0f, 0.0f, wsize.cx, wsize.cy);
mCmdList->RSSetViewports(1, &vp);

CD3DX12_RECT rc(0, 0, wsize.cx, wsize.cy);
mCmdList->RSSetScissorRects(1, &rc);

シーン情報コンスタントバッファも設定します。

mFbxRenderer->Draw();

FBXRenderer의 Draw를 호출합니다.

FBXRendererのDrawを呼び出します。

デプステストを有効にして描画するため、

これをPMXRendererのDrawの前後どちらで行っても問題ありません。

ビルドして確認してみましょう。
image.png
ステージモデルが描画されていますね。

まだシェーディングだけを処理しているので、地味な感じです。

マテリアルを適用して色が付くようにしてみましょう。

MaterialManager

PMXを描画する際は、PMXファイル自体にマテリアルが定義されているため、それをそのまま使用しましたが、FBXでは直接マテリアルを設定して使用するようにしたいと思います。

そして、メッシュ1つにつき1つのマテリアルを作成するのではなく、作成したマテリアルを複数のメッシュで共有して使用できるようにしたいと思います。

struct StandardLoadMaterial
{
	std::string name;
	DirectX::XMFLOAT4 diffuse;
	DirectX::XMFLOAT3 specular;
	float roughness;
	DirectX::XMFLOAT3 ambient;
	bool isBloom;
};

struct alignas(16) StandardUploadMaterial
{
	DirectX::XMFLOAT4 diffuse;
	DirectX::XMFLOAT3 specular;
	float roughness;
	DirectX::XMFLOAT3 ambient;
	float bloomFactor;
};

class MaterialManager
{
public:
	static MaterialManager& Instance();

	bool Init(Dx12Wrapper& dx);
	bool Exist(std::string name);

	void AddNewMaterial(Dx12Wrapper& dx, std::string name);
	void RemoveMaterial(std::string name);

	const std::vector<std::string>& GetNameList() const;

	bool GetMaterialData(std::string name, const StandardLoadMaterial** result) const;
	void SetMaterialData(std::string name, const StandardLoadMaterial& setData);

	void SetMaterialDescriptorHeaps(Dx12Wrapper& dx);
	void SetGraphicsRootDescriptorTableMaterial(Dx12Wrapper& dx, unsigned int rootParameterIndex, std::string name);

private:
	bool CreateBuffer(Dx12Wrapper& dx);
	bool CreateMaterialView(Dx12Wrapper& dx);

private:
	static MaterialManager mInstance;

	ComPtr<ID3D12Resource> mMaterialBuff = nullptr;
	ComPtr<ID3D12DescriptorHeap> mMaterialHeap = nullptr;
	char* mMappedMaterial = nullptr;

	std::vector<StandardLoadMaterial> mMaterialDataList;
	std::vector<std::string> mNameList;
	std::unordered_map<std::string, int> mIndexByName;
};

これは宣言部です。

マテリアル構造体としてStandardLoadMaterialとStandardUploadMaterialを定義しました。

一方はCPU側に保存するデータ用で、もう一方はGPUにアップロードするための構造体です。

シングルトンパターンで実装し、どこからでも参照できるようにします。

メソッドを見ながら確認していきましょう。

bool MaterialManager::Init(Dx12Wrapper& dx)
{
	bool result = CreateBuffer(dx);
	if (result == false)
	{
		return false;
	}

	result = CreateMaterialView(dx);
	if (result == false)
	{
		return false;
	}

	return true;
}

初期化メソッドです。

保存しておいたマテリアル情報に基づいて、コンスタントバッファとビューを作成します。

bool MaterialManager::CreateBuffer(Dx12Wrapper& dx)
{
	int materialBufferSize = sizeof(StandardUploadMaterial);
	materialBufferSize = (materialBufferSize + 0xff) & ~0xff;

	auto result = dx.Device()->CreateCommittedResource(
		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
		D3D12_HEAP_FLAG_NONE,
		&CD3DX12_RESOURCE_DESC::Buffer(materialBufferSize * mMaterialDataList.size()),
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(mMaterialBuff.ReleaseAndGetAddressOf()));

	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return false;
	}

	result = mMaterialBuff->Map(0, nullptr, (void**)&mMappedMaterial);
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return false;
	}

	char* mappedMaterialPtr = mMappedMaterial;

	for (const auto& material : mMaterialDataList)
	{
		StandardUploadMaterial* uploadMaterial = reinterpret_cast<StandardUploadMaterial*>(mappedMaterialPtr);
		uploadMaterial->diffuse = material.diffuse;
		uploadMaterial->specular = material.specular;
		uploadMaterial->roughness = material.roughness;
		uploadMaterial->ambient = material.ambient;
		uploadMaterial->bloomFactor = material.isBloom ? 1.0f : 0.0f;

		mappedMaterialPtr += materialBufferSize;
	}

	return true;
}

保存しているマテリアルの総数分のサイズでバッファを作成します。

作成したバッファにデータをマッピングします。

bool MaterialManager::CreateMaterialView(Dx12Wrapper& dx)
{
	D3D12_DESCRIPTOR_HEAP_DESC matDescHeapDesc = {};
	matDescHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	matDescHeapDesc.NodeMask = 0;
	matDescHeapDesc.NumDescriptors = mMaterialDataList.size();
	matDescHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;

	auto result = dx.Device()->CreateDescriptorHeap(&matDescHeapDesc, IID_PPV_ARGS(mMaterialHeap.ReleaseAndGetAddressOf()));
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return false;
	}

	auto materialBuffSize = sizeof(StandardUploadMaterial);
	materialBuffSize = (materialBuffSize + 0xff) & ~0xff;

	D3D12_CONSTANT_BUFFER_VIEW_DESC matCBVDesc = {};
	matCBVDesc.BufferLocation = mMaterialBuff->GetGPUVirtualAddress();
	matCBVDesc.SizeInBytes = materialBuffSize;

	auto matDescHeapHandle = mMaterialHeap->GetCPUDescriptorHandleForHeapStart();
	auto increaseSize = dx.Device()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

	for (int i = 0; i < mMaterialDataList.size(); i++)
	{
		dx.Device()->CreateConstantBufferView(&matCBVDesc, matDescHeapHandle);
		matDescHeapHandle.ptr += increaseSize;
		matCBVDesc.BufferLocation += materialBuffSize;
	}

	return true;

ディスクリプタヒープを生成し、コンスタントバッファビューを生成します。

ビューを1つずつ作成するたびに、BufferLocationをmaterialBuffSizeだけ移動させます。

こうすることで、インデックスで望むマテリアルのビューを使用できるようになります。

void MaterialManager::AddNewMaterial(Dx12Wrapper& dx, std::string name)
{
	if (mIndexByName.find(name) != mIndexByName.end())
	{
		return;
	}

	if (mMaterialDataList.size() <= mNameList.size())
	{
		mMaterialBuff.Reset();
		mMaterialHeap.Reset();

		StandardLoadMaterial newMaterial;
		newMaterial.name = name;

		mMaterialDataList.push_back(newMaterial);
		mNameList.push_back(name);
		mIndexByName[name] = mMaterialDataList.size() - 1;

		CreateBuffer(dx);
		CreateMaterialView(dx);

		return;
	}

	for (int i = 0; i < mMaterialDataList.size(); i++)
	{
		if (mMaterialDataList[i].name.empty() == false ||
			mMaterialDataList[i].name == mRemovedMaterialName)
		{
			continue;
		}

		mMaterialDataList[i].name = name;
		mNameList.push_back(name);
		mIndexByName[name] = i;

		break;
	}
}

これは新しいマテリアルを追加するメソッドです。

私はIMGUIでマテリアル関連のUIを作成し、このメソッドを呼び出すようにしました。

新しくマテリアルが追加されると、バッファとディスクリプタヒープを再作成する必要があるため、新しく追加する時には既存のバッファとディスクリプタヒープを解放し、新たに生成します。

void MaterialManager::SetMaterialData(std::string name, const StandardLoadMaterial& setData)
{
	auto it = mIndexByName.find(name);
	if (it == mIndexByName.end())
	{
		return;
	}

	unsigned int index = it->second;

	mMaterialDataList[index].diffuse = setData.diffuse;
	mMaterialDataList[index].specular = setData.specular;
	mMaterialDataList[index].roughness = setData.roughness;
	mMaterialDataList[index].ambient = setData.ambient;
	mMaterialDataList[index].isBloom = setData.isBloom;

	int materialBufferSize = sizeof(StandardUploadMaterial);
	materialBufferSize = (materialBufferSize + 0xff) & ~0xff;

	char* mappedMaterialPtr = mMappedMaterial;
	mappedMaterialPtr += materialBufferSize * index;

	StandardUploadMaterial* uploadMaterial = reinterpret_cast<StandardUploadMaterial*>(mappedMaterialPtr);
	uploadMaterial->diffuse = setData.diffuse;
	uploadMaterial->specular = setData.specular;
	uploadMaterial->roughness = setData.roughness;
	uploadMaterial->ambient = setData.ambient;
	uploadMaterial->bloomFactor = setData.isBloom ? 1.0f : 0.0f;
}

このメソッドは、既存のマテリアルのパラメータを修正するメソッドです。

void MaterialManager::SetMaterialDescriptorHeaps(Dx12Wrapper& dx)
{
	ID3D12DescriptorHeap* heaps[] = { mMaterialHeap.Get() };
	dx.CommandList()->SetDescriptorHeaps(1, heaps);
}

これにより、マテリアルのディスクリプタヒープをどこからでも設定することができます。

void MaterialManager::SetGraphicsRootDescriptorTableMaterial(Dx12Wrapper& dx, unsigned rootParameterIndex, std::string name)
{
	auto matDescHeapHandle = mMaterialHeap->GetGPUDescriptorHandleForHeapStart();

	auto it = mIndexByName.find(name);
	if (it == mIndexByName.end())
	{
		dx.CommandList()->SetGraphicsRootDescriptorTable(rootParameterIndex, matDescHeapHandle);

		return;
	}

	unsigned int index = it->second;

	auto increaseSize = dx.Device()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

	matDescHeapHandle.ptr += increaseSize * index;

	dx.CommandList()->SetGraphicsRootDescriptorTable(rootParameterIndex, matDescHeapHandle);
}

必要なマテリアルの名前とルートパラメータのインデックスをパラメータとして受け取り、設定できるようにします。

今回はFBXActorを描画する際にマテリアルを使用できるように修正します。

HRESULT FBXRenderer::CreateRootSignature()
{
	D3D12_DESCRIPTOR_RANGE descriptorRange[3] = {};

	//Scene Buffer
	descriptorRange[0].NumDescriptors = 1;
	descriptorRange[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	descriptorRange[0].BaseShaderRegister = 0;
	descriptorRange[0].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	//World Transform Buffer
	descriptorRange[1].NumDescriptors = 1;
	descriptorRange[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	descriptorRange[1].BaseShaderRegister = 1;
	descriptorRange[1].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
	
	//Material Buffer
	descriptorRange[2].NumDescriptors = 1;
	descriptorRange[2].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	descriptorRange[2].BaseShaderRegister = 2;
	descriptorRange[2].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
	
	CD3DX12_ROOT_PARAMETER rootParam[3] = {};

	rootParam[0].InitAsDescriptorTable(1, &descriptorRange[0], D3D12_SHADER_VISIBILITY_ALL);
	rootParam[1].InitAsDescriptorTable(1, &descriptorRange[1], D3D12_SHADER_VISIBILITY_ALL);
	rootParam[2].InitAsDescriptorTable(1, &descriptorRange[2], D3D12_SHADER_VISIBILITY_ALL);

	...

FBXRendererでマテリアルコンスタントバッファのルートパラメータを追加します。

b2レジスタを使用するようにします。

struct Output
{
	float4 svpos : SV_POSITION;
	float4 pos : POSITION;
	float4 normal : NORMAL;
};

struct PixelOutput
{
	float4 color : SV_TARGET0;
	float4 normal : SV_TARGET1;
	float4 highLum : SV_TARGET2;
};

Texture2D<float4> reflectionRenderTexture: register(t0);

SamplerState smp : register(s0);

cbuffer SceneBuffer : register(b0)
{
	matrix view;
	matrix proj;
	matrix invproj;
	matrix lightCamera;
	matrix shadow;
	float4 lightVec;
	float3 eye;
};

cbuffer Transform : register(b1)
{
	matrix world;
};

cbuffer Material : register(b2)
{
	float4 diffuse;
	float3 specular;
	float roughness;
	float3 ambient;
	float bloomFactor;
};

HLSLヘッダーにマテリアル構造体を追加します。

void FBXActor::Draw(Dx12Wrapper& dx, bool isShadow) const
{
	dx.CommandList()->IASetVertexBuffers(0, 1, &mVertexBufferView);
	dx.CommandList()->IASetIndexBuffer(&mIndexBufferView);
	dx.CommandList()->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	ID3D12DescriptorHeap* transformHeap[] = { mTransformHeap.Get() };
	dx.CommandList()->SetDescriptorHeaps(1, transformHeap);
	dx.CommandList()->SetGraphicsRootDescriptorTable(1, mTransformHeap->GetGPUDescriptorHandleForHeapStart());

	MaterialManager::Instance().SetMaterialDescriptorHeaps(dx);

	unsigned int indexOffset = 0;

	for (int i = 0; i < mMeshes.size(); i++)
	{
		MaterialManager::Instance().SetGraphicsRootDescriptorTableMaterial(dx, 2, mMeshMaterialNameList[i]);

		unsigned int indexCount = mMeshes[i].indexCount;

		dx.CommandList()->DrawIndexedInstanced(indexCount, 1, indexOffset, 0, 0);

		indexOffset += indexCount;
	}
}

FBXActorのDrawメソッドで、MaterialManagerのSetMaterialDescriptorHeapsメソッドを呼び出してディスクリプタヒープを設定し、SetGraphicsRootDescriptorTableMaterialメソッドを呼び出す際にルートパラメータインデックスとマテリアル名を渡してマテリアルコンスタントバッファを設定します。

IMGUIを使用して、メッシュごとに望むマテリアルの名前を設定できるようにしました。

この機能は自分で実装してみてください。

ピクセルシェーダーを今修正しましょう。

PixelOutput PS(Output input) : SV_TARGET
{
	float3 normal = normalize(input.normal);
	float3 light = normalize(lightVec);

	float nDotL = saturate(dot(normal, -light));  
	float3 color = diffuse * nDotL;

	PixelOutput output;
	output.color.rgb = color;
	output.color.a = 1.0f;
	output.highLum = 0.0f;
	output.normal.rgb = float3((input.normal.xyz + 1.0f) / 2.0f);
	output.normal.a = 0.0f;

	return output;
}

マテリアルのコンスタントバッファのdiffuseカラーにnDotLを掛けて出力します。

ビルドして確認してみましょう。
image 1.png
水色とグレーのマテリアルを追加して適用しました。

設定したマテリアルの色通りに正しく描画されています。

しかし、ここで終わらずにシェーダーをもう少し修正してみましょう。

簡単にスペキュラーを追加します。

PixelOutput PS(Output input) : SV_TARGET
{
	float3 normal = normalize(input.normal);
	float3 light = normalize(lightVec);
  float3 view = normalize(eye - input.pos);

	float nDotL = saturate(dot(normal, -light));  
	float3 diffuseColor = diffuse * nDotL;
	
	float3 lightReflect = reflect(light, normal);
	float specularWeight = saturate(dot(lightReflect, view));
	specularWeight = pow(specularWeight, 30);
	float3 specularColor = specularWeight * specular;
	
  float3 color = diffuseColor + specularColor + ambient;

	PixelOutput output;
	output.color.rgb = color;
	output.color.a = 1.0f;
	output.highLum = 0.0f;
	output.normal.rgb = float3((input.normal.xyz + 1.0f) / 2.0f);
	output.normal.a = 0.0f;

	return output;
}

スペキュラー値を計算し、マテリアルの色を掛け合わせます。

最終的な色を作成する際に、マテリアルのアンビエント値も適用します。
image 2.png
実際に結果を確認しても大きな変化は見られませんが、ディレクショナルライトやカメラを回転させてみるとスペキュラーが適用されているのが確認できるはずです。

以前にシャドウマッピングを実装したので、FBXモデルに影を付けてみましょうか?

影が付くように修正

FBXRendererでルートパラメータを追加します。

HRESULT FBXRenderer::CreateRootSignature()
{
	D3D12_DESCRIPTOR_RANGE descriptorRange[4] = {};

	//Scene Buffer
	descriptorRange[0].NumDescriptors = 1;
	descriptorRange[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	descriptorRange[0].BaseShaderRegister = 0;
	descriptorRange[0].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	//World Transform Buffer
	descriptorRange[1].NumDescriptors = 1;
	descriptorRange[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	descriptorRange[1].BaseShaderRegister = 1;
	descriptorRange[1].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
	
	//Material Buffer
	descriptorRange[2].NumDescriptors = 1;
	descriptorRange[2].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	descriptorRange[2].BaseShaderRegister = 2;
	descriptorRange[2].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
	
	//Shadow Depth Texture
	descriptorRange[3].NumDescriptors = 1;
	descriptorRange[3].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
	descriptorRange[3].BaseShaderRegister = 0;
	descriptorRange[3].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
	
	CD3DX12_ROOT_PARAMETER rootParam[4] = {};

	rootParam[0].InitAsDescriptorTable(1, &descriptorRange[0], D3D12_SHADER_VISIBILITY_ALL);
	rootParam[1].InitAsDescriptorTable(1, &descriptorRange[1], D3D12_SHADER_VISIBILITY_ALL);
	rootParam[2].InitAsDescriptorTable(1, &descriptorRange[2], D3D12_SHADER_VISIBILITY_ALL);
	rootParam[3].InitAsDescriptorTable(1, &descriptorRange[3], D3D12_SHADER_VISIBILITY_ALL);
	...

シャドウデプスマップをテクスチャとして使用できるように追加します。
t1レジスタを使用するようにします。

サンプラーも追加します。

...
samplerDesc[1] = samplerDesc[0];
samplerDesc[1].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
samplerDesc[1].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
samplerDesc[1].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
samplerDesc[1].ComparisonFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL;
samplerDesc[1].MaxAnisotropy = 1;
samplerDesc[1].Filter = D3D12_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR;
samplerDesc[1].ShaderRegister = 1;
...
mCmdList->SetDescriptorHeaps(1, mDepthSRVHeap.GetAddressOf());
auto handle = mDepthSRVHeap->GetGPUDescriptorHandleForHeapStart();
handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
mCmdList->SetGraphicsRootDescriptorTable(3, handle);

描画命令を行う前に、シャドウデプスマップをシェーダーで使用できるように設定します。

当然ながら、シャドウデプスマップを描画した後にFBXActorを描画しましょう。

これでCPU側のコードの修正は終わりました。

シェーダーで影の処理ができるように修正しましょう。

struct Output
{
	float4 svpos : SV_POSITION;
	float4 pos : POSITION;
	float3 normal : NORMAL;
	float4 tpos : TPOS;
};

Texture2D<float4> shadowDepthTexture: register(t0);

SamplerComparisonState shadowSmp : register(s1);

HLSLヘッダーのOutputにtposを追加し、テクスチャとサンプラーを追加します。

Output VS(
	float4 pos : POSITION,
	float3 normal : NORMAL,
	float2 uv : TEXCOORD) 
{
	Output output;

	pos = mul(world, pos);

	output.pos = pos;
	output.svpos = mul(mul(proj, view), output.pos);
	output.normal = mul((float3x3)world, normal);
	output.tpos = mul(lightCamera, output.pos);

	return output;
}

頂点シェーダーで光のビュー投影空間での座標を計算するようにします。

PixelOutput PS(Output input) : SV_TARGET
{
	float3 normal = normalize(input.normal);
	float3 light = normalize(lightVec);
  float3 view = normalize(eye - input.pos);

	float nDotL = saturate(dot(normal, -light));  
	float3 diffuseColor = diffuse * nDotL;
	
	float3 lightReflect = reflect(light, normal);
	float specularWeight = saturate(dot(lightReflect, view));
	specularWeight = pow(specularWeight, 30);
	float3 specularColor = specularWeight * specular;
	
  float3 color = diffuseColor + specularColor + ambient;
  
  float3 posFromLightVP = input.tpos.xyz / input.tpos.w;
	float2 shadowUV = (posFromLightVP + float2(1, -1)) * float2(0.5, -0.5);
	float depthFromLight = shadowDepthTexture.SampleCmp(shadowSmp, shadowUV, posFromLightVP.z - 0.005f);
	float shadowWeight = lerp(0.5f, 1.0f, depthFromLight);
	
	color *= shadowWeight;

	PixelOutput output;
	output.color.rgb = color;
	output.color.a = 1.0f;
	output.highLum = 0.0f;
	output.normal.rgb = float3((input.normal.xyz + 1.0f) / 2.0f);
	output.normal.a = 0.0f;

	return output;
}

ピクセルシェーダーも修正します。

以前みた内容とあまり変わりませんね。

ビルドして確認してみましょう。
image 3.png
影ができます。

でも影が伸びているように見えますね。

これはサンプラーが原因です。

シャドウデプスマップをサンプリングする際に使用するサンプラーの設定を覚えていますか?

	samplerDesc[1] = samplerDesc[0];
	samplerDesc[1].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
	samplerDesc[1].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
	samplerDesc[1].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
	samplerDesc[1].ComparisonFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL;
	samplerDesc[1].MaxAnisotropy = 1;
	samplerDesc[1].Filter = D3D12_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR;
	samplerDesc[1].ShaderRegister = 1;

AddressU、AddressV、AddressWをD3D12_TEXTURE_ADDRESS_MODE_CLAMPに設定しました。

この設定は、UVの値が範囲を超えた場合に、その値が超えないように調整する設定でした。

(0.0より小さければ0.0に、1.0より大きければ1.0に)

そのため、範囲を超える部分が制限されて、伸びているように見えるのです。

でも、なぜ範囲を超えてしまうのでしょうか?

シャドウデプスマップを見てみましょう。
image 4.png
ステージモデルはシャドウデプスマップに描画されていませんが、描画する時にシャドウデプスマップを利用して影の有無を判断しています。

では、上のシャドウデプスマップでステージがどこに描画されるか想像してみましょう。ミクさんの後ろに描画されるでしょう。

しかし、テクスチャを見るとステージがミクさんより大きいので、ほとんどがテクスチャの範囲を超えてしまいます。

結局、解決方法はこうです。

描画するピクセルの位置を通じてシャドウデプスマップでのUVを計算しましたね。

その時、もしUVの範囲を超えていれば影を描画しないようにすればいいのです。

そうすると、ピクセルシェーダーで影のUVが範囲を超えているかどうか判断する必要がありますね。

IF文を使えば簡単にできますが、シェーダーでは分岐を使うのは良くないと言われていましたね。

IF文を使わずに判断できるように修正してみましょう。

float IsUVOutOfRange(float2 uv)
{
	float uBelowZero = 1.0 - step(0.0, uv.x); 
	float uAboveOne = step(1.0, uv.x);  

	float vBelowZero = 1.0 - step(0.0, uv.y);  
	float vAboveOne = step(1.0, uv.y); 

	float isOutOfRange = uBelowZero + uAboveOne + vBelowZero + vAboveOne;
	return saturate(isOutOfRange);
}

ピクセルシェーダーにこのようなメソッドを追加しました。

step(a, b)は、bがa以上の場合は1を、そうでない場合は0を返します。

故に、上記のコードでは、isOutOfRangeが0の場合、UVの範囲が0〜1の範囲内にあることを意味し、1以上の場合は範囲外であることになります。

最後にsaturateを使用して返すと、結果は0か1になります。

そして、ピクセルシェーダーでこのメソッドを使用するように修正します。

PixelOutput PS(Output input) : SV_TARGET
{
	float3 normal = normalize(input.normal);
	float3 light = normalize(lightVec);
  float3 view = normalize(eye - input.pos);

	float nDotL = saturate(dot(normal, -light));  
	float3 diffuseColor = diffuse * nDotL;
	
	float3 lightReflect = reflect(light, normal);
	float specularWeight = saturate(dot(lightReflect, view));
	specularWeight = pow(specularWeight, 30);
	float3 specularColor = specularWeight * specular;
	
  float3 color = diffuseColor + specularColor + ambient;
  
  float3 posFromLightVP = input.tpos.xyz / input.tpos.w;
	float2 shadowUV = (posFromLightVP + float2(1, -1)) * float2(0.5, -0.5);
	float depthFromLight = shadowDepthTexture.SampleCmp(shadowSmp, shadowUV, posFromLightVP.z - 0.005f);
	float shadowWeight = lerp(0.5f, 1.0f, depthFromLight);
	
	shadowWeight = lerp(shadowWeight, 1, IsUVOutOfRange(shadowUV));
	
	color *= shadowWeight;

	PixelOutput output;
	output.color.rgb = color;
	output.color.a = 1.0f;
	output.highLum = 0.0f;
	output.normal.rgb = float3((input.normal.xyz + 1.0f) / 2.0f);
	output.normal.a = 0.0f;

	return output;
}

lerpを使用して、IsUVOutOfRangeの結果が0の場合はshadowWeightをそのまま使用し、1の場合はshadowWeightを1に変更して影ができないようにします。
image 5.png

これで影が伸びなくなりました。

まだ一つ問題が残っています。
image 6.png
詳しく見ると、影が途切れる場合があります。

これは影を狭い範囲で描画しているために発生しています。

ライトのビュー投影行列を更新する部分を修正します。

  auto wsize = Application::Instance().GetWindowSize();

	XMMATRIX projectionMatrix = XMMatrixPerspectiveFovLH(mFov, static_cast<float>(wsize.cx) / static_cast<float>(wsize.cy), 0.1f, 1000.0f);

	SceneMatricesData* mappedSceneMatricesData;
	mSceneConstBuff->Map(0, nullptr, (void**)&mappedSceneMatricesData);

	XMVECTOR det;
	mappedSceneMatricesData->view = mCameraTransform->GetViewMatrix();
	mappedSceneMatricesData->proj = projectionMatrix;
	mappedSceneMatricesData->invProj = XMMatrixInverse(&det, projectionMatrix);
	mappedSceneMatricesData->eye = mCameraTransform->GetPosition();

	XMFLOAT4 planeVec(0, 1, 0, 0);
	XMFLOAT3 lightPosition = mDirectionalLightTransform->GetPosition();
	mappedSceneMatricesData->shadow = XMMatrixShadow(XMLoadFloat4(&planeVec), -XMLoadFloat3(&lightPosition));

	XMFLOAT3 lightDirection = mDirectionalLightTransform->GetForward();
	mappedSceneMatricesData->light = XMFLOAT4(lightDirection.x, lightDirection.y, lightDirection.z, 0);

	XMMATRIX lightMatrix = mDirectionalLightTransform->GetViewMatrix();
	mappedSceneMatricesData->lightCamera = lightMatrix * XMMatrixOrthographicLH(40, 40, 1.0f, 100.0f);

	mSceneConstBuff->Unmap(0, nullptr);
}

以前はこのようになっていました。

ライトのビュー行列に直交行列を掛けていましたが、これを修正します。

mappedSceneMatricesData->lightCamera = lightMatrix * XMMatrixOrthographicLH(60, 60, 1.0f, 1000.0f);

より広範囲で遠くまで描画できるように調整します。

image 7.png
影が途切れなくなりました。

まとめ

FBXファイルを読み込んで描画できるようにしました。

Assimpを使用すると簡単にFBXを読み込むことができるので、それほど難しくありませんでした。

舞台があることで、ミクさんがどこにいるのかもよく認識できるようになったようです。

今回は舞台のマテリアルを色だけ指定して使用しましたが、テクスチャ情報も読み込んで適用できるように改善することもできるでしょう。

次の記事ではブルームエフェクトを実装してみます。

次回

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0