본문으로 바로가기

( D3DX12 / DirectX12 ) 2. DirectX12의 그리기 연산

category Graphics/DirectX 2020. 9. 8. 03:08
DirectX12의 그리기 연산에 대해 작성한다.
실제로 글이 끝날 때엔, 각 Vertex들이 특별한 색을 갖는 상자 하나를 띄워보겠음
이 글은 개발자가 변환에 대해 이미 학습을 했다고 가정하고 작성한다.

그리기 연산의 순서는 다음과 같다.

1. Vertex 정의와 Input Layout 작성
2. Vertex Buffer 와 Vertex Buffer View 작성
3. Index Buffer 와 Index Buffer View 작성 ( 선택 )
4. Vertex Shader와 Pixel Shader 작성
5. Constant Buffer Descriptor Heap 작성
6. Constant Buffer( Upload Heap )와 Constant Buffer View 작성 

 

1. Vertex 정의와 Input Layout 작성

 

Direct3D 에서는 Vertex에 공간적 위치 이외의 추가적인 정보를 작성할 수 있다. 

원하는 특성들을 가진 커스텀 Vertex를 만들어 보자. 일단 이러한 자료를 담을 구조체를 정의한다.

이번에 작성할 Vertex는 위치 정보와 색상 정보를 가지도록 한다.

 

Vertex 구조체를 정의한 다음에는 Vertex 구조체의 각 필드, 즉 Vertex의 각 성분으로 무엇을 해야하는지 Direct3D에게 알려주어야 한다. 그러한 정보를 Direct3D에게 알려주는 수단으로 쓰이는 것이 Input Layout 이다.

4번에서 작성할 Vertex Shader도 Input Layout과 매칭되어야 한다.

Input Layout 에 INPUT_ELEMENT_DESC를 작성하여 연결해줘야 비로소 사용할 수 있다.

 

잘보면 POSITON 과 COLOR Semantic 은 Vertex Shader 에서 각 인자와 매칭됨을 알 수 있다. 매칭되지 않을 시 오류

 

2. Vertex Buffer와 Vertex Buffer View 작성

GPU가 Vertex들의 배열에 접근하려면, 그 Vertex들을 Buffer라고 하는 GPU 자원 (ID3D12Resource)에 넣어두어야 한다.

Vertex들을 저장하는 Buffer를 Vertex Buffer라고 부른다. 

응용 프로그램에서 Vertex같은 자료 원소들의 배열을 GPU에게 제공해야 할 때는 항상 Buffer를 사용한다.

 

Vertex 는 기본적으로 바뀌지 않는 구조다.

( 각 Vertex들이 Transform 연산을 통해 위치를 바꿔나가는 구조지, Vertex들의 Local Position은 변하지 않음 )

그러므로 Default Buffer에 Vertex 들을 넣어놓는데, Default Buffer는 GPU만 접근할 수 있는 특별한 자원이므로

Upload Buffer -> Default Buffer 로의 복사가 필요하다. 다음과 같이 작성한다.

 

실제로 CommandList 가 작업을 한번 실행 ( Execute )한 뒤에 Upload Buffer에 있는 내용들이 Default Buffer로 복사

즉, Upload Buffer는 Execute가 되기 전까진 절대 삭제되면 안된다. 

 

다음은 실제로 Vertex Buffer를 만들어내는 소스이다.

 

CreateDefaultBuffer ( 위의 Default Buffer를 생성해내는 소스와 동일 ) 에서는 실제로 Upload Buffer에서 Default Buffer로 자원을 복사 ( Copy ) 하는 행위를 하므로, Buffer를 생성한 후 Execute 한번만 실행하면 된다.

 

이제 Vertex Buffer를 생성했으니, Vertex Buffer의 정보를 알려주기 위해 Vertex Buffer View를 생성한다.

Vertex Buffer View는 다른 View 들과 다르게 Descriptor Heap이 필요하지 않다.

 

BufferLocation = Vertex Buffer의 가상 GPU 주소가 어떻게 되는지 ?

StrideInBytes = 몇 Byte를 기준으로 새로운 정보가 나타나는지 ?

SizeInBytes = Vertex Buffer의 크기가 어떻게 되는지 ?

 

실제로 Vertex Buffer를 Pipeline State Object에 Binding 할땐 IASetVertexBuffers를 사용한다.

 

슬롯은 INPUT_ELEMENT_DESC에서 정의한다. 만약 슬롯이 다르다면 Offset을 증가하여 처리하지 않아도 된다.

 

3. Index Buffer 와 Index Buffer View 작성 ( 선택 )

 

Vertex Shader 에서 각 정점들이 넘어가고, 정점들에 대한 그리기 요청을 할 때, 

 

1. 한 정점이 여러번 계산 되는 것

2. 중복되는 정점이 많아 메모리 낭비가 있는 것

 

때문에 Index Buffer를 사용한다.

실제로 현재 작성되는 소스의 Byte는 224 이고, 2번의 중복이 일어난다면 Vertex Buffer 에는 448 Byte의 같은 내용의 정점이 들어있어야 한다. 하지만 하나의 정점과 SINT 의 Index를 사용한다면 226 Byte만으로 처리할 수 있다.

 

작성한 Vertex를 기준으로 Index 를 구축한다.

물론 Winding Order 를 준수하여야 한다.

 

 

Index 또한 Buffer에 담겨야 하는데, Index 도 변하지 않으므로 Default Buffer에 생성한다.

 

Index Buffer의 정보를 알려주어야 하므로, Index Buffer View를 생성한다.

 

Vertex Buffer View 와 다른점은, Stride를 사용하지 않는단 점인데 이는 

PRIMITIVE_TOPOLOGY 을 알면 Stride를 알 수 있기 때문이다. 

( 예를들어 PRIMITIVE가 TRIANGLE 이라면, 3개의 Index마다 새로운 Index가 나온다는 것을 알 수 있다. )

 

마지막으로 Index 를 이용하여 도형을 그리려면 DrawIndexedInstanced 함수를 사용해야 한다.

 

 

4. Vertex Shader와 Pixel Shader 작성

 

High Level Shading Language 를 통하여 작성한다. 이 언어는 문법이 C++과 비슷하기 때문에 쉽게 배울 수 있다.

Vertex 정의와 Input Layout 작성에서 Semantic에 대한 내용을 언급했다.

Vertex Shader와 Pixel Shader를 작성한다.

 

실제로 Vertex Shader와 Pixel Shader를 Pipeline State Object 에 Binding 해야한다.

이는 생략한다.

gWorldViewProj 은 상수 버퍼이므로, 다음 내용에서 다시 언급한다.

 

5. Constant Buffer Descriptor Heap 작성

 

Constant Buffer는 셰이더 프로그램에서 참조하는 자료를 담는 GPU 자원이다. 

Vertex Buffer나 Index Buffer와는 달리, 한 프레임마다 바뀌는 정보들은 상수 버퍼에 작성된다.

예를 들어 현재 소스에서는 상자들이 World 에서 변할 수도 있으므로, 각 프레임마다 상수 버퍼를 다시 재작성한다.

왜 이름이 상수버퍼인지? - 실제로 GPU에서 이 값을 읽어다 사용할 때 그 순간에는 값을 CPU가 변경할 수 없다.

즉 GPU 에서 사용하는 순간은 Constant 하다 -> 상수 버퍼

 

Constant Buffer View를 위한 Constant Buffer Descriptor Heap을 생성한다.

 

실제로 이 Descriptor Heap 에 Constant Buffer View들이 묶일 것이다.

 

6. Constant Buffer( Upload Heap )와 Constant Buffer View 작성 

 

이제 실제로 Constant Buffer부터 작성한다.

값을 계속 수정해야 하므로 Upload Buffer에 작성한다.

주의 : ByteSize는 최소 하드웨어 할당 크기 ( 256 바이트 ) 의 배수가 되게 만들어야 한다.

이를 위해 바이트를 계산하는 함수를 하나 작성한다.

이는 어떠한 바이트를 받으면 그 바이트를 256 바이트의 배수로 만들어주는 함수이다.

 

 

이제 UploadBuffer를 생성하는 함수를 하나 작성한다. 아래와 같다.

 

Map을 호출하고 BYTE* 타입의 포인터를 넘겨주면, UnMap을 호출하기 전까지는

UploadBuffer에 접근할 수 있는 CPU 주소를 넘겨준다. 이를 통해 매 프레임마다 값을 집어넣는다.

 

 

이제 Constant Buffer View 를 생성해야한다. 이미 5번에서 생성해둔 Descriptor Heap이 있으므로 그것을 이용한다.

 

 

이제 실제로, View를 Root Signature에 등록하여야 한다. 그런데 View들은 Root Signature의 어떤 Parameter로 들어가야하는지 모르는데, 이를 위해 Root Parameter를 작성하고, Root Signature를 작성한다.

 

PSO에 Binding 시키는 것은 생략한다.

 

0번 레지스터가 보이는가? 다시 4번의 Vertex Shader를 살펴보면, gWorldViewProj 는 b0 에 묶였는데,

b = constant buffer, 0 = 0번 레지스터 라는 뜻이다.

 

 

이제 Root Parameter를 통해 Root Signature를 생성했으면, 생성된 모든 것을 CommandList에 등록 하면 된다.

 

 

그럼 다음과 같은 상자가 나타난다.

 

모든 작성된 소스는 아래의 Github에서 다운로드 받을 수 있다.

 

github.com/kimduuukbae/Today-I-Learned/tree/master/Lunar-D3D12/Chapter6%20-%20Drawing%20in%20Direct3D