2024. 2. 19. 11:32ㆍProgramming/Computer Graphics
01. Framework 목표
- 앞으로 진행할 프로젝트의 틀이 될 간단한 프레임워크 구성
- 플레이어로 사용할 정육면체 렌더링
- 플레이어 컨트롤러 구현
- 플레이어를 추적하는 카메라 구현
- 제자리에서 회전하는 7 * 7 * 7개의 정육면체 렌더링
첫 프로젝트이니만큼 게시글이 상당히 길어질 것 같아, 게시글을 둘로 나눠 프로젝트 구조와 장치 초기화에 대해 설명드리고, 뒤이어 업데이트와 렌더링에 설명드리겠습니다.
GitHub - SH4MDEL/DirectX12-ComputerGraphics
Contribute to SH4MDEL/DirectX12-ComputerGraphics development by creating an account on GitHub.
github.com
위 리포지토리에서 전체 소스코드를 확인하실 수 있습니다.
이 프로젝트의 소스코드는 01. Framework입니다.
구현
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다.
RECT rect{ 0, 0, Settings::DefaultWindowWidth, Settings::DefaultWindowHeight };
HWND hWnd = CreateWindowW(szWindowClass, szTitle,
WS_OVERLAPPED | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_BORDER,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd) return false;
g_framework = make_unique<GameFramework>(Settings::DefaultWindowWidth, Settings::DefaultWindowHeight);
g_framework->OnCreate(hInst, hWnd);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return true;
}
프로세스가 초기화될 때 'GameFramework'라는 클래스를 동적 할당하게 되고, 이 클래스에서 DirectX3D의 초기화를 비롯한 게임 전체 로직을 관리하게 됩니다.
class GameFramework
void GameFramework::InitDirect3D()
{
CreateDevice();
CreateFence();
Check4xMSAAMultiSampleQuality();
CreateCommandQueueAndList();
CreateSwapChain();
CreateRtvDsvDescriptorHeap();
CreateRenderTargetView();
CreateDepthStencilView();
CreateRootSignature();
}
GameFramework 클래스에서는 다음과 같은 순서로 Direct3D를 초기화합니다. DirectX 12가 아닌 그래픽스 쪽에 집중하기 위한 프로젝트이기 때문에 구체적인 초기화 과정은 생략하겠습니다.
void GameFramework::CreateRootSignature()
{
CD3DX12_ROOT_PARAMETER rootParameter[2];
// GameObject : 월드 변환 행렬(16)
rootParameter[0].InitAsConstants(16, 0, 0, D3D12_SHADER_VISIBILITY_ALL);
// Camera : 뷰 변환 행렬(16) + 투영 변환 행렬(16)
rootParameter[1].InitAsConstants(32, 1, 0, D3D12_SHADER_VISIBILITY_ALL);
CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init(_countof(rootParameter), rootParameter, 0, NULL,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr<ID3DBlob> signature, error;
Utiles::ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc,
D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
Utiles::ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(),
signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature)));
}
루트 시그니처는 위과 같이 0번 셰이더 레지스터에 16 * 4바이트를 할당하였고, 1번 셰이더 레지스터에 32 * 4바이트를 할당하였습니다.
제 프레임워크에는 하나의 객체가 어떻게 그려질지를 추상화한 'GameObject' 클래스와 현재 관찰자의 시점을 추상화한 'Camera' 클래스가 존재합니다. GameObject 클래스에서는 그 객체의 월드 변환 행렬(World Matrix)을 관리하고 Camera 클래스에서는 뷰 변환 행렬(View Matrix)과 투영 변환 행렬(Projection Matrix)을 관리합니다.
그렇기에 0번 셰이더 레지스터에는 GameObject가 관리하게 되는 월드 변환 행렬을 제출하기 위해 16 * 4바이트를 할당한 것이고, 1번 셰이더 레지스터에는 Camera가 관리하게 되는 뷰 변환 행렬과 투영 변환 행렬을 제출하기 위해 32 * 4바이트를 할당한 것입니다.
보시다싶이 일단 루트 상수(Root Constant)를 이용해 루트 파라미터들을 서술하였습니다. 이후 루트 상수를 사용하지 않게끔 수정할 예정입니다.
void GameFramework::BuildObjects()
{
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
m_scene = make_unique<Scene>();
m_scene->BuildObjects(m_device, m_commandList, m_rootSignature);
m_commandList->Close();
ID3D12CommandList* ppCommandList[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandList), ppCommandList);
WaitForGpuComplete();
m_scene->ReleaseUploadBuffer();
m_timer.Tick();
}
Direct3D의 초기화를 마치면 실제 화면에 렌더링 될 오브젝트들을 생성합니다. 저는 'Scene' 클래스를 통해 한 장면에 그려질 모든 오브젝트들을 관리하도록 하였습니다. Scene 클래스의 BuildObjects 메서드를 호출하여 해당 장면의 모든 오브젝트를 메모리에 올립니다.
class Scene
class Scene
{
public:
Scene();
~Scene() = default;
void MouseEvent(HWND hWnd, FLOAT timeElapsed);
void KeyboardEvent(FLOAT timeElapsed);
void Update(FLOAT timeElapsed);
void Render(const ComPtr<ID3D12GraphicsCommandList>& commandList) const;
void BuildObjects(const ComPtr<ID3D12Device>& device,
const ComPtr<ID3D12GraphicsCommandList>& commandList,
const ComPtr<ID3D12RootSignature>& rootSignature);
void ReleaseUploadBuffer();
void MouseEvent(UINT message, LPARAM lParam);
void KeyboardEvent(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
private:
shared_ptr<Shader> m_shader;
shared_ptr<Camera> m_camera;
shared_ptr<Player> m_player;
vector<shared_ptr<GameObject>> m_objects;
shared_ptr<Mesh> m_cube;
};
Scene 클래스의 구성입니다. 내부에 'Shader', 'Camera', 'Player', 'GameObject', 'Mesh' 등 다양한 클래스들이 존재하는데, 각 클래스의 역할에 대해 간단히 서술하겠습니다.
class GameObject
class GameObject
{
public:
GameObject();
~GameObject() = default;
virtual void Update(FLOAT timeElapsed);
virtual void Render(const ComPtr<ID3D12GraphicsCommandList>& commandList) const;
virtual void UpdateShaderVariable(const ComPtr<ID3D12GraphicsCommandList>& commandList) const;
void Transform(XMFLOAT3 shift);
void Rotate(FLOAT pitch, FLOAT yaw, FLOAT roll);
void SetMesh(const shared_ptr<Mesh>& mesh);
void SetPosition(XMFLOAT3 position);
XMFLOAT3 GetPosition() const;
protected:
XMFLOAT4X4 m_worldMatrix;
XMFLOAT3 m_right;
XMFLOAT3 m_up;
XMFLOAT3 m_front;
shared_ptr<Mesh> m_mesh;
};
상술했듯 GameObject 클래스는 하나의 객체가 어떻게 그려질지를 관리합니다. 객체가 어떻게 그려질지를 결정하기 위해 어떤 정보가 필요할까요? 크게 '무엇을 그릴 지'와 '어디에 그릴 지'를 알고 있어야 한다고 생각합니다.
'무엇을 그릴 지'에 관한 정보는 Mesh 클래스를 통해 관리하도록 하였습니다. 보시다싶이 Mesh는 포인터(shared_ptr)입니다. 이는 같은 Mesh를 가진 서로 다른 GameObject가 하나의 Mesh 포인터만을 공유하도록 하기 위함입니다. 이렇게 하면 GameObject가 늘어나더라도 같은 Mesh의 인스턴스를 여러 개 생성하는 일이 없어집니다.
'어디에 그릴 지'에 관한 정보는 m_worldMatrix라고 하는 월드 변환 행렬에 저장합니다. 렌더링 명령이 내려지면, 이 월드 변환 행렬을 셰이더로 전송하고, 자신이 소유한 Mesh에 렌더링 명령을 내립니다.
- 어떻게 그리나요? : GameObject에서 관리
- 무엇을 그리나요? : Mesh에서 관리
- 어디에 그리나요? : World Matrix 행렬
정리하면 위와 같습니다.
class RotatingObject : public GameObject
{
public:
RotatingObject();
~RotatingObject() = default;
void Update(FLOAT timeElapsed) override;
private:
FLOAT m_rotatingSpeed;
};
회전하는 객체를 구현하기 위해 GameObject를 상속받는 RotatingObject 클래스를 정의해 주었습니다. RotatingObject는 초기화 시 랜덤한 float 변수 m_rotatingSpeed 값을 할당받는데, 이를 통해 회전할 속도를 구합니다.
또한 Update 함수를 재정의하여 매 프레임마다 조금씩 회전해 주도록 구현하였습니다.
class Player : public GameObject
{
public:
Player();
~Player() = default;
void MouseEvent(FLOAT timeElapsed);
void KeyboardEvent(FLOAT timeElapsed);
virtual void Update(FLOAT timeElapsed) override;
virtual void Render(const ComPtr<ID3D12GraphicsCommandList>& commandList) const override;
virtual void UpdateShaderVariable(const ComPtr<ID3D12GraphicsCommandList>& commandList) const override;
void SetCamera(const shared_ptr<Camera>& camera);
private:
shared_ptr<Camera> m_camera;
FLOAT m_speed;
};
플레이어를 구현하기 위해 역시 GameObject를 상속받는 Player 클래스를 정의해 주었습니다. Player는 플레이어의 조작에 따라 회전하고, 이동하게 됩니다. 따라서 Player의 속력을 정의한 m_speed 변수를 관리해 주도록 하였고, 카메라가 플레이어를 따라다니도록 구현할 예정이므로 카메라에 플레이어의 위치 정보를 전송해 주기 위해 Camera 클래스의 포인터(shared_ptr) 역시 가지고 있도록 하였습니다.
class Mesh
class Mesh abstract
{
public:
Mesh() = default;
~Mesh() = default;
virtual void Render(const ComPtr<ID3D12GraphicsCommandList>& commandList) const;
virtual void ReleaseUploadBuffer();
protected:
UINT m_vertices;
ComPtr<ID3D12Resource> m_vertexBuffer;
ComPtr<ID3D12Resource> m_vertexUploadBuffer;
D3D12_VERTEX_BUFFER_VIEW m_vertexBufferView;
};
Mesh 클래스는 상술했듯 '무엇을 그릴 지'에 대한 정보를 관리합니다. 정점 버퍼로 사용할 m_vertexBuffer와, 그 정점 버퍼를 초기화하기 위한 m_vertexUploadBuffer, 정점 버퍼 리소스를 서술하기 위한 m_vertexBufferView 변수를 가지고 있습니다.
Mesh는 추상 클래스로 정의했습니다. 이렇게 구현한 이유는, 위와 같은 정보들로는 정점이 어떻게 구성되어 있을지 알 수 없기 때문입니다. Mesh라고 할지라도 각 정점이 몇 개인지, 정점의 위치는 어떻게 되는지, 프리미티브 토폴로지는 어떤지 모릅니다. 또한 정점이 위치 정보만을 가지고 있을지, 컬러 또는 uv, 노말 등 기타 정보를 추가로 가지고 있을지는 앞으로 구현하게 될 Mesh에 따라 전부 다를 것이기 때문에, 공통된 정보만을 추상화하고 필요에 따라 재정의하여 사용할 수 있도록 구현하였습니다.
class CubeMesh : public Mesh
{
private:
struct Vertex
{
XMFLOAT3 position;
XMFLOAT4 colors;
};
public:
CubeMesh(const ComPtr<ID3D12Device>& device, const ComPtr<ID3D12GraphicsCommandList>& commandList);
~CubeMesh() = default;
};
이번 프로젝트의 목표대로 정육면체 큐브를 렌더링 하기 위해 CubeMesh 클래스를 정의해 보겠습니다. 먼저 CubeMesh 클래스에서 사용할 정점(Vertex) 하나의 정보를 구조체로 정의해 주었습니다. 이 정점 정보는 CubeMesh만이 알고 있으면 되고 외부에서는 전혀 참조할 일이 없기 때문에 private field 안에 선언하였습니다.
CubeMesh::CubeMesh(const ComPtr<ID3D12Device>& device, const ComPtr<ID3D12GraphicsCommandList>& commandList)
{
vector<Vertex> vertices;
const XMFLOAT3 LEFTDOWNFRONT = { -1.f, -1.f, -1.f };
const XMFLOAT3 LEFTDOWNBACK = { -1.f, -1.f, +1.f };
const XMFLOAT3 LEFTUPFRONT = { -1.f, +1.f, -1.f };
const XMFLOAT3 LEFTUPBACK = { -1.f, +1.f, +1.f };
const XMFLOAT3 RIGHTDOWNFRONT = { +1.f, -1.f, -1.f };
const XMFLOAT3 RIGHTDOWNBACK = { +1.f, -1.f, +1.f };
const XMFLOAT3 RIGHTUPFRONT = { +1.f, +1.f, -1.f };
const XMFLOAT3 RIGHTUPBACK = { +1.f, +1.f, +1.f };
// Front
vertices.emplace_back(LEFTUPFRONT, XMFLOAT4{ 0.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTUPFRONT, XMFLOAT4{ 0.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
// Up
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTUPBACK, XMFLOAT4{ 0.0f, 0.5f, 0.5f, 1.0f });
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(LEFTUPFRONT, XMFLOAT4{ 0.0f, 0.0f, 1.0f, 1.0f });
// Back
vertices.emplace_back(LEFTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTUPBACK, XMFLOAT4{ 0.0f, 0.5f, 0.5f, 1.0f });
vertices.emplace_back(LEFTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTUPBACK, XMFLOAT4{ 0.0f, 0.5f, 0.5f, 1.0f });
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
// Down
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 0.0f, 1.0f });
// Left
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(LEFTUPFRONT, XMFLOAT4{ 1.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
vertices.emplace_back(LEFTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 0.0f, 1.0f });
// Right
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTUPBACK, XMFLOAT4{ 0.0f, 0.5f, 0.5f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 1.0f, 1.0f });
m_vertices = static_cast<UINT>(vertices.size());
const UINT vertexBufferSize = m_vertices * sizeof(Vertex);
Utiles::ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&m_vertexBuffer)));
Utiles::ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_vertexUploadBuffer)));
D3D12_SUBRESOURCE_DATA vertexData{};
vertexData.pData = vertices.data();
vertexData.RowPitch = vertexBufferSize;
vertexData.SlicePitch = vertexData.RowPitch;
UpdateSubresources<1>(commandList.Get(), m_vertexBuffer.Get(), m_vertexUploadBuffer.Get(), 0, 0, 1, &vertexData);
commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_vertexBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER));
m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
m_vertexBufferView.SizeInBytes = vertexBufferSize;
m_vertexBufferView.StrideInBytes = sizeof(Vertex);
}
다음은 실제 CubeMesh를 생성하게 되는 CubeMesh 클래스의 생성자입니다.
코드가 다소 긴데, 1. 정점 정보를 정의하고, 2. 버텍스 버퍼와 업로드 버퍼를 생성하고, 3. 버퍼에 실제 데이터를 담고, 4. 서술자에 값을 채우는 네 가지 과정으로 정리할 수 있습니다.
vector<Vertex> vertices;
const XMFLOAT3 LEFTDOWNFRONT = { -1.f, -1.f, -1.f };
const XMFLOAT3 LEFTDOWNBACK = { -1.f, -1.f, +1.f };
const XMFLOAT3 LEFTUPFRONT = { -1.f, +1.f, -1.f };
const XMFLOAT3 LEFTUPBACK = { -1.f, +1.f, +1.f };
const XMFLOAT3 RIGHTDOWNFRONT = { +1.f, -1.f, -1.f };
const XMFLOAT3 RIGHTDOWNBACK = { +1.f, -1.f, +1.f };
const XMFLOAT3 RIGHTUPFRONT = { +1.f, +1.f, -1.f };
const XMFLOAT3 RIGHTUPBACK = { +1.f, +1.f, +1.f };
// Front
vertices.emplace_back(LEFTUPFRONT, XMFLOAT4{ 0.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTUPFRONT, XMFLOAT4{ 0.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
// Up
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTUPBACK, XMFLOAT4{ 0.0f, 0.5f, 0.5f, 1.0f });
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(LEFTUPFRONT, XMFLOAT4{ 0.0f, 0.0f, 1.0f, 1.0f });
// Back
vertices.emplace_back(LEFTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTUPBACK, XMFLOAT4{ 0.0f, 0.5f, 0.5f, 1.0f });
vertices.emplace_back(LEFTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTUPBACK, XMFLOAT4{ 0.0f, 0.5f, 0.5f, 1.0f });
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
// Down
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 0.0f, 1.0f });
// Left
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(LEFTUPFRONT, XMFLOAT4{ 1.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
vertices.emplace_back(LEFTUPBACK, XMFLOAT4{ 1.0f, 1.0f, 0.0f, 1.0f });
vertices.emplace_back(LEFTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 0.0f, 1.0f });
vertices.emplace_back(LEFTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 0.0f, 1.0f });
// Right
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTUPBACK, XMFLOAT4{ 0.0f, 0.5f, 0.5f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTUPFRONT, XMFLOAT4{ 0.5f, 0.5f, 0.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNBACK, XMFLOAT4{ 0.0f, 1.0f, 1.0f, 1.0f });
vertices.emplace_back(RIGHTDOWNFRONT, XMFLOAT4{ 1.0f, 0.0f, 1.0f, 1.0f });
1. 정점 정보를 정의해주는 부분입니다.
정점(Vertex)의 형식에 맞게 vector<Vertex>를 채워 주었습니다. 프리미티브 토폴로지(Primitive Topology)는 물론 삼각형입니다. 따라서 0번 배열부터 순서대로 세 개의 정점이 한 개의 삼각형을 이루게 됩니다. DirectX는 왼손 좌표계를 사용하기 때문에 정점을 시계 방향으로 정의해 주었습니다.
m_vertices = static_cast<UINT>(vertices.size());
const UINT vertexBufferSize = m_vertices * sizeof(Vertex);
Utiles::ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&m_vertexBuffer)));
Utiles::ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_vertexUploadBuffer)));
2. 버텍스 버퍼와 업로드 버퍼를 생성해주는 부분입니다.
Mesh는 기본적으로 정적입니다. 한번 정의되면 바뀌지 않습니다. 따라서 초기화 시 GPU에 메쉬를 이루는 정보를 알려주고 나면 더 이상 해당 리소스에 CPU가 정보를 전송할 일이 없습니다. 그렇기에 정점 버퍼는 일반적으로 CPU가 정보를 수정할 수 없는 디폴트 힙(D3D12_HEAP_TYPE_DEFAULT)에 넣는 것이 자연스럽습니다.
그런데 디폴트 힙은 CPU가 수정할 수 없는 영역이기 때문에, 초기화시에도 CPU가 접근할 수 없습니다. 따라서 먼저 CPU가 쓸 수 있는 업로드 힙(D3D12_HEAP_TYPE_UPLOAD)을 만들어 정점 정보를 올리고, 이를 디폴트 힙에 복사해야 합니다. 그래서 디폴트 힙(m_vertexBuffer)과 업로드 힙(m_vertexUploadBuffer) 두 가지를 생성해 주었습니다.
D3D12_SUBRESOURCE_DATA vertexData{};
vertexData.pData = vertices.data();
vertexData.RowPitch = vertexBufferSize;
vertexData.SlicePitch = vertexData.RowPitch;
UpdateSubresources<1>(commandList.Get(),
m_vertexBuffer.Get(), m_vertexUploadBuffer.Get(), 0, 0, 1, &vertexData);
commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_vertexBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER));
3. 버퍼에 실제 데이터를 담는 부분입니다.
UpdateSubresources 함수는 D3D12_SUBRESOURCE_DATA 구조체에 담긴 CPU 메모리 정보를 업로드 힙에 복사하고, 다시 이 업로드 힙의 자료를 디폴트 힙에 복사해 줍니다.
디폴트 힙을 생성할 때 리소스 배리어를 D3D12_RESOURCE_STATE_COPY_DEST, 즉 쓰기 전용 상태로 설정해 주었습니다. 따라서 복사가 완료된 디폴트 힙의 리소스 배리어를 읽기 가능하도록 수정해 줘야 합니다. 이 디폴트 힙은 정점 버퍼로 사용될 예정이므로 D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER로 수정해 주었습니다.
버텍스 버퍼에 자원을 올렸으니 업로드 전용 임시 버퍼인 m_vertexUploadBuffer의 사용은 끝났습니다. 그래도 아직 Reset 메소드를 호출해 업로드 버퍼를 해제하면 안 됩니다. 지금까지 과정은 커맨드 리스트에 명령을 채우는 과정이었고, 실제 명령의 실행은 ExecuteCommandLists 함수를 통해 커맨드 리스트가 커맨드 큐에 제출된 이후에 일어납니다. 따라서 이번 명령을 커맨드 큐에 제출한 후, GPU에서 명령이 끝났다고 알려 주면 그 이후에 업로드 버퍼를 해제해야 합니다.
m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
m_vertexBufferView.SizeInBytes = vertexBufferSize;
m_vertexBufferView.StrideInBytes = sizeof(Vertex);
4. 서술자에 값을 채우는 부분입니다.
버텍스 버퍼는 ID3D12Resource로 이루어진 자원일 뿐이고, 이 자원이 메쉬로 사용될지, 텍스처로 사용될지는 GPU 입장에서 알 수 없기 때문에 서술자에 값을 채워 렌더링 시 알려 줘야 합니다.
class Camera
class Camera
{
public:
Camera();
~Camera() = default;
virtual void Update(FLOAT timeElapsed) = 0;
virtual void UpdateEye(XMFLOAT3 position) = 0;
void UpdateShaderVariable(const ComPtr<ID3D12GraphicsCommandList>& commandList);
virtual void RotatePitch(FLOAT radian) = 0;
virtual void RotateYaw(FLOAT radian) = 0;
void SetLens(FLOAT fovy, FLOAT aspect, FLOAT minZ, FLOAT maxZ);
XMFLOAT3 GetEye() const;
XMFLOAT3 GetU() const;
XMFLOAT3 GetV() const;
XMFLOAT3 GetN() const;
protected:
void UpdateBasis();
protected:
XMFLOAT4X4 m_viewMatrix;
XMFLOAT4X4 m_projectionMatrix;
XMFLOAT3 m_eye;
XMFLOAT3 m_at;
XMFLOAT3 m_up;
XMFLOAT3 m_u;
XMFLOAT3 m_v;
XMFLOAT3 m_n;
};
class ThirdPersonCamera : public Camera
{
public:
ThirdPersonCamera();
~ThirdPersonCamera() = default;
void Update(FLOAT timeElapsed) override;
void UpdateEye(XMFLOAT3 position) override;
void RotatePitch(FLOAT radian) override;
void RotateYaw(FLOAT radian) override;
private:
FLOAT m_radius;
FLOAT m_phi;
FLOAT m_theta;
};
Camera 클래스는 현재 관찰자의 시점을 추상화합니다. 뷰 변환 행렬(View Matrix)과 투영 변환 행렬(Projection Matrix)를 관리하며, 렌더링 시 이를 셰이더에 전송해 줍니다. Camera 인스턴스는 SetLens 메소드를 통해 시야각, 종횡비, MinZ, MaxZ를 전달받아 투영 변환 행렬을 설정하게 됩니다. 뷰 변환 행렬을 설정해 주기 위해서는 카메라의 eye, at, up에 관한 정보가 필요합니다. 저는 이번 프로젝트에서 플레이어를 추적하는 3인칭 카메라를 만들 예정이고, up을 (0.0, 1.0, 0.0)으로 설정한다고 한다면 eye와 at을 구하는 일은 플레이어의 위치를 기반하게 됩니다. 그러나 다른 카메라를 만들 때에는 eye와 at을 구하는 과정이 플레이어의 위치에 기반하지 않을 수 있기 때문에 일단 Camera 클래스를 추상 클래스로 설정하고, 플레이어를 추적하는 카메라의 역할은 Camera 클래스를 상속받는 ThirdPersonCamera 클래스에서 전담하도록 나눠 주었습니다.
class Shader
class Shader
{
public:
Shader(const ComPtr<ID3D12Device>& device, const ComPtr<ID3D12RootSignature>& rootSignature);
~Shader() = default;
void UpdateShaderVariable(const ComPtr<ID3D12GraphicsCommandList>& commandList);
protected:
ComPtr<ID3D12PipelineState> m_pipelineState;
};
Shader 클래스는 파이프라인 상태 객체(ID3D12PipelineState)를 관리합니다. 렌더링 시 Shader 인스턴스를 관리하는 Scene에서 UpdateShaderVariable 메소드를 호출하게 되는데, 이를 통해 내가 가지고 있는 파이프라인 상태를 설정합니다. 그 후에는 파이프라인 상태에 맞는 적절한 오브젝트들에 렌더링 명령을 내려 주면 됩니다.
Shader::Shader(const ComPtr<ID3D12Device>& device, const ComPtr<ID3D12RootSignature>& rootSignature)
{
vector<D3D12_INPUT_ELEMENT_DESC> inputLayout = {
{ "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 } };
#if defined(_DEBUG)
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
ComPtr<ID3DBlob> mvsByteCode, mpsByteCode;
Utiles::ThrowIfFailed(D3DCompileFromFile(TEXT("shader.hlsl"), nullptr,
D3D_COMPILE_STANDARD_FILE_INCLUDE, "VERTEX_MAIN", "vs_5_1", compileFlags, 0, &mvsByteCode, nullptr));
Utiles::ThrowIfFailed(D3DCompileFromFile(TEXT("shader.hlsl"), nullptr,
D3D_COMPILE_STANDARD_FILE_INCLUDE, "PIXEL_MAIN", "ps_5_1", compileFlags, 0, &mpsByteCode, nullptr));
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc{};
psoDesc.InputLayout = { inputLayout.data(), (UINT)inputLayout.size() };
psoDesc.pRootSignature = rootSignature.Get();
psoDesc.VS = {
reinterpret_cast<BYTE*>(mvsByteCode->GetBufferPointer()),
mvsByteCode->GetBufferSize() };
psoDesc.PS = {
reinterpret_cast<BYTE*>(mpsByteCode->GetBufferPointer()),
mpsByteCode->GetBufferSize() };
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.DSVFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
psoDesc.SampleDesc.Count = 1;
psoDesc.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;
Utiles::ThrowIfFailed(device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState)));
}
Shader 클래스의 생성자에서는 파이프라인 객체를 초기화합니다. 여기서 컴파일 된 셰이더는 CubeMesh를 렌더링 하는 데 사용되므로, CubeMesh의 정점 정보와 동일하게 Input Layout을 설정해 주었습니다. 셰이더는 버텍스 셰이더와 픽셀 셰이더만을 사용하므로 두 셰이더를 등록해 주었고, 기타 래스터라이저, 깊이/스탠실, 블랜드 등은 아직 사용하지 않으므로 기본 상태로 설정해 주었습니다.
cbuffer GameObject : register(b0)
{
matrix g_worldMatrix : packoffset(c0);
};
cbuffer Camera : register(b1)
{
matrix g_viewMatrix : packoffset(c0);
matrix g_projectionMatrix : packoffset(c4);
};
struct VS_INPUT
{
float3 position : POSITION;
float4 color : COLOR;
};
struct PS_INPUT
{
float4 position : SV_POSITION;
float4 color : COLOR;
};
PS_INPUT VERTEX_MAIN(VS_INPUT input)
{
PS_INPUT output;
output.position = mul(float4(input.position, 1.0f), g_worldMatrix);
output.position = mul(output.position, g_viewMatrix);
output.position = mul(output.position, g_projectionMatrix);
output.color = input.color;
return output;
}
float4 PIXEL_MAIN(PS_INPUT input) : SV_TARGET
{
return input.color;
}
HLSL 코드는 다음과 같습니다. 상술하였듯 루트 시그니처를 통해 0번 레지스터에 GameObject가 관리할 16 * 4바이트의 월드 변환 행렬을, 1번 레지스터에 Camera가 관리할 합계 32 * 4바이트의 뷰 변환 행렬과 투영 변환 행렬을 등록했으므로 그에 맞게 정의해 주었고, 버텍스 셰이더의 입력으로 Input Layout과 일치하는 값이 주어지게끔 설정해 주었습니다.
버텍스 셰이더에서는 입력으로 들어온 input.position 좌표에 월드 변환, 뷰 변환, 투영 변환을 실시해주고 래스터라이저로 전송합니다.
래스터화를 마치고 픽셀 셰이더로 진입한 프래그먼트는 보간된 color 정보를 그대로 출력하도록 해 주었습니다.
GameFramework::BuildObjects
void GameFramework::BuildObjects()
{
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
m_scene = make_unique<Scene>();
m_scene->BuildObjects(m_device, m_commandList, m_rootSignature);
m_commandList->Close();
ID3D12CommandList* ppCommandList[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandList), ppCommandList);
WaitForGpuComplete();
m_scene->ReleaseUploadBuffer();
m_timer.Tick();
}
class Scene
void Scene::BuildObjects(const ComPtr<ID3D12Device>& device,
const ComPtr<ID3D12GraphicsCommandList>& commandList,
const ComPtr<ID3D12RootSignature>& rootSignature)
{
m_shader = make_shared<Shader>(device, rootSignature);
m_cube = make_shared<CubeIndexMesh>(device, commandList);
m_player = make_shared<Player>();
m_player->SetMesh(m_cube);
m_player->SetPosition(XMFLOAT3{ 0.f, 0.f, 0.f });
for (int x = -15; x <= 15; x += 5) {
for (int y = -15; y <= 15; y += 5) {
for (int z = -15; z <= 15; z += 5) {
auto object = make_shared<RotatingObject>();
object->SetMesh(m_cube);
object->SetPosition(XMFLOAT3{
static_cast<FLOAT>(x),
static_cast<FLOAT>(y),
static_cast<FLOAT>(z) });
m_objects.push_back(object);
}
}
}
m_camera = make_shared<ThirdPersonCamera>();
m_camera->SetLens(0.25 * XM_PI, g_framework->GetAspectRatio(), 0.1f, 1000.f);
m_player->SetCamera(m_camera);
}
다시 Scene 클래스로 돌아왔습니다. Scene의 BuildObjects 메소드가 호출되면 이와 같은 렌더링에 필요한 클래스들의 인스턴스들이 생성됩니다.
셰이더와 메시, 화면에 그려질 오브젝트들을 생성해 주었고, GameObject::SetMesh 메소드를 통해 같은 메시를 가진 다른 오브젝트들이 메시를 공유할 수 있도록 해 주었습니다.
카메라의 인스턴스를 생성해 시야각과 종횡비, MinZ, MaxZ를 설정해 주었고, 플레이어가 카메라의 포인터를 소유하여 플레이어의 상태 변화에 따라 카메라의 설정이 변경될 수 있도록 구현하였습니다.
void GameFramework::BuildObjects()
{
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
m_scene = make_unique<Scene>();
m_scene->BuildObjects(m_device, m_commandList, m_rootSignature);
m_commandList->Close();
ID3D12CommandList* ppCommandList[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandList), ppCommandList);
WaitForGpuComplete();
m_scene->ReleaseUploadBuffer();
m_timer.Tick();
}
Scene::BuildObjects가 종료되면 커맨드 리스트를 커맨드 큐에 제출합니다. WaitForGpuComplete 함수는 펜스 객체를 통해 커맨드 큐에 제출된 GPU 명령이 전부 끝났는지 검사합니다. GPU 명령이 끝났다는 것은 이제 정말로 임시 버퍼인 업로드 버퍼를 해제할 수 있는 상태가 되었음을 의미하므로 ReleaseUploadBuffer 메소드를 호출하여 업로드 버퍼를 해제해 주었습니다.
마지막으로 타이머의 Tick 메소드를 호출하여 최초의 타이머를 동작시켜 주면, 렌더링을 위한 모든 준비가 끝났습니다!
'Programming > Computer Graphics' 카테고리의 다른 글
[Computer Graphics with DirectX 12] 04. Geometry Shader (0) | 2024.04.14 |
---|---|
[Computer Graphics with DirectX 12] 03. Terrain (0) | 2024.03.06 |
[Computer Graphics with DirectX 12] 02. Texture (0) | 2024.02.26 |
[Computer Graphics with DirectX 12] 01. Framework - Update, Render (0) | 2024.02.20 |
[Computer Graphics with DirectX 12] 개요 (0) | 2024.02.07 |