[vk] Vertex buffers - Index buffer

2021. 8. 14. 03:24그래픽스/vk

1. Introduction

- 3D meshes 은 다수의 삼각형으로구성되며 정점들을 공유한다.

- 이것은 간단한 사각형에서도 알 수 있다.

- 두개의 삼각형으로 사각형을 그려보면, 6개의 정점이 필요하게된다.

- 문제는 두개의 정점이 중복된다는것이고 이것은 50%에 해당하는 수치이다. (4개의 정점중 2개)

- 이 문제에 대한 해결방안은 index buffer를 사용하는것이다.

 

- index buffer는 본질적으로 정점 버퍼에 대한 포인터 배열이다.

- 이것은 vertex data를 재정렬하는것을 허용해주며

- 존재하는 다수의 정점 데이터를 재사용하게 해준다.

- 위 그림은 정점 4개를 가진 정점버퍼와 인덱스버퍼를 사용하여 사각형을 나타내는 한 예시이다.

- - - - 첫번째 3개의 indices는 오른쪽 위 의 삼각형을 정의하며 

- - - - 마지막 3개의 indices는 왼쪽 아래 의 삼각형을 정의한다.

 

 

 

 

2. Index buffer creation

- 이번 챕터에서는 vertex data를 수정하고 index data를 추가하여 사각형을 그릴것이다. (위 그림처럼)

- 4개의 정점데이터를 가지도록 수정하자

const std::vector<Vertex> vertices = {
    {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}},
    {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}},
    {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}},
    {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}}
};

- top left 는 red

- top right 는 green

- bottom right 는 blue

- bottom left 는 white 

 

- 우리는 이제 새로운 배열 indices 를 추가하여 index buffer의 내용물을 표현할것이다.

const std::vector<uint16_t> indices = {
    0, 1, 2, 2, 3, 0
};

- 이것은 uint16_t 이나 uint32_t 를 사용할 수 있다. 

- index buffer는 vertices항목 수에 의존한다.

- 여기서는 uint16_t 타입을 사용한다 (65535 보다 작은 정점을 가짐)

 

- 이제 vertex data와 같이 이 indices를 VkBuffer 에 업로드하여 GPU에서 접근가능하게할 필요가 있다.

- 인덱스 버퍼의 리소스를 소유할 두개의 클래스 멤버를 정의하자

VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;

- createIndexBuffer 함수를 만들고,  createVertexBuffer 과 거의 유사하게 코드를 채워넣을 수 있다.

void initVulkan() {
    ...
    createVertexBuffer();
    createIndexBuffer();
    ...
}

void createIndexBuffer() {
    VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size();

    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
    memcpy(data, indices.data(), (size_t) bufferSize);
    vkUnmapMemory(device, stagingBufferMemory);

    createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer, indexBufferMemory);

    copyBuffer(stagingBuffer, indexBuffer, bufferSize);

    vkDestroyBuffer(device, stagingBuffer, nullptr);
    vkFreeMemory(device, stagingBufferMemory, nullptr);
}

- 두개의 차이점만 존재한다

- bufferSize 는 이제 indices 의 수에 indices 타입의 크기(uint16_t or uint32_t)를 곱한것과 같아졌고

- indexBuffer 목적으로 사용하므로 VK_BUFFER_USAGE_INDEX_BUFFER_BIT 를 사용한다

  (vertex buffer는 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)

- 다른 프로세스는 정확하게 같다.

- staging 버퍼에 indices를 복사해오고 그다음에 마지막으로 device local index buffer 로 복사시키는것이다.

 

- 인덱스 버퍼 또한 프로그램 마지막에 제거하고 메모리를 해제해줘야한다.

void cleanup() {
    cleanupSwapChain();

    vkDestroyBuffer(device, indexBuffer, nullptr);
    vkFreeMemory(device, indexBufferMemory, nullptr);

    vkDestroyBuffer(device, vertexBuffer, nullptr);
    vkFreeMemory(device, vertexBufferMemory, nullptr);

    ...
}

 

 

 

 

3. Using an index buffer

- index 버퍼를 사용하기위해서는 createCommandBuffers 에서 두개의 코드만 포함시키면된다.

- 우리는 첫번째로 index버퍼를 바인딩 시킬 필요가있다. (vertex 버퍼를 사용했던것처럼)

- 차이점은 오직 single indexbuffer만 사용가능하다는것이다.

  (vkCmdBindVertexBuffers 함수에는 바인딩할 버퍼의 개수를 지정할 수 있다.)

- 이것은 불행하게도 각각의 vertex attribute에 다른 indices을 사용하는것이 불가능하다.,

- 그래서 여전히 우리는 속성이 하나만 변하더라도 정점 데이터를 완전히 복제해야한다.

vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);

vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT16);

- 한 인덱스 버퍼는 vkCmdBindIndexBuffer 를 사용하여 바운드할 수 있다.

  (매개변수는 인덱스 버퍼, 바이트 오프셋, 인덱스 데이터 타입)

- 이전에 언급한것처럼 VK_INDEX_TYPE_UINT16 타입과 VK_INDEX_TYPE_UINT32 타입을 사용할 수 있다.

 

- 그저 인덱스 버퍼를 바인딩하기만하면 아직 아무것도 변하지 않는다.

- 우리는 또한 Vulkan에게 이 인덱스 버퍼를 사용한다고 말하기위해 drawing command 를 변경할 필요가있다.

- vkCmdDraw 함수를 지우고 vkCmdDrawIndexed로 교체해야한다. 

vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);

- 이 함수의 호출은  vkCmdDraw 와 매우 유사하다.

- 파라미터 2, 3은 indices의 개수를 명시하고, instances의 개수를 명시한다.

- - - - indices의 개수는 vertex buffer에 전달될 정점의 수를 나타낸다. (삼각형의 정점의 수, 총 6개)

- - - - instancing을 사용하지 않으므로 그냥 1로 설정해놓는다.

- 파라미터 4 는  index buffer에서의 offset이다.

- - - - 만일 1 로 설정되어있으면, 그래픽 카드가 두번째 인덱스부터 읽기 시작한다 

- 파라미터 5는 vertexOffset 이며, 인덱스 버퍼의 인덱스에 더할 오프셋을 지정한다. 

- - - -  indices[0] + offset , indices[1] + offset .... 

- - - -  specifies an offset to add to the indices in the index buffer

- - - - the value added to the vertex index before indexing into the vertex buffer.

- 파라미터 6는 첫번째 그릴 instance ID 이다.  (offset for instancing)                                                                                                                                                                                                                                                                                                                                                                                                           

- 이제 프로그램을 실행해보면 다음과 같은 사각형이 그려질것이다.              

- 이제 인덱스 버퍼를 사용하여 정점을 재사용함으로써 메모리를 절약하는 방법을 알게되었다.

- 이것은 향후챕터 3D모델을 로드할때 상당히 중요한것이다.

 

- 이전 챕터에서 다수의 리소스를 할당된 단일 메모리를 사용한 버퍼로 할당 해야한다고 언급하였다.

(The previous chapter already mentioned that you should allocate multiple resources like buffers from a single memory allocation)

- 그러나 사실 더 해야할게 있다.

-  Driver developers은 또한 multiple buffers를 하나의 단일 VkBuffer 로 저장 하는것을 권장한다.

   (vertex buffer 와 index buffer를 하나의 버퍼로, 그리고 vkCmdBindVertexBuffers같은 명령어에서 offset을 활용)

- 이경우 이것의 장점은 데이터가 좀더 캐시 친화적이라는것이다 (가까이 모여있으니)

- 이것은 심지어 다수의 리소스를 위한 동일한 chunk of memory를 재사용할 수 있다.

    (만일 데이터가 새로 고쳐지는경우 리소스들이 render operations 동안에 사용되지 않으면) 

- 이것은 aliasing 으로 알려져있으며, 

- 몇몇 Vulkan functions은 명시적인 flags을 통해 이것을 원하는대로 지정할 수있다.

 

C++ code / Vertex shader / Fragment shader

 

https://vulkan-tutorial.com/Vertex_buffers/Index_buffer