상세 컨텐츠

본문 제목

[DirectX11] Graphics Pipeline 1 - Input Assembler

DirectX 11

by 이나시오- 2024. 8. 24. 23:35

본문

Graphics Pipeline

Graphics Pipeline이란 그래픽스 API (DirectX)에서 물체가 화면에 렌더링 되는 일련의 과정을 의미한다. 아래 이미지는 DirectX11의 Graphics Pipeline의 전 과정을 나타낸다.

Graphics Pipeline in DirectX11

 

 이 과정을 이해하기 전에 우선 그래픽스 API를 통한 렌더링은 최종적으로는 픽셀로 나타나지만, 처음 주어지는 input 데이터는 vector 기반의 점 선 면의 기하라는 것을 알고 있어야 한다. 이때 점을 vertex(정점)라고 하고, 우리는 어떤 도형을 구성하는 정점들을 Graphics Pipeline에 넣어서 최종적으로 그 도형이 화면의 픽셀 값을 통해 어떻게 출력될 것인지를 계산하는 것이다.

 

Input Assembler Stage

 정점 정보를 받아오는 단계이다. 정점 정보는 사용자가 원하는 대로 구성할 수 있지만, 셰이더가 필요로 하는 정보를 포함하고 있어야 한다. 대표적으로 x,y,z 값을 가지고 있는 Position 정보, RGBA 값을 가지는 Color 정보, 텍스쳐의 UV 좌표 x,y 값을 포함하는 UV 정보 등이 있다. (각각의 정보는 float으로 구성되어야 한다.) 참고로 color 정보는 0~1까지 정규화되어 나타낸다.

struct Vtx
{
	Vec3	vPos;
	Vec2	vUV;
	Vec4	vColor; // RGBA (0.f ~ 1.f)
};

 

 이후 내가 렌더링하고 싶은 도형을 구성하는 정점들을 연속된 메모리 공간에 저장해서 Input Assembler에 전달해야 한다. Input Assembler에 전달할 때, CPU 메모리의 데이터를 바로 옮길 수 없고, GPU 메모리에 Buffer 객체로 만든 뒤에 전달할  수 있다. 이 데이터를 vertex buffer라고 한다. Buffer Description 객체를 만들어서 메모리 사이즈 (ByteWidth)와 BindFlags (vertex buffer 용도)를 설정해서 buffer을 생성한다. 또한 Usage를 Default로 설정하면 처음 생성 후 수정할 수 없는 Buffer가 되고, D3D11_SUBRESOURCE_DATA 구조체에 데이터를 넣고 전달해주면 초기 데이터를 설정할 수 있다.

D3D11_BUFFER_DESC m_VBDesc{};

m_VBDesc.ByteWidth = sizeof(Vtx) * m_VtxCount;
m_VBDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
m_VBDesc.Usage = D3D11_USAGE_DEFAULT;
m_VBDesc.CPUAccessFlags = 0;

D3D11_SUBRESOURCE_DATA tSub{};
tSub.pSysMem = m_VtxSysMem;	// CPU 메모리에서 데이터가 들어있는 주소 값

// m_VB : vertex buffer, ComPtr<ID3D11Buffer>
CreateBuffer(&m_VBDesc, &tSub, m_VB.GetAddressOf());

 

 생성한 vertex buffer을 Input Assembler에 전달한다. 이 때, vertex 데이터 하나의 메모리 사이즈를 같이 전달해줘서 연속된 메모리 공간에서 vertex를 하나씩 끊어서 해석할 수 있게 된다.

// Vertex Buffer 설정
UINT Stride = sizeof(Vtx);	 	// Buffer에서 정점 정보를 읽기 위한 메모리 간격(단위)
UINT Offset = 0;			// Buffer에서 메모리를 읽기 시작할 위치 (몇 번째 vertex 부터만 렌더링 해주세요)
CONTEXT->IASetVertexBuffers(0, 1, m_VB.GetAddressOf(), &Stride, &Offset);

 

 하지만, vertex 데이터 하나가 어떻게 해석되는 지는 알 수 없다. 따라서 Input Assembler 단계에서 입력 데이터를 어떻게 해석할 것인지에 대한 정보도 전달한다. 이를 input layout이라고 한다. Vertex 하나의 정보 안에서 어떤 순서로 어떤 데이터 (position, color, uv 등)가 있는지, 그리고 이를 셰이더에서 읽어오기 위해 Semantic을 정해서 Input Assembler에 전달한다. (m_VSBlob은 vertex shader의 binary code가 들어있는 메모리로, vertex shader을 생성할 때 만들어진 객체이다, ComPtr<ID3DBlob>)

// input layout
D3D11_INPUT_ELEMENT_DESC LayoutDesc[3]{};
LayoutDesc[0].AlignedByteOffset = 0;				// Offset (시작 위치)
LayoutDesc[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;		// float 3개로 구성된 vector3
LayoutDesc[0].InputSlot = 0;
LayoutDesc[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
LayoutDesc[0].InstanceDataStepRate = 0;
LayoutDesc[0].SemanticName = "POSITION";			// position을 지칭하기 위한 semantic name
LayoutDesc[0].SemanticIndex = 0;				// 같은 SemanticName에 index 붙이기 위한 변수 (POSITION1, POSITION2, ..)
								// 사실 웬만하면 semanticname을 다르게 쓸 것이므로 별로 쓸모 없음

LayoutDesc[1].AlignedByteOffset = 12;
LayoutDesc[1].Format = DXGI_FORMAT_R32G32_FLOAT;		// float 2개로 구성된 vector2
LayoutDesc[1].InputSlot = 0;
LayoutDesc[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
LayoutDesc[1].InstanceDataStepRate = 0;
LayoutDesc[1].SemanticName = "TEXCOORD";			// UV를 지칭하기 위한 semanticname
LayoutDesc[1].SemanticIndex = 0;

LayoutDesc[2].AlignedByteOffset = 20;
LayoutDesc[2].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;		// float 4개로 구성된 vector4
LayoutDesc[2].InputSlot = 0;
LayoutDesc[2].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
LayoutDesc[2].InstanceDataStepRate = 0;
LayoutDesc[2].SemanticName = "COLOR";				// color을 지칭하기 위한 semanticname
LayoutDesc[2].SemanticIndex = 0;

DEVICE->CreateInputLayout(LayoutDesc, 3
	, m_VSBlob->GetBufferPointer(), m_VSBlob->GetBufferSize()
	, m_Layout.GetAddressOf());

CONTEXT->IASetInputLayout(m_Layout.Get());

 

 이러면 Vertex 하나 하나의 정보를 읽을 수 있게 되지만, 이 vertex들로 어떤 도형을 어떻게 구성할 것인지에 대한 정보도 필요하다. 이를 topology라고 한다. 아래 이미지는 topology의 종류에 따라 도형이 구성되는 과정을 나타낸다.

Topology에 따른 도형 구성 방법

 

 예를 들어 topology를

Point List로 지정하면 각 vertex로 하나의 point를 만든다.

Line List로 지정하면 [ vtx0, vtx1, vtx2, vtx3]을 0-1 선분과 2-3 선분으로 인식한다.

Line Strip으로 지정하면, [ vtx0, vtx1, vtx2, vtx3]을 0-1 선분, 1-2 선분, 2-3 선분으로 인식하여 연속적인 도형을 구성한다.

Triangle List로 지정하면, [ vtx0, vtx1, vtx2, vtx3, vtx4, vtx5]를 0-1-2 삼각형, 3-4-5 삼각형으로 인식한다.

Triangle Strip으로 지정하면, [ vtx0, vtx1, vtx2, vtx3, vtx4, vtx5] 를 0-1-2 삼각형, 1-2-3 삼각형, 2-3-4 삼각형, 3-4-5 삼각형으로 연속적인 삼각형으로 도형을 구성한다.

( with adjacency 옵션은 뭔지 이해를 못해서 나중에 추가하겠다)

CONTEXT->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

 

 우리는 면을 구성하고 싶으므로 Triangle List로 설정했다는 가정 하에 진행할 것이다. 왼쪽과 같이 점 4개로 이루어진 사각형을 렌더링하고 싶다고 생각하자. 그렇다면 vertex buffer로 [0, 1, 2, 0, 2, 3]을 넣어줘야 오른쪽처럼 2개의 삼각형을 합쳐서 사각형을 만들 수 있다. 

 

 

 다만 이렇게 도형을 구성하면, 동일한 vertex가 중복되어 vertex buffer에 들어간다는 메모리 비효율이 발생한다. 예를 들어 아래와 같은 도형이 있으면 가운데 vertex는 7개의 삼각형을 구성할 때마다 필요해서 7번이나 vertex buffer에 중복되서 들어간다. 이러한 점을 해결하기 위해 Index Buffer가 존재한다. Index Buffer을 사용하면, Vertex Buffer에는 모든 vertex를 1번씩 전달하여 목록으로서 사용하고, 실제로 topology에 따라 도형을 구성할 때는 index 값을 사용한다. 예를 들어 Vertex Buffer에 [vtx0, vtx1, vtx2, vtx3, vtx4, vtx5, vtx6, vtx7]을 전달하고, Index Buffer로 [0,1,2,0,2,3,0,3,4,0,4,5,0,5,6,0,6,7,0,7,1]을 전달하면 아래 도형을 구성할 수 있는 것이다.

m_IBDesc.ByteWidth = sizeof(UINT) * m_IdxCount;
m_IBDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
m_IBDesc.Usage = D3D11_USAGE_DEFAULT;
m_IBDesc.CPUAccessFlags = 0;

D3D11_SUBRESOURCE_DATA tSub{};
tSub.pSysMem = m_IdxSysMem;

// m_IB : index buffer, ComPtr<ID3D11Buffer>
DEVICE->CreateBuffer(&m_IBDesc, &tSub, m_IB.GetAddressOf());

// Index Buffer 설정
CONTEXT->IASetIndexBuffer(m_IB.Get(), DXGI_FORMAT_R32_UINT, 0);

 

 

 Index Buffer을 설정하지 않으면 Vertex Buffer을 topology를 구성할 때 사용하고, Index Buffer을 설정하면 자동으로 Vertex Buffer을 vertex 목록으로 인식하고 topology를 구성할 때 Index Buffer을 사용한다.

관련글 더보기