0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DirectX12でミクさんを躍らせてみよう6-PMXモデル描画

Last updated at Posted at 2024-10-01

前回

こんにちは。

今回は、前回の記事で読み込んだPMXデータを実際に描画できるようにしてみましょう。

PMXデータを使用してレンダリングするだけで、後で追加するアニメーションを除けば、以前見た内容から大きな違いはありません

モデルを描画するために必要なものを確認してみましょう。

  • 頂点バッファ、頂点バッファビュー
  • インデックスバッファ、インデックスバッファビュー
  • ルートシグネチャ(ルートパラメータ)
  • パイプラインステート(頂点シェーダー、ピクセルシェーダー)
  • ディスクリプタヒープ(シェーダーリソースビュー、コンスタントバッファビュー)

必要な要素の数や構成方法は違くなりますが、必要なDirectXオブジェクトは変わっていません。

PMXモデルのクラスとして、PMXActorというクラスを作成しました。

class PMXActor : public IGetTra
{
public:
	PMXActor();
	~PMXActor();

	bool Initialize(const std::wstring& filePath, Dx12Wrapper& dx);
	void Update();
	void Draw(Dx12Wrapper& dx) const;

private:
	HRESULT CreateVbAndIb(Dx12Wrapper& dx);
	HRESULT CreateTransformView(Dx12Wrapper& dx);
	HRESULT CreateMaterialData(Dx12Wrapper& dx);
	HRESULT CreateMaterialAndTextureView(Dx12Wrapper& dx);

	void LoadVertexData(const std::vector<PMXVertex>& vertices);

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

	PMXFileData mPmxFileData;

	ComPtr<ID3D12Resource> mVertexBuffer = nullptr;
	ComPtr<ID3D12Resource> mIndexBuffer = nullptr;
	D3D12_VERTEX_BUFFER_VIEW mVertexBufferView = {};
	D3D12_INDEX_BUFFER_VIEW mIndexBufferView = {};
	
	struct UploadVertex
  {
	  XMFLOAT3 position;
	  XMFLOAT3 normal;
	  XMFLOAT2 uv;
  };

	UploadVertex* mMappedVertex;
	std::vector<UploadVertex> mUploadVertices;

	std::vector<ComPtr<ID3D12Resource>> mTextureResources;
	std::vector<ComPtr<ID3D12Resource>> mToonResources;
	std::vector<ComPtr<ID3D12Resource>> mSphereTextureResources;

	ComPtr<ID3D12DescriptorHeap> mTransformHeap = nullptr;

	DirectX::XMMATRIX* mMappedMatrices;
	ComPtr<ID3D12Resource> mTransformBuff = nullptr;

	ComPtr<ID3D12Resource> mMaterialBuff = nullptr;
	ComPtr<ID3D12DescriptorHeap> mMaterialHeap = nullptr;
	char* mMappedMaterial = nullptr;
	std::vector<LoadMaterial> mLoadedMaterial;

	struct MaterialForShader
	{
		XMFLOAT4 diffuse;
		XMFLOAT3 specular;
		float specularPower;
		XMFLOAT3 ambient;
	};
};

PMXActorは、PMXデータを受け取り、頂点バッファとインデックスバッファを生成して保持し、使用するテクスチャとマテリアルに関する情報も保持する予定です。
ルートシグネチャとパイプラインステートは?と思われるかもしれませんが、それらは後で作成するPMXRendererが保持する予定です。

それでは、頂点バッファから作成してみましょう。

頂点バッファ

まず、上記のコードでご覧のように、UploadVertexという構造体があります。

PMXを読み込んだ時に見たPMXVertexを覚えていますか。

struct PMXVertex
{
	DirectX::XMFLOAT3 position;        
	DirectX::XMFLOAT3 normal;          
	DirectX::XMFLOAT2 uv;              
	DirectX::XMFLOAT4 additionalUV[4]; 

	PMXVertexWeight weightType;
	int boneIndices[4];
	float boneWeights[4];
	DirectX::XMFLOAT3 sdefC;
	DirectX::XMFLOAT3 sdefR0;
	DirectX::XMFLOAT3 sdefR1;

	float edgeMag;
};

見てみると、ここには使用しないデータが多く含まれています。
まだアニメーションの処理を追加していませんが、CPUでスキニングを行うため、position、normal、uv以外はGPUで使用する必要がありません。そのため、実際にアップロードするデータのみを整理した構造体を別途作成します。

まずはGPUにアップロードするためのデータだけをコピーしましょう。

void PMXActor::LoadVertexData(const std::vector<PMXVertex>& vertices)
{
	mUploadVertices.resize(vertices.size());

	for (int index = 0; index < vertices.size(); ++index)
	{
		const PMXVertex& currentPmxVertex = vertices[index];
		UploadVertex& currentUploadVertex = mUploadVertices[index];

		currentUploadVertex.position = currentPmxVertex.position;
		currentUploadVertex.normal = currentPmxVertex.normal;
		currentUploadVertex.uv = currentPmxVertex.uv;
	}
}

特に変わったところはありません。position、normal、uvだけをコピーしています。

頂点バッファを作成しましょう。

D3D12_HEAP_PROPERTIES heapprop = {};
heapprop.Type = D3D12_HEAP_TYPE_UPLOAD;
heapprop.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapprop.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;

D3D12_HEAP_TYPE_UPLOADでヒーププロパティを作成します。D3D12_HEAP_TYPE_CUSTOMではないため、他の設定はUNKNOWNで構いません。

D3D12_RESOURCE_DESC resdesc = {};

size_t vertexSize = sizeof(UploadVertex);

resdesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
resdesc.Width = mUploadVertices.size() * vertexSize;
resdesc.Height = 1;
resdesc.DepthOrArraySize = 1;
resdesc.MipLevels = 1;
resdesc.Format = DXGI_FORMAT_UNKNOWN;
resdesc.SampleDesc.Count = 1;
resdesc.Flags = D3D12_RESOURCE_FLAG_NONE;
resdesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;

幅に全頂点データの合計サイズを指定します。

auto result = dx.Device()->CreateCommittedResource(&heapprop, D3D12_HEAP_FLAG_NONE, &resdesc, 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(mUploadVertices), std::end(mUploadVertices), mMappedVertex);
	mVertexBuffer->Unmap(0, nullptr);

上記で作成したD3D12_HEAP_PROPERTIESとD3D12_RESOURCE_DESCを使用して頂点バッファオブジェクトを生成し、Mapで頂点データをマッピングします。

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

頂点バッファービュー構造体のパラメーターを設定しましょう。

頂点バッファーの仮想アドレスを指定し、SizeInBytesには全データの総バイト数、StrideInBytesには1頂点あたりのバイト数を設定します。

ここまでで頂点バッファと頂点バッファビューの準備が完了です。
PMXファイルのデータを使用すること以外は、以前と変わりありません。
続いてインデックスバッファも作成しましょう。

インデックスバッファ

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;
	}

インデックスバッファは頂点バッファを作成する際に使用したD3D12_RESOURCE_DESCのWidthをインデックスデータの総サイズに変更し、そのまま使用して生成すれば良いです。

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;

データをすぐにマッピングし、インデックスバッファビューも作成します。

トランスフォームコンスタントバッファ

モデルのワールドトランスフォームを渡す定数バッファを作成しましょう。

auto buffSize = sizeof(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(mTransformBuff.ReleaseAndGetAddressOf())
	);

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

前回コンスタントバッファを作成した時に見ましたね。256バイトにアライメント!
他に特別なことはありません。

result = mTransformBuff->Map(0, nullptr, (void**)&mMappedMatrices);

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

	mMappedMatrices[0] = DirectX::XMMatrixIdentity();
	mTransformBuff->Unmap(0, nullptr);

すぐにマッピングします。
まだワールドトランスフォームを操作する手段がないため、(0, 0, 0)に位置するようXMMatrixIdentityを使用しましょう。

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 = mTransformBuff->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = buffSize;

auto handle = mTransformHeap->GetCPUDescriptorHandleForHeapStart();
auto incSize = dx.Device()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

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

ディスクリプタヒープを作成し、ヒープの最初の空間に定数ビューを生成します。

マテリアルバッファの作成

マテリアル情報を渡すためのバッファを作成しましょう。
マテリアルもPMXMaterialをそのまま使用せず、必要な情報のみを含む構造体を別途作成しました。

struct MaterialForShader
{
	XMFLOAT4 diffuse;
	XMFLOAT3 specular;
	float specularPower;
	XMFLOAT3 ambient;
};
int materialBufferSize = sizeof(MaterialForShader);
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 * mPmxFileData.materials.size()),
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(mMaterialBuff.ReleaseAndGetAddressOf())
	);
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

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

	mLoadedMaterial.resize(mPmxFileData.materials.size());

	char* mappedMaterialPtr = mMappedMaterial;

	int materialIndex = 0;
	for (const auto& material : mPmxFileData.materials)
	{
		mLoadedMaterial[materialIndex].visible = true;
		mLoadedMaterial[materialIndex].name = UnicodeUtil::WstringToString(material.name);
		mLoadedMaterial[materialIndex].diffuse = material.diffuse;
		mLoadedMaterial[materialIndex].specular = material.specular;
		mLoadedMaterial[materialIndex].specularPower = material.specularPower;
		mLoadedMaterial[materialIndex].ambient = material.ambient;
		mLoadedMaterial[materialIndex].isTransparent = false;
		materialIndex++;

		MaterialForShader* uploadMat = reinterpret_cast<MaterialForShader*>(mappedMaterialPtr);
		uploadMat->diffuse = material.diffuse;
		uploadMat->specular = material.specular;
		uploadMat->specularPower = material.specularPower;
		uploadMat->ambient = material.ambient;

		mappedMaterialPtr += materialBufferSize;
	}

	mMaterialBuff->Unmap(0, nullptr);

	return S_OK;

コンスタントバッファとして使用されるため、256バイトにアラインメントしてバッファを生成します。
そして、PMXMaterialデータから必要な部分だけをコピーしてマッピングします。

このマテリアルバッファのコンスタントバッファビューは、後でテクスチャと同じディスクリプタヒープを使用するため、後で作成します。

Dx12Wrapperにシーン情報バッファを追加

モデルを描画するためには、ワールドトランスフォーム以外にもビュー行列、投影行列が必要です。
このような情報はオブジェクトごとに変わる情報ではないため、Dx12Wrapperで管理することにしましょう。
構造体を定義します。

	struct SceneMatricesData
	{
		DirectX::XMMATRIX view;
		DirectX::XMMATRIX proj;
		DirectX::XMFLOAT3 eye;
	};

今回は使用しませんが、後で使用する予定があるため、あらかじめカメラの位置を渡せるようにしておきます。

バッファリソースとディスクリプタヒープをメンバーとして追加しましょう。

ComPtr<ID3D12Resource> mSceneConstBuff = nullptr;
ComPtr<ID3D12DescriptorHeap> mSceneDescHeap = nullptr;

バッファを生成し、データをマッピングし、ディスクリプタヒープまで一度に作成します。

DXGI_SWAP_CHAIN_DESC1 desc = {};
auto result = mSwapChain->GetDesc1(&desc);
auto heapProp = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
auto resDesc = CD3DX12_RESOURCE_DESC::Buffer((sizeof(SceneMatricesData) + 0xff) & ~0xff);

result = mDev->CreateCommittedResource(
		&heapProp,
		D3D12_HEAP_FLAG_NONE,
		&resDesc,
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(mSceneConstBuff.ReleaseAndGetAddressOf())
	);

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

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

XMFLOAT3 eye(0, 10, -30);
XMFLOAT3 target(0, 10, 0);
XMFLOAT3 up(0, 1, 0);

XMMATRIX lookMatrix = XMMatrixLookAtLH(XMLoadFloat3(&eye), XMLoadFloat3(&target), XMLoadFloat3(&up));
XMMATRIX projectionMatrix = XMMatrixPerspectiveFovLH(XM_PIDIV4, static_cast<float>(desc.Width) / static_cast<float>(desc.Height), 0.1f, 1000.0f);

mappedSceneMatricesData->view = lookMatrix;
mappedSceneMatricesData->proj = projectionMatrix;
mappedSceneMatricesData->eye = eye;

mSceneConstBuff->Unmap(0, nullptr);

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

result = mDev->CreateDescriptorHeap(&descHeapDesc, IID_PPV_ARGS(mSceneDescHeap.ReleaseAndGetAddressOf()));

auto heapHandle = mSceneDescHeap->GetCPUDescriptorHandleForHeapStart();

D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = mSceneConstBuff->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = mSceneConstBuff->GetDesc().Width;

mDev->CreateConstantBufferView(&cbvDesc, heapHandle);

return result;

バッファの作成部分については、もう慣れているでしょう。

ここで注目すべき部分は、ビュー行列と投影行列を作成するところです。
DirectXではこのような行列を簡単に作成するためのメソッドを提供しています。

ビュー行列はXMMatrixLookAtLHメソッドで作成します。
パラメータとしてカメラの位置、カメラが向いている位置、UPベクトルを渡します。
カメラが向いている位置の代わりに、カメラが見ている方向のベクトルを渡すこともできます。

投影行列はXMMatrixPerspectiveFovLHで作成できます。
このメソッド名のLHはLeft Handです。つまり左手座標系の透視投影行列を作成する関数です。
透視投影以外にも正投影行列を作成するメソッドもあります。

画面比率をパラメータとして渡す必要があるため、バックバッファのサイズを取得して比率を計算しています。
最初のパラメータはFov(Field of View)です。XM_PIDIV4は45度です。
最後のパラメータは、ニアクリッピング平面とファークリッピング平面の距離です。

カメラを移動させる手段がまだないので、適当に定義しておきます。
後でカメラが固定されておらず移動可能になった場合は、毎フレームデータを再マッピングする必要がありますね。

マッピングが完了したら、ディスクリプタヒープを作成し、コンスタントビューを生成します。

これでシーン情報も渡すことができます。
外部からシーン情報のディスクリプタヒープをバインドできるようにメソッドも追加しましょう。

void Dx12Wrapper::SetSceneBuffer(int rootParameterIndex) const
{
	ID3D12DescriptorHeap* sceneheaps[] = { mSceneDescHeap.Get() };
	mCmdList->SetDescriptorHeaps(1, sceneheaps);
	mCmdList->SetGraphicsRootDescriptorTable(rootParameterIndex, mSceneDescHeap->GetGPUDescriptorHandleForHeapStart());
}

Dx12Wrapper では、使用する側がシーン情報バッファがどのルートパラメータ番号なのかを知ることができないため、呼び出し側からパラメータを渡せるようにします。

Dx12Wrapperにテクスチャ管理を追加

Dx12Wrapperにテクスチャ名を渡すと、そのテクスチャ画像がプロジェクトフォルダにあるかを確認し、存在する場合はその画像のテクスチャリソースを生成して返すメソッドを追加します。
すべてのテクスチャリソースはDx12Wrapperが保持することになります。

Dx12Wrapper に次のように追加します。

using LoadLambda_t = std::function<HRESULT(const std::wstring& path, DirectX::TexMetadata*, DirectX::ScratchImage&)>;
std::map<std::string, LoadLambda_t> mLoadLambdaTable;

このようにラムダ式で追加する理由は、DirectXTexで画像を読み込む際にファイル形式によって異なるメソッドを使用する必要があるからです。

	mLoadLambdaTable["sph"]
		= mLoadLambdaTable["spa"]
		= mLoadLambdaTable["bmp"]
		= mLoadLambdaTable["png"]
		= mLoadLambdaTable["jpg"]
		= [](const std::wstring& path, TexMetadata* meta, ScratchImage& img)
		-> HRESULT
	{
		return LoadFromWICFile(path.c_str(), WIC_FLAGS_NONE, meta, img);
	};

	mLoadLambdaTable["tga"]
		= [](const std::wstring& path, TexMetadata* meta, ScratchImage& img)
		-> HRESULT
	{
		return LoadFromTGAFile(path.c_str(), meta, img);
	};

	mLoadLambdaTable["dds"]
		= [](const std::wstring& path, TexMetadata* meta, ScratchImage& img)
		-> HRESULT
	{
		return LoadFromDDSFile(path.c_str(), DDS_FLAGS_NONE, meta, img);
	};

TGAファイルにはLoadFromTGAFile、DDSファイルにはLoadFromDDSFileを使用する必要があるため、ファイル形式に応じて異なるメソッドが呼び出されるようにしています。

これからテクスチャリソースを生成するメソッドを作成する必要がありますが、
以前にロードしたことがある画像をロードしようとする場合、わざわざ再度生成する必要はないですよね?

そのため、生成が完了したテクスチャリソースはコンテナに保存しておき、すでにロードされたことがある場合は保存していたリソースを返すようにします。

Dx12Wrapperのメンバーに以下のように追加します。

std::map<std::wstring, ComPtr<ID3D12Resource>> mResourceTableW;

テクスチャリソースを外部に返すメソッドは次のとおりです。

ComPtr<ID3D12Resource> Dx12Wrapper::GetTextureByPath(const std::wstring& texpath)
{
	auto it = mResourceTableW.find(texpath);
	if (it != mResourceTableW.end())
	{
		return mResourceTableW[texpath];
	}
	else
	{
		return ComPtr<ID3D12Resource>(CreateTextureFromFile(texpath));
	}
}

テクスチャリソースを生成するメソッドは次のとおりです。

ID3D12Resource* Dx12Wrapper::CreateTextureFromFile(const std::wstring& texpath)
{
	TexMetadata metadata = {};
	ScratchImage scratchImg = {};

	auto ext = GetExtension(texpath);

	auto result = mLoadLambdaTable[ext](
		texpath,
		&metadata,
		scratchImg
		);

	if (FAILED(result))
	{
		return nullptr;
	}

	auto img = scratchImg.GetImage(0, 0, 0);

	D3D12_HEAP_PROPERTIES texHeapProp = CD3DX12_HEAP_PROPERTIES(D3D12_CPU_PAGE_PROPERTY_WRITE_BACK, D3D12_MEMORY_POOL_L0);

	D3D12_RESOURCE_DESC resDesc = CD3DX12_RESOURCE_DESC::Tex2D(
		metadata.format,
		metadata.width,
		metadata.height,
		metadata.arraySize,
		metadata.mipLevels
	);

	ID3D12Resource* texbuff = nullptr;
	result = mDevice->CreateCommittedResource(
		&texHeapProp,
		D3D12_HEAP_FLAG_NONE,
		&resDesc,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		nullptr,
		IID_PPV_ARGS(&texbuff)
	);

	if (FAILED(result))
	{
		return nullptr;
	}

	result = texbuff->WriteToSubresource(
		0,
		nullptr,
		img->pixels,
		img->rowPitch,
		img->slicePitch
	);

	if (FAILED(result))
	{
		return nullptr;
	}

	mResourceTableW[texpath] = texbuff;
	return texbuff;
}

ファイル形式に応じて異なるメソッドが呼び出されるようにラムダ式を使用している部分と、生成されたリソースをコンテナに保存する部分以外は、以前に見た内容とほとんど変わりません。

さらに、テクスチャ画像がプロジェクトフォルダに存在しない場合に使用するデフォルトの単色テクスチャを作成しておきましょう。

ID3D12Resource* Dx12Wrapper::CreateDefaultTexture(size_t width, size_t height)
{
	auto resDesc = CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R8G8B8A8_UNORM, width, height);
	auto texHeapProp = CD3DX12_HEAP_PROPERTIES(D3D12_CPU_PAGE_PROPERTY_WRITE_BACK, D3D12_MEMORY_POOL_L0);
	ID3D12Resource* buff = nullptr;
	auto result = mDevice->CreateCommittedResource(
		&texHeapProp,
		D3D12_HEAP_FLAG_NONE,
		&resDesc,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		nullptr,
		IID_PPV_ARGS(&buff)
	);
	if (FAILED(result)) {
		assert(SUCCEEDED(result));
		return nullptr;
	}
	return buff;
}

このように、好みの解像度でテクスチャオブジェクトを簡単に作成できるようメソッドを追加します。

ID3D12Resource* Dx12Wrapper::CreateWhiteTexture()
{
	ID3D12Resource* whiteBuff = CreateDefaultTexture(4, 4);

	std::vector<unsigned char> data(4 * 4 * 4);
	std::fill(data.begin(), data.end(), 0xff);

	auto result = whiteBuff->WriteToSubresource(
		0,
		nullptr,
		data.data(),
		4 * 4,
		data.size()
	);

	assert(SUCCEEDED(result));

	return whiteBuff;
}

これは白色で満たされたテクスチャを作成するメソッドです。
std::fillで白色の値でデータを埋めます。
他の単色のテクスチャもこのように作成できます。

ディスクリプタヒープを作成し、シェーダーリソースビューとコンスタントバッファビューを生成しましょう。

ディスクリプタヒープ

D3D12_DESCRIPTOR_HEAP_DESC matDescHeapDesc = {};
matDescHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
matDescHeapDesc.NodeMask = 0;
matDescHeapDesc.NumDescriptors = mPmxFileData.materials.size() * 4;
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 result;
}

ディスクリプタの数はマテリアル数の4倍です。
その理由は、マテリアルごとに使用するテクスチャとマテリアル値が異なるためです。
マテリアル値、カラーテクスチャ、トゥーンテクスチャ、スフィアテクスチャをディスクリプタヒープに生成するため、マテリアル1つにつき4つのビューを作成します。
これらのテクスチャがどのようなものかについては、後ほど説明しましょう。

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

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

D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;

auto matDescHeapH = mMaterialHeap->GetCPUDescriptorHandleForHeapStart();
auto incSize = dx.Device()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

マテリアルのコンスタントビューとテクスチャのシェーダーリソースビューを生成するためにディスクリプタを作成します。

for (int i = 0; i < mPmxFileData.materials.size(); ++i)
	{
		dx.Device()->CreateConstantBufferView(&matCBVDesc, matDescHeapH);

		matDescHeapH.ptr += incSize;
		matCBVDesc.BufferLocation += materialBuffSize;

		if (mTextureResources[i] == nullptr)
		{
			ComPtr<ID3D12Resource> whiteTexture = dx.GetWhiteTexture();
			srvDesc.Format = whiteTexture->GetDesc().Format;
			dx.Device()->CreateShaderResourceView(whiteTexture.Get(), &srvDesc, matDescHeapH);
		}
		else
		{
			srvDesc.Format = mTextureResources[i]->GetDesc().Format;
			dx.Device()->CreateShaderResourceView(mTextureResources[i].Get(),&srvDesc,matDescHeapH);
		}

		matDescHeapH.ptr += incSize;

		if (mToonResources[i] == nullptr)
		{
			ComPtr<ID3D12Resource> gradTexture = dx.GetWhiteTexture();
			srvDesc.Format = gradTexture->GetDesc().Format;
			dx.Device()->CreateShaderResourceView(gradTexture.Get(), &srvDesc, matDescHeapH);
		}
		else
		{
			srvDesc.Format = mToonResources[i]->GetDesc().Format;
			dx.Device()->CreateShaderResourceView(mToonResources[i].Get(), &srvDesc, matDescHeapH);
		}

		matDescHeapH.ptr += incSize;

		if (mSphereTextureResources[i] == nullptr)
		{
			ComPtr<ID3D12Resource> whiteTexture = dx.GetWhiteTexture();
			srvDesc.Format = whiteTexture->GetDesc().Format;
			dx.Device()->CreateShaderResourceView(whiteTexture.Get(), &srvDesc, matDescHeapH);
		}
		else
		{
			srvDesc.Format = mSphereTextureResources[i]->GetDesc().Format;
			dx.Device()->CreateShaderResourceView(mSphereTextureResources[i].Get(), &srvDesc, matDescHeapH);
		}

		matDescHeapH.ptr += incSize;
	}

	return result;

マテリアルの数だけ、マテリアル値のコンスタントバッファビュー、カラーテクスチャのシェーダーリソースビュー、トゥーンテクスチャのシェーダーリソースビュー、スフィアテクスチャのシェーダーリソースビューを順番に生成します。

コンスタントバッファビューとシェーダーリソースビューのサイズは同じであるため、ハンドルはGetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV)ずつ移動させます。

ココンスタントバッファビューは1つ作成するたびに、D3D12_CONSTANT_BUFFER_VIEW_DESCのバッファ位置をバッファサイズ分だけ移動させます。

現在のマテリアルのテクスチャ画像が存在せず、テクスチャオブジェクトを作成できなかった場合は、単色テクスチャを設定します。

ここまでの内容をInitializeメソッドで全て呼び出すように記述し、ビルドしてみてください。
正しく従っていれば、問題なくビルドできるはずです。
これで必要なリソースは全て準備できました。

これからレンダリングに必要なものを管理するクラスを作成しましょう。

Rendererクラス

パイプラインステートとルートシグネチャを持つPMXRendererクラスを作成します。
「Actorがすべて持っていてはいけないの?」と思われるかもしれません。
このように分ける理由は、複数のPMXモデルをロードした際に対応できるようにするためです。
どのPMXファイルでもロードしてPMXActorオブジェクトを作成すれば、すべてPMXRendererを通してレンダリングできると便利ですよね。

class PMXRenderer
{
private:
	Dx12Wrapper& mDx12;
	template<typename T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;

	ComPtr<ID3D12PipelineState> mPipeline = nullptr;
	ComPtr<ID3D12RootSignature> mRootSignature = nullptr;

	HRESULT CreateGraphicsPipelineForPMX();
	HRESULT CreateRootSignature();

	bool CheckShaderCompileResult(HRESULT result, ID3DBlob* error = nullptr);

	std::vector<std::shared_ptr<PMXActor>> mActors;


public:
	PMXRenderer(Dx12Wrapper& dx12);
	~PMXRenderer();
	void Update();

	void BeforeDrawForwardPipeline();
	void Draw() const;

	void AddActor(std::shared_ptr<PMXActor> actor);
	const PMXActor* GetActor();

	ID3D12PipelineState* GetPipelineState();
	ID3D12RootSignature* GetRootSignature();

	Dx12Wrapper& GetDirect() const;
};

BeforeDrawForwardPipelineでは使用するパイプラインステートとルートシグネチャを設定する命令を行います。
そして、Drawでは登録されたActorのDrawを呼び出します。
シーン情報バッファもバインドしましょう。

void PMXRenderer::BeforeDrawAtForwardPipeline()
{
	auto cmdList = mDx12.CommandList();
	cmdList->SetPipelineState(mPipeline.Get());
	cmdList->SetGraphicsRootSignature(mRootSignature.Get());
 	
    mDx12.SetSceneBuffer(0);
}
void PMXRenderer::Draw() const
{
	for (auto& actor : mActors)
	{
		actor->Draw(_dx12);
	}
}

それでは、ルートシグネチャとパイプラインステートを生成します。

ルートシグネチャ

ルートパラメータを作成する前に、まずディスクリプタレンジを作成します。

D3D12_DESCRIPTOR_RANGE descTblRange[4] = {};
descTblRange[0].NumDescriptors = 1;
descTblRange[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
descTblRange[0].BaseShaderRegister = 0;
descTblRange[0].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

descTblRange[1].NumDescriptors = 1;
descTblRange[1].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
descTblRange[1].BaseShaderRegister = 1;
descTblRange[1].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

descTblRange[2].NumDescriptors = 1;
descTblRange[2].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
descTblRange[2].BaseShaderRegister = 2;
descTblRange[2].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

descTblRange[3].NumDescriptors = 3;
descTblRange[3].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
descTblRange[3].BaseShaderRegister = 0;
descTblRange[3].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

最初のコンスタントバッファはシーンに関する情報です。ディレクショナルライトの方向、カメラの位置など、現在のシーンに関する情報をシェーダーで使用できるようにする予定です。まだコンスタントバッファは作成していませんが、後で使用するため、あらかじめ入れておきましょう。
描画する時に使用しない場合、ルートパラメータにビューをバインドしなくても問題ありません。

2番目のコンスタントバッファはモデルのワールドトランスフォームです。
3番目はマテリアル値です。

4番目からテクスチャ用のディスクリプタレンジです。
NumDescriptorsを3に設定しました。BaseShaderRegisterが0なので、0から始まる3つのレジスタを使用することになります。t0、t1、t2を使用することになりますね。このように、1つのディスクリプタレンジで複数のディスクリプタを使用するように設定できます。

これで作成したディスクリプタレンジを使用してルートパラメータを作成できます。

D3D12_ROOT_PARAMETER rootparam[3] = {};
rootparam[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootparam[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
rootparam[0].DescriptorTable.pDescriptorRanges = &descTblRange[0];
rootparam[0].DescriptorTable.NumDescriptorRanges = 1;

rootparam[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootparam[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
rootparam[1].DescriptorTable.pDescriptorRanges = &descTblRange[1];
rootparam[1].DescriptorTable.NumDescriptorRanges = 1;

rootparam[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootparam[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
rootparam[2].DescriptorTable.pDescriptorRanges = &descTblRange[2];
rootparam[2].DescriptorTable.NumDescriptorRanges = 2;

0番と1番のインデックスには特に変わったところはありません。
2番を見てみましょう。
ルートパラメータも複数のディスクリプタレンジを設定できます。

NumDescriptorRangesを2に設定し、ディスクリプタレンジの開始アドレスとしてdescTblRangeの2番目のインデックスのアドレスを渡しました。

これにより、descTblRangeの2番目と3番目のインデックスが設定されます。

そうすると、レンダリング時にルートパラメータのインデックス2で一度にマテリアルのコンスタントバッファビュー1つと、テクスチャのシェーダーリソース3つを設定するよう指示できます。

以前にディスクリプタヒープを作成した際、マテリアル1つにつきコンスタントバッファビュー1つ、シェーダーリソースビュー3つの順で作成したのを覚えていますか?

後で各マテリアルの開始位置のハンドルを取得してバインドすればよいでしょう。

作成したルートパラメータをD3D12_ROOT_SIGNATURE_DESCに設定しましょう。

D3D12_ROOT_SIGNATURE_DESC rootSignatureDesc = {};

rootSignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
rootSignatureDesc.pParameters = rootparam;
rootSignatureDesc.NumParameters = 3;

サンプラーも設定しましょう。

D3D12_STATIC_SAMPLER_DESC samplerDesc[3] = {};

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;

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].Filter = D3D12_FILTER_ANISOTROPIC;
samplerDesc[1].ShaderRegister = 1;

今回は2つのサンプラーを使用します。1番目のインデックスのサンプラーはトゥーンテクスチャに使用する予定です。

D3D12_ROOT_SIGNATURE_DESCに設定します。

rootSignatureDesc.pStaticSamplers = samplerDesc;
rootSignatureDesc.NumStaticSamplers = 3;

D3D12_ROOT_SIGNATURE_DESCの作成も完了したので、ルートシグネチャを生成します。

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

auto result = D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &rootSigBlob, &errorBlob);

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

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

return result;

次はパイプラインステートです。

パイプラインステート

使用するシェーダーをまずコンパイルします。

ComPtr<ID3DBlob> vsBlob = nullptr;
ComPtr<ID3DBlob> psBlob = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;

	UINT flags = 0;
#if defined( DEBUG ) || defined( _DEBUG )
	flags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif

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

if (!CheckShaderCompileResult(result, errorBlob.Get()))
{
	assert(0);
	return result;
}
	
result = D3DCompileFromFile(L"PMXPixelShader.hlsl",
		nullptr,
		D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"PS",
		"ps_5_0",
		flags,
		0,
		&psBlob,
		&errorBlob);

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

シェーダーはまだ作成していませんが、先に作成しておきましょう。

使用する頂点構造体を覚えていますか?

struct UploadVertex
{
	XMFLOAT3 position;
	XMFLOAT3 normal;
	XMFLOAT2 uv;
};

これに合わせて入力レイアウトを作成します。

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 gpipeline = {};

gpipeline.pRootSignature = _rootSignature.Get();

gpipeline.VS.pShaderBytecode = vsBlob->GetBufferPointer();
gpipeline.VS.BytecodeLength = vsBlob->GetBufferSize();
gpipeline.PS.pShaderBytecode = psBlob->GetBufferPointer();
gpipeline.PS.BytecodeLength = psBlob->GetBufferSize();

gpipeline.SampleMask = D3D12_DEFAULT_SAMPLE_MASK;

gpipeline.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
gpipeline.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;

gpipeline.BlendState.AlphaToCoverageEnable = false;
gpipeline.BlendState.IndependentBlendEnable = false;

gpipeline.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);

gpipeline.InputLayout.pInputElementDescs = inputLayout;
gpipeline.InputLayout.NumElements = _countof(inputLayout);
gpipeline.IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED;
gpipeline.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;

gpipeline.NumRenderTargets = 1;
gpipeline.RTVFormats[0] = DXGI_FORMAT_B8G8R8A8_UNORM;

gpipeline.SampleDesc.Count = 1;
gpipeline.SampleDesc.Quality = 0;

gpipeline.DepthStencilState.DepthEnable = true;
gpipeline.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
gpipeline.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
gpipeline.DepthStencilState.StencilEnable = false;
gpipeline.DSVFormat = DXGI_FORMAT_D32_FLOAT;

result = _dx12.Device()->CreateGraphicsPipelineState(&gpipeline, IID_PPV_ARGS(_pipeline.ReleaseAndGetAddressOf()));
	if (FAILED(result)) 
    {
		assert(SUCCEEDED(result));
	}
return result;

パイプラインステートにシェーダーをバインドします。

CullModeはD3D12_CULL_MODE_NONEにします。キャラクターの口などの部分があるため、バックフェイスカリングを行うと描画されない部分が生じます。

ブレンドステートは、とりあえずCD3DX12_BLEND_DESCを使用してデフォルト値にします。

そして今回は深度テストを行えるようにDepthEnableをtrueにします。

デプステスト

デプステストについて説明しておきましょう。

デプステストは深度値に基づいて描画するかどうかを決定するプロセスです。

DirectXでデプステストを有効にし、デプスバッファを作成してバインドしてから描画すると、結果の深度値が記録されます。(もちろん、記録しないようにすることもできます。)
これがどのように使用されるか説明しましょう。

次のような球体オブジェクトをレンダリングすると想像してみましょう。
image.png
次に、カメラからより遠い立方体オブジェクトをレンダリングします。
image.png
正しく描画されていれば、このような画面になります。
image.png
しかし、デプステストを行わずに球体→立方体の順で描画すると、このような画面になります。
image.png
立方体がより遠くにあるのに画面上で球が見えないのであれば、望ましい結果ではありません。

それでは、カメラから遠いオブジェクトから描画すればいいのではないでしょうか?
そうすれば、望む結果が見えるはずです。

しかし、問題があります。
立方体を描画し、次に球が描画される部分を考えてください。

立方体を描画するためにピクセルシェーダーを呼び出し、球を描画するためにもう一度ピクセルシェーダーで描画することになります。
このように1つのピクセルを複数回重ね描きすることをピクセルオーバードローと呼びます。

単純に考えても、不必要なピクセルシェーダーの呼び出しを引き起こすため、計算量が増えるでしょう。
image.png
では、まとめると2つの問題があります。

  1. 近いオブジェクトから描画すると、近いオブジェクトが隠れてしまう。
  2. 遠いオブジェクトから描画すると、ピクセルオーバードローが発生する。

これを解決するためにデプステストを使用します。
オブジェクトを描画するたびに深度値をデプスバッファに記録し、カメラとの距離に関係なくレンダリングすると仮定しましょう。
カメラに近いオブジェクトを先に描画すると、その部分の深度値が記録されます。
image.png
図の値は例示です。

次に遠いオブジェクトを描画する際、既に描画されたピクセルと描画すべきピクセルの深度値を記録された深度値と比較します。

記録された深度値より値が大きい場合(近いほど0に近く、遠いほど1に近くなる)、ピクセルシェーダー自体が呼び出されなくなります。
image.png
問題が解決されました。
順序に関係なく、遠くのオブジェクトが近くのオブジェクトを隠すことはなく、近くのオブジェクトから描画してもピクセルオーバードローは発生しません。

そのため、ほとんどの場合、最適化のためにオーバードローが発生しないように、近くのオブジェクトから描画できるように並べ替えます(不透明オブジェクトの場合です。透明オブジェクトは異なります)。

シェーダーの作成

それではシェーダーを作成しましょう。
頂点シェーダーとピクセルシェーダーのファイルを作成する際、プロパティでエントリーポイント名を実際のコードのメソッド名と一致させることを忘れないでください。シェーダーモデルを5.0に設定することも忘れずに。

まず、HLSLヘッダーファイルから作成していきましょう。

struct Output
{
	float4 svpos : SV_POSITION;
	float2 uv : TEXCOORD;
};

Texture2D<float4> tex : register(t0);
Texture2D<float4> toon : register(t1);
Texture2D<float4> sph : register(t2);
SamplerState smp : register(s0);
SamplerState smpToon : register(s1);

cbuffer SceneBuffer : register(b0)
{
	matrix view;
	matrix proj;
	float3 eye;
};

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

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

Outputは頂点シェーダーの結果として返される構造体です。
今回は基本的なものだけを返すようにします。

テクスチャは色、トゥーン、スフィアの3枚を使用します。
サンプラーは2つ
順番にシーンの情報を伝えるコンスタントバッファ
ワールドトランスフォームバッファ
マテリアルバッファです。

次は頂点シェーダーです。

#include "PmxShaderHeader.hlsli"

Output VS(
	float4 pos : POSITION,
	float3 normal : NORMAL,
	float2 uv : TEXCOORD)
{
	Output output;
	pos = mul(world, pos);
	output.svpos = mul(mul(proj, view), pos);
	output.uv = uv;
	return output;
}

今回は頂点の射影空間までの空間変換のみを行います。
そして、uvも渡します。

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

#include "PmxShaderHeader.hlsli"

float4 PS(Output input) : SV_TARGET
{
	float4 color = tex.Sample(smp, input.uv);
	return color;
}

今回は色テクスチャのみをサンプリングして色だけを出力するようにします。

深度ステンシルバッファの生成

先ほどパイプラインステートを生成する際に深度テストを有効化しましたね。
今回は深度テストに使用するバッファを生成します。

Dx12Wrapperにメンバーを追加します。

ComPtr<ID3D12DescriptorHeap> mDepthStencilViewHeap = nullptr;
ComPtr<ID3D12DescriptorHeap> mDepthSRVHeap = nullptr;
ComPtr<ID3D12Resource> mDepthBuffer = nullptr;

作成したバッファを深度ステンシルビューとしても使用し、シェーダーリソースビューとしても使用する必要があるため、ディスクリプタヒープを2つ用意します。

上記のメンバーを初期化するメソッドも追加します。

HRESULT Dx12Wrapper::CreateDepthStencilView()
{
  DXGI_SWAP_CHAIN_DESC1 desc = {};
  auto result = mSwapChain->GetDesc1(&desc);

  D3D12_RESOURCE_DESC depthResDesc = CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R32_TYPELESS, desc.Width, desc.Height);
  depthResDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;

  D3D12_HEAP_PROPERTIES depthHeapProp = {};
  depthHeapProp.Type = D3D12_HEAP_TYPE_DEFAULT;
	depthHeapProp.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
	depthHeapProp.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;

	D3D12_CLEAR_VALUE depthClearValue = {};
	depthClearValue.DepthStencil.Depth = 1.0f;
	depthClearValue.Format = DXGI_FORMAT_D32_FLOAT;

	result = mDevice->CreateCommittedResource(
		&depthHeapProp,
		D3D12_HEAP_FLAG_NONE,
		&depthResDesc,
		D3D12_RESOURCE_STATE_DEPTH_WRITE,
		&depthClearValue,
		IID_PPV_ARGS(mDepthBuffer.ReleaseAndGetAddressOf())
	);
	if (FAILED(result))
	{
		return result;
	}

	D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
	dsvHeapDesc.NumDescriptors = 1;
	dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
	dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;

	result = mDevice->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(mDepthStencilViewHeap.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;

	auto handle = mDepthStencilViewHeap->GetCPUDescriptorHandleForHeapStart();

	mDevice->CreateDepthStencilView(mDepthBuffer.Get() ,&dsvDesc, handle);

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

	result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&mDepthSRVHeap));

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

	D3D12_SHADER_RESOURCE_VIEW_DESC depthSrvResDesc = {};
	depthSrvResDesc.Format = DXGI_FORMAT_R32_FLOAT;
	depthSrvResDesc.Texture2D.MipLevels = 1;
	depthSrvResDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
	depthSrvResDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;

  auto srvHandle = mDepthSRVHeap->GetCPUDescriptorHandleForHeapStart();
	mDevice->CreateShaderResourceView(mDepthBuffer.Get(), &depthSrvResDesc, srvHandle);

  return result;
}

スワップチェーンのバッファと同じサイズのバッファをDXGI_FORMAT_D24_UNORM_S8_UINTフォーマットで生成します。

作成したバッファを使用して、それぞれのディスクリプタヒープに深度ステンシルビューとシェーダーリソースビューを作成します。

深度ステンシルバッファのバインディング

auto dsvHeapPointer = mDepthStencilViewHeap->GetCPUDescriptorHandleForHeapStart();

mCmdList->OMSetRenderTargets(1, &rtvH, false, &dsvHeapPointer);

深度ステンシルビューはOMSetRenderTargetsの最後のパラメータとして設定します。
このように設定すると、深度テストを行う際にここで設定した深度バッファを使用することになります。

mCmdList->ClearDepthStencilView(dsvHeapPointer, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);

すべての描画が終わったら、デプスバッファもクリアすることを忘れないでください。

PMXActorのDrawメソッド

PMXActorのDrawメソッドをまだ作成していません。
これは、この記事で追加したものを実際に使用することになる最も重要な部分です。

まず、ルートパラメータをどのような順序で設定したか思い出してみましょう。
0番目はシーン情報に関するコンスタントバッファ
1番目はワールドトランスフォームに関するコンスタントバッファ
2番目はマテリアルコンスタントバッファとテクスチャバッファ
でした。

では、PMXActorのDrawメソッドが呼び出される前にシーン情報バッファをバインディングする必要があります。

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

そして Draw を呼び出します。

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

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

	ID3D12DescriptorHeap* mdh[] = { mMaterialHeap.Get() };

	dx.CommandList()->SetDescriptorHeaps(1, mdh);

	auto cbvSrvIncSize = dx.Device()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) * 4;

	auto materialH = mMaterialHeap->GetGPUDescriptorHandleForHeapStart();
	unsigned int idxOffset = 0;

	for (int i = 0; i < mPmxFileData.materials.size(); i++)
	{
		unsigned int numFaceVertices = mPmxFileData.materials[i].numFaceVertices;

		dx.CommandList()->SetGraphicsRootDescriptorTable(2, materialH);
		dx.CommandList()->DrawIndexedInstanced(numFaceVertices, 1, idxOffset, 0, 0);

		materialH.ptr += cbvSrvIncSize;
		idxOffset += numFaceVertices;
	}
}

頂点バッファとインデックスバッファをバインドします。

1番目のルートパラメータにワールドトランスフォームのコンスタントバッファビューをバインドします。

そして最後の2番目にマテリアルとテクスチャのビューをバインドします。

マテリアル単位でディスクリプタヒープハンドルを移動させ、インデックスのオフセットも移動させています。

描画

では、画面に正しく描画されているか確認してみましょう。
image.png
おお、ミクさんがうまく描画されています。しかし、顔が少し変ですね。
そして、色だけが塗られているので、まだ単調に見えます。

デバッグツールPIX

これまで描画された結果は、ビルドした結果を通して確認してきました。
もしビルドしたのに結果がおかしい場合、どうすればいいでしょうか?

頂点が正しく渡されたか、テクスチャがバインドされたか、コンスタントバッファがバインドされたかを確認したいはずです。
そのような場合、レンダリングデバッガーツールを使用して確認することができます。

ここではMicrosoftのPIXを使用します。

https://devblogs.microsoft.com/pix/download/
お使いの環境に合ったバージョンをダウンロードしてインストールしてください。

私が使用したバージョンは2208.10です。

インストールが完了したら、プログラムを実行してみてください。
image.png
このような画面が表示されます。

image.png
Windowsアプリケーションでプログラムを作成しているため、左側からLaunch Win32を選択してください。
そして、Path to executableでBrowseをクリックし、ビルドされたexeファイルを探して指定してください。
image.png
その下にあるLaunchボタンを押すとexeファイルが実行され、プログラムが起動します。
image.png
プログラムが実行されている間、下にあるGPU Captureをクリックすると現在のフレームシーンがキャプチャーされます。
image.png
キャプチャーされたシーンをクリックするとデバッグ画面が開きます。

image.png
この画面からさまざまな情報を得ることができますが、
今回は私が主に使用した部分だけについてお話しします。

image.png
上部でPipelineを選択してください。

image.png
すると、その下のコマンドリストに入れて実行されたコマンドが順番に並んでいるのを確認できます。
各コマンドをクリックすると、下部でより詳細な情報を確認することができます。

image.png
ここでは実際に使用された頂点のリストやインデックスリストも確認することができ、
渡されたコンスタントバッファの値、テクスチャ画像、出力結果も確認することができます。

今回、ミクさんを描画するために深度値の記録を有効にしましたね。
PIXで深度バッファにどのように描画されたか見てみましょう。
image.png
深度値が正しく記録されていることがわかります。

これ以外にも、PIXでは描画されたピクセルがどのように描画されたかをシェーダーデバッグすることもできます。
結果画像からピクセルを1つ選択してみてください。
image.png
画像からピクセルを選択すると、右側に選択されたピクセルの色値が表示され、下にはDebug Pixelボタンが表示されます。
このボタンを押してみましょう。

image.png
すると、デバッグタブに移動し、どのシェーダーが使用されたかをコードで確認することができます。

そして、Visual Studioのコードデバッグとほぼ同じように、ブレークポイントを設定して1行ずつデバッグすることも可能です。

このようにシェーダーコードを確認するためには、シェーダーをコンパイルする際に必ずデバッグフラグを入れる必要があります。

PIXの機能は私が説明したもの以外にもたくさんあります。
この記事ですべての機能について説明すると長くなりすぎるので、ここで終わりにします。

実際に使ってみると、どんな機能があり、どのように使用すればいいのかわかるようになると思います。

これから進めていく中で、正しく描画されているか気になったり、バッファに正しい値が渡されているか知りたい場合は、ぜひPIXでデバッグしてみてください。

まとめ

今回はここでまとめます。

読み込んだPMXモデルデータでミクさんを描画することに成功しました。

今回は本当に描画するだけでした。まだ顔に黒いものが見えますが、これはパイプラインステートでブレンド設定をしていないためです。

次回はアルファブレンドについて見ていきましょう。

さらに、ディレクショナルライトの情報をシェーダーで受け取れるようにして、陰影効果を出せるようにシェーダーも修正します。

トゥーンテクスチャやスフィアテクスチャも使用するように修正する予定です。

今回は実際、最初に四角形を描画したときと比べて新しいことはあまりありませんでした。ミクさんを綺麗に描くために必要な準備物を追加しただけですね。

これからもこのように準備物を追加していく過程が続くでしょう。
では、ここで本当に終わりにしたいと思います。ありがとうございました。

次回

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?