Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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 29 30 31
Archives
Today
Total
관리 메뉴

codingfarm

6. Direct3D의 그리기 연산 - 정점 버퍼(vectex buffer) 본문

computer graphics/DX12 book

6. Direct3D의 그리기 연산 - 정점 버퍼(vectex buffer)

scarecrow1992 2021. 6. 2. 00:01

버퍼

  • GPU가 접근 가능한 GPU 자원(ID3D12Resource) 공간
    • 응용 프로그램에서 정점 같은 자료 원소들의 배열을 GPU에 제공해야 할 때에는 항상 버퍼를 사용함
  • 텍스처보다도 단순한 자원(다차원이 아님)
  • mipmap이나 filter, 다중 표본화 기능이 없음

 

1. 정점 버퍼(vertex buffer)

  • GPU가 정점들의 배열에 접근가능하도록 정점들을 저장하는 버퍼

 

정점 버퍼 생성

앞서 배웠던 리소스의 생성과정과 동일함

  1. D3D12_RESOURCE_DESC를 채운다
  2. ID3D12Device::CreateCommittedResource 메서드를 호출해 ID3D12Resource 객체를 생성함

Diret3D 12는 편의용 생성자들가 메서드들을 추가한 C++ 래퍼 클래스 CD3DX12_RESOURCE_DESC를 제공함

1
2
3
4
5
6
7
8
9
10
11
static inline CD3DX12_RESOURCE_DESC Buffer( 
        UINT64 width,
        D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE,
        UINT64 alignment = 0 )
{
    return CD3DX12_RESOURCE_DESC(
        D3D12_RESOURCE_DIMENSION_BUFFER,
        alignment, width, 111
        DXGI_FORMAT_UNKNOWN, 10,
        D3D12_TEXTURE_LAYOUT_ROW_MAJOR, flags );
}
cs

범용 GPU 자원으로서의 버퍼에서 너비(width)는 가로길이가 아니라 버퍼의 바이트 개수를 뜻함.

가령 float 64개를 담는 버퍼의 너비는 64 * sizeof(float) 이다.

 

 

기본 힙(default heap)에 저장된 버퍼 초기화

정점 기하 구조(프레임 마다 변하지 않는 기하구조 - 나무, 건물, 지형, 무기 등...)를 그릴때는 최적의 성능을 위해 정점 버퍼들을 기본 힙(D3D12_HEAP_TYPE_DEFAULT)에 넣는다. 이런 정점 기하구조는 정점 버퍼를 초기화한 후에는 GPU만 버퍼의 정점들을 읽으므로(기하구조를 그리기 위해) CPU가 접근할 필요성이 없다. 그러므로 default heap에 넣는것이 합당하다.

앞서 말했듯이 CPU는 기본 힙에 있는 정점 버퍼를 수정하지 못한다. 그런데, 애초에 응용 프로그램이 정점 버퍼를 최초에 초기화 하기 위해선 어떻게든 CPU가 관여 해야만 한다.

이를 위해 응용 프로그램은 D3D12_HEAP_TYPE_UPLOAD 형식의 힙에 임시 업로드용 버퍼 자원을 생성해야 한다.

우리는 앞서 Depth Stencil Buffer와 View의 생성에 대해 배울 당시 해당 버퍼를 업로드 힙에 만들었으며, 업로드 힙에 자원을 맡겨야 CPU 메모리에서 GPU 메모리로 자료를 복사할 수 있음을 배웠다.

업로드 버퍼를 사용하여 default heap에 저장된 자원(vertex buffer)을 초기화 하는 방법은 아래와 같다.

  1. 업로드 버퍼(D3D12_HEAP_TYPE_UPLOAD)를 생성한다.
  2. 시스템 메모리에 있는 정점 자료를 업로드 버퍼에 복사한다.
  3. 업로드 버퍼의 정점 자료를 실제 정점 버퍼(D3D12_HEAP_TYPE_DEFAULT)로 복사한다.

즉, 기본 버퍼(D3D12_HEAP_TYPE_DEFAULT 형식의 힙에 있는 버퍼)의 자료를 초기화 하려면 항상 업로드 버퍼(D3D12_HEAP_TYPE_UPLOAD 형식의 힙에 있는 버퍼)가 필요하므로, 이 책의 예제 프레임워크는 아래와 같은 편의용 함수를 제공한다.(d3dUtil.h/.cpp)

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer(
    ID3D12Device* device,
    ID3D12GraphicsCommandList* cmdList,
    const void* initData,
    UINT64 byteSize,
    Microsoft::WRL::ComPtr<ID3D12Resource>& uploadBuffer)
{
    ComPtr<ID3D12Resource> defaultBuffer;
 
    // Create the actual default buffer resource.
    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())));
 
    // In order to copy CPU memory data into our default buffer, we need to create
    // an intermediate upload heap. 
    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())));
 
 
    // Describe the data we want to copy into the default buffer.
    D3D12_SUBRESOURCE_DATA subResourceData = {};
    subResourceData.pData = initData;
    subResourceData.RowPitch = byteSize;
    subResourceData.SlicePitch = subResourceData.RowPitch;
 
    // Schedule to copy the data to the default buffer resource.  At a high level, the helper function UpdateSubresources
    // will copy the CPU memory into the intermediate upload heap.  Then, using ID3D12CommandList::CopySubresourceRegion,
    // the intermediate upload heap data will be copied to mBuffer.
    cmdList->ResourceBarrier(1&CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(), 
        D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST));
    UpdateSubresources<1>(cmdList, defaultBuffer.Get(), uploadBuffer.Get(), 001&subResourceData);
    cmdList->ResourceBarrier(1&CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
        D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_GENERIC_READ));
 
    // Note: uploadBuffer has to be kept alive after the above function calls because
    // the command list has not been executed yet that performs the actual copy.
    // The caller can Release the uploadBuffer after it knows the copy has been executed.
 
 
    return defaultBuffer;
}
 
cs 

생성될 buffer의 정보를 전달받는 3번째 매개변수가 void*임을 주목하라, vertex buffer 뿐만 아니라 index buffer 등 그 외의 모든 기본 버퍼도 이 함수로 생성할 수 있다.

앞서 정리했던 "업로드 버퍼를 사용하여 default heap에 저장된 자원(vertex buffer)을 초기화 하는 방법"을 상기하며 위 코드를 분석해보자.

1. 21번째 줄에서 upload buffer를 생성함

2, 3. 41번째 줄에서 subresource를 통해 시스템 메모리에 있는 자료를 upload buffer에 복사한 후, default buffer에 마저 복사한다.

 

 

Subresource

리소스를 보다 작은 단위로 분리시킨것

자세한 정보는 추후 조사

 

UpdateSubresources<UINT T>

자세한 정보는 추후 조사

 

D3D12_SUBRESOURCE_DATA

1
2
3
4
5
typedef struct D3D12_SUBRESOURCE_DATA {
  const void *pData;
  LONG_PTR   RowPitch;
  LONG_PTR   SlicePitch;
} D3D12_SUBRESOURCE_DATA;
cs

subresource data를 서술하는 구조체

 

매개변수

1. const void *pData;

  • 버퍼 초기화용 자료를 담은 시스템 메모리 배열을 가리키는 포인터
  • 버퍼에 n개의 정점을 담을 수 잇다고 할때, 버퍼 전체를 초기화 하려면 해당 시스템 메모리 배열에 적어도 n개의 정점이 있어야 함

2. LONG_PTR RowPitch;

  • 버퍼의 경우, 복사할 자료의 크기(바이트 갯수)

3. LONG_PTR SlicePitch

  • 버퍼의 경우, 복사할 자료의 크기(바이트 갯수)

 

아래 코드는 각자 다른 색이 부여된 입방체의 정점 여덟 개를 저장하는 기본 버퍼를 CreateDefaultBuffer 메서드를 이용해서 기본 힙(D3D12_HEAP_TYPE_DEFAULT)생성하는 방법을 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Vertex{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};
 
std::array<Vertex, 8> vertices = {
    Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
    Vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) }),
    Vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) }),
    Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) }),
    Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }),
    Vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) }),
    Vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) }),
    Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) })
};
 
const UINT vbByteSize = 8 * sizeof(Vertex);
 
ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
 
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
        mCommandList.Get(), vertices, vbByteSize, VertexBufferUploader);
cs

 

 

2. 정점 버퍼 뷰(Vertex Buffer View)

  • 정점 버퍼(resource)를 파이프라인에 묶기 위해 자원을 서술하는 descriptor
  • RTV와는 달리 VBV에는 descriptor heap이 필요 없음
  • D3D12_VERTEX_BUFFER_VIEW구조체로 대표됨
1
2
3
4
5
typedef struct D3D12_VERTEX_BUFFER_VIEW {
  D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
  UINT                      SizeInBytes;
  UINT                      StrideInBytes;
} D3D12_VERTEX_BUFFER_VIEW;
cs

1. D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;

  • 생성할 뷰의 대상이 되는 정점 버퍼 자원의 가상 주소
  • 이 주소는 ID3D12Resource::GetGPUVirtualAddress 메서드로 얻을 수 있다.

 

2. UINT SizeInBytes;

  • BufferLocation에서 시작하는 정점 버퍼의 크기(바이트 갯수)

 

3. UINT StrideInBytes;

  • 버퍼에 담긴 한 정점 원소의 크기(바이트 갯수)

 

3. Binding

  • 정점 버퍼(resource)와 뷰(descriptor)까지 생성했다면, 정점 버퍼 뷰를 pipeline의 한 입력슬롯에 묶을 수(bind) 있다.
  • vertex buffer view의 binding이 끝나면 pipeline의 입력 조립 단계(IA)에 진입함
  • ID3D12GraphicsCommandList::IASetVertexBuffers 메서드로 vertex buffer를 pipeline에 묶을 수 있다.
1
2
3
4
5
void ID3D12GraphicsCommandList::IASetVertexBuffers(
  UINT                           StartSlot,
  UINT                           NumViews,
  const D3D12_VERTEX_BUFFER_VIEW *pViews
);
cs

1. UINT StartSlot;

  • 시작 슬롯. 즉, 첫째 vertex buffer를 묶을(binding) 입력 슬롯의 index
  • 입력 슬롯은 총 16개이다(0~15)

 

2. UINT NumViews;

  • 입력 슬롯들에 묶을(binding) 정점 버퍼 갯수
  • 시작 슬롯의 색인이 k이고 묶을 버퍼가 n개 이면, 버퍼들은 입력슬롯 $I_k, I_{k+1}, \cdots , I_{k+n-1}$에 묶이게 된다.

 

3. const D3D12_VERTEX_BUFFER_VIEW *pViews

  • binding될 정점 버퍼 뷰 배열의 첫 원소를 가리키는 포인터

 

메서드 호출 예

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
29
30
31
32
33
34
35
struct Vertex{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};
 
std::array<Vertex, 8> vertices = {
    Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
    Vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) }),
    Vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) }),
    Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) }),
    Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }),
    Vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) }),
    Vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) }),
    Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) })
};
 
const UINT vbByteSize = 8 * sizeof(Vertex);
 
ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
 
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
        mCommandList.Get(), vertices, vbByteSize, VertexBufferUploader);
 
 
/* vertex buffer들과 view들을 생성한다 */
D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = sizeof(Vertex);
vbv.SizeInBytes = 8 * sizeof(Vertex);
 
D3D12_VERTEX_BUFFER_VIEW vertexBuffers[1= { vbv };
 
// vertex buffer view 1을 pipeline에 binding 한다.
mCommandList->IASetVertexBuffers(01&vertexBuffers);
cs

 

여러개의 vertex buffer view를 pipeline의 0번 input slot에 binding 할 경우 skeletal code

1
2
3
4
5
6
7
8
9
10
11
12
13
ID3D12Resource* mVB1;
ID3D12Resource* mVB2;
 
D3D12_VERTEX_BUFFER_VIEW mVBView1;
D3D12_VERTEX_BUFFER_VIEW mVBView2;
 
/* vertex buffer들과 view들을 생성한다 */
 
// vertex buffer view 1을 pipeline에 binding 한다.
mCommandList->IASetVertexBuffers(01&VBView1);
 
// vertex buffer view 2를 pipeline에 binding 한다.
mCommandList->IASetVertexBuffers(01&VBView2);
cs

 

4. 명령 제출

vertex buffer view를 pipeline의 input slot에 binding 했다고 정점들이 그려지는것은 아니다. 단지 이 vertex들을 pipeline에 공급할 준비가 된것 뿐이다. vertex를 실제로 그리려면 ID3D12GraphicsCommandList::DrawInstanced 메서드를 호출하여 command list에 명령을 넣어야 한다.

1
2
3
4
5
6
void DrawInstanced(
  UINT VertexCountPerInstance,
  UINT InstanceCount,
  UINT StartVertexLocation,
  UINT StartInstanceLocation
);
cs

1. UINT VertexCountPerInstance;

  • 그릴 정점들의 갯수(인스턴스당)

 

2. UINT InstanceCount;

  • 그릴 인스턴스 갯수
  • 인스턴싱이라는 고급기법에서 쓰임(지금은 1을 설정)

 

3. UINT StartVertexLocation;

  • 정점 버퍼에서 이 그리기 호출로 그릴 일련의 정점들 중 첫 정점의 index(0 기반)

 

4. UINT StartInstanceLocation;

  • 고급 기법인 인스턴싱에 쓰임(지금은 0을 설정)

 

지금은 vertex buffer의 vertex들 중 startVertexLocation지점에서 VertexCountPerInstance개 만큼 그리기 호출에 쓰이는 vertex들이 결정된다 정도로 이해하자

지정될 정점들을 Direct3D가 점들로 취급할지 아니면 선 목록이나 삼각형 목록으로 취급될지는 ID3D12CommandList::IASetPrimitiveTopology 메서드로 설정하는 기본 도형 위상 구조 상태가 결정한다.

메서드의 호출 예

1
cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
cs

 


정리

vertex buffer를 만들어, 화면에 출력하기 위한 일련의 과정은 아래와 같다.

  1. vertex buffer(ID3D12Resource)를 생성한다(ID3D12Device::CreateCommittedResource, D3DUtil::CreateDefaultBuffer)
  2. descriptor인 vertex buffer view(D3D12_VERTEX_BUFFER_VIEW)를 생성한다.
  3. pipeline에 binding 한다.(ID3D12GraphicsCommandList::IASetVertexBuffers)
  4. 기본 도형 위상 구조를 설정한다.(ID3D12CommandList::IASetPrimitiveTopology)
  5. cmd list에 그리기 명령을 제출 한다.(ID3D12GraphicsCommandList::DrawInstanced)
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#define WHITE    0
#define BLACK    1
#define RED        2
#define GREEN    3
#define BLUE    4
#define YELLOW    5
#define CYAN    6
#define MAGENTA    7
 
struct Vertex{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};
 
typedef struct D3D12_VERTEX_BUFFER_VIEW {
  D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
  UINT                      SizeInBytes;
  UINT                      StrideInBytes;
} D3D12_VERTEX_BUFFER_VIEW;
 
std::array<Vertex, 8> org_vertices = {
    Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
    Vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) }),
    Vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) }),
    Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) }),
    Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }),
    Vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) }),
    Vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) }),
    Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) })
};
 
std::array<Vertex, 36> vertices = {
    org_vertices[0], org_vertices[1], org_vertices[2], 
    org_vertices[0], org_vertices[2], org_vertices[3],
 
    org_vertices[4], org_vertices[6], org_vertices[5],
    org_vertices[4], org_vertices[7], org_vertices[6],
 
    org_vertices[4], org_vertices[5], org_vertices[1],
    org_vertices[4], org_vertices[1], org_vertices[0],
 
    org_vertices[3], org_vertices[2], org_vertices[6],
    org_vertices[3], org_vertices[6], org_vertices[7],
 
    org_vertices[1], org_vertices[5], org_vertices[6],
    org_vertices[1], org_vertices[6], org_vertices[2],
 
    org_vertices[4], org_vertices[0], org_vertices[3],
    org_vertices[4], org_vertices[3], org_vertices[7] };
 
const UINT vbByteSize = (UINTvertices.size() * sizeof(Vertex);
 
ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
 
// 1. vertex buffer 생성
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
        mCommandList.Get(), vertices, vbByteSize, VertexBufferUploader);
 
UINT VertexByteStride = sizeof(Vertex);
UINT VertexBufferByteSize = (UINT)vertices.size() * sizeof(Vertex);
 
 
// 2. vertex buffer view 생성
D3D12_VERTEX_BUFFER_VIEW vbv;
 
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = VertexByteStride;
vbv.SizeInBytes = VertexBufferByteSize;
 
 
// 3. binding
mCommandList->IASetVertexBuffers(01&vbv);

// 4. 기본 도형 위상 구조 설정   
mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
 
// 5. 그리기 명령 제출
mCommandList->DrawInstanced(
        (UINT)vertices.size(), 
        100);
cs

이해를 위해 모든 코드를 선형적으로 작성했다.

위 과정에서 1~2는 한번만 호출되도 되지만, 3~5는 화면 갱신을 반영하기 위해 윈도우 메시지를 처리하기 위한 반복문 내에서 호출되게 해줘야 한다.

 

 

 

 

 

 

 

Comments