DirectX

43. Light, Material_Depth Stencil View

devRiripong 2024. 2. 11.
반응형

실습을 통해 알아보자

 

1. 11. DepthStencilDemo클래스 생성해 Main에 세팅하고, 카메라의 위치를 변경하고, CreateDefaultMesh를 호출하는 ResourceManager::Init호출부를 윗줄로 옮기기

 

탐색기에서 10. GlobalTestDemo를 복제해서 11. DepthStencilDemo라고 이름을 정한다. 그리고Client/Game/Week2에 넣는다.

코드의 내용을 DepthStencilDemo에 맞게 수정한다.

Main.cpp에서

#include "11. DepthStencilDemo.h"

를 추가하고

desc.app = make_shared<DepthStencilDemo>(); // 실행 단위

이렇게 설정한다.

 

DepthStencil은 무엇을 하는 것이고 어떻게 응용을 할 수 있을까

카메라를 조금 보기 편하게 우리 쪽으로 당겨 본다.

DepthStencilDemo::Init에서

_camera->GetOrAddTransform()->SetPosition(Vec3{0.f, 0.f, -10.f});

그리고 // Object에 있는

RESOURCES->Init();

을 DepthStencilDemo::Init의 상단으로 옮겨준다.

 

2. DepthStencilDemo::Init()에서 큐브 object를 더 추가해서 출력하기

DepthStencilDemo::Init()에서

 

Sphere에 Veigar라는 이미지를 입힌 오브젝트가 있는데 추가적으로 //Object2를 만들어 볼 것이다.

// Object
	_obj = make_shared<GameObject>(); 
	_obj->GetOrAddTransform(); 
	_obj->AddComponent(make_shared<MeshRenderer>());
	{
		_obj->GetMeshRenderer()->SetShader(_shader);
	}
	{
		auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
		_obj->GetMeshRenderer()->SetMesh(mesh); 
	}
	{
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\veigar.jpg");
		_obj->GetMeshRenderer()->SetTexture(texture); 
	}

 

11. DepthStencilDemo.h 에

shared_ptr<GameObject> _obj2;

를 추가한다.

 

DepthStencilDemo::Init에서 Object내용을 복제하고 위치를 옮기고, Cube로 해줬다.

// Object2
	_obj2 = make_shared<GameObject>();
	_obj2->GetOrAddTransform()->SetPosition(Vec3{ 0.5f, 0.f, 2.f });
	_obj2->AddComponent(make_shared<MeshRenderer>());
	{
		_obj2->GetMeshRenderer()->SetShader(_shader);
	}
	{
		auto mesh = RESOURCES->Get<Mesh>(L"Cube");
		_obj2->GetMeshRenderer()->SetMesh(mesh);
	}
	{
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\veigar.jpg");
		_obj2->GetMeshRenderer()->SetTexture(texture);
	}

 

DepthStencilDemo::Update에서

_obj2->Update();

를 해준다.

 

이렇게 준비를 끝냈다.

 

보고 싶은 건 물체가 2개 있을 때 어떻게 동작할지가 궁금하다.

 

실행을 하면 큐브와 구가 보이고 있다.

 

 3. 깊이가 더 깊은 물체가 그려지는 현상의 이유와 해결책

구가 깊이가 0

큐브는 깊이를 2로 더 뒤에 세팅해 놨지만

큐브가 앞에 있는 것처럼 그려지고 있는게 이상하다.

 

코드에서 그리는 순서가 구를 먼저 그리고 그다음에 큐브를 그리고 있기 때문에 큐브가 앞에 있는 거처럼 보이는 게 아닐까 생각이 든다. 코드의 순서를 바꿔보면 깊이 값에 맞게 지금과 반대로 뜨는 걸 알 수 있다.

 

지금까지는 렌더링 파이프라인에서 여러개의 물체를 동시에 그리는 걸 생각한 적이 없었다.

그냥 셰이더에서 렌더링파이프라인 순서대로 물체를 그려주기 때문에 발생한 이슈다.

 

필요한 건 이미 물체가 그려져 있었으면 그 물체를 신경 써서 다른 물체도 신경 쓸 필요가 있다는 것이다.

멀티스레드까지 가는 건 아니고, 이 물체가 그려져 있는지 판별하기 위해서는 이전의 물체들 중에서 해당 픽셀에 대한 좌표 중에서 더 앞에 그려진 물체가 있었으면 안 그리면 된다.

 

깊이를 0~1 사이의 값을 가져올 수 있었는데 그걸로 판별하면 되는데

코드로 하는 건 아니고 DepthStencil이라는 개념이 있다.

그 부분을 처음에 세팅할 때 안해놔서 지금처럼 나오는 것이었다.

 

4. Graphics클래스에 _depthStencilTexture, _depthStencilView를 선언하고 CreateDepthStencilView 함수로 채워주기

세팅을 해보자.

 

Engine단에서 하는 것이다.

Engine/02. Managers/Graphics.cpp로 가서 보면

그려지는 온갖 것과 관련된 것들을 여기에 넣었었다.

그중에서 _device, _deviceContext가 커멘드 센터처럼 리소스를 파이프라인과 바인딩하는 양대 산맥이었고,

_swapChain은 후면, 전면 버퍼 왔다 갔다 해 주는 거였고,

_renderTargetView는 쉐이더에서 계산을 한 화면을 그려주는 그 텍스쳐를 말한다.

 

여기에 추가적으로 하나가 더 있다.

_renderTargetView가 최종 결과물을 그려주는 버퍼였다고 하면,

// DSV
	ComPtr<ID3D11Texture2D> _depthStencilTexture; 
	ComPtr<ID3D11DepthStencilView> _depthStencilView;

이걸 이제 채워줘야 한다.

 

void CreateDepthStencilView();

이 함수를 파준 다음에

여기에 기능들을 넣어준다.

일단 Graphics::Init에서 이 함수를 호출해 준다.

void Graphics::Init(HWND hwnd)
{
	_hwnd = hwnd;

	CreateDeviceAndSwapChain();
	CreateRenderTargetView(); 
	CreateDepthStencilView();
	SetViewport();
}

 

RenderTargetView를 만들자마자 DepthStencilView도 만들도록 세팅을 해줄 것이다.

3D 작업을 할 때 장치 초기화 할 때 같이 만드는 부분 중 하나다

 

Texture는 이미지 파일이라고 할 수 있는데 그리는 용도로 사용하는 것이 아니라 다양한 용도로 세팅을 해 줄 수 있었다. 이번에는 깊이를 기록하는 용도로 사용할 것이다.

 

나중에 설계가 깔끔하게 되어 클래스화가 되면, Texture를 범용적으로 사용할 수 있겠지만 지금은 간단하게 만들어 본다.

1) ComPtr<ID3D11Texture2D> _depthStencilTexture; 만들기 

void Graphics::CreateDepthStencilView()
{
	{
		D3D11_TEXTURE2D_DESC desc = { 0 }; 
		ZeroMemory(&desc, sizeof(desc)); 
		desc.Width = static_cast<uint32>(GAME->GetGameDesc().width); 
		desc.Height = static_cast<uint32>(GAME->GetGameDesc().height); 
		desc.MipLevels = 1;
		desc.ArraySize = 1;
		desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; // 포멧 옵션, DepthStencil을 연결하는 부분에서 연관성이 있다
		desc.SampleDesc.Count = 1;
		desc.SampleDesc.Quality = 0;
		desc.Usage = D3D11_USAGE_DEFAULT;
		desc.BindFlags = D3D11_BIND_DEPTH_STENCIL; // DepthStencil 용도로 활용을 할 것이다.
		desc.CPUAccessFlags = 0;
		desc.MiscFlags = 0;
	
		HRESULT hr = DEVICE->CreateTexture2D(&desc, nullptr, _depthStencilTexture.GetAddressOf());
		CHECK(hr); 
	}
}

이렇게 해서 텍스쳐가 만들어졌다.

 

2) ComPtr<ID3D11DepthStencilView> _depthStencilView;만들기

이 텍스쳐를 어떠한 용도로 활용할지 지정을 해 준 상태이고,

얘를 이용해서 묘사를 해주는 View로 이 아이가 어떤 용도로 활용이 되는지를 묘사하는 얘를 하나 더 만들어 준다.

RTV, DSV 이런 식으로

DX12로 넘어가면 텍스쳐를 만든 다음에 RTV용도로 활용하겠다고 한 번 더 거쳐서 만들게 되는데 그걸 비슷하게 하고 있다고 볼 수 있다.

DX12에서는 뷰라는 개념이 통합이 되어서 좀 더 사용하기 복잡하게 되어 있다. DX11에서 조금 더 명확하다.

결론적으로 얘를 묘사하기 위한 뭔가를 또 만들어 줘야 한다.

void Graphics::CreateDepthStencilView()
{
	{
		D3D11_TEXTURE2D_DESC desc = { 0 };
		ZeroMemory(&desc, sizeof(desc));
		desc.Width = static_cast<uint32>(GAME->GetGameDesc().width);
		desc.Height = static_cast<uint32>(GAME->GetGameDesc().height);
		desc.MipLevels = 1;
		desc.ArraySize = 1;
		desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; // 포멧 옵션, DepthStencil을 연결하는 부분에서 연관성이 있다
		desc.SampleDesc.Count = 1;
		desc.SampleDesc.Quality = 0;
		desc.Usage = D3D11_USAGE_DEFAULT;
		desc.BindFlags = D3D11_BIND_DEPTH_STENCIL; // DepthStencil 용도로 활용을 할 것이다.
		desc.CPUAccessFlags = 0;
		desc.MiscFlags = 0;

		HRESULT hr = DEVICE->CreateTexture2D(&desc, nullptr, _depthStencilTexture.GetAddressOf());
		CHECK(hr);
	}

	{
		D3D11_DEPTH_STENCIL_VIEW_DESC desc; 
		ZeroMemory(&desc, sizeof(desc)); 
		desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; 
		desc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D; 
		desc.Texture2D.MipSlice = 0; 

		HRESULT hr = DEVICE->CreateDepthStencilView(_depthStencilTexture.Get(), &desc, _depthStencilView.GetAddressOf());
		CHECK(hr);
	}
}

앞으로 이 View를 이용하여 사실상 건네줄 수 있게 된 것이다.

 

GPU에게 부탁을 할 때 Texture를 이용하는 게 아니라

DepthStencillView라는 티켓을 발급하는 것이다.

 

여기까지 하나의 텍스쳐가 만들어진 것이다.

그 텍스쳐는 어떠한 용도로 사용할 것이냐를 나중에 지정해 주게 되는데

이걸 지정하는 건 마지막 단계이다.

OutputMerger 단계에서 세팅하는 부분이 있었다.

 

5. Graphics::RenderBegin에서 _deviceContext->OMSetRenderTargets의 인자로 DepthStencilView를 세팅하기

Graphics::RenderBegin을 보면

OMSetRenderTargets에서 맨 마지막 인자가

DepthStencilView라는 걸 볼 수 있다.

지금까지는 이것을 활용 안 하고 있었다.

최종적으로 그려진 깊이값들을 여기다가 기록을 해달라고 부탁하는 것이다.

void Graphics::RenderBegin()
{
	_deviceContext->OMSetRenderTargets(1, _renderTargetView.GetAddressOf(), _depthStencilView.Get());

마지막 인자를 포인터로 채워서

매번 화면을 그릴 때마다 최종적으로 그렸던 깊이 값을 여기다가 채워준다는 얘기가 된다.

 

픽셀마다 깊이 갚을 기록을 하는데

그려졌다 함은 깊이가 0~1 사이여서 그려졌다고 볼 수 있다.

상황에서 다른 물체가 그려졌을 때 겹치는 영역을 판별해서 깊이 값을 비교해서 더 깊은 곳에 위치해 있는 건 안 그려주고 스킵이 된다.

 

여기까지 하고 잘 그려지는지 실행을 해본다.

 

이제는 문제가 아무것도 안 그려진다.

 

6. Graphics::RenderBegin에서 _deviceContext->ClearDepthStencilView를 통해 매 프레임마다 깊이 값을 1로 초기화하기

깊이를 기록하는 DepthStencilVIew 즉 텍스쳐에 기록한 건 맞지만

그 값을 매 프레임마다 깔끔하게 다시 밀어줘야 하는데 그 미는 작업을 안 했기 때문에 한 프레임은 정상적으로 그려졌겠지만 그다음 프레임부터는 이미 동일한 값으로 깊이가 들어가 있는 상태여서 안 그려진 거라 볼 수 있다.

 

Game→RenderBegin은 Game::Update에서 호출해주고 있기 때문에 매 프레임마다

Graphics::RenderBegin에서

_deviceContext->ClearDepthStencilView(_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1 ,0); )

이렇게 깊이 값을 1로 초기화해준다.

 

이제는 구가 앞에 그려지는 것을 볼 수 있다.

그리는 순서에 따라 그려지고 안그려지던 문제가 해결이 되었다.

 

 

7. Depth의 의의

깊이값을 하나의 텍스쳐로 관리하고 있고,

그걸 이용해서 최종적으로 화면에 그려질지 안 그려질지를 판별해주고 있기 때문에

깔끔하게 처리가 되었다고 볼 수 있다.

 

깊이라는 게 다른 기법들이란 엮여 있는 경우가 많다.

그림자 만들 때도 깊이가 중요하다.

카메라가 바라보는 방향으로 하진 않겠지만 어떤 물체가 그림자가 그려져야 한다는 건 어딘가에 조명이 있고, 빛을 쏘고 있는데 카메라를 조명위치에 배치해서 한 번 더 렌더링을 하는 것처럼 묘사를 해보고, 깊이 값을 계산하는 것이다. 깊이를 추적해서 어떤 물체가 앞에 있다면 뒤에 그림자를 그려주거나 이런 식으로 깊이랑 연관이 있다. 앞에 물체가 있으면 빛이 막히는 게 생기기 때문이다.

이런 온갖 연산을 할 때도 깊이가 여러모로 개입을 하게 된다고 보면 된다.

 

지금은 물체의 전후 순서를 판단하기에 필수적이다는 걸 알 수 있다.

 

2D때는 높이에 따라 재 정렬을 해야 했는데 지금은 알아서 자연스럽게 된다는 것이다.

 

8. Stencil은 무엇일까? 

아까 void Graphics::RenderBegin()의

_deviceContext->ClearDepthStencilView(_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1 ,0);

의 인자 중에 D3D11_CLEAR_STENCIL은 무엇일까?

 

판에 구멍을 뚫어서 그림을 그리는 걸 스텐실이라고 한다.

깊이뿐만 아니라 Stencil 값을 적어 놓게 되면

원하는 위치에다가 스텐실 구멍을 뚫어주면 그 값을 조립해서 모양을 만들거나 뭔가를 해줄 수 있게 되는 것이다.

스텐실은 구멍 뚫은 부분만 다른 색으로 바꾼다거나 해줄 수 있는데 그 기법과 연관성이 있다. 당장은 활용하지 않을 것이다.

 

9. Format으로 본 Depth와 Stencil에 사용하는 데이터 크기

Graphics::CreateDepthStencilView()에서

desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;

24 비트 깊이로, 8비트는 스텐실 용도로 사용한다고 했으니 스텐실 값은 1바이트만 사용할 수 있는 것이다. 그 정도로 충분한 경우가 많다. 0~255 사이의 숫자를 넣어서 사용하면 된다.

깊이는 3바이트니 일반적인 float보단 정밀하진 않겠지만 깊이값 비교 용도로만 사용하기엔 충분하다. 물론 다른 포맷도 있다.

 

10. 맺음말

 

3D게임을 만들 때는

void CreateRenderTargetView();
void CreateDepthStencilView();

CreateDepthStencilView는 CreateRenderTargetView와 함께 등장하는 중요한 요소라 볼 수 있다.

깊이값을 적어주는 용도의 버퍼라는 걸 기억하자.

반응형

'DirectX' 카테고리의 다른 글

45. Light, Material_Diffuse  (0) 2024.02.13
44. Light, Material_Ambient  (0) 2024.02.13
42. Light, Material_Global Shader  (0) 2024.02.11
41. DirectX11 3D 입문_Mesh  (0) 2024.02.10
40. DirectX11 3D 입문_Normal  (0) 2024.02.09

댓글