본문 바로가기

Vulkan

Descriptor layout and buffer

이전 글 : 행렬

다음 글 : Descriptor pool and sets




Descriptor layout and buffer


Introduction


이제 각 정점에 대해 임의의 속성을 정점 셰이더에 전달할 수 있습니다. 그러나 전역 변수는 어떻게 전달 할까요? 이번 강좌를 통해서 우리는 3D 그래픽의 세계로 넘어갈것 입니다. 그러기 위해서는 우리는 모델 뷰 프로젝션 매트릭스가 필요합니다. 버텍스 데이터로 포함시킬 수는 있지만 그럴경우 메모리 낭비가 있으며, 또한 좌표변환이 될 때마다 버텍스 버퍼를 업데이트 해야만 합니다. 이는 많은 비용이 들어갈 것 입니다. 변환(transformation) 은 모든 단일 프레임을 쉽게 바꿀 수 있습니다.


Vulkan에서 이 문제를 해결하는 올바른 방법은 리소스 설명자(resource descriptor) 를 사용하는 것입니다. 이 설명자는 셰이더가 버퍼 및 이미지와 같은 리소스에 자유롭게 액세스 할 수 있는 방법입니다. 우리는 변환 행렬을 포함하고 버텍스 쉐이더가 디스크립터를 통해 액세스 할 수 있도록 버퍼를 설정하려고 합니다. 설명자의 사용은 세 부분으로 구성됩니다.


  • 파이프 라인 생성 중에 디스크립터 레이아웃 지정

  • 디스크립터 풀에서 디스크립터 세트 할당

  • 렌더링시에 디스크립터 세트를 바인드합니다.


디스크립터 레이아웃은 렌더링 패스가 액세스 할 첨부 유형을 지정하는 것처럼 파이프 라인에서 액세스 할 리소스 유형을 지정합니다. 디스크립터 세트는 프레임 버퍼가 패스 첨부물을 렌더링하기 위해 바인딩 할 실제 이미지 뷰를 지정하는 것처럼 디스크립터에 바인딩 될 실제 버퍼 또는 이미지 리소스를 지정합니다. 그 후, 디스크립터 세트는, 정점 버퍼 및 프레임 버퍼와 같이, 드로잉 커멘드에 바인드됩니다.


많은 유형의 디스크립터가 있지만 이 장에서는 UBO (uniform buffer objects)를 사용하여 작업 할 것입니다. 다음 장에서 다른 유형의 디스크립터를 살펴 보겠지만 기본 프로세스는 동일합니다. 버텍스 쉐이더가 다음과 같은 C 구조체로 데이터가 있다고 가정 해 봅시다.


struct UniformBufferObject {
   glm::mat4 model;
   glm::mat4 view;
   glm::mat4 proj;
};


그런 다음 VkBuffer 에 데이터를 복사하고 다음과 같이 정점 셰이더의 유니폼 버퍼 객체 설명자를 통해 액세스 할 수 있습니다.


layout(binding = 0) uniform UniformBufferObject {
   mat4 model;
   mat4 view;
   mat4 proj;
} ubo;

void main() {
   gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
   fragColor = inColor;
}


이전 장의 사각형을 3D로 회전시키기 위해 매 프레임마다 모델, 뷰 및 투영 행렬을 업데이트 할 것입니다.






Vertex Shader


위에서 지정한 uniform 버퍼 객체를 포함하도록 버텍스 쉐이더를 수정 하십시오. 일단 우리는 MVP 변환에 익숙하다고 가정합니다. 그렇지 않은 경우 첫 번째 장에서 언급 한 자료를 참조하십시오.



#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 0) uniform UniformBufferObject {
   mat4 model;
   mat4 view;
   mat4 proj;
} ubo;

layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;

layout(location = 0) out vec3 fragColor;

out gl_PerVertex {
   vec4 gl_Position;
};

void main() {
   gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
   fragColor = inColor;
}


uniform, in / out 선언의 순서는 중요하지 않습니다. 바인딩 지시문은 속성에 대한 위치 지시문과 비슷합니다. 우리는 디스크립터 레이아웃에서 이 바인딩을 참조 할 것입니다.. gl_Position이 있는행은 변환을 사용하여 클립 좌표의 최종 위치를 계산하도록 변경됩니다. 2D 삼각형과 달리 클립 좌표의 마지막 구성 요소는 1이 아니므로 화면의 최종 표준화 장치 좌표로 변환 할 때 나눗셈이 발생합니다. 이것은 원근 투영으로 투시 투영에 사용되며 더 가까운 물체를 더 멀리 보이는 물체보다 크게 보이게하는 데 필수적입니다.



Descriptor set layout


다음 단계는 C ++ 측에서 UBO를 정의하고 Vulkan에게 버텍스 셰이더에서 이 설명자를 알리는 것입니다.


struct UniformBufferObject {
   glm::mat4 model;
   glm::mat4 view;
   glm::mat4 proj;
};



GLM에서 데이터 유형을 사용하여 셰이더의 정의와 정확하게 일치시킬 수 있습니다. 행렬의 데이터는 셰이더가 예상하는 방식과 이진 호환이 가능하므로 나중에 UniformBufferObject를 VkBuffer에 memcpy 할 수 있습니다.


우리는 파이프 라인 생성을 위해 셰이더에서 사용 된 모든 설명자 바인딩에 대한 세부 정보를 제공해야합니다. 모든 정점 특성과 위치 인덱스에 대해 수행해야 했던 것처럼 말입니다. 우리는 createDescriptorSetLayout이라는 이 모든 정보를 정의하는 새로운 함수를 설정할 것입니다.  파이프 라인이 필요하기 때문에 파이프 라인 생성 전에 바로 호출해야합니다.


void initVulkan() {
   ...
   createDescriptorSetLayout();
   createGraphicsPipeline();
   ...
}

...

void createDescriptorSetLayout() {

}

모든 바인딩은 VkDescriptorSetLayoutBinding 구조체를 통해 작성 되어야 합니다.


void createDescriptorSetLayout() {
   VkDescriptorSetLayoutBinding uboLayoutBinding = {};
   uboLayoutBinding.binding = 0;
   uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
   uboLayoutBinding.descriptorCount = 1;
}


처음 두 필드는 셰이더에서 사용되는 바인딩과 디스크립터 유형을 지정합니다. 이는 uniform 버퍼 객체입니다. 셰이더 변수는 uniform 버퍼 객체의 배열을 나타낼 수 있으며, descriptorCount는 배열의 값 수를 지정합니다. 이것은 스켈레탈 애니메이션을 위한 스켈레톤에서 각 뼈에 대한 변형을 지정하는데 사용될 수 있습니다. MVP 변환은 단일 uniform 버퍼 객체이므로 descriptorCount를 1로 사용하고 있습니다.


uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;


또한 설명자가 참조 될 셰이더 단계를 지정해야합니다. stageFlags 필드는 VkShaderStageFlagBits 값 또는 VK_SHADER_STAGE_ALL_GRAPHICS 값의 조합 일 수 있습니다. 여기서는 버텍스 쉐이더의 디스크립터만을 참조하고 있습니다.


uboLayoutBinding.pImmutableSamplers = nullptr; // Optional


pImmutableSamplers 필드는 나중에 이미지 샘플링 관련 설명자와 관련이 있습니다. 이 값을 기본값으로 둘 수 있습니다.


모든 디스크립터 바인딩은 하나의 VkDescriptorSetLayout 객체로 결합됩니다. pipelineLayout 위에 새로운 클래스 멤버를 정의하십시오.


VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;


그런 다음 vkCreateDescriptorSetLayout 을 사용하여 생성 할 수 있습니다. 이 함수는 바인딩 배열을 가진 간단한 VkDescriptorSetLayoutCreateInfo 를 받아들입니다 :


VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &uboLayoutBinding;

if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
   throw std::runtime_error("failed to create descriptor set layout!");
}


Vulkan에게 셰이더가 사용할 설명자를 알려주기 위해 파이프 라인 생성 중에 설명자 세트 레이아웃을 지정해야 합니다. 디스크립터 셋 레이아웃은 파이프 라인 레이아웃 객체에 지정됩니다. 레이아웃 개체를 참조하도록 VkPipelineLayoutCreateInfo 를 수정합니다.


VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;


단일 디스크립션에 이미 모든 바인딩이 포함되어 있기 때문에 여러 디스크립터 세트 레이아웃을 지정하는 것이 가능한 이유가 궁금 할 수 있습니다. 다음 장에서 디스크립터 풀과 디스크립터 세트를 살펴 보겠습니다.


디스크립터 레이아웃은 새로운 그래픽 파이프 라인을 생성하는 동안, 즉 프로그램이 끝날 때까지 계속되어야합니다.


void cleanup() {
   cleanupSwapChain();

   vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);

   ...
}




Uniform buffer

다음 장에서는 셰이더에 대한 UBO 데이터가 들어있는 버퍼를 지정 하겠지만 먼저 이 버퍼를 만들어야합니다. 우리는 매 프레임마다 새로운 데이터를 유니폼 버퍼에 복사 할 것이므로 스테이징 버퍼를 갖는 것은 실제로 의미가 없습니다. 그럴 경우 추가적인 오버 헤드가 발생하고 성능이 저하될 가능성이 큽니다.


uniformBuffer 및 uniformBufferMemory에 대한 새 클래스 멤버를 추가합니다.


VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;

VkBuffer uniformBuffer;
VkDeviceMemory uniformBufferMemory;



마찬가지로 createIndexBuffer 다음에 호출되고 버퍼를 할당하는 새 함수 createUniformBuffer를 만듭니다.


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

...

void createUniformBuffer() {
   VkDeviceSize bufferSize = sizeof(UniformBufferObject);
   createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffer, uniformBufferMemory);
}


우리는 유니폼 버퍼를 매 프레임마다 새로운 변환으로 업데이트하는 별도의 함수를 작성하려고합니다. 따라서 여기에는 vkMapMemory가 없습니다. uniform 데이터는 모든 그리기 호출에 사용되므로 이 데이터를 포함하는 버퍼는 마지막만 제거 되어야합니다.


void cleanup() {
   cleanupSwapChain();

   vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
   vkDestroyBuffer(device, uniformBuffer, nullptr);
   vkFreeMemory(device, uniformBufferMemory, nullptr);

   ...
}





Updating uniform data


새 함수 updateUniformBuffer를 만들고 main 루프에서 이 함수에 대한 호출을 추가합니다.


void mainLoop() {
   while (!glfwWindowShouldClose(window)) {
       glfwPollEvents();

       updateUniformBuffer();
       drawFrame();
   }

   vkDeviceWaitIdle(device);
}

...

void updateUniformBuffer() {

}


이 함수는 모든 프레임마다 새로운 변환을 생성하여 지오메트리를 회전시킵니다. 이 기능을 구현하려면 두 개의 새로운 헤더를 포함해야합니다.


#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <chrono>


glm / gtc / matrix_transform.hpp 헤더는 glm :: rotate와 같은 모델 변환을 생성하고 glm :: lookAt 와 같은 변환을 사용할 수 있으며, glm :: perspective와 같이 투영 변환을 생성하는 데 사용할 수 있는 함수를 제공합니다. GLM_FORCE_RADIANS 정의는 glm :: rotate와 같은 함수가 가능한 혼동을 피하기 위해 인수로 라디안을 사용하는지 확인하는 데 필요합니다.


chrono 표준 라이브러리 헤더는 정확한 시간 계시 기능을 제공합니다. 이 기능을 사용하여 프레임 속도에 관계없이 지오메트리가 초당 90도 회전하도록 할 수 있습니다.


void updateUniformBuffer() {
   static auto startTime = std::chrono::high_resolution_clock::now();

   auto currentTime = std::chrono::high_resolution_clock::now();
   float time = std::chrono::duration<float, std::chrono::seconds::period>(currentTime - startTime).count();
}



updateUniformBuffer 함수는 렌더링이 부동 소수점 정확도로 시작된 이후의 시간을 초 단위로 계산하기 위해 하용됩니다..


이제 uniform 버퍼 객체에서 모델, 뷰 및 투영 변환을 정의 할 것입니다. 모델 회전은 시간 변수를 사용하여 Z 축을 중심으로 간단한 회전이 됩니다.


UniformBufferObject ubo = {};
ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));


glm :: rotate 함수는 변환, 회전 각 및 회전 축을 매개 변수로 사용합니다. glm :: mat4 (1.0f) 생성자는 단위 행렬을 반환합니다. 회전 각도 * glm :: 라디안 (90.0f)을 사용하면 초당 90도 회전의 목적을 달성합니다.


ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));


뷰 변환을 위해 위에서 45도 각도로 물체들을 볼수 있습니다. glm :: lookAt 함수는 눈 위치, 중심 위치 및 상향 축을 매개 변수로 취합니다.


ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f);


저는 45 도의 수직 시야를 가진 투시 투영법을 사용하기로했습니다. 다른 매개 변수는 종횡비, 근거리 및 원거리 뷰 평면입니다. 크기 변경 후 창의 새로운 너비와 높이를 고려하여 가로 세로 비율을 계산하려면 현재 스왑 체인 범위를 사용하는 것이 중요합니다.


ubo.proj[1][1] *= -1;


GLM은 원래 OpenGL 용으로 설계되었으므로 클립 좌표의 Y 좌표가 반전됩니다. 이를 보상하는 가장 쉬운 방법은 투영 행렬에서 Y 축의 배율 인수를 부호로 바꾸는 것입니다. 이렇게하지 않으면 이미지가 거꾸로 렌더링됩니다.


이제 모든 변환이 정의되므로 uniform 버퍼 객체의 데이터를 uniform 버퍼에 복사 할 수 있습니다. 스테이징 버퍼가없는 경우를 제외하고는 정점 버퍼와 똑같은 방식으로 발생합니다.


void* data;
vkMapMemory(device, uniformBufferMemory, 0, sizeof(ubo), 0, &data);
   memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBufferMemory);



이런 방식으로 UBO를 사용하면 자주 변경되는 값을 셰이더에 전달하는 가장 효율적인 방법이 아닙니다. 작은 데이터 버퍼를 셰이더에 전달하는보다 효율적인 방법은 푸시 상수입니다. 우리는 앞으로 이 내용을 살펴볼 것 입니다.


다음 장에서는 실제로 셰이더가 이 변환 데이터에 액세스 할 수 있도록 VkBuffer를 유니폼 버퍼 설명자에 바인딩하는 설명자 집합을 살펴볼 것입니다


C++ code / Vertex shader / Fragment shader


이전 글 : 행렬

다음 글 : Descriptor pool and sets



'Vulkan' 카테고리의 다른 글

Descriptor pool and sets  (1) 2018.02.07
행렬  (0) 2018.02.05
Index buffer  (0) 2018.01.25
Staging buffer  (0) 2018.01.25
Vertex buffer creation  (0) 2018.01.25