일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 장치 초기화
- light
- TCP/IP
- 게임 디자인 패턴
- 게임 프로그래밍
- 큐브 매핑
- Render Target
- gitlab
- DirectX12
- effective C++
- InputManager
- Frustum Culling
- 조명 처리
- 네트워크
- C++
- 입방체 매핑
- DirectX
- FrameResource
- gitscm
- 디퍼드 렌더링
- 직교 투영
- direct3d
- Dynamic Indexing
- 게임 클래스
- 네트워크 게임 프로그래밍
- Deferred Rendering
- 절두체 컬링
- 동적 색인화
- 노멀 맵핑
- Direct3D12
- Today
- Total
코승호딩의 메모장
[Mesh] 본문
이번 글에서는 Mesh 클래스를 사용하여 간단한 오브젝트를 그려보고자 한다. 초기화 부분에서 정점 정보와 인덱스 정보를 넘겨주고 버퍼를 생성한 뒤 렌더링 함수에서 위 버퍼들에 대한 서술자를 셋 해주고 이동 관련 상수 버퍼를 업데이트하여 결과적으로 해당하는 메쉬에 대한 이동을 수행한다.
정점 정보는 디폴트 힙에 만들어야 한다. 일반적으로 모델링의 로컬 정점들은 변하지 않기 때문이다. 따라서 업로드 힙을 생성하여 정점 정보를 넘겨주고 업로드 힙의 내용을 디폴트 힙의 버퍼에 복사해야 한다. 이를 위해 편의용 함수를 구현한다.
ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer(ComPtr<ID3D12Device> device,
ComPtr<ID3D12GraphicsCommandList> cmdList, const void* initData,
UINT64 byteSize, ComPtr<ID3D12Resource>& uploadBuffer)
{
ComPtr<ID3D12Resource> defaultBuffer;
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(defaultBuffer.GetAddressOf())));
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(uploadBuffer.GetAddressOf())));
D3D12_SUBRESOURCE_DATA subResourceData = {};
subResourceData.pData = initData;
subResourceData.RowPitch = byteSize;
subResourceData.SlicePitch = subResourceData.RowPitch;
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST));
UpdateSubresources<1>(cmdList.Get(), defaultBuffer.Get(), uploadBuffer.Get(), 0, 0, 1, &subResourceData);
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_GENERIC_READ));
return defaultBuffer;
}
이 함수는 간단하게 디폴트 힙의 버퍼와 업로드 힙의 버퍼에 해당하는 리소스들을 생성하여 업로드 힙 버퍼의 내용을 디폴트 힙의 버퍼에 복사해 주고 결과적으로 생성된 디폴트 힙 버퍼를 반환해 준다.
void Mesh::CreateVertexBuffer(const vector<Vertex>& buffer)
{
_vertexCount = static_cast<uint32>(buffer.size());
uint32 bufferSize = _vertexCount * sizeof(Vertex);
_vertexBuffer = d3dUtil::CreateDefaultBuffer(DEVICE, CMD_LIST, buffer.data(), bufferSize, _vertexBufferUploader);
_vertexBufferView.BufferLocation = _vertexBuffer->GetGPUVirtualAddress();
_vertexBufferView.StrideInBytes = sizeof(Vertex);
_vertexBufferView.SizeInBytes = bufferSize;
}
void Mesh::CreateIndexBuffer(const vector<uint32>& buffer)
{
_indexCount = static_cast<uint32>(buffer.size());
uint32 bufferSize = _indexCount * sizeof(uint32);
_indexBuffer = d3dUtil::CreateDefaultBuffer(DEVICE, CMD_LIST, buffer.data(), bufferSize, _indexBufferUploader);
_indexBufferView.BufferLocation = _indexBuffer->GetGPUVirtualAddress();
_indexBufferView.Format = DXGI_FORMAT_R32_UINT;
_indexBufferView.SizeInBytes = bufferSize;
}
따라서 간단하게 메쉬의 초기화 부분에서 정점 버퍼와 인덱스 버퍼를 생성할 때, 이 함수를 사용하면 다음과 같다.
void Mesh::Render()
{
CMD_LIST->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
CMD_LIST->IASetVertexBuffers(0, 1, &_vertexBufferView);
CMD_LIST->IASetIndexBuffer(&_indexBufferView);
CB(CONSTANT_BUFFER_TYPE::OBJECT)->PushData(&_objectConstant, sizeof(ObjectConstants));
gEngine->GetTableDescHeap()->CommitTable();
CMD_LIST->DrawIndexedInstanced(_indexCount, 1, 0, 0, 0);
}
그리고 위 코드를 통해 정점 버퍼 서술자와 인덱스 버퍼 서술자를 셋하고 이동 관련 상수 버퍼의 내용을 업데이트하고 CommitTable을 통해 테이블에 상수 버퍼 서술자를 셋 한다. 그리고 인덱스에 대한 그리기 명령을 제출한다.
void Game::Init(const WindowInfo& info)
{
GEngine->Init(info);
vector<Vertex> vec(4);
vec[0].pos = Vec3(-0.5f, 0.5f, 0.5f);
vec[0].color = Vec4(1.f, 0.f, 0.f, 1.f);
vec[1].pos = Vec3(0.5f, 0.5f, 0.5f);
vec[1].color = Vec4(0.f, 1.f, 0.f, 1.f);
vec[2].pos = Vec3(0.5f, -0.5f, 0.5f);
vec[2].color = Vec4(0.f, 0.f, 1.f, 1.f);
vec[3].pos = Vec3(-0.5f, -0.5f, 0.5f);
vec[3].color = Vec4(0.f, 1.f, 0.f, 1.f);
vector<uint32> indexVec;
{
indexVec.push_back(0);
indexVec.push_back(1);
indexVec.push_back(2);
}
{
indexVec.push_back(0);
indexVec.push_back(2);
indexVec.push_back(3);
}
mesh->Init(vec, indexVec);
shader->Init(L"..\\Resources\\Shader\\default.hlsli");
GEngine->GetCmdQueue()->WaitSync();
}
void Game::Update()
{
GEngine->RenderBegin();
shader->Update();
{
Transform t;
t.offset = Vec4(0.f, 0.f, 0.f, 0.f);
mesh->SetTransform(t);
mesh->Render();
}
GEngine->RenderEnd();
}
이제 Game에서 메쉬를 생성하여 정점 정보와 인덱스 정보를 넘겨주고 렌더링 부분에 메쉬의 렌더 함수를 호출하면 끝이다. 그리고 곧바로 프로그램을 실행해 보면...
(???...)
아무것도 그려지지 않는다. 호출 내용을 보니 이미 닫힌 명령 리스트에 대해 API 호출을 시도했을 때 발생한 문제이다. 명령 리스트를 닫은 후에는 더 이상 명령을 추가하거나 명령 리스트 관련 작업을 수행할 수 없다. 그런데 나는 분명히 FrameResource의 현재 프레임에 해당하는 리소스를 가져와서 아래와 같이 Reset을 수행하여 열어줬다.
auto cmdAlloc = mCurrFrameResource->CmdAlloc;
cmdAlloc->Reset();
_cmdList->Reset(cmdAlloc.Get(), nullptr);
현재 명령 리스트를 열어줬는데 왜 위 문제가 나타나는 것일까? 문제는 다음과 같다. 글의 맨 처음으로 돌아가 CreateDefaultBuffer를 살펴보면 커맨드 리스트를 넘겨주는 것을 볼 수 있다. CreateDefaultBuffer 함수는 Mesh의 Init에서 정점 버퍼와 인덱스 버퍼를 생성하는 부분에서 호출하게 되며 결국 Mesh의 Init은 Game의 Init 부분에서 실행된다. 그러나 위 코드에 나온 Reset 부분은 Mesh Update에서 일어난다. 즉, Init이 Update보다 빨리 실행되기 때문에 Reset을 통해 커맨드 리스트를 열지도 않았는데 Init을 통해 이 커맨드 리스트의 명령을 호출하였기 때문이다.
해결 방법으로는 프레임 리소스에 있는 명령 할당자뿐만 아니라 기존에 커맨드 큐 클래스에서 가지고 있던 명령 할당자를 생성한 다음 초기화 부분에서 명령 할당자를 열고 Init을 끝낸 다음 이 명령 할당자를 닫는 것이다.
class CommandQueue
{
public:
//...
void BuildFrameResource(ComPtr<ID3D12Device> device);
private:
//...
ComPtr<ID3D12CommandQueue> _cmdQueue;
ComPtr<ID3D12CommandAllocator> _cmdAlloc;
ComPtr<ID3D12GraphicsCommandList> _cmdList;
std::vector<uptr<FrameResource>> mFrameResources;
FrameResource* mCurrFrameResource = nullptr;
int mCurrFrameResourceIndex = 0;
};
다음과 같이 커맨드 큐 클래스는 프레임 리소스의 명령할당자 뿐만 아니라 자신의 명령 할당자도 가지고 있다. 이 명령 할당자를 Init에서 생성한 다음 커맨드 리스트는 닫혀 있는 상태일 것이다.
void Game::Init(const WindowInfo& info)
{
gEngine->Init(info);
CMD_LIST->Reset(CMD_ALLOC.Get(), nullptr);
//... Mesh Init
ThrowIfFailed(CMD_LIST->Close());
ID3D12CommandList* cmdsLists[] = { CMD_LIST.Get() };
CMD_QUEUE->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
gEngine->GetCmdQueue()->WaitSync();
}
닫혀 있는 커맨드 리스트를 Mesh를 Init 하기 위해서 FrameResource가 아닌 CommandQueue 클래스의 명령 할당자를 이용해서 열어주고 모두 완료한 다음 다시 닫고 명령을 제출한다.
이렇게 어떠한 오류가 떴을 때 가장 좋은 대책은 출력창에 어떠한 오류가 떠있는지 확인하는 것이다. 넓고 넓은 코드 중에 오류를 단번에 파악하는 것은 말이 안 되고 최소한 어떤 부분에서 오류가 떴는지를 알게 되면 확인해야 할 사항이 대폭 줄어들기 때문이다. 특히 Direct3D는 오류가 자주 발생하며(개발자의 실수에 대한 오류) 잡기가 힘들기 때문에 출력 창을 잘 확인하도록 하자.
마지막으로 쉐이더를 클래스화 하여 파이프라인 상태 객체를 생성하여 업데이트 함수에서 셋 할 수 있도록 한다.
void Shader::Init(const wstring& path)
{
CreateVertexShader(path, "VS_Main", "vs_5_1");
CreatePixelShader(path, "PS_Main", "ps_5_1");
D3D12_INPUT_ELEMENT_DESC desc[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
_pipelineDesc.InputLayout = { desc, _countof(desc) };
_pipelineDesc.pRootSignature = ROOT_SIGNATURE.Get();
_pipelineDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
_pipelineDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
_pipelineDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
_pipelineDesc.SampleMask = UINT_MAX;
_pipelineDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
_pipelineDesc.NumRenderTargets = 1;
_pipelineDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
_pipelineDesc.DSVFormat = gEngine->GetDepthStencilBuffer()->GetDSVFormat();
_pipelineDesc.SampleDesc.Count = 1;
DEVICE->CreateGraphicsPipelineState(&_pipelineDesc, IID_PPV_ARGS(&_pipelineState));
}
void Shader::Update()
{
CMD_LIST->SetPipelineState(_pipelineState.Get());
}
// hlsl
struct ObjectConstants
{
float4 offset0;
float4 offset1;
};
ConstantBuffer<ObjectConstants> gObjConstants : register(b0);
struct VS_IN
{
float3 pos : POSITION;
float4 color : COLOR;
};
struct VS_OUT
{
float4 pos : SV_Position;
float4 color : COLOR;
};
VS_OUT VS_Main(VS_IN input)
{
VS_OUT output = (VS_OUT)0;
output.pos = float4(input.pos, 1.f);
output.pos += gObjConstants.offset0;
output.color = input.color;
output.color += gObjConstants.offset1;
return output;
}
float4 PS_Main(VS_OUT input) : SV_Target
{
return input.color;
}
이번 예제에서는 책의 예제와 강의의 예제를 합치는 과정에서 FrameResource를 사용하다보니 오류가 생긴 부분이 있었고 여기서 커맨드 리스트의 Reset을 확실히 배웠다. 이전까지는 그저 예제를 따라하였기 때문에 커맨드 리스트의 오류와 친하지 않았고 쉽게 생각하였다. 그러나 커맨드 큐 클래스의 명령 할당자와 FrameResource의 명령 할당자 총 4개의 할당자가 있음에도 불구하고 나는 커맨드 큐 클래스의 명령 할당자를 잊고 있었던 것이었다. 메쉬를 만드는 부분은 엔진 프로젝트가 아닌 클라이언트 프로젝트에서 일어나며 커맨드 리스트의 Reset은 엔진 프로젝트의 커맨드 큐 렌더링 부분에서 일어난다. 순서는 Game::Init > Engine::Init > Game::Update > Engine::Update > Engine::Render 이기 때문에 메쉬를 만드는 부분인 Game::Init에서 Engine::Init의 호출보다 늦게 일어난다 하더라도 커맨드 큐가 Reset 되어 Open되는 시기인 Engine::Render 보다 일찍 일어나기 때문에 오류가 발생하였던 것이었다. 간단히 FrameResource의 명령 할당자가 아닌 커맨드 큐 클래스의 명령 할당자를 사용하여 Reset 및 ExecuteCommandLists를 통해 오류를 해결할 수 있었다. 이처럼 많은 삽질(?)을 통해서 발견한 보물을 소중히 다뤄야 겠다는 생각이 들었다.
'DirectX12 > DirectX12 응용' 카테고리의 다른 글
[SceneManager] (1) | 2023.10.08 |
---|---|
[Component] (0) | 2023.10.07 |
[InputManager와 GameTimer] (1) | 2023.10.07 |
[Material] (0) | 2023.10.07 |
[Texture Mapping] (0) | 2023.10.07 |
[ConstantBuffer와 FrameResource] (0) | 2023.10.07 |
[장치 초기화] (1) | 2023.10.07 |