코승호딩의 메모장

[Render Target] 본문

DirectX12/DirectX12 응용

[Render Target]

코승호딩 2023. 10. 22. 15:23

이번 글에서는 렌더 타겟을 구현하기로 한다. 기존에 Forward 렌더링 방식은 제한되는 것과 성능이 좋지 않기 때문에 Deferred 렌더링 방식으로 변경해야 한다. 과거에는 대부분 Forward 방식을 사용했지만 현대 게임들은 대부분 Deferred 방식을 사용한다. 지연 렌더링을 의미하는 Deferred 렌더링은 위키백과에 자세히 설명이 되어 있다. 이번 글에서는 Deferred을 구현하는 것이 아니라 Deferred 렌더링을 하기 위해서 여러 개의 렌더 타겟에 쉐이더 프로그램으로 계산한 결과를 출력하는 것까지가 목표이다. 

 

Deferred shading - Wikipedia

From Wikipedia, the free encyclopedia Screen-space shading technique Diffuse Color G-BufferZ-BufferSurface Normal G-BufferFinal compositing (to calculate the shadows shown in this image, other techniques such as shadow mapping, shadow feelers or a shadow v

en.wikipedia.org


Deferred_ps

Forward_vs와 Deferred_vs의 내용은 당연히 동일하다. 따라서 Deferred 쉐이더의 픽셀 쉐이더만 변경하면 된다. 가장 먼저 해야할 것은 픽셀 쉐이더에서 출력 픽셀의 정보가 달라져야 한다. 

 

float4 PS_Main(VS_OUT pin) : SV_Target

 Forward_ps에서는 한 번의 패스에서 조명과 매핑을 통해 계산하여 얻은 픽셀의 색상 값 float4만을 SV_Target에 출력한다. SV_Target에 뒤에 아무것도 명시하지 않으면 암시적으로 SV_Target0에 해당 픽셀 값을 출력하게 된다. 그러나 Deferred_ps에서는 한 번의 패스에서 들어온 정보들을 바로 조명 계산을 하는 것이 아니다. 이 정보들을 우선 SV_Target[n]에 각각 출력하고 나중에 이를 합치는 것이다. 따라서 Deferred 즉 지연 쉐이더라는 말이 일맥상통하다.

 

struct PS_OUT
{
    float4  position : SV_Target0;
    float4  normal : SV_Target1;
    float4  diffuseAlbedo : SV_Target2;
    float3  fresnelR0 : SV_Target4;
    float   shiniess : SV_Target3;
};

PS_OUT PS_Main(VS_OUT pin)

우선 조명처리를 위해 필요한 값들은 머티리얼(diffuseAlbedo, fresnelR0, shininess)과 월드 좌표, 노멀이 있다. 따라서 이 값들을 Deferred_ps에서 해당 렌더 타겟에 출력해야 한다. 참고로 SV_Target을 PS_Main 인자의 뒤에서 빼야 한다.

 

PS_OUT PS_Main(VS_OUT pin)
{
	//...
	PS_OUT pout = (PS_OUT)0;
	pout.position = float4(pin.posW, 0.f);
	pout.normal = float4(bumpedNormalW, 0.f);
	pout.diffuseAlbedo = diffuseAlbedo;
	pout.shiniess = shininess;
	pout.fresnelR0 = fresnelR0;
	
	return pout;
}

지연 렌더링의 픽셀 쉐이더에서는 각 표본을 추출하여 얻은 값들을 출력 구조체에 채워넣는다.


Multiple RenderTarget

이제 렌더 타겟을 여러 개를 사용하여 쉐이더에서 계산한 내용을 출력할 것이기 때문에 MultipleRenderTarget이라는 클래스를 생성할 것이다. 이 클래스에서는 텍스처로 출력할 데이터를 관리하게 된다.

 

enum class RENDER_TARGET_GROUP_TYPE : uint8
{
	SWAP_CHAIN, // BACK_BUFFER, FRONT_BUFFER
	G_BUFFER, // POSITION, NORMAL, DIFFUSEALBEDO, SHINESS, FRESNELR0
	END,
};

enum
{
	RENDER_TARGET_G_BUFFER_GROUP_COUNT = 5,
	RENDER_TARGET_GROUP_COUNT = static_cast<uint8>(RENDER_TARGET_GROUP_TYPE::END)
};

struct RenderTarget
{
	sptr<Texture> target;
	float clearColor[4];
};

스왑 체인이 가지고 있는 후면, 전면 버퍼 두 개, 그리고 G-buffer 즉 Deferred 렌더링을 통해 출력할 데이터 5개를 enum class로 관리한다. 그리고 이들은 모두 하나의 텍스처에 해당하므로 하나의 렌더 타겟이 텍스처와 클리어 색상을 갖고 있도록 한다. 따라서 스포일러를 하자면 현재 스왑 체인 클래스가 가지고 있는 RTV를 MultipleRenderTarget 클래스가 가지고 있게 될 것이다. 주의할 점은 하나의 MultipleRenderTarget 클래스는 하나의 RENDER_TARGET_GROUP을 뜻한다. 이게 무슨 말이냐면 MultipleRenderTarget 하나의 클래스는 SWAP_CHAIN의 그룹이거나 아니면 G_BUFFER 그룹이어야 한다는 것이다. 예를 들어서 G_BUFFER 용도의 MultipleRenderTarget 클래스는 오직 G_BUFFER 렌더 타겟들만 가지고 있다.

 

class MultipleRenderTarget
{
	//...
private:
	RENDER_TARGET_GROUP_TYPE _groupType;
	vector<RenderTarget> _rtVec;
	uint32 _rtCount;
	sptr<Texture> _dsTexture;
	ComPtr<ID3D12DescriptorHeap> _rtvHeap;

	uint32 _rtvHeapSize;
	D3D12_CPU_DESCRIPTOR_HANDLE _rtvHeapBegin;
	D3D12_CPU_DESCRIPTOR_HANDLE _dsvHeapBegin;
};

그렇다면 MultipleRenderTarget 클래스는 다음과 같은 멤버 변수를 갖게 될 것이다. 자신이 어떠한 그룹 타입인지를 갖고 있으며 이 그룹에 해당하는 렌더 타겟 배열 그리고 렌더 타겟을 설정할 때, 깊이 값도 가지고 있어야 하기 때문에 내부적으로 깊이 스텐실 버퍼의 텍스처도 갖고 있도록 한다. 그리고 하나의 서술자 힙에 자신이 가지고 있는 렌더 타겟 서술자들을 넣어줘야 하기 때문에 렌더 타겟 서술자 힙도 가지고 있는 것을 볼 수 있다.

 

void MultipleRenderTarget::Create(RENDER_TARGET_GROUP_TYPE groupType, vector<RenderTarget>& rtVec, sptr<Texture> dsTexture)
{
	_groupType = groupType;
	_rtVec = rtVec;
	_rtCount = static_cast<uint32>(rtVec.size());
	_dsTexture = dsTexture;

	D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
	heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	heapDesc.NumDescriptors = _rtCount;
	heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	heapDesc.NodeMask = 0;

	DEVICE->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&_rtvHeap));

	_rtvHeapSize = DEVICE->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
	_rtvHeapBegin = _rtvHeap->GetCPUDescriptorHandleForHeapStart();
	_dsvHeapBegin = _dsTexture->GetDSV()->GetCPUDescriptorHandleForHeapStart();

	for (uint32 i = 0; i < _rtCount; ++i) {
		uint32 destSize = 1;
		D3D12_CPU_DESCRIPTOR_HANDLE destHandle = CD3DX12_CPU_DESCRIPTOR_HANDLE(_rtvHeapBegin, i * _rtvHeapSize);
		
		uint32 srcSize = 1;
		ComPtr<ID3D12DescriptorHeap> srcRtvHeapBegin = _rtVec[i].target->GetRTV();
		D3D12_CPU_DESCRIPTOR_HANDLE srcHandle = srcRtvHeapBegin->GetCPUDescriptorHandleForHeapStart();

		DEVICE->CopyDescriptors(1, &destHandle, &destSize, 1, &srcHandle, &srcSize, D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
	}
}

멀티 렌더 타겟을 생성하는 함수인데 생각보다 어렵지 않다. 우선 그룹 타입과 렌더 타겟 배열들과 깊이 스텐실 텍스처를 받아서 내부에 저장한다. 그리고 이 렌더 타겟들의 서술자를 담아줄 RTV 서술자 힙을 생성한다. 그리고 for문을 통해 받아온 렌더 타겟의 뷰들을 앞에서 생성한 SRV 서술자 힙에 담는 게 전부이다. 

 

void MultipleRenderTarget::OMSetRenderTargets()
{
	CMD_LIST->OMSetRenderTargets(_rtCount, &_rtvHeapBegin, TRUE, &_dsvHeapBegin);
}

void MultipleRenderTarget::ClearRenderTargetView()
{
	for (uint32 i = 0; i < _rtCount; ++i) {
		D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = CD3DX12_CPU_DESCRIPTOR_HANDLE(_rtvHeapBegin, i * _rtvHeapSize);
		CMD_LIST->ClearRenderTargetView(rtvHandle, _rtVec[i].clearColor, 0, nullptr);
	}

	CMD_LIST->ClearDepthStencilView(_dsvHeapBegin, D3D12_CLEAR_FLAG_DEPTH, 1.f, 0, 0, nullptr);
}

그리고 커맨드 큐의 렌더링을 하기 전 RenderBegin에서 렌더 타겟과 깊이 버퍼를 클리어와 셋을 해야 하는 것은 다 알고 있는 사실일 것이다. 따라서 모든 렌더 타겟들을 셋 하고 클리어할 수 있도록 함수를 구현한다. 추가적으로 만약 하나의 그룹의 모든 렌더 타겟들을 같이 클리어 혹은 셋을 하고 싶지 않다면 따로 offset을 인자로 받아 그 렌더 타겟만 출력하도록 함수를 추가한다. 예를 들어서 후면 버퍼 딱 하나만 설정하는 경우에 하나의 렌더 타겟만 셋, 클리어 하도록 해야할 것이다.


CreateTexture

이제 렌더 타겟 텍스처들을 MRT에서 관리할 것이기 때문에 기존의 스왑 버퍼의 RTV와 DSV를 삭제하고 MRT를 통해서 DSV와 RTV를 생성하여 교체할 것이다. 그러기 전에 우선 텍스처를 생성할 때, dds 파일 입출력뿐만 아니라 처음부터 리소스를 직접 생성하여 텍스처를 생성하거나 스왑 체인의 GetBuffer를 통해 리소스를 얻어와서 텍스처를 만드는 함수가 필요하다.

 

void Texture::Create(DXGI_FORMAT format, uint32 width, uint32 height, const D3D12_HEAP_PROPERTIES& property, D3D12_HEAP_FLAGS heapFlags, RENDER_GROUP_TYPE groupType, D3D12_RESOURCE_FLAGS resFlags, Vec4 clearColor)
{
	D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Tex2D(format, width, height);
	desc.Flags = resFlags;

	D3D12_CLEAR_VALUE optimizedClearValue = {};
	D3D12_RESOURCE_STATES resStates = D3D12_RESOURCE_STATE_COMMON;

	if (resFlags & D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL) {
		resStates = D3D12_RESOURCE_STATE_DEPTH_WRITE;
		optimizedClearValue = CD3DX12_CLEAR_VALUE(DXGI_FORMAT_D32_FLOAT, 1.f, 0);
	}
	else if (resFlags & D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET) {
		resStates = D3D12_RESOURCE_STATE_RENDER_TARGET;
		float arrFloat[4] = { clearColor.x, clearColor.y, clearColor.z, clearColor.w };
		optimizedClearValue = CD3DX12_CLEAR_VALUE(format, arrFloat);
	}

	ThrowIfFailed(DEVICE->CreateCommittedResource(
		&property,
		heapFlags,
		&desc,
		resStates,
		&optimizedClearValue,
		IID_PPV_ARGS(&_resource)
	));

	CreateFromResource(_resource, groupType);
}

위 함수는 리소스 인자를 설정하고 직접 생성해 주는 함수이다. 인자로 리소스 Flags를 받아 렌더 타겟용으로 사용할지 깊이 버퍼용으로 사용할 텍스처인지 구분하여 resource states를 정해준다. 그리고 생성된 디스크립션으로 리소스를 생성한다.

 

void Texture::CreateFromResource(ComPtr<ID3D12Resource> resource, RENDER_GROUP_TYPE groupType)
{
	_resource = resource;
	
	D3D12_RESOURCE_DESC desc = _resource->GetDesc();

	// DSV
	if (desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL) {
		D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
		heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
		heapDesc.NumDescriptors = 1;
		heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
		heapDesc.NodeMask = 0;
		DEVICE->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&_dsvHeap));

		D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = _dsvHeap->GetCPUDescriptorHandleForHeapStart();
		DEVICE->CreateDepthStencilView(_resource.Get(), nullptr, dsvHandle);
	}
	else
	{
		if (desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET)
		{
			// RTV
			D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
			heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
			heapDesc.NumDescriptors = 1;
			heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
			heapDesc.NodeMask = 0;
			DEVICE->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&_rtvHeap));

			D3D12_CPU_DESCRIPTOR_HANDLE rtvHeapBegin = _rtvHeap->GetCPUDescriptorHandleForHeapStart();
			DEVICE->CreateRenderTargetView(resource.Get(), nullptr, rtvHeapBegin);
		}

		switch (groupType)
		{
		case RENDER_GROUP_TYPE::SWAP_CHAIN:
		{
			// SRV
			D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {};
			srvHeapDesc.NumDescriptors = 1;
			srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
			srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
			DEVICE->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&_srvHeap));

			_srvHeapBegin = _srvHeap->GetCPUDescriptorHandleForHeapStart();

			D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
			srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
			srvDesc.Format = resource->GetDesc().Format;
			srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
			srvDesc.Texture2D.MipLevels = 1;
			DEVICE->CreateShaderResourceView(resource.Get(), &srvDesc, _srvHeapBegin);
		}
			break;
		case RENDER_GROUP_TYPE::G_BUFFER:
			CreateSRVFromDescHeap(TEXTURE_TYPE::TEXTURE2D);
			break;
		default:
			break;
		}


	}
}

다음 함수는 좀 길지만 별거 없다. 그저 생성된 리소스의 서술자를 생성해주는 함수이다. 각 텍스처마다 서술자 힙을 가지고 있도록 변경하고 서술자 힙을 생성하여 힙을 기반으로 서술자들을 생성한다. 단, 이전에 구현한 텍스처들은 자신의 서술자 힙을 하나씩 갖고 있지 않도록 구현하였다. TableDescriptorHeap이라는 클래스에서 서술자 힙 한 개를 가지고 있고 이 서술자 힙 하나만 사용하여 모든 텍스처의 서술자들을 저장해 줬다. 따라서 SWAP_CHAIN의 버퍼인 경우만 자신의 힙을 갖고 있도록 하고 다른 텍스처들은 기존 TableDescriptorHeap의 서술자 힙을 사용하도록 한다.

 

void Texture::CreateSRVFromDescHeap(TEXTURE_TYPE type)
{
	_type = type;
	_texHeapIndex = TexHeapIndex++;

	D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
	srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
	srvDesc.Format = _resource->GetDesc().Format;
	srvDesc.Texture2D.MostDetailedMip = 0;
	srvDesc.Texture2D.MipLevels = -1;

	switch (type)
	{
	case TEXTURE_TYPE::TEXTURE2D:
		srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
		break;
	case TEXTURE_TYPE::TEXTURECUBE:
		srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
		srvDesc.TextureCube.MipLevels = _resource->GetDesc().MipLevels;
		srvDesc.TextureCube.ResourceMinLODClamp = 0.f;
		DESCHEAP->SetSkyTexHeapIndex(_texHeapIndex);
		break;
	}

	CD3DX12_CPU_DESCRIPTOR_HANDLE srvHeapBegin = CD3DX12_CPU_DESCRIPTOR_HANDLE(DESCHEAP->GetSRVHandle());
	srvHeapBegin.Offset(_texHeapIndex, DESCHEAP->GetCbvSrvDescriptorSize());

	DEVICE->CreateShaderResourceView(_resource.Get(), &srvDesc, srvHeapBegin);
}

이 함수가 바로 TableDescriptorHeap 클래스의 서술자 힙으로 서술자들을 하나의 서술자 배열에 생성해 주는 역할을 한다. 굳이 SWAP_CHAIN의 버퍼는 같은 서술자 힙에 담지 않고 따로 자신만의 텍스처의 서술자 힙에 담아준 이유가 무엇일까? 바로 스왑 체인에서 사용하는 버퍼는 총 2개가 있다. 그러나 만약 현재 후면 버퍼가 아닌 전면 버퍼를 담아주면 무엇인가 이상할 것이다. 또한 이렇게 되면 후면 버퍼와 전면 버퍼 두 개를 셋 해야 하므로 서술자 개수도 늘어날뿐더러 프로젝트 구현을 머티리얼과 텍스처를 미리 만들어 놓은 상태에서 가져오는 방식을 사용하는데 런타임에 계속 후면 버퍼와 전면 버퍼를 바꿔가면서 텍스처 색인을 바꿔가면서 하는 것은 귀찮다. 

 

void TableDescriptorHeap::Update(uint8 currBackIndex)
{
	CD3DX12_CPU_DESCRIPTOR_HANDLE destHandle = CD3DX12_CPU_DESCRIPTOR_HANDLE(_srvHeap->GetCPUDescriptorHandleForHeapStart());
	destHandle.Offset(Texture::TexHeapIndex, _cbvSrvDescriptorSize);

	auto srcHandle = gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::SWAP_CHAIN)->GetRTTexture(currBackIndex)->GetSRVHandle();
	uint32 destSize = 1;
	uint32 srcsize = 1;

	DEVICE->CopyDescriptors(1, &destHandle, &destSize, 1, &srcHandle, &srcsize, D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
}

따라서 스왑 체인의 후면 버퍼 2개에 따라서 총 2개의 후면 버퍼 SRV Heap을 생성한다. 그리고 TableDescriptorHeap에서 따로 프레임마다 실행되는 Update함수를 호출하는데 이 함수에서는 현재 후면 버퍼 텍스처 Heap을 MRT에서 가져와서 자신의 Heap에 복사시켜 준다. 따라서 TableDescriptorHeap의 서술자 힙의 한 배열 공간에 두 개의 텍스처 힙이 교대로 복사되는 것이다.

 

void CommandQueue::RenderBegin(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect)
{
	int8 backIndex = _swapChain->GetBackBufferIndex();
	DESCHEAP->Update(backIndex);
    
	ID3D12DescriptorHeap* descHeap = gEngine->GetTableDescHeap()->GetSRV().Get();
	_cmdList->SetDescriptorHeaps(1, &descHeap);
	//...
}

다음과 같이 서술자 힙을 셋 하기 전 서술자 힙을 업데이트하여 현재 백 버퍼의 텍스처를 서술자 힙에 복사하는 것이다. 


class Engine
{
private:
	void CreateMultipleRenderTarget();

private:
	array<sptr<MultipleRenderTarget>, RENDER_TARGET_GROUP_COUNT> _mrt;
};

void Engine::Init(const WindowInfo& info)
{
	//...
	CreateMultipleRenderTarget();
}

이제 MRT를 엔진에 추가한다. 그리고 MRT를 생성하는 함수에서 깊이 버퍼와 후면 버퍼 그리고 G 버퍼를 생성하도록 하자.

 

void Engine::CreateMultipleRenderTarget()
{
	// DepthStencil
	sptr<Texture> dsTexture = GET_SINGLE(Resources)->CreateTexture("DepthStencil",
	DXGI_FORMAT_D32_FLOAT, _window.width, _window.height,
	CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
	D3D12_HEAP_FLAG_NONE, RENDER_GROUP_TYPE::DEPTH_STENCIL,
	D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL);
	//...
}

다음은 깊이 스텐실 버퍼와 DSV까지 생성하는 과정이다. 깊이 스텐실 버퍼는 따로 받아올 곳이 없이 새롭게 생성해야 한다. 따라서 CreateTexture를 통해서 깊이 버퍼 리소스를 생성하기 위한 정보를 인자로 넘겨준다. 이후 CreateTextureFromResource 함수를 통해서 DSV까지 생성이 될 것이다.

 

void Engine::CreateMultipleRenderTarget()
{
	// DepthStencil...
	// SwapChain Group
	vector<RenderTarget> rtVec(SWAP_CHAIN_BUFFER_COUNT);

	for (uint32 i = 0; i < SWAP_CHAIN_BUFFER_COUNT; ++i)
	{
		string name = "SwapChainTarget_" + std::to_string(i);

		ComPtr<ID3D12Resource> resource;
		_swapChain->GetSwapChain()->GetBuffer(i, IID_PPV_ARGS(&resource));
		rtVec[i].target = GET_SINGLE(Resources)->CreateTextureFromResource(name, resource, RENDER_GROUP_TYPE::SWAP_CHAIN);
	}

	_mrt[static_cast<uint8>(RENDER_TARGET_GROUP_TYPE::SWAP_CHAIN)] = make_shared<MultipleRenderTarget>();
	_mrt[static_cast<uint8>(RENDER_TARGET_GROUP_TYPE::SWAP_CHAIN)]->Create(RENDER_TARGET_GROUP_TYPE::SWAP_CHAIN, rtVec, dsTexture);
}

다음은 스왑 체인의 2개의 후면 버퍼 뷰를 생성하는 과정이다. 스왑 체인으로부터 후면 버퍼 리소스를 받아와서 리소스가 존재하기 때문에 CreateTextureFromResource 함수를 통해서 RTV와 SRV까지 생성한다. 

 

void Engine::CreateMultipleRenderTarget()
{
	//...
    
	vector<RenderTarget> rtVec(RENDER_TARGET_G_BUFFER_GROUP_COUNT);

	rtVec[0].target = GET_SINGLE(Resources)->CreateTexture("PositionTarget",
		DXGI_FORMAT_R32G32B32A32_FLOAT, _window.width, _window.height, 
		CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE, RENDER_GROUP_TYPE::G_BUFFER,
		D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET);

	rtVec[1].target = GET_SINGLE(Resources)->CreateTexture("NormalTarget",
		DXGI_FORMAT_R32G32B32A32_FLOAT, _window.width, _window.height,
		CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE, RENDER_GROUP_TYPE::G_BUFFER,
		D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET);

	rtVec[2].target = GET_SINGLE(Resources)->CreateTexture("DiffuseTarget",
		DXGI_FORMAT_R8G8B8A8_UNORM, _window.width, _window.height,
		CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE, RENDER_GROUP_TYPE::G_BUFFER, 
		D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET);

	rtVec[3].target = GET_SINGLE(Resources)->CreateTexture("FresnelTarget",
		DXGI_FORMAT_R8G8B8A8_UNORM, _window.width, _window.height,
		CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE, RENDER_GROUP_TYPE::G_BUFFER, 
		D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET);

	rtVec[4].target = GET_SINGLE(Resources)->CreateTexture("ShininessTarget",
		DXGI_FORMAT_R8_UNORM, _window.width, _window.height,
		CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE, RENDER_GROUP_TYPE::G_BUFFER, 
		D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET);

	_mrt[static_cast<uint8>(RENDER_TARGET_GROUP_TYPE::G_BUFFER)] = make_shared<MultipleRenderTarget>();
	_mrt[static_cast<uint8>(RENDER_TARGET_GROUP_TYPE::G_BUFFER)]->Create(RENDER_TARGET_GROUP_TYPE::G_BUFFER, rtVec, dsTexture);
}

이제 마지막으로 G 버퍼 텍스처를 생성하는 코드이다. 새로운 리소스를 만들어 리소스로부터 SRV와 RTV를 생성한다. 당연히 G 버퍼 텍스처들에 RTV를 생성해야 하는 이유는 출력 타깃으로 각 텍스처의 RTV를 지정해야 하기 때문이다. 이렇게 총 2개의 MRT가 생성되었다. 이제 디퍼드 렌더링을 위해 G 버퍼의 MRT에 그려주고 포워드 렌더링을 위해 스왑 체인 MRT에 그려주면 된다.

 

int8 backIndex = gEngine->GetSwapChain()->GetBackBufferIndex();
gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::SWAP_CHAIN)->ClearRenderTargetView(backIndex);
gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::G_BUFFER)->ClearRenderTargetView();

_mainCamera->GetCamera()->SortGameObject();
gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::G_BUFFER)->OMSetRenderTargets();
_mainCamera->GetCamera()->Render_Deferred();

gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::SWAP_CHAIN)->OMSetRenderTargets(1, backIndex);
_mainCamera->GetCamera()->Render_Forward();

씬의 렌더 함수에서는 메인 카메라를 렌더링 할 때, 먼저 디퍼드 쉐이더에 해당하는 오브젝트를 G 버퍼의 텍스처에 그려주고 다음으로 후면 버퍼에 포워드 쉐이더에 해당하는 오브젝트를 그린다. 참고로 당연히 디퍼드 렌더링부터 진행해야 하기 때문에 카메라에서 SortGameObject를 통해서 멤버 변수에 디퍼드 오브젝트와 포워드 오브젝트를 나눠주도록 하였다.

 

int8 backIndex = _swapChain->GetBackBufferIndex();

D3D12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(
gEngine->GetMRT(RENDER_TARGET_GROUP_TYPE::SWAP_CHAIN)->GetRTTexture(backIndex)->GetResource().Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);

마지막으로 잊지 않고 이제는 스왑 체인 클래스의 후면 버퍼가 아닌 스왑 체인 MRT의 후면 버퍼로 바꿔준다.


unordered_map<string, sptr<Texture>> _textures;
unordered_map<string, uint8> _textures;

참고로 씬이 가지고 있는 텍스처들의 맵을 실제 텍스처를 담는 것이 아닌 uint8 값을 가지고 있도록 변경하였다. 왜냐하면 동적 색인화를 통해서 이제 텍스처를 CPU, GPU 핸들이 아닌 머티리얼 구조적 버퍼의 필드로 색인으로 사용하기 때문이다. 

 

void Scene::LoadTestTexturesFromResource()
{
	vector<string> texNames = {
		"PositionTarget",
		"NormalTarget",
		"DiffuseTarget",
		"FresnelTarget",
		"ShininessTarget",
	};

	for (int i = 0; i < RENDER_TARGET_G_BUFFER_GROUP_COUNT; ++i) {
		auto texMap = GET_SINGLE(Resources)->Get<Texture>(texNames[i]);
		_textures[texNames[i]] = texMap->GetTexHeapIndex();
	}
}

이렇게 텍스처 이름을 텍스처 맵에 넣어주면 밸류로 서술자 힙의 몇 번에 위치하는지를 주도록 하면 된다. 텍스처의 texHeapIndex는 서술자 힙의 배열에 놓인 자신의 위치 색인이다. 따라서 만약 0이라면 서술자 힙의 0번째, 10이라면 서술자 힙의 10번째 배열에 놓여 있는 상태다. 때문에 우리는 그저 머티리얼의 구조적 버퍼에 이 텍스처 색인값만 넣어주면 끝이다.

 

void Scene::BuildMaterials()
{
	auto pos = make_shared<Material>();
	pos->SetMatCBIndex(0);
	pos->SetDiffuseSrvHeapIndex(_textures["PositionTarget"]);
	shared_ptr<Shader> shader = GET_SINGLE(Resources)->Get<Shader>("Forward");
	pos->SetShader(shader);
	_materials["position"] = move(pos);
	
	//... normal, diffuseAlbedo, fresnel, shininess 
    
	auto newjeans = make_shared<Material>();
	newjeans->SetMatCBIndex(5);
	newjeans->SetDiffuseSrvHeapIndex(_textures["newjeans"]);
	shared_ptr<Shader> shader = GET_SINGLE(Resources)->Get<Shader>("Deferred");
	newjeans->SetShader(shader);
	_materials["newjeans"] = move(newjeans);
    
	//... other material
}

다음과 같이 머티리얼의 SetDiffuseSrvHeapIndex 함수에 앞서 매핑한 텍스처 맵의 이름을 넣어주면 텍스처 서술자가 있는 힙의 위치를 반환하여 색인으로 사용할 수 있도록 하였다. 이제 실행해 보도록 하자.

 

(???...)

보다시피 위쪽에 각각 포지션, 노말, 텍스처, 프레넬, 매끄러움 텍스처들이 떠야 한다. 아래 물체들이 검은색인 이유는 깊이 값이 남아 있기 때문인데 이건 아직 디퍼드 렌더링을 진행하지 않아서이다. 이들을 그리는 명령을 따로 진행하지 않았음에도 깊이 값 때문에 검은색으로 뜨는 것이 정상이다. 그렇다면 왜 도대체 텍스처들이 뜨지 않을까?

 

우선 확인을 위해서 다음과 같이 Pos 텍스처 한 개만 그린 상태로 Pix를 돌려서 텍스처 상태를 확인하였다.

 

Pix를 통해서 텍스처에 제대로 텍스처가 로드가 됐는지 확인한 결과 텍스처는 문제없이 잘 돌아갔다 그렇다면 무엇이 문제일까. uv가 문제일까 싶어서 다 확인해 봤지만 문제는 없었다. 10시간 넘게 아무리 확인해 봐도 문제 있는 부분은 보이지 않았다. 혹시나 UI 오브젝트를 너무 작게 그려서 스케일 과정에서 이상한 값이 들어간 것이 아닐까 의심했고 결국 스케일링을 통해 크기를 늘렸다.

 

아니나 다를까 크기를 키우니 Pos의 텍스처가 잘 그려지는 것이 아닌가. 그런데 이미 텍스처는 들어간 상황인데 어째서 uv의 값도 변동이 없는데 그대로 텍스처를 맵핑하지 않고 크기가 작아졌다고 해서 그려지지 않는 것일까. 오른쪽은 스케일을 줄여가며 확인한 텍스처이다. 줄일수록 무엇인가 잘못되어 가고 결국 텍스처가 사라진다. 이유가 무엇일까

 

간단한 스크립트를 붙여 크기를 줄여보았더니 어느 정도 작아지면 점점 사라지게 된다. 혹시 몰라서 포지션 텍스처뿐만 아니라 노말 텍스처도 한 번 매핑해서 확인해 봤다. 이상하게 거리에 따라서 가까워질수록 잘 보이고 멀어질수록 잘 안 보이는 것이다. 따로 라이팅을 한 적도 없는데 자동으로 어두워졌다 밝아졌다. 이상하다 생각해서 쉐이더 부분을 살펴봤다.

 

따로 조명 처리를 하지 않았는데도 텍스처가 밝아졌다 어두워졌다 하는 것이 무엇인가 샘플링이 잘못되었다고 생각하였다. 그래서 비등방 필터링이 아닌 점 필터를 사용해 봤다. 아니나 다를까 멀어지면 어두워지는 것은 똑같지만 불규칙적으로 자연스럽지 않게 어두워졌다 밝아지는 것이다. 그렇다면 분명 샘플링 부분에서 문제가 있는 것이라고 파악하였다. 

 

그러나 곰곰이 생각해 보니 문뜩 로드한 텍스처는 밉맵이 dds파일이기 때문에 자동으로 압축되어 있는데 직접 만든 텍스처는 밉맵이 없지 않을까?라는 생각이 들었다. 당연히 직접 만든 텍스처는 밉맵이 존재하지 않고 결국 SRV를 생성할 때 밉맵을 설정하는 부분을 살펴봤다.

 

이 부분이 SRV를 생성하는 부분이다. 가만히 살펴보면 Texture2D.MipLevels가 -1로 되어 있다는 것을 볼 수 있다. 이 뜻은 텍스처가 가진 모든 밉맵 레벨을 사용하여 리소스 뷰를 생성하라는 뜻이다. 그렇다면 이 전에 밉맵 레벨이 없이 만든 텍스처는 어떻게 될까. 바로 위 상황처럼 샘플링이 되는 과정에서 밉맵 레벨이 존재하지 않기 때문에 이상하게 그려지는 것이다.

 

srvDesc.Texture2D.MipLevels = 1;

 그래서 곧바로 1 즉, 최소 레벨 1만 사용하겠다로 변경하여 실행해 보니 문제가 깔끔하게 사라졌다.

 

다음과 같이 작아져도 문제없이 잘 그려졌고 거리가 멀어져도 상관없이 잘 그려졌다.

 

이제 모든 G 버퍼를 그렸다. 차례대로 포지션, 노말, 텍스처, 프레넬, 매끄러움이다. 자세히 보면 G 버퍼의 텍스처에서 지지직 거리는 부분과 물체들을 그리지 않았음에도 검은색으로 그려져 있는 모습을 볼 수 있는데 이는 다음 글에서 설명할 디퍼드 렌더링에서 모두 수정이 가능하다.


코딩 생활을 하면서 이번 렌더 타겟을 구현하는데 가장 큰 어려움을 겪었다. 고작 1에서 -1로 숫자를 변경했다고 10시간이 넘는 시간을 소비했으며 엄청난 좌절감을 맛 봤다. 버그를 찾는 과정에서는 정말정말 힘들었다. 또한 프로젝트가 커지다 보니 텍스처를 다시 설계하는 과정에서 쉽지 않았고, 구현을 진행하며 수백번 어떻게 구현하는 것이 가장 좋을지를 생각하며 코드를 작성하였다. 가장 최적의 방법은 찾는다고 찾았지만 더 좋은 방법이 존재할것이다. 그래도 -1을 발견했을 때 그 쾌락은 잊을수가 없다. 10시간 고생한 것이 단 몇 초만에 날아가는 느낌을 받았다. 이게 코딩의 묘미가 아닐까. 사실 코딩이란 것이 백만 줄이 있다고 하더라도 키보드 하나 잘못 누르면 안돌아가게 되어 있다. 결국 프로그래머들은 모두 완벽주의자가 아닐까라는 생각이 들 정도이다. 아무튼 이번 렌더 타겟을 구현하면서 힘이 들기도 했지만 렌더 타겟을 나눠서 각 텍스처에 출력을 한 모습을 보니 굉장히 뿌듯하였고 디퍼드 렌더링, 포스트 프로세싱 뭐든 구현할 수 있을 것만 같았다.

'DirectX12 > DirectX12 응용' 카테고리의 다른 글

[Deferred Rendering]  (0) 2023.10.30
[Orthographic Projection]  (1) 2023.10.22
[Dynamic Indexing]  (1) 2023.10.21
[Frustum Culling]  (0) 2023.10.21
[CubeMap]  (1) 2023.10.16
[Normal Mapping]  (0) 2023.10.12
[Light-2]  (1) 2023.10.11