실습을 통해 알아보자
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 |
댓글