오늘은~~ 알록달록 정육면체를 렌더링하는 날이었다. 처음으로 메쉬를 렌더링하는 날이니 세팅할 게 정말 많았다. 그래도 이론으로 배운 렌더링 파이프라인의 단계들을 하나 하나 구현한다는 마음으로 수업을 들으니 훨씬 감이 잘 왔다.
오늘은 정육면체를 그리기 위해 입력 조립기 단계, 정점 셰이더 단계, 래스터화기 단계, 픽셀 쎼이더 단계와 출력 병합기 단계를 거칠 것이다!
입력 배치 세팅
우선, 알록달록 정육면체를 그리려면 무엇이 필요할까? 각 정점의 위치와 색상이 필요하다. 위치와 색상을 담은 간단한 구조체 하나를 만들어주었다.
struct Vertex
{
XMFLOAT3 pos;
XMFLOAT4 color;
};
이 상태의 데이터를 GPU가 받는다면 각 성분이 어떤 용도인지, 무슨 자료형인지 등등... 하나도 모를 것이다.
Swap Chain도, Command Queue도 GPU에게 주는 설명서처럼 Descriptor를 작성해서 올렸기 때문에 이 구조체의 각 성분의 용도와 얼만큼 큰지, 무엇으로 구성되어있는지를 알려주는 것이 중요하다. 이걸 입력 배치 서술(Input Layout Descriptor)이라고 한다.
// ------- 입력 배치 -------
vector<D3D12_INPUT_ELEMENT_DESC> mInputLayout;
구조체에 성분은 더 추가될 수 있기에 vector로 선언해주었다.
typedef struct D3D12_INPUT_ELEMENT_DESC
{
LPCSTR SemanticName; // 각 요소에 부여된 문자열 이름
UINT SemanticIndex; // 부여된 인덱스 (Semantic Name이 같은 경우 Index로 구분)
DXGI_FORMAT Format; // 자료 형식
UINT InputSlot; // 요소를 가져올 정점 버퍼 슬롯의 인덱스 (0~15)
UINT AlignedByteOffset; // 요소의 시작 위치
D3D12_INPUT_CLASSIFICATION InputSlotClass; // 입력 데이터 클래스 식별 값 (대부분 기본값)
UINT InstanceDataStepRate; // 동일한 인스턴스별 데이터를 사용하여 그릴 인스턴스 수(?)(대부분 0)
} D3D12_INPUT_ELEMENT_DESC;
입력 배치 서술자는 이렇게 구성된다.
// Shader와 연결됨
mInputLayout =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}, // 0 ~ 11 (12)
// Local Position
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0} // 12 ~ 27 (16)
};
Position은 float(4byte) * 3이니 R32G32B32_FLOAT 포맷이고, Color는 알파값까지 더해 flaot(4byte)*4이니 R32G32B32A32_FLOAT 이 포맷이 되는 것이다.
여기서 눈 여겨볼 게 시작 지점이다. Position은 0에서부터 시작하지만, Color는 12부터 시작한다. Position은 12바이트이기 때문에 0~11까지를 쓰고, Color는 그 다음인 12가 시작 지점이 되는 것이다!
버텍스 버퍼 & 인덱스 버퍼
Position은 이렇고요, Color는 이런 변수예요~ 라고 말만 한다고 해서 정육면체의 정점들을 GPU가 받을 수 있는 것은 아니다. GPU가 정점들의 배열에 접근하려면, 버퍼(Buffer)라는 리소스에 넣어주어야 한다. 정점들을 저장하는 버퍼는 Vertex Buffer, 인덱스를 저장하는 버퍼는 Index Buffer이다.
렌더링할 물체들이 기본적으로 가지고 있어야 할 요소들을 담을 구조체를 만들었다.
// 오브젝트 구조체
struct RenderItem
{
RenderItem() = default;
// 상수 버퍼에서 쓰일 오브젝트 인덱스
UINT objCbIndex = -1;
// 월드 포지션
XMFLOAT4X4 world = MathHelper::Identity4x4();
D3D12_PRIMITIVE_TOPOLOGY primitiveTopology = D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
// 정점 버퍼 뷰
ComPtr<ID3D12Resource> vertexBuffer = nullptr;
D3D12_VERTEX_BUFFER_VIEW vertexBufferView = {};
// 인덱스 버퍼 뷰
ComPtr<ID3D12Resource> indexBuffer = nullptr;
D3D12_INDEX_BUFFER_VIEW indexBufferView = {};
// 정점 개수
int vertexCount = 0;
// 인덱스 개수
int indexCount = 0;
};
리소스가 있으면, 뷰도 있어야하는 게 인지상정이므로 뷰도 만들어주었다. 이따 쓰일 거지만, 개별 상수 버퍼에서 쓰일 인덱스와 없으면 안 될 월드 좌표까지 넣어주었다.
정점 묶기
정육면체 그리기의 꽃! 8개의 정점을 만들고 정점의 인덱스를 시계 방향으로 묶어줄 것이다. 이거 하느라 머리 싸매고 고생했는데, 결국은 인덱스를 0~7까지가 아니라 1~8까지로 써서 잘못 나왔다는 것을 알고 정말 속상하고 어이가 없었다... 바보냐
아무튼 정점을 위 그림처럼 설정했다.
// 정점 정보
std::array<Vertex, 8> vertices =
{
Vertex({XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT4(Colors::Magenta)}),
Vertex({XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(Colors::Yellow)}),
Vertex({XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT4(Colors::Blue)}),
Vertex({XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT4(Colors::Red)}),
Vertex({XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT4(Colors::Magenta)}),
Vertex({XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT4(Colors::Yellow)}),
Vertex({XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Blue)}),
Vertex({XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Red)}),
};
그리고 시계방향으로 각 삼각형들을 묶었다!
그림판으로 열심히 묶어봤다. 묶은 것을 코드에서 보자.
std::array<std::uint16_t, 36> indices =
{
0,1,2,
0,2,3,
3,2,6,
3,6,7,
6,5,4,
6,4,7,
2,5,6,
2,1,5,
1,0,4,
1,4,5,
0,3,7,
0,7,4
};
버텍스 버퍼 생성
버텍스 버퍼는 업로드 타입으로 만들어주었다. 헬퍼 클래스의 도움을 받아 간결하게 작성할 수 있었다.
// upload buffer (CPU, GPU 모두 건드릴 수 있음)
D3D12_HEAP_PROPERTIES heapProperty = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Buffer(vbByteSize); // 정점의 크기만큼 버퍼 만듦
md3dDevice->CreateCommittedResource
(
&heapProperty,
D3D12_HEAP_FLAG_NONE,
&desc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&cube->vertexBuffer)
);
CPU도 버퍼의 값을 방금 만든 8개의 정점들로 초기화해주어야 하므로 업로드 버퍼로 만들었다. 버퍼에 직접적으로 접근해서 값을 초기화할 수는 없으므로...
Map 함수를 이용해 액세스할 수 있는 포인터를 얻음 > 그 포인터에 데이터 복사 > UnMap으로 해제
순서로 데이터를 넣어줄 것이다.
// 직접적으로 접근 X
void* vertexDataBuffer = nullptr;
CD3DX12_RANGE vertexRange(0, 0);
cube->vertexBuffer->Map(0, &vertexRange, &vertexDataBuffer); // 연다
// vertexDataBuffer에 접근할 수 있는 포인터가 됨
memcpy(vertexDataBuffer, &vertices, vbByteSize); // 복사
// vertexDataBuffer에 정점 정보들을 복사
cube->vertexBuffer->Unmap(0, nullptr); // 다시 닫기
// 해제
마지막으로, 정점 버퍼 뷰까지 설정해주면 끝이다!
cube->vertexBufferView.BufferLocation = cube->vertexBuffer->GetGPUVirtualAddress();
cube->vertexBufferView.StrideInBytes = sizeof(Vertex);
cube->vertexBufferView.SizeInBytes = vbByteSize;
인덱스 버퍼 생성
인덱스 버퍼도 버텍스 버퍼와 별다를 것 없다.
// 인덱스 버퍼 생성
cube->indexCount = (UINT)indices.size();
const UINT ibByteSize = cube->indexCount * sizeof(std::uint16_t);
// upload buffer (CPU, GPU 모두 건드릴 수 있음)
md3dDevice->CreateCommittedResource
(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(ibByteSize),
D3D12_RESOURCE_STATE_GENERIC_READ, // 읽기만 가능
nullptr,
IID_PPV_ARGS(&cube->indexBuffer)
);
void* IndexDataBuffer = nullptr;
CD3DX12_RANGE indexRange(0, 0);
cube->indexBuffer->Map(0, &indexRange, &IndexDataBuffer); // 연다
memcpy(IndexDataBuffer, &indices, ibByteSize); // 복사
cube->indexBuffer->Unmap(0, nullptr); // 다시 닫기
cube->indexBufferView.BufferLocation = cube->indexBuffer->GetGPUVirtualAddress();
cube->indexBufferView.Format = DXGI_FORMAT_R16_UINT;
cube->indexBufferView.SizeInBytes = ibByteSize;
CreateCommittedResource로 함수로 업로드 버퍼를 만들어주고, Map 함수를 이용해서 데이터 복사, IndexBufferView를 설정해주었다!
vector<unique_ptr<RenderItem>> mRenderItems;
렌더링할 물체들을 넣는 복합 데이터를 선언하고,
mRenderItems.push_back(move(cube));
버텍스 버퍼, 뷰, 인덱스 버퍼, 뷰까지 모두 가지고 있는 오브젝트를 넣어주었다. 몇 가지의 작업을 더 거치고 Draw 함수에서 이 녀석을 렌더링해줄 것이다.
정점 셰이더 & 픽셀 셰이더
셰이더 코드
정점 셰이더는 본질적으로 하나의 함수이다. C++의 함수와 똑같이 매개변수를 받고 반환값이 있으며, 매개변수에 out 키워드를 붙여 출력 매개변수로 사용할 수도 있다.
HLSL 언어이며, .hlsl 확장자이다. 나는 자동 완성이 안 되는 게 불편해서 따로 외부 도구를 설치해주었다.
아까 입력 배치 단계에서 위치와 색상 값을 넣었으니, 그것을 묶는 구조체와 버텍스 셰이더가 반환할 구조체를 만들어주었다.
struct VertexIn
{
float3 PosL : POSITION; // Local Position
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
입력 배치 때 문자열로 넣어주었던 POSITION, COLOR라는 세그먼트가 있다. 똑같이 입력해주면, 입력 매개변수에 대응된다. 출력 매개변수에도 SV_POSITION, COLOR라는 세그먼트가 있다. 이는 다음 단계 (기하 셰이더, 픽셀 셰이더)의 입력 매개변수에 대응시키는 역할을 한다.
SV_POSITION은 System Value Position인데 이는 투영을 거친 정점의 위치를 나타낸다. GPU는 절단, 깊이 판정, 래스터화 등의 특별한 연산을 적용하므로 중요하다!
VertexOut VS(VertexIn vin)
{
VertexOut vout;
vout.PosH = float4(vin.PosL, 1.0f);
vout.Color = vin.Color;
return vout;
}
아직 상수 버퍼를 만들지 않아서 월드라든가, 투영, 뷰 행렬이라든가... 모두 없지만, 일단은 로컬 포지션을 flaot4로 변경해 그대로 반환하는 버텍스 셰이더를 만들었다.
버텍스 셰이더가 반환한 정점 특성들은 래스터화 단계에서 삼각형의 픽셀들을 따라 보간되며 그 결과가 픽셀 셰이더에 입력된다. (기하 셰이더 과정 생략)
픽셀 셰이더는 픽셀 단편마다 실행되며 색상을 계산한다. 깊이, 스텐실, 판정 등 도중에 페기되는 픽셀들도 있어서 백 버퍼의 한 픽셀에는 최종적으로 그 픽셀이 될 수 있는 후보로서 픽셀 단편들이 여러 개 존재할 수 있다고 한다. 흥미롭다!
float4 PS(VertexOut pin) : SV_Target
{
return pin.Color;
}
아직 조명이나 그런 것들이 없어 얻은 색상을 그대로 반환하는 함수를 만들었다. SV_TARGET이라는 세그먼트는 이 함수의 반환 값 형식이 렌더 타겟의 형식과 일치해야 함을 뜻한다.
셰이더 컴파일
셰이더를 만들었으니, C++ 코드에서 접근할 수 있게 변수로 만들어주고 컴파일해주었다.
// Vertex Shader, Pixel Shader
ComPtr<ID3DBlob> mvsByteCode = nullptr;
ComPtr<ID3DBlob> mpsByteCode = nullptr;
마이크로소프트에서 찾아보았는데, 셰이더를 컴파일하는 API에서 개체 코드 및 오류 메시지를 반환한다고 한다. 이 자료형으로 셰이더 코드 객체를 만들어주었다.
mvsByteCode = d3dUtil::CompileShader(L"Color.hlsl", nullptr, "VS", "vs_5_0");
mpsByteCode = d3dUtil::CompileShader(L"Color.hlsl", nullptr, "PS", "ps_5_0");
편리하게 d3dUtil에서 셰이더를 컴파일하는 함수를 불러와주었다. 파일명, 매크로, 함수 이름, 각 버전을 매개변수로 넣어주었다.
상수 버퍼
상수 버퍼 만들기
상수 버퍼는 셰이더 프로그램에서 참조하는 GPU 자원이다. 자원이란 말과 같이 텍스쳐, 버퍼 등등 모두 세이더 프로그램에서 참조할 수 있다. CPU가 프레임당 한 번 갱신하는 게 일반적이다. 예를 들어, 카메라가 매 프레임 움직인다면, 당연히 상수 버퍼에 있는 view 행렬을 갱신해주어야 한다. 그렇기 때문에, 상수 버퍼는 업로드 힙으로 만든다.
또, 상수 버퍼는 256바이트의 배수여야 한다! 따라서 비트 연산을 해 주어진 크기에 가장 가까운 256의 배수를 구해서 돌려주어야 한다.
// 개별 오브젝트 상수 (World)
struct ObjectConstants
{
XMFLOAT4X4 world = MathHelper::Identity4x4(); // 단위 행렬
};
// 공용 상수 (View Projection)
struct PassConstants
{
XMFLOAT4X4 view = MathHelper::Identity4x4();
XMFLOAT4X4 invView = MathHelper::Identity4x4(); // inverse
XMFLOAT4X4 proj = MathHelper::Identity4x4();
XMFLOAT4X4 invProj = MathHelper::Identity4x4();
XMFLOAT4X4 viewProj = MathHelper::Identity4x4();
};
상수 버퍼에 보낼 데이터를 구조체를 만들어주었다. 상수 버퍼 하나는 개별 오브젝트 상수로 월드 행렬 값을, 공용 상수로는 뷰 행렬과 투영 행렬을 보낼 것이다.
// 개별 오브젝트 상수 버퍼
ComPtr<ID3D12Resource> mObjectCB = nullptr; // 개별 오브젝트 상수 버퍼
BYTE* mObjectMappedData = nullptr; // 복사할 포인터
UINT mObjectByteSize = 0;
// 공용 오브젝트 상수 버퍼
ComPtr<ID3D12Resource> mPassCB = nullptr; // 공용 오브젝트 상수 버퍼
BYTE* mPassMappedData = nullptr; // 복사할 포인터
UINT mPassByteSize = 0;
각자 버퍼를 만들어주었다. 인덱스 버퍼, 버텍스 버퍼를 만들 때와 다를 게 없다! 그것도 업로드 버퍼로 만들었으니까... Map 함수를 이용해서 복사하는 것까지 똑같다.
그럼 각자 버퍼를 만들어보자 ^__^
UINT size = sizeof(ObjectConstants);
mObjectByteSize = (size + 255) & ~255;
size에 가장 가까운 256의 배수로 만들어주었다. 저 비트 연산이 어떻게 결과를 가지고 오는 걸까?
만약에 size가 255였다고 해보자! 저 연산을 수행한다면...
(255 + 255) & ~255
= 510 & ~255
255 = 0000 1111 1111
~255 = 1111 0000 0000
510 & ~255
= 0001 1111 1110
& 1111 0000 0000
= 0001 0000 0000
= 256
256이 나오게 된다! 하위 비트들을 256보다 작은 비트들을 모두 0으로 만드는 것이다.
D3D12_HEAP_PROPERTIES heapProperty = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Buffer(mObjectByteSize);
md3dDevice->CreateCommittedResource
(
&heapProperty,
D3D12_HEAP_FLAG_NONE,
&desc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mObjectCB)
);
// 그냥 열어놓음
mObjectCB->Map(0, nullptr, reinterpret_cast<void**>(&mObjectMappedData));
커밋 리소스로 버퍼를 만들어주었고 Update에서 갱신할 수 있도록 Map 함수를 써주었다. UnMap은 굳이 하지 않았다.
UINT size = sizeof(PassConstants);
mPassByteSize = (size + 255) & ~255;
// 올림을 해서 256의 배수 값으로 바꿔주는 코드~~
D3D12_HEAP_PROPERTIES heapProperty = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Buffer(mPassByteSize);
md3dDevice->CreateCommittedResource
(
&heapProperty,
D3D12_HEAP_FLAG_NONE,
&desc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mPassCB)
);
// 그냥 열어놓음
mPassCB->Map(0, nullptr, reinterpret_cast<void**>(&mPassMappedData));
공용 상수 버퍼도 동일하게 만들어주었다.
상수 버퍼 갱신
상수 버퍼를 만들었으니, 이제 매 프레임마다 갱신해주어야 한다.
먼저, 개별 상수 버퍼를 먼저 갱신해주었다. 개별 상수 버퍼에는 그 오브젝트의 크기, 회전, 위치를 담은 행렬의 값으로 갱신해주었다.
for (auto& e : mRenderItems)
{
// 현재 Render Object의 행렬 가지고 옴
XMMATRIX world = XMLoadFloat4x4(&e->world);
ObjectConstants objectConstants;
XMStoreFloat4x4(&objectConstants.world, XMMatrixTranspose(world));
UINT elementIdx = e->objCbIndex;
UINT elementByteSize = (sizeof(ObjectConstants) + 255) & ~255;
// 상수 버퍼에 복사
memcpy(&mObjectMappedData[elementIdx * elementByteSize], &objectConstants, sizeof(ObjectConstants));
}
미리 설정해준 오브젝트의 Index로 복사할 자리를 만들어주는 게 핵심이다. 한 요소에 크기 * 인덱스를 곱해준다면 그 자리의 시작 위치가 될 것이다.
공용 상수 버퍼는 투영 행렬, 뷰 행렬, 둘을 곱한 것 등에 관한 정보를 매 프레임마다 갱신해주었다.
PassConstants passConstants;
XMMATRIX view = XMLoadFloat4x4(&mView);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX viewProj = XMMatrixMultiply(view, proj);
// 역행렬
XMMATRIX invView = XMMatrixInverse(&XMMatrixDeterminant(view), view);
XMMATRIX invProj = XMMatrixInverse(&XMMatrixDeterminant(proj), proj);
XMStoreFloat4x4(&passConstants.view, XMMatrixTranspose(view));
XMStoreFloat4x4(&passConstants.proj, XMMatrixTranspose(proj));
XMStoreFloat4x4(&passConstants.invView, XMMatrixTranspose(invView));
XMStoreFloat4x4(&passConstants.invProj, XMMatrixTranspose(invProj));
XMStoreFloat4x4(&passConstants.viewProj, XMMatrixTranspose(viewProj));
memcpy(mPassMappedData, &passConstants, sizeof(PassConstants));
상수 버퍼도 갱신 완료 ^__^!!!
루트 시그니처
루트 시그니처는 렌더링 전 파이프라인에 묶어야 하는 자원들이 무엇이고, 어떤 레지스터에 대응되는지를 정의한다. 자원들은 특정한 레지스터 슬롯에 묶이며, 셰이더는 그 슬롯을 통해 자원에 접근한다!
방금 만든 개별 상수 버퍼는 b0 레지스터, 공용 상수 버퍼는 b1 레지스터에 묶인다면, 셰이더에도 그 표시를 해주어야 한다. 레지스터도 다양한 자원들에 따라 달라진다.
b0 > 상수 버퍼
t0 > 텍스쳐
s0 > 샘플러 (표본 추출기)
지금은 바인딩만 하지만, 이따 파이프라인 상태 객체를 만들 때 루트 시그니처를 할당해준다.
ComPtr<ID3D12RootSignature> mRootSignature = nullptr;
ID3D12RootSignature 변수를 만들어주었다. 이제 세팅해볼 것이다.
CD3DX12_ROOT_PARAMETER param[2];
param[0].InitAsConstantBufferView(0); // 0번 -> b0 -> CBV
param[1].InitAsConstantBufferView(1); // 1번 -> b1 -> CBV
만든 상수 버퍼가 2개이기 때문에 루트 파라미터도 2개로 만들어준다.
0번에는 b0, 개별 상수 버퍼를. 1번에는 b1, 공용 상수 버퍼를 바인딩해줄 것이다.
D3D12_ROOT_SIGNATURE_DESC sigDesc = CD3DX12_ROOT_SIGNATURE_DESC(_countof(param), param);
sigDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
ComPtr<ID3DBlob> blobSignature;
ComPtr<ID3DBlob> blobError;
D3D12SerializeRootSignature(&sigDesc, D3D_ROOT_SIGNATURE_VERSION_1, &blobSignature, &blobError);
Root Signature Descriptor를 만들어주었다. 리소스를 더 추가해도 이 코드는 변하지 않게 상수 대신 _countof 매크로를 썼다.
D3D12SerializeRootSignature은 CreateRootSignature에 전달할 수 있는 루트 시그니처 버전을 직렬화하는 함수라고 하지만... 아무래도 잘 모르겠다.
md3dDevice->CreateRootSignature
(
0,
blobSignature->GetBufferPointer(),
blobSignature->GetBufferSize(),
IID_PPV_ARGS(&mRootSignature)
);
아무튼, 가져온 값을 CreateRootSignature 함수의 매개변수로 넣어 루트 시그니처를 만들어주었다. 버퍼 뷰를 넣어주는 건 렌더링 파이프라인 상태 객체를 만든 후에 해보자.
셰이더 상수 버퍼 선언
상수 버퍼를 셰이더에 보낼 것이기 때문에 셰이더 또한 상수 버퍼에 대한 정보를 가지고 있어야 한다.
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
}
cbuffer cbPass : register(b1)
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
}
구조체를 선언하는 것과 비슷하게 써주었다. 옆에 레지스터를 작성해주는 것도 잊지 않아야 한다.
파이프라인 상태 객체
지금까지 여러가지 렌더링 준비 과정이 있었지만, 렌더링 파이프라인에 묶는 방법은 아직 이야기하지 않았다! 모든 재료들을 가지런히 썰어놨는데, 볶지만 않은 것이다!
렌더링 파이프라인의 상태를 제어하는 파이프라인 상태 객체(PSO)라는 집합체의 값을 설정하고 객체를 생성해 Command List에서 설정해줄 것이다.
ComPtr<ID3D12PipelineState> mPSO = nullptr;
만든 파이프라인을 저장할 객체를 만들어준다. 나중에는 Opqaue, Transparent 등등 재질에 나눠서 파이프라인을 설정해주는데, 지금은 하나만 만들어줄 것이다.
PSO를 만들기 위해서는 여느 때와 같이 Dsecriptor(D3D12_GRAPHICS_PIPELINE_STATE_DESC)을 채워주어야 한다. 각 요소들을 잘 정리해준 사진이 있어 가져와봤다.
psoDesc.InputLayout = { mInputLayout.data(), (UINT)mInputLayout.size() };
psoDesc.pRootSignature = mRootSignature.Get();
psoDesc.VS =
{
reinterpret_cast<BYTE*>(mvsByteCode->GetBufferPointer()), mvsByteCode->GetBufferSize()
};
psoDesc.PS =
{
reinterpret_cast<BYTE*>(mpsByteCode->GetBufferPointer()), mpsByteCode->GetBufferSize()
};
지금까지 만든 입력 배치, 루트 시그니처, 셰이더들을 모두 묶어주었다.
psoDesc.RTVFormats[0] = mBackBufferFormat;
psoDesc.DSVFormat = mDepthStencilFormat;
psoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
psoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
포맷이나 다중 표본화 값 같은 경우에도 미리 설정해두었던 데이터에 기반해 정해주었다.
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); // Default로 하면 기본값으로 세팅
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
여기는 거의 다 Default 값으로... 토폴로지는 삼각형으로 묶어주었으니, TRIANGLE로 설정해주었다. 렌더 타겟도 백 버퍼 하나이다.
md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO));
모든 컴 객체들이 그렇듯, Descriptor를 채우는 데는 오래 걸리지만, 생성하는 코드는 짧다. PSO를 생성해주었다! 이제 만든 재료들을 Command List에 매 프레임씩 모두 넣기만 하면 된다!
렌더링
드~ 디~ 어~ 커맨드 리스트에 명령들을 넣고~ 커맨드 큐가 GPU에게 명령을 할 때가 된 것이다~
저번에는 DrawBegin, DrawEnd로 렌더 루프가 돌아가게 했으므로, 오늘은 Draw에 가서 정육면체를 그릴 것이다!
mCommandList->SetPipelineState(mPSO.Get());
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
// 공용 상수 버퍼 뷰 설정
D3D12_GPU_VIRTUAL_ADDRESS passCBAddress = mPassCB->GetGPUVirtualAddress();
mCommandList->SetGraphicsRootConstantBufferView(1, passCBAddress);
Command List에 먼저 미리 만들어 놓은 렌더링 파이프라인 상태 객체와 루트 시그니처를 설정했다. 그 다음에는 공용 상수 버퍼를 넣어주었다. 아까 0-b0은 개별, 1-b1은 공용 상수 버퍼였으므로 1을, 공용 상수 버퍼의 GPU 가상 주소값을 매개변수로 넣어주었다.
렌더링할 아이템을 모아놓은 복합데이터(지금은 정육면체밖에 없지만...)도 Command List에 반복문을 사용해 차근차근 넣어주었다.
UINT objCBByteSize = (sizeof(ObjectConstants) + 255) & ~255;
for (size_t i = 0; i < mRenderItems.size(); i++)
{
auto item = mRenderItems[i].get();
//cbv
D3D12_GPU_VIRTUAL_ADDRESS objCBAdress = mObjectCB->GetGPUVirtualAddress();
objCBAdress += item->objCbIndex * objCBByteSize; // 하나가 아니므로
mCommandList->SetGraphicsRootConstantBufferView(0, objCBAdress);
//vertex
mCommandList->IASetVertexBuffers(0, 1, &item->vertexBufferView);
//index
mCommandList->IASetIndexBuffer(&item->indexBufferView);
//topology
mCommandList->IASetPrimitiveTopology(item->primitiveTopology);
// Render
mCommandList->DrawIndexedInstanced(item->indexCount, 1, 0, 0, 0);
}
개별 상수 버퍼의 시작 위치를 전과 같이 오브젝트의 인덱스 * 사이즈로 설정했다. 공용 상수 버퍼와 같이 0번째에 현재 시작 주소를 넣고 버텍스 버퍼, 인덱스 버퍼, 토폴로지를 묶어 입력 조립기에 세팅했다.
그리고 DrawIndexedInstanced 함수를 호출해 인덱스 정보에 맞게 물체를 그렸다!
만약에, 정점 정보에 맞게 물체를 그리려면 아래 함수를 사용하면 된다.
결과
하하! 정말 판박이다! 호기심에 지금 데이터를 그대로 버텍스 버퍼를 렌더링해보았다.
요상한 모양이 나왔다. 아마 012 345 삼각형만 렌더링이 되었을 것이다.
렌더링까지 길고도 힘든 여정이었다~
입력 배치, 버텍스·인덱스 버퍼, 셰이더, 상수 버퍼, 루트 시그니처, 렌더링 파이프라인 상태 객체, 커맨드 리스트 등등... 렌더링을 준비하기 위한 과정은 정말 정말 정말!!!!!! 힘들다는 걸 뼈저리게 느꼈다. 수업을 들을 때는 분명 다 이해했다고 느꼈는데, 실제로 정리해보니 부족한 부분도 정말 많았고... 복습의 중요성을 느꼈다.
정육면체 렌더링뿐이지만, DX12에 한층 더 가깝게 다가간 느낌이라 정말 좋았다!!!
참고
'Direct X' 카테고리의 다른 글
[DX12] 로컬 > 월드 > 뷰 > 투영 이론 (1) | 2023.02.27 |
---|---|
[DX12] 렌더링 파이프라인에 대해 알아보자! (3) | 2023.02.27 |
[DX12] 빈 화면을 렌더링해보자! (1) | 2023.02.26 |
[DX12] 장치를 초기화 해보자! (3) (RTV, DSV, Viewport) (0) | 2023.02.26 |
[DX12] 장치를 초기화 해보자! (2) (Command Object, Swap Chain) (0) | 2023.02.13 |