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でミクさんを躍らせてみよう13-シャドウマッピング

Last updated at Posted at 2024-10-01

前回

シャドウマッピング

こんにちは。

この記事では、ついに描画に関する話題に戻ります。

今回はシャドウマッピングを実装します。
影を描画するということですね。

現在は、シェーダーで任意の光の方向に対して単純にシェーディングを行っているだけです。
しかし、本来であれば何かによって光が遮られる部分には影が生じるはずです。

シャドウマッピング

3Dグラフィックスにおいて影を作成する方法はシャドウマッピング以外にも存在しますが、シャドウマッピングが現時点で最も広く使用されている方法です。

この手法の要点は、光源の視点からシーンを投影して影が生じる部分を判断し、その後オブジェクトを描画する際に現在のピクセルに影があるかどうかを決定することです。

シャドウマッピングのプロセスを順を追って詳しく説明しましょう。

まず最初に、光源の視点からデプスマップを生成する必要があります。

ミクさんを描画する際は、カメラの視点からシーンを描画しました。

しかし今回は、カメラの視点ではなく光源の視点からシーンを描画します。

光源と言えば、私たちはディレクショナルライト1つだけを使用するため、ディレクショナルライトを基準にビュー行列を生成してシーンをレンダリングすればよいです。

そして、ディレクショナルライトのデプスマップは透視投影ではなく平行投影を行います。

2番目に、オブジェクトをレンダリングする際に、上で描画したデプスマップを使用して影があるかどうかを判断します。

光源基準の変換行列があれば、現在描画するオブジェクトの座標を光源基準の座標に変換することも可能です。

このように求めた座標をテクスチャ座標に変換することで、現在のピクセルに影が生じるべきかどうかを判断することができます。

image.png

この図はシャドウマッピングの内容をまとめたものです。

シャドウマッピングについて長々と説明しましたが、初めて見る方には理解しづらいかもしれません。

しかし、実際に実装してみると、そこまで難しい内容ではないことがわかるでしょう。

それでは、シャドウマッピングのために新たに追加する必要があるものを整理してみましょうか?

  1. 光源基準のViewProjection行列:シャドウデプステクスチャの描画にも必要で、影の有無を判断するためにも必要です。
  2. シャドウデプステクスチャ:デプスを描画するためのテクスチャが必要です。深度ステンシルバッファとシェーダーリソースとして使用可能である必要があります。
  3. シャドウマッピング用レンダーパイプライン:シャドウデプスの描画のみを担当するレンダーパイプラインが必要です。(シェーダーを含む)

それでは必要なものを追加していきましょう。

Transformクラス

現在は、ミクさんのピクセルシェーダーで任意の光の方向を直接書いて使用しています。

これからはディレクショナルライトの光の方向を直接調整できるようにしたいと思います。

そこで、オブジェクトのトランスフォームを管理するクラスを作成し、これを使って光の方向を扱うようにします。

ディレクショナルライトだけでなく、描画するすべてのオブジェクトもこれを使用できるでしょう。

class Transform
{
public:
	Transform();
	~Transform() = default;

	void SetPosition(float x, float y, float z);
	const DirectX::XMFLOAT3& GetPosition() const;

	void SetRotation(float x, float y, float z);
	const DirectX::XMFLOAT3& GetRotation() const;
	const DirectX::XMFLOAT3 GetRotationRadians() const;
	const DirectX::XMFLOAT4 GetQuaternion() const;

	void SetScale(float x, float y, float z);
	const DirectX::XMFLOAT3& GetScale() const;

	void AddTranslation(float x, float y, float z);

	DirectX::XMFLOAT3 GetForward() const;
	DirectX::XMFLOAT3 GetRight() const;
	DirectX::XMFLOAT3 GetUp() const;

	DirectX::XMMATRIX& GetTransformMatrix() const;
	DirectX::XMMATRIX& GetViewMatrix() const;

private:
	DirectX::XMFLOAT3 mPosition;
	DirectX::XMFLOAT3 mRotation;
	DirectX::XMFLOAT3 mScale;

位置、回転、スケールの値を持っており、これらを使用してトランスフォームの変換行列を返すメソッドもあります。

回転はラジアンで保存し、取得する側でオイラー角、ラジアン、クォータニオンのいずれかの形式で取得できるようにメソッドを提供しています。

#include "Transform.h"

Transform::Transform():
	mPosition(DirectX::XMFLOAT3(0.0f, 0.0f, 0.0f)),
	mRotation(DirectX::XMFLOAT3(0.0f, 0.0f, 0.0f)),
	mScale(DirectX::XMFLOAT3(1.0f, 1.0f, 1.0f))
{
}

void Transform::SetPosition(float x, float y, float z)
{
	mPosition.x = x;
	mPosition.y = y;
	mPosition.z = z;
}

const DirectX::XMFLOAT3& Transform::GetPosition() const
{
	return mPosition;
}

void Transform::SetRotation(float x, float y, float z)
{
	mRotation.x = DirectX::XMConvertToRadians(x);
	mRotation.y = DirectX::XMConvertToRadians(y);
	mRotation.z = DirectX::XMConvertToRadians(z);
}

const DirectX::XMFLOAT3& Transform::GetRotation() const
{
	return DirectX::XMFLOAT3(DirectX::XMConvertToDegrees(mRotation.x), DirectX::XMConvertToDegrees(mRotation.y), DirectX::XMConvertToDegrees(mRotation.z));
}

const DirectX::XMFLOAT3 Transform::GetRotationRadians() const
{
	return DirectX::XMFLOAT3(mRotation.x, mRotation.y, mRotation.z);
}

const DirectX::XMFLOAT4 Transform::GetQuaternion() const
{
	DirectX::XMFLOAT4 result {}; 
	DirectX::XMVECTOR quaternion = DirectX::XMQuaternionRotationRollPitchYaw(mRotation.x, mRotation.y, mRotation.z);
	DirectX::XMStoreFloat4(&result, quaternion);
	return result;
}

void Transform::SetScale(float x, float y, float z)
{
	mScale.x = x;
	mScale.y = y;
	mScale.z = z;
}

const DirectX::XMFLOAT3& Transform::GetScale() const
{
	return mScale;
}

void Transform::AddTranslation(float x, float y, float z)
{
	mPosition.x += x;
	mPosition.y += y;
	mPosition.z += z;
}

DirectX::XMFLOAT3 Transform::GetForward() const
{
	DirectX::XMMATRIX rotation = DirectX::XMMatrixRotationRollPitchYaw(mRotation.x, mRotation.y, mRotation.z);
	DirectX::XMFLOAT3 result = DirectX::XMFLOAT3(rotation.r[2].m128_f32[0], rotation.r[2].m128_f32[1], rotation.r[2].m128_f32[2]);
	return result;
}

DirectX::XMFLOAT3 Transform::GetRight() const
{
	DirectX::XMMATRIX rotation = DirectX::XMMatrixRotationRollPitchYaw(mRotation.x, mRotation.y, mRotation.z);
	DirectX::XMFLOAT3 result = DirectX::XMFLOAT3(rotation.r[0].m128_f32[0], rotation.r[0].m128_f32[1], rotation.r[0].m128_f32[2]);
	return result;
}

DirectX::XMFLOAT3 Transform::GetUp() const
{
	DirectX::XMMATRIX rotation = DirectX::XMMatrixRotationRollPitchYaw(mRotation.x, mRotation.y, mRotation.z);
	DirectX::XMFLOAT3 result = DirectX::XMFLOAT3(rotation.r[1].m128_f32[0], rotation.r[1].m128_f32[1], rotation.r[1].m128_f32[2]);
	return result;
}

DirectX::XMMATRIX& Transform::GetTransformMatrix() const
{
	DirectX::XMMATRIX s = DirectX::XMMatrixScaling(mScale.x, mScale.y, mScale.z);
	DirectX::XMMATRIX r = DirectX::XMMatrixRotationRollPitchYaw(mRotation.x, mRotation.y, mRotation.z);
	DirectX::XMMATRIX t = DirectX::XMMatrixTranslation(mPosition.x, mPosition.y, mPosition.z);
	DirectX::XMMATRIX result = s * r * t;

	return result;
}

DirectX::XMMATRIX& Transform::GetViewMatrix() const
{
	DirectX::XMFLOAT3 position = GetPosition();
	DirectX::XMFLOAT3 forward = GetForward();
	DirectX::XMFLOAT3 up = GetUp();

	DirectX::XMVECTOR positionVector = DirectX::XMLoadFloat3(&position);
	DirectX::XMVECTOR forwardVector = DirectX::XMLoadFloat3(&forward);
	DirectX::XMVECTOR upVector = DirectX::XMLoadFloat3(&up);

	DirectX::XMMATRIX result = DirectX::XMMatrixLookToLH(positionVector, forwardVector, upVector);
	return result;
}

実装部の内容に特別なものはないため、説明は省略します。

注目すべき点は、このトランスフォームがワールド変換行列を返すメソッドだけでなく、トランスフォームを基準としたビュー行列を返すメソッドも持っているということです。

これで、ディレクショナルライトの方向を調整できるようになりました。

カメラの位置もこれで置き換えました。

ライトトランスフォームを追加

ディレクショナルライトを1つだけ使用するので、

Dx12Wrapperクラスにメンバーとしてトランスフォームを追加します。

Transform* mCameraTransform;
Transform* mDirectionalLightTransform;

光の方向を設定するメソッドを追加します。

void Dx12Wrapper::SetDirectionalLightRotation(float vec[3])
{
	mDirectionalLightTransform->SetRotation(vec[0], vec[1], vec[2]);
	DirectX::XMFLOAT3 storeVector = mDirectionalLightTransform->GetForward();
	DirectX::XMVECTOR lightDirection = DirectX::XMLoadFloat3(&storeVector);
	DirectX::XMVECTOR sceneCenter = DirectX::XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f);

	float shadowDistance = 50.0f;
	DirectX::XMVECTOR lightPosition = DirectX::XMVectorMultiplyAdd(lightDirection, XMVectorReplicate(-shadowDistance), sceneCenter);
	DirectX::XMStoreFloat3(&storeVector, lightPosition);

	mDirectionalLightTransform->SetPosition(storeVector.x, storeVector.y, storeVector.z);
}

オイラー角として回転値を受け取り、mDirectionalLightTransformに設定されるようにします。

ディレクショナルライトは実際には方向が最も重要ですが、これを基準にビュー行列を作成できるようにするために、位置の値も適切に設定する必要があります。

しかし、ディレクショナルライトは太陽のような存在です。位置を定義することができるでしょうか?

そのため、shadowDistanceという値で任意に設定することにします。

原点の座標を0,0,0とし、そこから光の反対方向に50だけ離れた位置を設定します。
Light.jpg

現在描画しているオブジェクトはミクさんくらいしかないため、50程度離れていれば十分です。

もしオブジェクトがもっと多く広範囲に散らばっている場合に全てに影を生じさせたい場合は、shadowDistanceを大きな値に調整すれば良いです。

しかし、その場合に問題が一つ生じます。

シャドウデプスマップは結局2Dテクスチャですが、実際に影を描画する領域は透視投影された空間です。このため、カメラに近い場所に生じる影にはエイリアシングが発生します。

image 1.png

この問題を解決するために、距離に応じて複数のシャドウデプスマップを描画するカスケードシャドウマッピングを使用します。

カスケードシャドウマッピングについて気になる方は調べてみてください。

今回は基本的なシャドウマッピングのみを実装します。

シーン情報コンスタントバッファの修正

光のViewProjection行列と光の方向をGPUに渡す必要があります。
すでに以前にシーン情報を渡すための定数バッファを作成していました。
今回は構造体に新たに必要なパラメータを追加するだけです。

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

SceneMatricesDataストラクチャにパラメータを追加してください。

lightCameraは光のViewProjection行列です

lightは光の方向です。

SceneMatricesData* mappedSceneMatricesData;
mSceneConstBuff->Map(0, nullptr, (void**)&mappedSceneMatricesData);
mappedSceneMatricesData->view = mCameraTransform->GetViewMatrix();
mappedSceneMatricesData->proj = projectionMatrix;
mappedSceneMatricesData->eye = mCameraTransform->GetPosition();

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

コンスタントバッファにデータをマッピングします。

毎フレームマッピングするように変更しました。

lightCameraは光を基準とするView-Projection行列であるため、光のビュー行列を取得して直交投影行列と掛け合わせます。

XMMatrixOrthographicLHで直交投影行列を作成することができます。

シェーダーも修正する必要がありますが、パイプラインステートを作成する際に一緒に修正することにします。

今回はシャドウデプスを描画するためのパイプラインステートを作成します。

シャドウデプスパイプラインステート

PMXRendererに新しいパイプラインステートを追加します。

class PMXRenderer
{
private:
...
	ComPtr<ID3D12PipelineState> mShadowPipeline = nullptr;
...
}

シャドウデプスを描画するために別のルートパラメータを準備する必要がないため、新しいルートシグネチャを作成する必要はありません。

デプスのみを描画するため、ピクセルシェーダーも必要なく、サンプラーも重要ではありません。

そのため、通常のパイプラインステートを作成する際に使用したD3D12_GRAPHICS_PIPELINE_STATE_DESCのパラメータを少し修正して使用します。

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

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

	gpipeline.VS = CD3DX12_SHADER_BYTECODE(vsBlob.Get());
	gpipeline.PS.BytecodeLength = 0;
	gpipeline.PS.pShaderBytecode = nullptr;
	gpipeline.RTVFormats[0] = DXGI_FORMAT_UNKNOWN;
	result = _dx12.Device()->CreateGraphicsPipelineState(&gpipeline, IID_PPV_ARGS(mShadowPipeline.ReleaseAndGetAddressOf()));
	if (FAILED(result)) {
		assert(SUCCEEDED(result));
	}

	return result;

深度ステンシルバッファにのみ深度値を記録するため、レンダーターゲットには何も描画しません。

そのため、レンダーターゲットのフォーマットをDXGI_FORMAT_UNKNOWNに設定し、ピクセルシェーダーも設定しません。

頂点シェーダーではShadowVSというエントリーポイントでコンパイルされるようになっていますね。

頂点シェーダーにShadowVSを追加しましょう。

まず先ほど話したように、コンスタントバッファにパラメータを追加する必要がありますね。

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

HLSLヘッダーファイルのSceneBufferにパラメータを追加します。

今回は頂点シェーダーコードに新しいメソッドを追加します。

Output ShadowVS(
	float4 pos : POSITION,
	float3 normal : NORMAL,
	float2 uv : TEXCOORD)
{
	return mul(lightCamera, pos);
}

光を基準とするView-Projection行列を位置に掛けるだけで十分です。

今回はシャドウデプスマップを描画するバッファを追加します。

シャドウデプスバッファの追加

以前にデプステスト用のバッファを作成した時と方法はあまり変わりません。

ComPtr<ID3D12Resource> mLightDepthBuffer = nullptr;

Dx12Wrapperのメンバーを追加してください。

デプステストバッファを作成したメソッドにシャドウデプスバッファも作成するように修正します。

HRESULT Dx12Wrapper::CreateDepthStencilView()
{
...
	D3D12_RESOURCE_DESC depthResDesc = CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_R32_TYPELESS, desc.Width, desc.Height);
...

	depthResDesc.Width = 1024;
	depthResDesc.Height = 1024;
	result = mDevice->CreateCommittedResource(&depthHeapProp,
		D3D12_HEAP_FLAG_NONE,
		&depthResDesc,
		D3D12_RESOURCE_STATE_DEPTH_WRITE,
		&depthClearValue,
		IID_PPV_ARGS(mLightDepthBuffer.ReleaseAndGetAddressOf()));
	if (FAILED(result))
	{
		return result;
	}
	
	D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
	dsvHeapDesc.NumDescriptors = 2;
	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);

	handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);

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

	D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
	heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	heapDesc.NodeMask = 0;
	heapDesc.NumDescriptors = 2;
	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);

	srvHandle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

	mDevice->CreateShaderResourceView(mLightDepthBuffer.Get(), &depthSrvResDesc, srvHandle);

   return result;
}

デプステストのバッファとD3D12_RESOURCE_DESCには違いがありません。

そのまま持ってきて幅と高さだけ修正して使用します。

今回は1024x1024でシャドウデプステクスチャを作成します。

この解像度を上げればより良質の影を見ることができます。しかし、その分メモリを占有します。

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

今回はシャドウデプスマップが描画されるようにしましょう。

シャドウデプスマップのレンダリング

PMXRendererにシャドウデプスマップパイプラインを設定するメソッドを追加します。

void PMXRenderer::BeforeDrawFromLight() const
{
	auto cmdList = _dx12.CommandList();
	cmdList->SetPipelineState(mShadowPipeline.Get());
	cmdList->SetGraphicsRootSignature(mRootSignature.Get());
}

Dx12Wrapper にもメソッドを追加します。

void Dx12Wrapper::PreDrawShadow() const
{
	auto handle = mDepthStencilViewHeap->GetCPUDescriptorHandleForHeapStart();
	handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);

	mCmdList->OMSetRenderTargets(0, nullptr, false, &handle);

	mCmdList->ClearDepthStencilView(handle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
	auto wsize = Application::Instance().GetWindowSize();

	ID3D12DescriptorHeap* heaps[] = { mSceneDescHeap.Get() };

	heaps[0] = mSceneDescHeap.Get();
	mCmdList->SetDescriptorHeaps(1, heaps);
	auto sceneHandle = mSceneDescHeap->GetGPUDescriptorHandleForHeapStart();
	mCmdList->SetGraphicsRootDescriptorTable(0, sceneHandle);

	D3D12_VIEWPORT vp = CD3DX12_VIEWPORT(0.0f, 0.0f, 1024, 1024);
	mCmdList->RSSetViewports(1, &vp);

	CD3DX12_RECT rc(0, 0, 1024, 1024);
	mCmdList->RSSetScissorRects(1, &rc);
}

これは以前にデプステスト用の深度ステンシルビューを設定したのと似ています。

シャドウデプスバッファのビューの位置がデプスバッファの次にあるため、GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV)分だけハンドルの位置を移動させます。

シャドウデプスバッファには深度値のみを記録するため、レンダーターゲットを登録する必要はありません。

シーン情報のバッファを0番ルートパラメータにバインドし、

シャドウデプステクスチャの解像度に合わせてビューポートとシザーレクトも設定します。

PMXActorで影を描画する際にDrawメソッドが異なる動作をするように修正します。

void PMXActor::Draw(Dx12Wrapper& dx, bool isShadow = false) 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);

	if (isShadow == true)
	{
		dx.CommandList()->DrawIndexedInstanced(mPmxFileData.faces.size() * 3, 1, 0, 0, 0);
	}
	else
	{
		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;
		}
	}
}

シャドウがtrueの場合、一度に描画します。

深度値のみを描画するため、マテリアルに関する情報は必要ありません。

PMXレンダラーにシャドウレンダリングメソッドを追加します。

void PMXRenderer::DrawFromLight() const
{
	for (auto& actor : mActors)
	{
		actor->Draw(_dx12, true);
	}
}

void PMXRenderer::Draw() const
{
	for (auto& actor : mActors)
	{
		actor->Draw(_dx12, false);
	}
}

もともとあったDrawメソッドも修正します。

Renderクラスで実際のオブジェクトを描画する前に、以下のように呼び出します。

mPmxRenderer->BeforeDrawFromLight();
mDx12->PreDrawShadow();
mPmxRenderer->DrawFromLight();

シャドウデプスマップの確認

ここまで問題なければ、光がミクさんの右上から当たるように設定してビルドして確認してみてください。
おそらく画面上では変化は見られないでしょう。
なぜなら、まだ描画されたシャドウデプスマップを実際のオブジェクトで使用するように修正していないからです。

しかし、その前にPIXを使ってシャドウデプスマップが正しく描画されたかどうか確認することができますね。

image 2.png
シルエットが見づらいかもしれませんが、よく見るとミクさんの右上からの視点で深度値が描画されているのが分かります。

ミクさんに影をつけよう

シャドウデプスマップも準備できたので、これを使って影をつけていきます。

ピクセルシェーダーでシャドウデプスマップテクスチャを使用してサンプリングする必要があるため、ルートパラメータを追加し、サンプラー設定も追加する必要があります。

PMXRendererでルートパラメータを設定するメソッドを修正します。

D3D12_DESCRIPTOR_RANGE descTblRange[5] = {};
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;

descTblRange[4] = CD3DX12_DESCRIPTOR_RANGE(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 4);

t4レジスタを使用するようにディスクリプタレンジを追加します。

CD3DX12_DESCRIPTOR_RANGEを使用すると簡単に設定できます。

D3D12_ROOT_PARAMETER rootparam[5] = {};
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;

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

ルートパラメータに新しく追加されたディスクリプタレンジを設定します。

D3D12_ROOT_SIGNATURE_DESC rootSignatureDesc = {};

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

ルートパラメータが追加されたので、D3D12_ROOT_SIGNATURE_DESCのNumParametersパラメータも4に修正します。

サンプラーも追加しますが、今回追加するサンプラーは既存のサンプラーとは少し違います。

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

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

このように追加してください。
s2レジスタを使用するサンプラーです。

特殊な点は、ComparisonFuncをD3D12_COMPARISON_FUNC_LESS_EQUALに設定したことです。

このように設定することで、シェーダーでSamplerStateではなくSamplerComparisonStateを使用してサンプリングされる値と特定の値を比較することができます。

D3D12_COMPARISON_FUNC_LESS_EQUALの場合、サンプリングされた値が特定の値以下であれば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);

シャドウデプスバッファのビューをルートパラメータのインデックス3に設定します。

シェーダーを修正しましょう。

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

struct Output
{
	float4 svpos : SV_POSITION;
	float3 normal : NORMAL0;
	float2 uv : TEXCOORD;
	float3 ray : VECTOR;
	float4 tpos : TPOS;
};

Texture2D<float4> tex : register(t0);
Texture2D<float4> toon : register(t1);
Texture2D<float4> sph : register(t2);
Texture2D<float4> lightDepthTex : register(t4);

SamplerState smp : register(s0);
SamplerState smpToon : register(s1);
SamplerComparisonState shadowSmp : register(s2);

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にtposを追加し、

lightDepthTexとshadowSmpを追加します。

shadowSmpは他のサンプラーとは異なり、SamplerComparisonStateです。

頂点シェーダーを修正します。

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.normal = mul((float3x3)world, normal);
	output.tpos = mul(lightCamera, output.pos);
	output.uv = uv;
	output.ray = normalize(pos.xyz - eye);

	return output;
}

tposに頂点の座標を光の視点-投影空間のクリップ座標に変換して入れます。

今回はピクセルシェーダーを修正します。

float4 BasicPS(Output input) : SV_TARGET
{
	float3 light = normalize(lightVec);
	float3 normal = normalize(input.normal);

	float diffuseB = saturate(dot(-light, normal));
	float4 toonDif = toon.Sample(smpToon, float2(0, 1.0 - diffuseB));

  float3 refLight = normalize(reflect(light, normal));
  float specularB = pow(saturate(dot(refLight, -input.ray)), specular.a);
  float3 specularColor = specular.rgb * specularB;

	float4 color = tex.Sample(smp, input.uv);

  color.rgb = color.rgb * toonDif + specularColor;
  
  float3 posFromLightVP = input.tpos.xyz / input.tpos.w;
	float2 shadowUV = (posFromLightVP + float2(1, -1)) * float2(0.5, -0.5);
	float depthFromLight = lightDepthTex.SampleCmp(shadowSmp, shadowUV, posFromLightVP.z - 0.005f);
	float shadowWeight = lerp(0.5f, 1.0f, depthFromLight);
	
	color.rgb *= shadowWeight;

	return color;
}

シャドウデプステクスチャにおける現在の座標に対応するUV値を計算する必要があります。

そのため、頂点シェーダーでtposを計算しました。

tposは光の視点-投影空間のクリップ座標でした。

同次座標であるため、wで割るとNDC座標に変換されます。つまり-1から1の範囲の座標に変換されるのです。

これを再びUV座標(0から1の範囲)に変換します。

次はSamplerComparisonStateサンプラーを使用する番です。

このサンプラーは、設定した比較関数を使用してサンプリングした値と特定の値を比較した結果を返すサンプラーです。

先ほど計算したposFromLightVPはNDC座標でした。つまり、posFromLightVPのz値は光の視点投影空間における現在のピクセルの深度値です。

シャドウデプステクスチャをサンプリングした値も深度値です。

したがって、これら2つの値を比較することで、現在のピクセルが光に露出されるべきかどうかを判断することができます。

サンプリングされた値がposFromLightVPのz値以下である場合、0を返します。

つまり、現在のピクセルがシャドウデプスマップの深度よりも深いということです。

そのため、この場合はshadowWeightの値が0.5となり、最終的な色に乗算されることで影が生じているように見えます。

結果

ビルドして確認してみましょう。
image 3.png

光が右上から照らしているため、光が当たらない部分に影ができているのが見えます。

まとめ

今回の記事では、広く使用されているシャドウマッピングを実装しました。

シャドウマッピングは光源視点からのデプス値を使用して実現できましたが、このようにデプス値は影だけでなく、他の効果でも重要に使用される場面が多くあります。

もし今回適用した影の品質に満足できない場合は、シャドウマップの解像度を上げてください。ただし、環境によってはシャドウマップのサイズが大きいと、メモリ帯域幅の問題でパフォーマンスに影響が出る可能性があります。

あるいは、追加でカスケードシャドウマップを実装するのもいいでしょう。

次の記事では、ポストプロセッシングのための下準備について取り上げます。ありがとうございました。

参考リンク

https://docs.unity3d.com/kr/2019.4/Manual/shadow-cascades.html
https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

次回

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?