0
0

DirectX12でミクさんを躍らせてみよう19-Reflection

Last updated at Posted at 2024-10-01

前回

Reflection

こんにちは。

今回は反射を実装してみたいと思います。

反射とは、光が物体に当たった時に、物体の表面で反射して跳ね返る現象です。

鏡のような滑らかな表面であれば、光がそのまま反射して、同じ物体がもう一つあるように見えることです。

私はミクさんが踊っているステージの床がとても滑らかな床だと仮定し、ここにミクさんが反射されるようにします。

では、反射はどのように実装すべきでしょうか?

上で話した反射の原理をそのまま使えばいいのです。

同じオブジェクトを反対の姿で一度描画すればいいです。

もう少し詳しく言えば、ある平面を基準に反射されるようにオブジェクトをもう一つ描くということです。

これをPlaner Reflection(表面反射)と言います。

Planer Reflection

平面反射を実装する方法を考えてみましょう。

先ほど言ったように、ある平面を基準にオブジェクトを描画すればいいのです。

そうすると、ミクさんがある平面を基準に反射される位置のワールドトランスフォームを計算し、それをシェーダーに渡せば終わりです。

DirectX::XMMATRIX& Transform::GetPlanarReflectionsTransform(const DirectX::XMFLOAT3& planeNormal, const DirectX::XMFLOAT3& planePosition) const
{
	DirectX::XMVECTOR planeNormalVector = DirectX::XMVector3Normalize(DirectX::XMLoadFloat3(&planeNormal));
	DirectX::XMVECTOR planePositionVector = DirectX::XMLoadFloat3(&planePosition);

	float d = -DirectX::XMVectorGetX(DirectX::XMVector3Dot(planeNormalVector, planePositionVector));
	DirectX::XMFLOAT4 plane(DirectX::XMVectorGetX(planeNormalVector), DirectX::XMVectorGetY(planeNormalVector), DirectX::XMVectorGetZ(planeNormalVector), d);

	DirectX::XMMATRIX reflectionMatrix = DirectX::XMMatrixReflect(XMLoadFloat4(&plane));

	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;

	result = reflectionMatrix * result;

	return result;
}

Transformクラスに平面の法線ベクトルと位置を渡すと、現在のワールドトランスフォームを平面基準で対称化したトランスフォームを返すメソッドを追加しました。

平面と原点の間の距離を求め、法線ベクトルとこの値を使用して平面を定義します。

平面を定義すると、DirectX::XMMatrixReflectで反射行列を求めることができます。

現在のワールドトランスフォームと反射行列を掛け合わせることで、最終的に対称移動されたトランスフォームを得ることができます。

この反射されたワールドトランスフォームを渡すことができるように、PMXActorにコンスタントバッファを追加しましょう。

ComPtr<ID3D12Resource> mReflectionTransformBuff = nullptr;

PMXActorにメンバーを追加して

HRESULT PMXActor::CreateTransformView(Dx12Wrapper& dx)
{
	...
	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(mReflectionTransformBuff.ReleaseAndGetAddressOf())
	);

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

	result = mReflectionTransformBuff->Map(0, nullptr, (void**)&mMappedReflectionMatrices);

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

	cbvDesc.BufferLocation = mReflectionTransformBuff->GetGPUVirtualAddress();
	handle.ptr+= incSize;

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

	return S_OK;
}

反射トランスフォーム用のバッファを作成し、ディスクリプタヒープにビューも生成します。

void PMXActor::DrawReflection(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);

	auto transformHandle = mTransformHeap->GetGPUDescriptorHandleForHeapStart();
	auto incSize = dx.Device()->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

	transformHandle.ptr += incSize;
	dx.CommandList()->SetGraphicsRootDescriptorTable(1, transformHandle);

	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;

		if (mLoadedMaterial[i].visible == true)
		{
			dx.CommandList()->SetGraphicsRootDescriptorTable(2, materialH);
			dx.CommandList()->DrawIndexedInstanced(numFaceVertices, 1, idxOffset, 0, 0);
		}

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

反射されたミクさんを描画するためのメソッドも追加しましょう。

元のDrawメソッドとの違いは、反射されたトランスフォームを渡すコンスタントバッファをバインドしたことだけです。

これを呼び出してください。

ビルドして確認してみましょう。

ステージに隠れて見えませんね。
ステージを描画するコードを無効化してみましょう。
image.png
ミクさんがもう一つ描画されているのを確認しました。

問題が発生しましたね。

ステージに反射オブジェクトが隠れてしまいます。

これを解決する方法は次のようになります。

  • ステージをミクさんより先に描画する。
  • 反射されるミクさんを深度テストを使用せずに、元のミクさんより先に描画する。
  • 通常のミクさんを描画する。

このようにすれば、とりあえずステージに反射しているように描画されると思います。

しかし、この方法にも問題があります。

ステージの床に反射されるものなので、反射されたミクさんはステージの床にのみ描画されるべきです。

しかし、上記の方法だとカメラアングルによってはミクさんがステージの下を貫いて描画されているように見えたり、

ステージの後ろや下から見るとミクさんが貫通して見えてしまうでしょう。

これを解決するにはステンシルテストを使えばよいのですが、まだステンシルテストについて話したことがありません。

しかし、今回はステンシルテストを使用しません。

そしてもう一つ不自然な点があります。

ミクさんだけが反射されていて、ステージが反射されていないということです。

それなら、ステージも平面反射させればいいでしょうか?

しかし、もし舞台だけでなく舞台上に100個のオブジェクトがあったらどうでしょうか?

最初に話したとおり、平面反射は同じオブジェクトをもう一度描画することです。つまり、元のドローコールが100回だとすれば、さらに100回追加で行う必要があるということです。

これが平面反射の欠点です。最も現実的な反射方法ではありますが、反射させるオブジェクトの数が多いとドローコールが多くなりすぎてしまいます。

そこで、上で話した問題と平面反射の欠点を解決する方法について話してみましょう。

SSR(Screen Space Reflection)

SSRは一種のポストプロセスで、画面に描画された情報のみを利用して反射を表現する方法です。

そのため、画面上にいくつのオブジェクトが存在しても、常に一定のコストで反射効果を作成することができます。

しかし、欠点があります。

当然ながら、カメラから見えない部分は反射されません。

そのため、キャラクターをSSRで反射させると不自然な感じがします。

これを解消するために、私は平面反射とSSRを組み合わせて反射を表現します。

ミクさんには平面反射を使用し、舞台にはSSRを使用して、両方を組み合わせます。

描画プロセスの整理

それでは、守るべき点を整理し、それに基づいてどのように描画するか列挙してみましょう。

一つ、SSRを描画するのは舞台だけでなければなりません。ミクさんはSSRに含まれてはいけません。

二つ、平面反射で描画されるミクさんが手前にあるオブジェクトを隠してはいけません。

三つ、舞台の床にのみ反射した形状が見えるべきです。

上記の内容ができるよう、次の2つの処理を行います。

一つ目、反射が見える部分だけをマスキングします。

テクスチャを準備して特定の領域だけをマスキングできるようにします。シーンには描画されず、別のバッファに記録します。

デプステストを使用して記録するため、隠れる部分があればデプステストによって隠れるので、反射された部分が他のオブジェクトを隠すことはありません。

これはステンシルを使用しても可能ですが、マスキングするメッシュを簡単にシーンでも確認できるようにするために、シェーダーを作成して実装します。

2つ目、前フレームの反射結果をステージの床に適用します。

このようにする理由は、SSRと平面反射の結果を合わせて、それをさらにステージの床に適用することを1フレームのレンダリング過程で行おうとすると、考慮すべき要素が多くなるからです。

しかし、反射の結果を今のフレームに適用せず、クリアしない状態で次のフレームで使用すれば、簡単に適用できます。

そして、フレーム間の時間差は人間が感じるほど大きくないため、悪くない結果を得ることができます。

それでは、描画の順序を次のようにまとめます。

  1. 平面反射の描画
  2. シャドウマップの描画
  3. FBXモデルの描画
  4. ペンライトの描画
  5. SSRマスクの描画
  6. SSRの描画(ここで描画されたSSRの結果は次のフレームで使用される)
  7. PMXの描画
  8. ポストプロセス

新たに追加する必要がある要素は以下の通りです。

  1. SSRマスクバッファ(パイプラインステート、シェーダーを含む)
  2. SSRバッファ(パイプラインステート、シェーダーを含む)

SSRマスク

マスキングを行うためのオブジェクトを準備します。
ステージの床の上に平面を配置します。
平面を定義します。
まず、このような簡単な形状を今後追加しやすくするために抽象クラスを作成しました。

class IGeometry
{
public:
	virtual const std::vector<Vertex>& GetVertices() const = 0;
	virtual const std::vector<unsigned int>& GetIndices() const = 0;
};
class PlaneGeometry : public IGeometry
{
public:
    PlaneGeometry()
    {
        CreateGeometry();
    }

    virtual const std::vector<Vertex>& GetVertices() const override;
	virtual const std::vector<unsigned int>& GetIndices() const override;

private:
    void CreateGeometry();

private:
    std::vector<Vertex> mVertices{};
    std::vector<unsigned int> mIndices{};
};

inline const std::vector<Vertex>& PlaneGeometry::GetVertices() const
{
    return mVertices;
}

inline const std::vector<unsigned>& PlaneGeometry::GetIndices() const
{
    return mIndices;
}

inline void PlaneGeometry::CreateGeometry()
{
    mVertices = {
        { { -0.5f,  0.0f, -0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f } },
        { {  0.5f,  0.0f, -0.5f }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 1.0f } },
        { {  0.5f,  0.0f,  0.5f }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 0.0f } },
        { { -0.5f,  0.0f,  0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f } },
    };

    mIndices = {
        0, 1, 2, 0, 2, 3,
    };
}

これはIGeometryを継承して平面を定義したクラスです。

このような単純な形状のモデルに対するクラスを作成します。

class GeometryActor :
{
public:
    GeometryActor(const IGeometry& geometry);
    ~GeometryActor() override = default;

    void Initialize(Dx12Wrapper& dx);
    void Draw(Dx12Wrapper& dx) const;
    void Update();
    void EndOfFrame(Dx12Wrapper& dx);

private:
    HRESULT CreateVertexBufferAndIndexBuffer(Dx12Wrapper& dx);
    HRESULT CreateTransformBuffer(Dx12Wrapper& dx);

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

    const std::vector<Vertex>& mVertices;
    const std::vector<unsigned int>& mIndices;
    ComPtr<ID3D12Resource> mVertexBuffer = nullptr;
    ComPtr<ID3D12Resource> mIndexBuffer = nullptr;
    D3D12_VERTEX_BUFFER_VIEW mVertexBufferView = {};
    D3D12_INDEX_BUFFER_VIEW mIndexBufferView = {};
    Vertex* mMappedVertex = nullptr;
    unsigned int* mMappedIndex = nullptr;

    ComPtr<ID3D12Resource> mTransformBuffer = nullptr;
    std::unique_ptr<Transform> mTransform{};
};

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

頂点バッファビュー、インデックスバッファビュー、ワールドトランスフォームバッファを持っています。

もうこの部分は特に説明しなくても大丈夫そうです。

私の記事を順番に追ってきた方なら、実装部を見なくても自分で作成できるでしょう。

これにもRendererクラスが必要になりますね。

今回は別のクラスを作成せず、前回の記事で追加したInstancingRendererに内容を追加します。

ComPtr<ID3D12RootSignature> mSSRRootSignature = nullptr;
ComPtr<ID3D12PipelineState> mSSRPipeline = nullptr;

std::vector<std::shared_ptr<GeometryActor>> mSSRActorList = {};

InstancingRendererにSSRマスク用のメンバーを追加しました。

HRESULT InstancingRenderer::CreateSSRRootSignature()
{
	//Scene Buffer
	D3D12_DESCRIPTOR_RANGE sceneBufferDescriptorRange = {};
	sceneBufferDescriptorRange.NumDescriptors = 1;
	sceneBufferDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	sceneBufferDescriptorRange.BaseShaderRegister = 0;
	sceneBufferDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	CD3DX12_ROOT_PARAMETER rootParam[2] = {};

	rootParam[0].InitAsDescriptorTable(1, &sceneBufferDescriptorRange, D3D12_SHADER_VISIBILITY_VERTEX);
	rootParam[1].InitAsConstantBufferView(1, 0, D3D12_SHADER_VISIBILITY_VERTEX); // TransformBuffer;

	CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc = {};
	rootSignatureDesc.Init(_countof(rootParam), rootParam, 0, nullptr, 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 = mDirectX.Device()->CreateRootSignature(0, rootSignatureBlob->GetBufferPointer(), rootSignatureBlob->GetBufferSize(), IID_PPV_ARGS(mSSRRootSignature.ReleaseAndGetAddressOf()));
	if (FAILED(result) == true)
	{
		assert(SUCCEEDED(result));
		return result;
	}

	return S_OK;
}

ルートシグネチャを生成します。

0番目のルートパラメータはシーンの情報バッファ

1番目のルートパラメータはワールドトランスフォームバッファ

で設定します。

HRESULT InstancingRenderer::CreateSSRGraphicsPipeline()
{
	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 = mSSRRootSignature.Get();
	graphicsPipelineDesc.SampleMask = D3D12_DEFAULT_SAMPLE_MASK;
	graphicsPipelineDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
	graphicsPipelineDesc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK;
	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_ZERO;
	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.NumRenderTargets = 1;

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

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

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

	result = D3DCompileFromFile(L"SSRObjectPixelShader.hlsl",
		nullptr,
		D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"main",
		"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 = mDirectX.Device()->CreateGraphicsPipelineState(&graphicsPipelineDesc, IID_PPV_ARGS(mSSRPipeline.ReleaseAndGetAddressOf()));
	if (FAILED(result))
	{
		assert(SUCCEEDED(result));
		return result;
	}

	return S_OK;
}

パイプラインステートを生成します。

デプステストを有効にし、1つのレンダーターゲットのみを使用します。

ここで使用するシェーダーは簡単です。

#include "SSRObjectHeader.hlsli"

VertexOutput main(
	float4 pos : POSITION,
	float4 normal : NORMAL,
	float2 uv : TEXCOORD)
{
	VertexOutput output;

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

	return output;
}

頂点シェーダーは空間変換を行い

#include "SSRObjectHeader.hlsli"

float4 main(VertexOutput input) : SV_TARGET
{
	return float4(1.0f, 1.0f, 1.0f, 1.0f);
}

ピクセルシェーダーで白色を出力するだけです。

void InstancingRenderer::DrawSSR()
{
	for (auto& actor : mSSRActorList)
	{
		actor->Draw(mDirectX);
	}
}

GeometryActorオブジェクトのDrawを呼び出すようにメソッドを追加します。

void Dx12Wrapper::SetRenderTargetByMainFrameBuffer() const
{
	auto rtvHeapPointer = mPeraRTVHeap->GetCPUDescriptorHandleForHeapStart();
	auto dsvHeapPointer = mDepthStencilViewHeap->GetCPUDescriptorHandleForHeapStart();

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

Dx12Wrapperに画面テクスチャのみをレンダーターゲットとして設定するメソッドを追加しました。

void InstancingRenderer::BeforeDrawAtSSRPipeline()
{
	auto cmdList = mDirectX.CommandList();
	cmdList->SetPipelineState(mSSRPipeline.Get());
	cmdList->SetGraphicsRootSignature(mSSRRootSignature.Get());

	mDirectX.SetRenderTargetByMainFrameBuffer();
}

このようにパイプラインステートを設定し、レンダーターゲットを画面テクスチャに設定した後、

mInstancingRenderer->BeforeDrawAtSSRPipeline();
mInstancingRenderer->DrawSSR();

このように呼び出して描画します。

image 1.png
平面を適切な位置に配置してください。

今回は、ひとまず平面の配置のためにシーンに見えるようにしました。

最終的に平面は画面テクスチャには描画されず、マスキングテクスチャにのみ描画される必要があります。

マスキングテクスチャを準備し、それをレンダーターゲットとして使用するようにします。

class Dx12Wrapper
{
   ...
   ComPtr<ID3D12Resource> mSsrMaskBuffer;
   ...
}

Dx12Wrapperにマスクテクスチャとして使用するバッファを追加します。

HRESULT Dx12Wrapper::CreatePostProcessResource()
{
  ...
  
  auto result = mDevice->CreateCommittedResource(
			&heapProp,
			D3D12_HEAP_FLAG_NONE,
			&resDesc,
			D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
			&clearValue,
			IID_PPV_ARGS(mBloomResultResource.ReleaseAndGetAddressOf())
		);

		if (FAILED(result))
		{
			return result;
		}
		
	auto result = mDevice->CreateCommittedResource(
			&heapProp,
			D3D12_HEAP_FLAG_NONE,
			&resDesc,
			D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
			&clearValue,
			IID_PPV_ARGS(mSsrMaskBuffer.ReleaseAndGetAddressOf())
		);

		if (FAILED(result))
		{
			return result;
		}
  
	auto result = mDevice->CreateCommittedResource(
			&heapProp,
			D3D12_HEAP_FLAG_NONE,
			&resDesc,
			D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
			&clearValue,
			IID_PPV_ARGS(mHighLumShrinkResource.ReleaseAndGetAddressOf())
		);
		resDesc.Width >>= 1;

		if (FAILED(result))
		{
			return result;
		}
		
  auto heapDesc = mRtvHeaps->GetDesc();
	heapDesc.NumDescriptors = 6;
	result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessRTVHeap.ReleaseAndGetAddressOf()));
	
	D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {};
	rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
	rtvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	
	...
  
  handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
  mDevice->CreateRenderTargetView(mSsrMaskBuffer.Get(), &rtvDesc, handle);
  
  heapDesc.NumDescriptors = 6;
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
	heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	heapDesc.NodeMask = 0;
	
	result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessSRVHeap.ReleaseAndGetAddressOf()));
	if (FAILED(result))
	{
		return result;
	}
	
	D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
	srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
	srvDesc.Format = rtvDesc.Format;
	srvDesc.Texture2D.MipLevels = 1;
	srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
	
	...
	
	handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
	mDevice->CreateShaderResourceView(mSsrMaskBuffer.Get(), &srvDesc, handle);
	
	return result;
}

バックバッファと同じサイズのバッファを作成し、ディスクリプタヒープにレンダーターゲットビューとシェーダーリソースビューを生成します。

void Dx12Wrapper::SetRenderTargetSSRMaskBuffer() const
{
	auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(mSsrMaskBuffer.Get(),
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		D3D12_RESOURCE_STATE_RENDER_TARGET);

	mCmdList->ResourceBarrier(1, &barrier);

	auto rtvHeapPointer = mPeraRTVHeap->GetCPUDescriptorHandleForHeapStart();
	rtvHeapPointer.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV) * 7;

	auto dsvHeapPointer = mDepthStencilViewHeap->GetCPUDescriptorHandleForHeapStart();

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

	float clearColorBlack[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
	mCmdList->ClearRenderTargetView(rtvHeapPointer, clearColorBlack, 0, nullptr);
}

Dx12Wrapper にSSRマスクテクスチャをレンダーターゲットとして設定するメソッドを追加します。

mInstancingRenderer->BeforeDrawAtSSRMask();
mInstancingRenderer->DrawSSR();

このように描画するように呼び出し、PIXでマスクテクスチャを確認してみましょう。

image 2.png
SSRを適用する箇所のみマスキングが白くなります。

これで材料が準備できたので、SSRを実装します。

SSRの実装

上記の内容に基づいて、必要な材料を列挙します。

  • 画面カラーテクスチャ
  • 画面法線テクスチャ
  • SSRマスキングテクスチャ
  • 深度テクスチャ
  • 平面反射テクスチャ
  • SSRレンダーターゲットテクスチャ

最後の2つは新しくバッファを追加する必要があります。

平面反射テクスチャは後で追加し、まずSSRのレンダーターゲットとパイプラインステートを追加します。

ComPtr<ID3D12RootSignature> mSsrRootSignature = nullptr;
ComPtr<ID3D12PipelineState> mSsrPipeline = nullptr;
ComPtr<ID3D12Resource> mSsrTexture = nullptr;

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

HRESULT Dx12Wrapper::CreatePostProcessResource()
{
  ...
  
  auto result = mDevice->CreateCommittedResource(
			&heapProp,
			D3D12_HEAP_FLAG_NONE,
			&resDesc,
			D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
			&clearValue,
			IID_PPV_ARGS(mSsrTexture.ReleaseAndGetAddressOf())
		);

		if (FAILED(result))
		{
			return result;
		}
		
	...
		
  auto heapDesc = mRtvHeaps->GetDesc();
	heapDesc.NumDescriptors = 7;
	result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessRTVHeap.ReleaseAndGetAddressOf()));
	
	D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {};
	rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
	rtvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	
	...
  
  handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
  mDevice->CreateRenderTargetView(mSsrTexture.Get(), &rtvDesc, handle);
  
  heapDesc.NumDescriptors = 7;
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
	heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	heapDesc.NodeMask = 0;
	
	result = mDevice->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(mPostProcessSRVHeap.ReleaseAndGetAddressOf()));
	if (FAILED(result))
	{
		return result;
	}
	
	D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
	srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
	srvDesc.Format = rtvDesc.Format;
	srvDesc.Texture2D.MipLevels = 1;
	srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
	
	...
	
	handle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
	mDevice->CreateShaderResourceView(mSsrTexture.Get(), &srvDesc, handle);
	
	return result;
}

SSRテクスチャバッファを生成し、ディスクリプタヒープにレンダーターゲットビューとシェーダーリソースビューを生成します。

bool Dx12Wrapper::CreateSSRPipeline()

このメソッドでルートシグネチャとパイプラインステートを生成します。

内容が長いので、ちょっとずつ切り抜いてみていきま。

bool Dx12Wrapper::CreateSSRPipeline()
{
	UINT flags = 0;
#if defined( DEBUG ) || defined( _DEBUG )
	flags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif

	//Scene Buffer
	D3D12_DESCRIPTOR_RANGE sceneBufferDescriptorRange = {};
	sceneBufferDescriptorRange.NumDescriptors = 1;
	sceneBufferDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	sceneBufferDescriptorRange.BaseShaderRegister = 0;
	sceneBufferDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	//Color 
	D3D12_DESCRIPTOR_RANGE colorTexDescriptorRange = {};
	colorTexDescriptorRange.NumDescriptors = 1;
	colorTexDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
	colorTexDescriptorRange.BaseShaderRegister = 0;
	colorTexDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	//Normal
	D3D12_DESCRIPTOR_RANGE normalTexDescriptorRange = {};
	normalTexDescriptorRange.NumDescriptors = 1;
	normalTexDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
	normalTexDescriptorRange.BaseShaderRegister = 1;
	normalTexDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	//SSR Mask
	D3D12_DESCRIPTOR_RANGE ssrMaskTexDescriptorRange = {};
	ssrMaskTexDescriptorRange.NumDescriptors = 1;
	ssrMaskTexDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
	ssrMaskTexDescriptorRange.BaseShaderRegister = 2;
	ssrMaskTexDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	//Planer Reflection
	D3D12_DESCRIPTOR_RANGE planerReflectionTexDescriptorRange = {};
	planerReflectionTexDescriptorRange.NumDescriptors = 1;
	planerReflectionTexDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
	planerReflectionTexDescriptorRange.BaseShaderRegister = 3;
	planerReflectionTexDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	//Depth
	D3D12_DESCRIPTOR_RANGE depthTexDescriptorRange = {};
	depthTexDescriptorRange.NumDescriptors = 1;
	depthTexDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
	depthTexDescriptorRange.BaseShaderRegister = 4;
	depthTexDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	CD3DX12_ROOT_PARAMETER range[6] = {};

	range[0].InitAsDescriptorTable(1, &sceneBufferDescriptorRange, D3D12_SHADER_VISIBILITY_PIXEL);
	range[1].InitAsDescriptorTable(1, &colorTexDescriptorRange, D3D12_SHADER_VISIBILITY_PIXEL);
	range[2].InitAsDescriptorTable(1, &normalTexDescriptorRange, D3D12_SHADER_VISIBILITY_PIXEL);
	range[3].InitAsDescriptorTable(1, &ssrMaskTexDescriptorRange, D3D12_SHADER_VISIBILITY_PIXEL);
	range[4].InitAsDescriptorTable(1, &planerReflectionTexDescriptorRange, D3D12_SHADER_VISIBILITY_PIXEL);
	range[5].InitAsDescriptorTable(1, &depthTexDescriptorRange, D3D12_SHADER_VISIBILITY_PIXEL);
	
	D3D12_ROOT_SIGNATURE_DESC rsDesc = {};
	rsDesc.NumParameters = 6;
	rsDesc.pParameters = range;

	D3D12_STATIC_SAMPLER_DESC sampler = CD3DX12_STATIC_SAMPLER_DESC(0);
	sampler.Filter = D3D12_FILTER_COMPARISON_MIN_MAG_MIP_POINT;
	sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
	sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
	sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
	sampler.MipLODBias = 0.0f;
	sampler.MaxAnisotropy = 1;
	sampler.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS;
	sampler.MinLOD = 0;
	sampler.MaxLOD = D3D12_FLOAT32_MAX;
	rsDesc.pStaticSamplers = &sampler;
	rsDesc.NumStaticSamplers = 1;
	rsDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;

	ComPtr<ID3DBlob> rsBlob;
	ComPtr<ID3DBlob> errBlob;

	auto result = D3D12SerializeRootSignature(&rsDesc, D3D_ROOT_SIGNATURE_VERSION_1, rsBlob.ReleaseAndGetAddressOf(), errBlob.ReleaseAndGetAddressOf());
	if (FAILED(result))
	{
		assert(0);
		return false;
	}

	result = mDevice->CreateRootSignature(0, rsBlob->GetBufferPointer(), rsBlob->GetBufferSize(), IID_PPV_ARGS(mSsrRootSignature.ReleaseAndGetAddressOf()));
	if (FAILED(result))
	{
		assert(0);
		return false;
	}

ルートパラメータを作成し、ルートシグネチャを生成します。

シーン情報バッファ、スクリーンカラーテクスチャ、法線テクスチャ、SSRマスクテクスチャ、平面反射テクスチャ、深度テクスチャを使用できるようにします。

	ComPtr<ID3DBlob> vs;
	ComPtr<ID3DBlob> ps;

	D3D12_INPUT_ELEMENT_DESC layout[2] =
	{
		{"POSITION",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.DepthStencilState.DepthEnable = false;
	graphicsPipelineDesc.DepthStencilState.StencilEnable = false;
	graphicsPipelineDesc.InputLayout.NumElements = _countof(layout);
	graphicsPipelineDesc.InputLayout.pInputElementDescs = layout;
	graphicsPipelineDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
	graphicsPipelineDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
	graphicsPipelineDesc.NumRenderTargets = 1;
	graphicsPipelineDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
	graphicsPipelineDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
	graphicsPipelineDesc.SampleMask = D3D12_DEFAULT_SAMPLE_MASK;
	graphicsPipelineDesc.SampleDesc.Count = 1;
	graphicsPipelineDesc.SampleDesc.Quality = 0;
	graphicsPipelineDesc.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;
	graphicsPipelineDesc.pRootSignature = mSsrRootSignature.Get();

	result = D3DCompileFromFile(L"SSRVertexShader.hlsl",
		nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"main", "vs_5_0", flags, 0, vs.ReleaseAndGetAddressOf(), errBlob.ReleaseAndGetAddressOf());

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

	result = D3DCompileFromFile(L"SSRPixelShader.hlsl",
		nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE,
		"main", "ps_5_0", flags, 0, ps.ReleaseAndGetAddressOf(), errBlob.ReleaseAndGetAddressOf());

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

	graphicsPipelineDesc.VS = CD3DX12_SHADER_BYTECODE(vs.Get());
	graphicsPipelineDesc.PS = CD3DX12_SHADER_BYTECODE(ps.Get());
	result = mDevice->CreateGraphicsPipelineState(&graphicsPipelineDesc, IID_PPV_ARGS(mSsrPipeline.ReleaseAndGetAddressOf()));

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

	return true;
}

パイプラインステートを生成します。

SSRのシェーダーを書いてみましょう。

その前に、SSRを実装方法について話します。

image 3.png
上の図のように鏡が置かれており、その後ろには赤い球が置かれています。

では赤い矢印が指している部分にはどんな色が見えるでしょうか。

現実では物体が光を受けると、その光が跳ね返り、さらにその光が鏡に反射して私たちの目に入ることで、その色が見えるようになります。

image 4.png

これを逆にすると、目から見る方向を基準にどのような色が反射されているかを推測することもできるでしょう。

つまり、カメラから見る方向ベクトルで鏡面の反射ベクトルを求め、そのベクトルが当たる部分の色を取得することでSSRを実装することができます。

image 5.png
表示された部分にどのような色が反射されるか計算してみましょう。

カメラからピクセルに向かうベクトル

上で話した内容通りなら、まずカメラから該当ピクセルまでのベクトルを求める必要があります。

SSRは一種のポストプロセスであるため、描画が完了したテクスチャ以外に情報を得る方法がありません。

どのような情報でカメラからピクセルに向かうベクトルを求めることができるでしょうか?

image 6.png
深度値を使って求めることができます。

どのように求めるのでしょうか?

ここで逆投影行列を使用します。

UVの値はもちろんSSR計算を行う時点で知っており、このUVを使って現在のピクセルの深度値を取得できます。

そうすると、これらでピクセルのNDC座標を作成できます。

NDC座標のx、yはスクリーン座標系の座標で、zは深度値です。

UVを-1から1の範囲に変換するだけでNDC座標を作成できます。

$$
(u * 2+-1,v *-2+,1, depth, 1)
$$

上記のようにすればNDC座標を求めることができます。

深度値は深度テクスチャをサンプリングするだけで取得できます。

次は逆投影行列の番です。

求めたNDC座標を掛けると、ピクセルのビュー空間座標を得ることができます。

この事実が理解できない場合は、レンダリングパイプラインの座標変換について再度確認しておくことをお勧めします。

$$
viewpos=invProj*ndc
$$

とにかく、このようにしてビュー空間座標を得ることができます。

しかし、NDC座標は投影された座標でした。つまり、同次座標だったのです。

そのため、上記の式通りに行っても同次座標を変換しただけで、まだ3D空間の座標ではありません。

そこで、w値でx、y、z値を割ります。

$$
viewpos.xyz=viewpos.xyz/viewpos.w
$$

これにより、ビュー空間での3D座標を取得できます。

そして、この座標を正規化すれば完了です。

$$
normalize(viewpos)
$$

カメラがビュー空間の原点であるため、正規化するとカメラから該当ピクセルへ向かうベクトルになります。

反射ベクトル

該当ピクセル部分にカメラの視線ベクトルが当たった時に反射されるベクトルを求めます。これは簡単です。

法線ベクトルさえあれば、簡単に反射ベクトルを求めることができます。

法線テクスチャからサンプリングして法線値を取得し、法線値と視線ベクトルを使って反射ベクトルを計算します。

レイマーチング

反射ベクトルが得られたので、ピクセルの位置から反射ベクトルの方向に進んだ時に、他のオブジェクトに当たるかどうかを計算します。
image 7.png

このように少しずつ一定の間隔でレイを前進させて物体の形状を見つける方法をレイマーチングと呼びます。

SSR以外にも様々な効果に使用されます。

物体に当たるかどうかを判断するのも、先ほど話したように深度値さえあれば判別が可能です。

詳細については実際にシェーダーを作成しながら話すことにしましょう。

とにかく、物体に当たった時にその部分のUV値を求め、その値でサンプリングした値を反射された色の値として使用すれば、SSRの実装は完了です。

では実際にシェーダーを作成しましょう。

SSRシェーダー

Texture2D<float4> texColor : register(t0);
Texture2D<float4> texNormal : register(t1);
Texture2D<float4> texSSRMask : register(t2);
Texture2D<float4> texPlanerReflection : register(t3);
Texture2D<float> texDepth : register(t4);
SamplerState smp : register(s0);

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

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

ヘッダーです。

ルートパラメータを作成する際に使用するテクスチャについては既に説明しました。

#include "SSRHeader.hlsli"

Output main(float4 pos:POSITION, float2 uv : TEXCOORD)
{
	Output output;
	output.svpos = pos;
	output.uv = uv;
	return output;
}

頂点シェーダーもクワッドに描画するため、クリップ座標とUVをそのまま返します。

float4 main(Output input) : SV_TARGET
{
	float4 ssrMask = texSSRMask.Sample(smp, input.uv);

	if (ssrMask.r != 1.0f)
	{
		return float4(0.0f, 0.0f, 0.0f, 1.0f);
	}

	float4 planerReflectionColor = texPlanerReflection.Sample(smp, input.uv);
	float luminance = dot(planerReflectionColor.rgb, float3(0.299, 0.587, 0.114));
	if (luminance > 0.0f)
	{
		return planerReflectionColor;
	}

	float depth = texDepth.Sample(smp, input.uv);

	float4 viewPos = mul(invproj, float4(input.uv * float2(2, -2) + float2(-1, 1), depth, 1));
	viewPos.xyz = viewPos.xyz / viewPos.w;
	float3 viewDir = normalize(viewPos);

	float3 normal = normalize((texNormal.Sample(smp, input.uv).xyz * 2) - 1);
	float3 reflectionDir = normalize(reflect(viewDir, normal));

	float3 reflectionColor = RayMarching(viewPos, reflectionDir);

	return float4(reflectionColor, 1.0f);
}

これがピクセルシェーダーの全体的な内容です。

分けて見ていきましょう。

float4 ssrMask = texSSRMask.Sample(smp, input.uv);

if (ssrMask.r != 1.0f)
{
	return float4(0.0f, 0.0f, 0.0f, 1.0f);
}

現在のUVを使用してSSRマスクテクスチャをサンプリングします。

image 8.png
SSRマスクテクスチャを見ると、このような感じになるでしょう。

SSRリフレクションは、このテクスチャの白い部分のみで計算します。

そのため、サンプリングされた値が白色かどうかを検査し、白色でない場合は黒色を返して終了します。

float4 planerReflectionColor = texPlanerReflection.Sample(smp, input.uv);
float luminance = dot(planerReflectionColor.rgb, float3(0.299, 0.587, 0.114));
if (luminance > 0.0f)
{
	return planerReflectionColor;
}

平面反射テクスチャをサンプリングし、黒色でない場合はSSR反射を計算するのではなく、平面反射の色を使用するようにします。

まだ平面反射を別のテクスチャに描画する処理を追加していませんが、先にシェーダーに追加しておきましょう。

image 9.png
テクスチャをあらかじめお見せすると、このようになります。

床に反射されたミクさんだけが描画されており、残りは全て黒色です。

float depth = texDepth.Sample(smp, input.uv);

float4 viewPos = mul(invproj, float4(input.uv * float2(2, -2) + float2(-1, 1), depth, 1));
viewPos.xyz = viewPos.xyz / viewPos.w;
float3 viewDir = normalize(viewPos);

先ほど説明した内容です。

デプステクスチャからデプス値をサンプリングし、UVとデプス値を使用して現在のピクセルのNDC座標を生成します。

NDC座標に逆投影行列を乗算してビュー空間座標に変換し、すべての要素をw値で割って3D座標系に変換します。

正規化まで行えば、カメラからピクセルに向かうベクトルの計算が完了します。

float3 normal = normalize((texNormal.Sample(smp, input.uv).xyz * 2) - 1);
float3 reflectionDir = normalize(reflect(viewDir, normal));

法線テクスチャから法線値を取得します

そして、reflectメソッドを使用して反射ベクトルを計算します。

image 10.png
法線テクスチャを見ると、このような感じになるでしょう。

float3 reflectionColor = RayMarching(viewPos, reflectionDir);

これで反射されたベクトルの方向に少しずつ前進しながら、衝突する場所がないかを検査します。

RayMarchingメソッドを詳しく見てみましょう。

float3 RayMarching(float3 position, float3 direction)
{
	const int maxSteps = 50;
	const float maxDistance = 1000.0;
	float2 screenPos;

	[unroll(maxSteps)]
	for (int i = 0; i < maxSteps; ++i)
	{
		position += direction * ssrStepSize;
		screenPos = PosToScreen(position);

		if (screenPos.x < 0 || screenPos.x > 1 || 
			screenPos.y < 0 || screenPos.y > 1) 
		{
			break;
		}

		float detectDepth = texDepth.Sample(smp, screenPos);
		float4 viewPos = mul(invproj, float4(screenPos * float2(2, -2) + float2(-1, 1), detectDepth, 1));
		viewPos.xyz = viewPos.xyz / viewPos.w;

		if (viewPos.z > position.z)
		{
			return texColor.Sample(smp, screenPos).rgb;
		}

		if (length(position) > maxDistance)
		{
			break;
		}
	}

	return float3(0, 0, 0);
}
float3 RayMarching(float3 position, float3 direction)

パラメータとして開始位置と進行方向を受け取ります。

const int maxSteps = 50;
const float maxDistance = 1000.0;

最大進行回数と最大距離を設定します。

この値は状況によって変更しても構いません。

[unroll(maxSteps)]
for (int i = 0; i < maxSteps; ++i)

forループに入る前に[unroll(maxSteps)]というものがありますが、

これはHLSLでループアンローリングを指示するキーワードです。

ループアンローリングとは、forループを複数の繰り返しコードに展開することです。

for (int i = 0; i < maxSteps; ++i)
{
 //作業
}

このようなコードをコンパイラが次のように変換します。

// i = 0に該当する作業
// i = 1に該当する作業
// i = 2に該当する作業
// i = 3に該当する作業
...

以前にも話しましたが、GPUは条件分岐に弱いです。

そのため、forループの反復条件の評価を排除し、意図的に繰り返されたコードを使用する方が良い場合があります。

ただし、[unroll(maxSteps)]を使用したからといって、必ずループアンローリングが行われるわけではありません。コンパイラが状況に応じてその可否を判断します。

for文の中身を見てみましょう。

position += direction * 0.1f;
screenPos = PosToScreen(position);

if (screenPos.x < 0 || screenPos.x > 1 || 
		screenPos.y < 0 || screenPos.y > 1) 
{
	  break;
}

現在の位置から前進方向に0.1だけ前進します。

どれだけ前進するかも状況によって調整しましょう。

そして、前進した位置をPosToScreenメソッドを使用してスクリーン座標系に変換します。

float2 PosToScreen(float3 pos)
{
	float4 clipPos = mul(proj, float4(pos, 1.0));
	return clipPos.xy / clipPos.w * 0.5 + 0.5;
}

PosToScreenメソッドの内容です。

パラメータとして渡された座標はビュー空間の座標です。

そして、投影行列を掛けることでクリップ空間座標に変換することができます。

そして、xとyをwで割るとNDC座標に変換され、* 0.5 + 0.5で0から1の範囲に変換することができます。

つまり、これは前進した座標に対応するUV座標に変換したということです。

float detectDepth = texDepth.Sample(smp, screenPos);
float4 viewPos = mul(invproj, float4(screenPos * float2(2, -2) + float2(-1, 1), detectDepth, 1));
viewPos.xyz = viewPos.xyz / viewPos.w;

前進した座標に対応するUV値でデプステクスチャをサンプリングします。

このデプス値とUV値を使用してビュー空間座標を作成します。

if (viewPos.z > position.z)
{
	return texColor.Sample(smp, screenPos).rgb;
}

先ほど作成した座標値と前進した位置値のZを比較すると、

前進したレイが何らかの物体に接触したかどうかを判別することができます。

前進したレイのz値がより小さい場合、物体に接触したということです。

そして、前進したレイのUV値でサンプリングした色を反射の結果値として使用すれば完了です。

このようにプロセスを一つずつ見ていくと、それほど難しくないです。

最後に確かめておきたいのは平面反射です。

この記事の最初に行った内容をそのまま使用すれば良いです。

SSRに使用する平面反射テクスチャバッファを生成し、これをレンダーターゲットに設定して描画するだけです。

ComPtr<ID3D12Resource> mReflectionBuffer;

バッファを生成するコードをお見せしなくても、これまで見てきた内容であれば十分に理解できると思います。

SSRレンダリング

このような順序で描画します。

  1. 平面反射の描画
  2. ステージの描画
  3. SSRマスクテクスチャの描画
  4. SSRの描画
  5. PMXモデルの描画
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(mScreenResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mNormalResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);
	
barrier = CD3DX12_RESOURCE_BARRIER::Transition(mSsrTexture.Get(),
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		D3D12_RESOURCE_STATE_RENDER_TARGET);
	mCmdList->ResourceBarrier(1, &barrier);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mSsrMaskBuffer.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mReflectionBuffer.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);
	
barrier = CD3DX12_RESOURCE_BARRIER::Transition(mDepthBuffer.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);
	
auto wsize = Application::Instance().GetWindowSize();

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

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

mCmdList->SetPipelineState(mSsrPipeline.Get());
mCmdList->SetGraphicsRootSignature(mSsrRootSignature.Get());

mCmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
mCmdList->IASetVertexBuffers(0, 1, &mPeraVertexBufferView);

auto rtvBaseHandle = mPeraRTVHeap->GetCPUDescriptorHandleForHeapStart();
rtvBaseHandle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV) * 6;

float clearColor[4] = { 0.0,0.0,0.0,1.0 };
mCmdList->ClearRenderTargetView(rtvBaseHandle, clearColor, 0, nullptr);
mCmdList->OMSetRenderTargets(1, &rtvBaseHandle, false, nullptr);

mCmdList->SetDescriptorHeaps(1, mSceneDescHeap.GetAddressOf());
mCmdList->SetGraphicsRootDescriptorTable(0, mSceneDescHeap->GetGPUDescriptorHandleForHeapStart());

mCmdList->SetDescriptorHeaps(1, mPeraSRVHeap.GetAddressOf());

auto rtvSrvIncreaseSize = mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

auto screenHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();
mCmdList->SetGraphicsRootDescriptorTable(1, screenHandle) //mScreenResource

auto normalHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();
normalHandle.ptr += rtvSrvIncreaseSize;
mCmdList->SetGraphicsRootDescriptorTable(2, normalHandle); //mNormalResource

auto ssrMaskHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();
ssrMaskHandle.ptr += rtvSrvIncreaseSize * 5;
mCmdList->SetGraphicsRootDescriptorTable(3, ssrmaskHandle); //mSsrMaskBuffer

auto planerReflectionHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();
planerReflectionHandle.ptr += rtvSrvIncreaseSize * 7;
mCmdList->SetGraphicsRootDescriptorTable(4, planerReflectionHandle); //mReflectionBuffer

mCmdList->SetDescriptorHeaps(1, mDepthSRVHeap.GetAddressOf());
auto depthHandle = mDepthSRVHeap->GetGPUDescriptorHandleForHeapStart();
mCmdList->SetGraphicsRootDescriptorTable(5, depthHandle);

mCmdList->DrawInstanced(4, 1, 0, 0);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mSsrTexture.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

少し長いですね。

少しずつ分けて見ていきましょう。

auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(mScreenResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mNormalResource.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);
	
barrier = CD3DX12_RESOURCE_BARRIER::Transition(mSsrTexture.Get(),
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
		D3D12_RESOURCE_STATE_RENDER_TARGET);
	mCmdList->ResourceBarrier(1, &barrier);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mSsrMaskBuffer.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mReflectionBuffer.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);
	
barrier = CD3DX12_RESOURCE_BARRIER::Transition(mDepthBuffer.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

SSRには以下のものが必要です。

画面のカラーテクスチャ、

法線テクスチャ、

SSRマスクテクスチャ、

平面反射テクスチャ、

深度テクスチャ

が必要です。

SSRの前に舞台を描画したため、リソースの状態をシェーダーリソースに変更します。

そして、SSRのレンダーターゲットとして使用するバッファの状態も変更します。

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

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

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

mCmdList->SetPipelineState(mSsrPipeline.Get());
mCmdList->SetGraphicsRootSignature(mSsrRootSignature.Get());

mCmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
mCmdList->IASetVertexBuffers(0, 1, &mPeraVertexBufferView);

auto rtvBaseHandle = mPeraRTVHeap->GetCPUDescriptorHandleForHeapStart();
rtvBaseHandle.ptr += mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV) * 6;

float clearColor[4] = { 0.0,0.0,0.0,1.0 };
mCmdList->ClearRenderTargetView(rtvBaseHandle, clearColor, 0, nullptr);
mCmdList->OMSetRenderTargets(1, &rtvBaseHandle, false, nullptr);

SSRテクスチャをレンダーターゲットとして設定します。

mCmdList->SetDescriptorHeaps(1, mSceneDescHeap.GetAddressOf());
mCmdList->SetGraphicsRootDescriptorTable(0, mSceneDescHeap->GetGPUDescriptorHandleForHeapStart());

mCmdList->SetDescriptorHeaps(1, mPeraSRVHeap.GetAddressOf());

auto rtvSrvIncreaseSize = mDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

auto screenHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();
mCmdList->SetGraphicsRootDescriptorTable(1, screenHandle) //mScreenResource

auto normalHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();
normalHandle.ptr += rtvSrvIncreaseSize;
mCmdList->SetGraphicsRootDescriptorTable(2, normalHandle); //mNormalResource

auto ssrMaskHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();
ssrMaskHandle.ptr += rtvSrvIncreaseSize * 5;
mCmdList->SetGraphicsRootDescriptorTable(3, ssrmaskHandle); //mSsrMaskBuffer

auto planerReflectionHandle = mPeraSRVHeap->GetGPUDescriptorHandleForHeapStart();
planerReflectionHandle.ptr += rtvSrvIncreaseSize * 7;
mCmdList->SetGraphicsRootDescriptorTable(4, planerReflectionHandle); //mReflectionBuffer

mCmdList->SetDescriptorHeaps(1, mDepthSRVHeap.GetAddressOf());
auto depthHandle = mDepthSRVHeap->GetGPUDescriptorHandleForHeapStart();
mCmdList->SetGraphicsRootDescriptorTable(5, depthHandle);

シェーダーで使用するバッファーとテクスチャをバインドします。

mCmdList->DrawInstanced(4, 1, 0, 0);

barrier = CD3DX12_RESOURCE_BARRIER::Transition(mSsrTexture.Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET,
		D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
	mCmdList->ResourceBarrier(1, &barrier);

描画命令を実行し、SSRテクスチャを次のフレームでステージを描画する際に使用するため、状態をシェーダーリソースに変更します。

このようにしてPIXでSSRの結果を確認すると、このような感じになります。

image 11.png
SSRマスクに白色で記録された部分のみSSRリフレクションが適用された様子です。

次のフレームでステージをレンダリングする際にこのテクスチャを使用して、最終的に画面に表示させましょう。

FBX描画修正

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

	...
	//Reflection Render Texture
	descriptorRange[4].NumDescriptors = 1;
	descriptorRange[4].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
	descriptorRange[4].BaseShaderRegister = 1;
	descriptorRange[4].OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

	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);
	rootParam[4].InitAsDescriptorTable(1, &descriptorRange[4], D3D12_SHADER_VISIBILITY_ALL);
	...
}

FBXRendererでSSRテクスチャを使用できるようにルートパラメータを追加します。

t1レジスタを使用します。

シェーダーでSSRテクスチャを使用するように修正しましょう。

FBXモデルシェーダーヘッダーを開きます。

Texture2D<float4> reflectionRenderTexture: register(t1);

SSRテクスチャを追加します。

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

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);
	
	float2 ndc;
	ndc.x = input.svpos.x / 1600;
	ndc.y = input.svpos.y / 800;
	ndc.x = ndc.x * 2.0f - 1.0f;
	ndc.y = 1.0f - ndc.y * 2.0f;

	float2 renderTextureUV = ndc * 0.5f + 0.5f;
	renderTextureUV.y = 1.0f - renderTextureUV.y;
	float4 reflection = reflectionRenderTexture.Sample(smp, renderTextureUV);
	
	float4 color = float4(lerp(finalColor.rgb, reflection.rgb, 0.4), 1.0f);
	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;
}
float2 ndc;
ndc.x = input.svpos.x / 1600;
ndc.y = input.svpos.y / 800;
ndc.x = ndc.x * 2.0f - 1.0f;
ndc.y = 1.0f - ndc.y * 2.0f;

float2 renderTextureUV = ndc * 0.5f + 0.5f;
renderTextureUV.y = 1.0f - renderTextureUV.y;
float4 reflection = reflectionRenderTexture.Sample(smp, renderTextureUV);
	
float4 color = float4(lerp(finalColor.rgb, reflection.rgb, 0.4), 1.0f);

このコードが追加されました。

ステージをレンダリングする際に、SSRのテクスチャをサンプリングして、反射の結果が床に適用されるようにします。

SSRのテクスチャをサンプリングする際に使用するUV値は、現在のピクセルのスクリーン座標を計算して使用する必要があります。

svposのxとyを画面の幅と高さで割って、0から1の範囲に正規化します。

そして、NDC座標系に変換します。

このコードでは説明を簡略化するために画面の幅と高さを直接記述して使用していますが、実際には別のバッファを作成して渡す方が良いでしょう。

最終的に、NDC座標系をUV座標に変換してSSRテクスチャをサンプリングします。

サンプリングした色とステージの色を混ぜ合わせて最終的な色として使用します。

舞台が描画される様子を確認すると、このような感じになります。

image 12.png

まとめ

swinng.gif

平面反射とSSR反射を組み合わせて反射を実装しました。
SSRはドローコールを減らすための良い解決策にはなりますが、画面に表示されない部分は反射されなかったり、視点によっては平面反射よりも不自然な結果を示すのが欠点です。
そして、予想以上に多くのテクスチャを使用して描画しました。この部分は状況によって最適化が必要だと思います。
それでは、今回はここで終わりにします。
ありがとうございました。

参考リンク

https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html

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