[vk] Texture Mapping - Images

2021. 8. 16. 20:06그래픽스/vk

1. Introduction

- 지오메트리는 per-vertex colors를 사용하여 색을 입혔지만, 이것은 다소 제한적인 접근법이다.

- 이 챕터에서는 지오메트리를 더 흥미롭게 보이도록 텍스쳐 매핑을 구현할것이다.

- 여기서는 또한 또한 다음장에서 3D 모데들을 로드하고 그릴 수 있게 해준다.

 

- 텍스처를 추가하려면 다음 단계들이 필요하다.

  • Create an image object backed by device memory
  • Fill it with pixels from an image file
  • Create an image sampler
  • Add a combined image sampler descriptor to sample colors from the texture

- 우리는 이미 image object로 작업을 했지만, swapchain extension에 의해 자동적으로 생성되었다.

- 이번에는 직접 생성할 것이다.

- 이미지를 생성하고 데이터로 채우는것은 vertex buffer를 생성하는것과 유사하다.

- staging resource를 생성하고, 그것을 pixel data로 채워야한다.

- 그리고 그다음에 렌더링에 사용할 final image object로 복사할 수 있다.

 

- 비록 staging image를 이러한 목적으로 생성하는것도 가능하지만

- Vulkan은 또한 VkBuffer 로부터 이미지 pixels을 복사하는것을 허락하며, 이런 API는 몇몇 하드웨어에서 더 빠르다

  (Although it is possible to create a staging image for this purpose, Vulkan also allows you to copy pixels from a VkBuffer

   to an image and the API for this is actually faster on some hardware. )

 

- 우리는 첫번째로 이 버퍼를 생성할 것이며, pixel 값으로 채울것이다.

- 그런다음 픽셀을 복사할 이미지를 생성할것이다.

- 이미지를 생성하는것은 buffers을 생성하는것과 유사하며,

- 이전처럼 메모리 요구사항을 쿼리하고, 장치 메모리를 할당하고, 그것을 바인딩하는 작업을 수행할것이다.

 

- 그러나 이미지로 작업할 때 처리해야할 추가사항이 있다.

- 이미지들은 픽셀이 메모리에서 구성되는 방법에 영향을주는 다양한 layouts을 가질 수 있다.

- 예를들어, 그래픽스 하드웨어가 작동하는 방식으로 인해,

- 단순히 행단위로 픽셀을 저장하는 것만으로는 최상의 성능을 발휘 하지 못할 수 있다.

- 그러므로 이미지에 대한 작업을 수행할 때, 해당 작업에 사용하기에 최적인 layout이 있는지 확인해야한다.

- 렌더패스를 지정할때 실제로 이러한 레이아웃중 일부를 이미 보았다.

 

  • VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: Optimal for presentation
  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: Optimal as attachment for writing colors from the fragment shader
  • VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: Optimal as source in a transfer operation, like vkCmdCopyImageToBuffer
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: Optimal as destination in a transfer operation, like vkCmdCopyBufferToImage
  • VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: Optimal for sampling from a shader

- 이미지 레이아웃을 전환하는 일반적인 방법중 하나는  pipeline barrier 이다.

- Pipeline barriers 는 주로 이미지를 읽기전 쓰기가 완료되었는지 확인하는 리소스 접근 동기화를 위해 사용되지만,

- 또한 layouts 전환을 위해 사용할 수 있다.

- 이번 챕터에서는 우리는 어떻게 pipeline barriers 를 이런 목적으로 사용할것인지 살펴볼것이다.

- 게다가 VK_SHARING_MODE_EXCLUSIVE 를 사용하여 queue family 소유권을 전환할 때도 Barriers을 사용할 수 있다.

 

 

 

 

2. Image library

- There are many libraries available for loading images,

- and you can even write your own code to load simple formats like BMP and PPM.

- In this tutorial we'll be using the stb_image library from the stb collection.

- The advantage of it is that all of the code is in a single file, so it doesn't require any tricky build configuration.

- Download stb_image.h and store it in a convenient location, like the directory where you saved GLFW and GLM.

- Add the location to your include path.

 

Visual Studio

- Add the directory with stb_image.h in it to the Additional Include Directories paths.

Makefile

- Add the directory with stb_image.h to the include directories for GCC:

VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64
STB_INCLUDE_PATH = /home/user/libraries/stb

...

CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH)

 

 

 

 

3. Loading an image

- 헤더파일을 추가하자

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

- 이 헤더파일은 기본적으로 프로토타입으로 정의되어있다.

- 따라서 STB_IMAGE_IMPLEMENTATION 정의를 통해 함수 구현부를 포함시킬 필요가 있다. (그렇지않으면 링크에러)

void initVulkan() {
    ...
    createCommandPool();
    createTextureImage();
    createVertexBuffer();
    ...
}

...

void createTextureImage() {

}

- 이미지를 로드하고 Vulkan image object로 업로드하는  createTextureImage 함수를 만들자

- command buffers을 사용하므로 createCommandPool 뒤에서 호출해야한다.

 

- 새로운 폴더 textures 를 생성하고 거기에 texture images을 저장하자

- 이 폴더로부터 texture.jpg 이미지를 불러올것이다.

CC0 licensed image 이 이미지를 512x512로 리사이즈한 후 사용할것이다. (다른 이미지 사용해도 상관없음)

- stbi 라이브러리는 대부분 이미지 포맷을 지원한다. (JPEG, PNG, BMP and GIF)

- 라이브러리를 사용하여 쉽게 이미지를 로드할 수 있다.

void createTextureImage() {
    int texWidth, texHeight, texChannels;
    stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
    VkDeviceSize imageSize = texWidth * texHeight * 4;

    if (!pixels) {
        throw std::runtime_error("failed to load texture image!");
    }
}

stbi_load 함수는 파일 경로와, 로드할 채널의 개수를 인자로받는다.

- - - -  STBI_rgb_alpha 값은 이 이미지를 alpha channel로 강제로 로드한다 (가지고있지않더라도). 

- - - - 이렇게 강제적으로 로드하면, 다른 텍스쳐와의 일관성을 유지하는데 좋다.

- - - - 중간의 3개의 파라미터는 실제 이미지의 width, height, channels 출력값이다.

- - - - 이함수는 pixel values의 배열의 첫번째 원소의 주소를 리턴한다.

- - - - 이 픽셀들은 픽셀당 4바이트로 행별로 배치된다.

 

 

 

 

4. Staging buffer

-우리는 vkMapMemory 를 사용하고 픽셀을 복사하기위해서

- host visible memory를 생성해야한다. 

- 따라서 이러한 임시 버퍼 변수를 createTextureImage 함수에 추가하자.

VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;

- 이 버퍼는 매핑할 수 있도록 host visible memory 이어야하고,

- 이미지를 복사하기위해 transfer source로 사용할 수 있어야한다.

createBuffer(imageSize, 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, imageSize, 0, &data);
    memcpy(data, pixels, static_cast<size_t>(imageSize));
vkUnmapMemory(device, stagingBufferMemory);

- 원래의 pixel array를 정리해주는것을 잊지말아야한다.

stbi_image_free(pixels);

 

 

 

 

 

5. Texture Image

- 버퍼안의 픽셀값에 접근하도록 셰이더를 설정할 수 있지만,

- Vulkan에서는 이러한 목적을 위해 image object를 사용하는것이 좋다.

- Image objects 을 사용하면 2D좌표를 사용할 수 있으므로 색상을 더 쉽고 빠르게 검색할 수 있다.

- image object안의 pixels은 texels 라고 알려져있으므로 우리는 이 이름을 사용할것이다.

 

- 다음과 같은 새로운 클래스 멤버를 추가해야한다.

VkImage textureImage;
VkDeviceMemory textureImageMemory;

 

 

5.1 VkImageCreateInfo

- image를 위한 파라미터인 VkImageCreateInfo 구조체를 기술해야한다.

VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = static_cast<uint32_t>(texWidth);
imageInfo.extent.height = static_cast<uint32_t>(texHeight);
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;

- imageType : 이미지 타입을 지정하여 Vulkan에게 이미지의 texels 이 처리될 좌표계 종류를 말해준다

- - - - 이것은 또한 1D, 2D, 3D images 도 가능하며 예를들면

- - - - 1차원 이미지는 data 또는 gradient 배열을 저장하는데 사욯할 수 있고

- - - - 2차원 이미지는 주로 텍스쳐에 사용되며

- - - - 3차원 이미지는 voxel volumes를 저장하는데 사용되어진다.

- extent : 이미지의 치수들(dimensions)을 지정, 기본적으로 각 축에 있는 texels 수

- - - - depth 는 0대신 1이 와야한다.

- 우리의 텍스쳐는 array가 아니며, mipmapping을 지금 사용하지 않으므로 전부 1로설정하였다.

 

imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB;

- vulkan은 많은 이미지 format들을 지원하지만, 우리는 texels에 버퍼 픽셀과 같은 포맷을 사용해야한다. 

  (그렇지 않으면 copy 연산이 실패함)

 

imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;

- tiling 필드는 다음 둘중 하나의 값이 올 수있다.

  • VK_IMAGE_TILING_LINEAR: Texels are laid out in row-major order like our pixels array
  • VK_IMAGE_TILING_OPTIMAL Texels are laid out in an implementation defined order for optimal access

- 이미지의 layout과 같지않게, tiling mode는 나중에 변경될 수 없다.

- 만일 직접 image memory안의 texels에 접근하려면 VK_IMAGE_TILING_LINEAR 를 사용해야한다.

- 우리는 staging image 대신에 staging buffer를 사용할것이며,

- 효율적으로 셰이더로부터 접근하기위해 VK_IMAGE_TILING_OPTIMAL 를 사용할것이다.

 

imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

- initialLayout : 이미지의 모든 하위리소스의 초기 VkImageLayout를 지정, 다음 둘 중 하나의 값을 사용할 수 있다.

  • VK_IMAGE_LAYOUT_UNDEFINED: Not usable by the GPU and the very first transition will discard the texels.
  • VK_IMAGE_LAYOUT_PREINITIALIZED: Not usable by the GPU, but the first transition will preserve the texels.

- texel을 첫번째 전환후 보존해야할 필요가있는 몇몇 상황들이있다.

- 한가지 예는 VK_IMAGE_TILING_LINEAR 레이아웃과 함께 이미지를 staging image로 사용하려는 경우이다.

- 이러한 경우는 texel data를 업로드한 다음 데이터 손실없이 image를 transfer source로 전환하고싶을 때이다.

- 하지만 우리의 경우는 먼저 image를 transfer destination으로 전환한 다음 버퍼 객체에서 texel data를 복사하므로

- 이 속성이 필요하지않으며, VK_IMAGE_LAYOUT_UNDEFINED 를 안전하게 사용할 수 있다. 

 

imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;

- usage : 버퍼를 생성할 때의 의미와 동일하며, 이미지 사용의도를 지정하는 VkImageUsageFlagBits 의 비트마스크이다.

- - - - 이미지는 버퍼를 복사하는 destination로서 사용되어지므로 transfer destination으로 설정하였다.

- - - - 우리는 또한 mesh에 색칠하기위해 shader에서 이미지에 접근가능하기를 원하므로,

          (We also want to be able to access the image from the shader to color our mesh,)

- - - - usage는VK_IMAGE_USAGE_SAMPLED_BIT 도 포함해야한다.

 

imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

- sharingMode : 이미지는 graphics transfer operation를 지원하는 오직 하나의 queue family 만 사용되어질것이다.

 

imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.flags = 0; // Optional

- samples : 이 플래그는 multisampling과 관련이있다.

- 이것은 attachments로 사용할 이미지에만 해당되므로, 하나의 sample을 사용하였다.

   (This is only relevant for images that will be used as attachments, so stick to one sample. )

- flag : sparse images과 관련된 이미지에 대한 몇몇 선택적인 플래그가 있다.

- - - - 이미지의 추가 매개변수를 설명하는  VkImageCreateFlagBits 의 비트마스크

- - - - Sparse images은  특정 영역만 실제 메모리에의해 지원되는 이미지이다.

        (Sparse images are images where only certain regions are actually backed by memory.)

- - - - 예를들어 복셀 지형에 3Dtexture 를 사용하는 경우,

- - - - 대량의 "공기"값 을 저장하기 위해 메모리를 할당하는것을 방지할 수 있음.

- - - - 현재는 사용하지 않으므로 기본값인 0 으로 남겼다.

 

 

 

5.2 vkCreateImage

if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) {
    throw std::runtime_error("failed to create image!");
}

- vkCreateImage 를 사용함으로써 이미지를 생성할 수 있다. (특별히 주목할만한 파라미터는 없다)

- VK_FORMAT_R8G8B8A8_SRGB 포맷이 graphics hardware에서 지원 안할 가능성이 있다.

- 그러므로 허용가능한 대안적인 리스트가 있어야하며, 지원되는것중 가장 좋은것을 선택해야한다.

- 하지만 이런 특정 format에 대한 지원은 매우 광범위하므로 이단계를 건너뛸것이다.

- 다른 formats을 사용하기 위해서는 성가신 변환이 필요하다.

- 이와같은 시스템을 구현할 depth buffer chapter에서 다룰 예정이다.

 

 

5.3 vkAllocateMemory

VkMemoryRequirements memRequirements;
vkGetImageMemoryRequirements(device, textureImage, &memRequirements);

VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

if (vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate image memory!");
}

vkBindImageMemory(device, textureImage, textureImageMemory, 0);

- 이미지를 위한 메모리를 할당하는 작업은 버퍼를 위한 메모리를 할당하는것과 정확히 같은 방식이다.

- 차이점은 vkGetBufferMemoryRequirements 대신에 vkGetImageMemoryRequirements를 사용한것이고

- vkBindBufferMemory 대신에 vkBindImageMemory 를 사용한것이다.

 

 

 

5.4 createImage

- 기존의 작성하고있는 함수가 꽤커졌고, 향후 챕터에서 이미지를 더 생성할 필요가 있으므로

- 버퍼와 같이, 우리는 이미지생성을 createImage 함수를통해 추상화해야한다.

- 함수를 생성하고, image object creation 부분을 옮기고, 메모리 할당 부분도 옮겨야한다.

- 그리고  width, height, format, tiling mode, usage, and memory properties 를 파라미터로 받아야한다.

  (이 튜토리얼 전체에서 생성할 이미지마다 모두 다르기 때문)

void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) {
    VkImageCreateInfo imageInfo{};
    imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
    imageInfo.imageType = VK_IMAGE_TYPE_2D;
    imageInfo.extent.width = width;
    imageInfo.extent.height = height;
    imageInfo.extent.depth = 1;
    imageInfo.mipLevels = 1;
    imageInfo.arrayLayers = 1;
    imageInfo.format = format;
    imageInfo.tiling = tiling;
    imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    imageInfo.usage = usage;
    imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
    imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    if (vkCreateImage(device, &imageInfo, nullptr, &image) != VK_SUCCESS) {
        throw std::runtime_error("failed to create image!");
    }

    VkMemoryRequirements memRequirements;
    vkGetImageMemoryRequirements(device, image, &memRequirements);

    VkMemoryAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocInfo.allocationSize = memRequirements.size;
    allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);

    if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) {
        throw std::runtime_error("failed to allocate image memory!");
    }

    vkBindImageMemory(device, image, imageMemory, 0);
}

 

 

 

5.5 createTextureImage

- createTextureImage  함수는 이제 간소화되었다.

void createTextureImage() {
    int texWidth, texHeight, texChannels;
    stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
    VkDeviceSize imageSize = texWidth * texHeight * 4;

    if (!pixels) {
        throw std::runtime_error("failed to load texture image!");
    }

    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    createBuffer(imageSize, 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, imageSize, 0, &data);
        memcpy(data, pixels, static_cast<size_t>(imageSize));
    vkUnmapMemory(device, stagingBufferMemory);

    stbi_image_free(pixels);

    createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);
}

 

 

 

 

6. Layout transitions

- 이제 우리가 작성하려는 함수는 명령 버퍼를 다시 기록하고 실행하는것과 관련이 있으므로

- 이제 해당 논리를 도우미 함수 한 두개에 옮기기에 좋은 때이다.

VkCommandBuffer beginSingleTimeCommands() {
    VkCommandBufferAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandPool = commandPool;
    allocInfo.commandBufferCount = 1;

    VkCommandBuffer commandBuffer;
    vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);

    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

    vkBeginCommandBuffer(commandBuffer, &beginInfo);

    return commandBuffer;
}

void endSingleTimeCommands(VkCommandBuffer commandBuffer) {
    vkEndCommandBuffer(commandBuffer);

    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffer;

    vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
    vkQueueWaitIdle(graphicsQueue);

    vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
}

- 위 코드들은 copyBuffer 에 있던 기존의 코드들을 옮긴것이다.

- 이제 copyBuffer 함수는 더욱 간소화되었다.

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
    VkCommandBuffer commandBuffer = beginSingleTimeCommands();

    VkBufferCopy copyRegion{};
    copyRegion.size = size;
    vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

    endSingleTimeCommands(commandBuffer);
}

- 여전히 버퍼를 사용하고 있으므로

 

- 이제 vkCmdCopyBufferToImage를 기록하고 실행하여 작업을 완료하는 함수를 작성해야하지만,

- 이 명령을 사용하려면 먼저 이미지가 올바른 레이아웃에 있어야한다.

  (If we were still using buffers, then we could now write a function to record and

  execute vkCmdCopyBufferToImage to finish the job, but this command requires the image to be in the right layout first.)

 

 

 

 

6.1 transitionImageLayout

- layout 전환을 다루는 새로운함수를 만들어보자

void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) {
    VkCommandBuffer commandBuffer = beginSingleTimeCommands();

    endSingleTimeCommands(commandBuffer);
}

- layout 전환을 수행하는 한가지 일반적인 방법은 image memory barrier 를 사용하는것이다.

- 이와같은 pipeline barrier 는 일반적으로 리소스에 대한 접근을 동기화하기위해 사용된다.

   (리소스를 읽기전에 쓰기가 완료됨을 보장하는것과 같은 동기화)

- 그러나 이것은 또한 이미지 레이아웃 전환에서도 사용할 수 있으며 

- VK_SHARING_MODE_EXCLUSIVE 모드일때 transfer queue family 의 ownership 을 전환할때에도 사용할 수 있다.

- 버퍼에 대해 이를 수행하는 동등한 버퍼 메모리 barrier가 있다.

 

    (One of the most common ways to perform layout transitions is using an image memory barrier.

    A pipeline barrier like that is generally used to synchronize access to resources,

    like ensuring that a write to a buffer completes before reading from it, but it can also be used to transition image layouts and

    transfer queue family ownership when VK_SHARING_MODE_EXCLUSIVE is used.

    There is an equivalent buffer memory barrier to do this for buffers.)

 

 

6.2 VkImageMemoryBarrier

VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = oldLayout;
barrier.newLayout = newLayout;

- oldLayoutnewLayout : layout 전환을 지정

- - - - VK_IMAGE_LAYOUT_UNDEFINEDoldLayout 에 사용함으로써 기존의 이미지 레이아웃을 무시할 수 있음.

barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;

- srcQueueFamilyIndex , dstQueueFamilyIndex 

- - - - transfer queue family ownership 을 목적으로 barrier를 사용할때 필요한 필드

- - - - 두필드들은 queue families의 인덱스를 나타낸다.

- - - - 이러한 목적이 아닐 경우 VK_QUEUE_FAMILY_IGNORED 로 설정해야한다. (기본값이 아니므로 필수) 

barrier.image = image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;

- image, subresourceRange

- - - - 영향을 받는 이미지와 이미지의 특정 부분을 명시

- - - - 튜토리얼에서의 이미지는 array가 아니고, mipmapping levels이 아니기 때문에 하나의 level과 하나의 layout.

barrier.srcAccessMask = 0; // TODO
barrier.dstAccessMask = 0; // TODO

- srcAccessMask, dstAccessMask

- - - - Barriers 는 주로 synchronization 목적으로 사용되므로

- - - - Barrier 보다 먼저 발생해야하는 리소스와 관련된 작업 연산과

- - - - Barrier 에서 대기하는 리소스와 관련된 연산을 지정해야한다. 

- - - - 우리는 이미 수동으로 동기화하기위해 vkQueueWaitIdle를 사용하고 있음에도 불고하고 이를 수행해야한다.

- - - - 적절한 값은 old, new layout에 의존하며, 일단 사용할 전환을 파악한 후 다시 이값을 다룰것이다.

 

 

 

6.3 vkCmdPipelineBarrier

vkCmdPipelineBarrier(
    commandBuffer,
    0 /* TODO */, 0 /* TODO */,
    0,
    0, nullptr,
    0, nullptr,
    1, &barrier
);

- 모든 pipeline barriers 의 타입들은 같은 함수를 사용함으로써 제출되어진다.

- 파라미터2 : barrier 이전에 발생해야하는 작업이 어느 파이프라인 단계에서 발생하는지 지정

- 파라미터3 : 연산이 barrier에서 대기하는 pipeline stage를 지정

- - - - barrier 전후에 지정할 수 있는 pipeline stages은 barrier 전후에 리소스를 사용하는 방법에 의존한다.

- - - - 허용되는 값은 명세서의  this table  리스트에 적혀있다.

- - - - 예를들어, 만일 barrier이후에 uniform(UBO)으로부터 데이터를 읽으려면,

- - - - VK_ACCESS_UNIFORM_READ_BIT 로 명시해야한다.

- - - - 그리고, 유니폼을 사용하는 첫번째 셰이더가 프로그래먼트 셰이더일경우, 

- - - - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT 로 명시해야한다.

- - - - 이 경우 셰이더 단계가 아닌것을 지정하는것은 의미가 없으며,

- - - - 타입의 사용과 명시된 파이프라인 스테이지가 맞지않으면 유효성 검사 레이어에서 경고할것이다.

- 파라미터4 : 0 또는 VK_DEPENDENCY_BY_REGION_BIT 을 사용해야한다

- - - - 후자는, barrier을 지역별 조건(per-region condition)으로 바꾼다. (barrier가 자원영역의 특정조건이됨)

- - - - 이것의 의미는 지금까지 작성된 리소스 부분에서 구현이 이미 읽기를 시작할 수 있음을 의미한다.

- - - - 다른 지역에 관계없이 전송이 완료되는 즉시 구현에서 지역을 읽을 수 있음을 의미

- - - - 이를 통해 현재 아키텍처의 최적화를 사용할 수 있어 성능을 더욱 높일 수 있다.

- 파라미터5~ : 메모리 barrier, 버퍼 barrier, 이미지 barrier의 세가지 이용가능한 타입에 대한

- - - - pipeline barrier 배열의 참조

 

- 아직 VkFormat 파라미터(transitionImageLayout함수의 파라미터) 를 사용하지 않고 있지만,

- 깊이 버퍼챕터에서 특별한 전환을 위해 이 매개변수를 사용할것이다.

 

 

 

 

 

 

7. Copying buffer to image

- createTextureImage 로 돌아가기전에 우리는 한가지 helper 함수를 작성해야한다.

- copyBufferToImage 함수를 만들어보자.

void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) {
    VkCommandBuffer commandBuffer = beginSingleTimeCommands();

    endSingleTimeCommands(commandBuffer);
}

 

 

 

7.1 VkBufferImageCopy

- buffer copy와 같이, 복사되어질 버퍼의 부분과 이미지의 부분을 지정해야한다.

- 이러한 데이터는 VkBufferImageCopy 구조체에 기술해야한다

VkBufferImageCopy region{};
region.bufferOffset = 0;
region.bufferRowLength = 0;
region.bufferImageHeight = 0;

region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;

region.imageOffset = {0, 0, 0};
region.imageExtent = {
    width,
    height,
    1
};

- 대부분의 필드들은 자명하다.

- bufferOffset : 픽셀값이 시작하는 버퍼안에서의 바이트 offset

- bufferRowLength, bufferImageHeight : 픽셀이 메모리에 배치되는 방식을 지정

- - - - 예를들어 이미지 행사이에 padding bytes이 있을 수 있다.

- - - - 둘다 0으로 설정하면 imageExtent에 따라 tightly packed으로 픽셀이 배치되어있음을 명시

- - - -(RGBA 라서 0으로 설정?)

- imageSubresource, imageOffset, imageExtent : 픽셀을 복사하길원하는 이미지의 부분을 나타낸다.

- - - - aspectMask는 VkImageAspectFlagBits의 조합이며,  복사할 color, depth , stencil 측면을 선택한다.

 

 

 

7.2 vkCmdCopyBufferToImage

- Buffer를 이미지에 복사하는 연산은 vkCmdCopyBufferToImage 를 사용함으로써 큐에 제출되어진다.

vkCmdCopyBufferToImage(
    commandBuffer,
    buffer,
    image,
    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
    1,
    &region
);

- 파라미터4 : 현재 사용하고있는 이미지 레이아웃을 나타낸다.

- - - - 이때 여기선 이미 픽셀을 복사할 최적의 레이아웃으로 전환을 했다고 가정함.

- 지금당장 우리는 오직 하나의 전체 이미지 픽셀 덩어리를 복사하지만

- VkBufferImageCopy를 배열로 지정하여,

- 한번의 operation으로 버퍼에서 이미지로 많은 다른 복사를 수행할 수 있다.

 

 

 

 

8. Preparing the texture image

- 우리는 이제 texture image 설정을 완료하는데 필요한 모든 도구가 있다.

- 그래서 createTextureImage 함수로 돌아가 텍스쳐 이미지를 생성한 이후의 단계를 작성할 것이다.

- 다음 단계는 staging buffer를 texture image로 복사하는것이며, 아래의 두 스텝들을 포함한다.

  • Transition the texture image to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
  • Execute the buffer to image copy operation

- 우리가 이미 생성한 함수들을 사용하여쉽게 구현할 수 있다.

transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));

- 이미지를 VK_IMAGE_LAYOUT_UNDEFINED 레이아웃으로 생성하였으므로,

- textureImage 를 변환할때 old layout에 이 값을 명시해야한다

- 이를 통해 복사작업을 수행하기전의 내용물을 신경쓰지않고 이 작업을 수행할 수 있다.

  (Remember that we can do this because we don't care about its contents before performing the copy operation.)

 

- 그런 다음 프래그먼트 셰이더에서 텍스처를 샘플링할 수 있으려면,

- 셰이더에서 접근가능할 수 있도록 준비하는 최종 전환을 수행해야한다.

transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

 

 

 

 

9. Transition barrier masks

- 만일 지금 요효성검사 레이어를활성화하고 실행하게되면,

- transitionImageLayout 의 pipeline stages 그리고 access masks 가 타당하지 않다고 경고할것이다.

- 그러므로 전환의 layout을 기반으로 설정해야한다.

 

- 튜토리얼에서는 두가지 전환을 처리하고있다.

  • Undefined → transfer destination: transfer writes that don't need to wait on anything (아무것도 대기할 필요없는 전송쓰기)
  • Transfer destination → shader reading: shader reads should wait on transfer writes, specifically the shader reads in the fragment shader, because that's where we're going to use the texture (셰이더 읽기는 전송쓰기를 기다려야함, 특히 텍스쳐를 사용하는 프래그먼트 셰이더에서 셰이더 읽기를 수행해야함) 

- access masks 그리고 pipeline stages 를 사용하여 이러한 규칙을 지정해야한다.

VkPipelineStageFlags sourceStage;
VkPipelineStageFlags destinationStage;

if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
    barrier.srcAccessMask = 0;
    barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

    sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
    destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

    sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
} else {
    throw std::invalid_argument("unsupported layout transition!");
}

vkCmdPipelineBarrier(
    commandBuffer,
    sourceStage, destinationStage,
    0,
    0, nullptr,
    0, nullptr,
    1, &barrier
);

- 앞에서 언급한 테이블에서 볼 수 있듯이, transfer writes 는 pipeline transfer stage에서 발생해야만한다.

- writes은 아무것도 기달릴 필요가 없기 때문에 (다른 작업에 의존하지 않음) 사전 barrier operations 에대해

- access mask를 0 또는 null (empty access mask) 이나

- 가장 첫번째 파이프라인인 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT 을 지정할 수 있다.

- 그리고 VK_PIPELINE_STAGE_TRANSFER_BIT

- 실제 graphics 와 compute 파이프라인의 실제 단계가 아님을 유의해야한다.

- 이것은 transfers 이 일어나는 pseudo-stage 에 가깝다.

-  the documentation 에서 더 많은 정보와 예시를 볼 수 있다.

 

- image는 같은 pipeline stage에서 쓰여질것이고 (같은 pass에서 쓰여지고 읽혀짐)

- 차후에 fragment shader에서 읽혀질것이다. (프래그먼트 셰이더가 이미지의 메모리에 접근할 수 있음을 나타내야함)

- 그러므로 fragment shader pipeline stage에서 shader reading 접근을 지정해야한다.

 

- 미래에 더많은 전환이 필요해지면 이 함수를 확장해야한다.

- 보이는것은 아직 변하지 않았지만, 응용프로그램은 이제 성공적으로 실행될것이다.

 

?????

- 이 코드에서 한가지 주목해야할 점은 명령버퍼 제출이 시작부분에서

- 암시적인 VK_ACCESS_HOST_WRITE_BIT 동기화를 발생시킨다는것이다.

- transitionImageLayout 함수는 하나의 명령으로 구성된 command buffer를 실행하므로 이 동기화를 사용할 수 있다.

- 이 암시적인 동기화를 사용하고싶고, layout 전환에서 VK_ACCESS_HOST_WRITE_BIT 종속성이 필요하다면 

- srcAccessMask 를 0으로 설정하면된다.

- 이것에 대해 명시적으로 사용을 원하는지에 대한 여부는 사용자에게 달려있다.

One thing to note is that command buffer submission results in implicit VK_ACCESS_HOST_WRITE_BIT synchronization at the beginning. Since the transitionImageLayout function executes a command buffer with only a single command, you could use this implicit synchronization and set srcAccessMask to 0 if you ever needed a VK_ACCESS_HOST_WRITE_BIT dependency in a layout transition. It's up to you if you want to be explicit about it or not, but I'm personally not a fan of relying on these OpenGL-like "hidden" operations.

 

 

- image layout 의 특별한 타입인 VK_IMAGE_LAYOUT_GENERAL 는 모든 연산을 지원한다.

- 문제는 확실히 어떠한 연산도 좋은 성능을 제공하지 않는다는것이다.

- 이것은 한 이미지를 입력과 출력으로 사용하거나, 사전에 초기화된 레이아웃을 떠난 후 이미지를 읽는 등

- 특수한 케이스일때 사용할 수 있다.

It is required for some special cases, like using an image as both input and output, or for reading an image after it has left the preinitialized layout.

 

 

 

- 지금까지 명령을 제출하는 모든 helper 함수는

- queue가 idle 상태가 될때까지 대기하여 동기적으로 실행되도록 설정하였다.

- 실제 응용 프로그램의 경우, single command buffer에서 작업을 결합하여 처리량을 높일 수 있다.

- 특히 createTextureImage함수의 전환 및 복사관련 코드는 비동기적으로 실행하는것이 좋다.

- 연습으로 setupCommandBuffer 라는 helper 함수를 만들고 명령을 기록하자

- 그리고 flushSetupCommands 함수를 추가하여 지금까지 기록된 명령을 실행하도록해보자.

- texture resources이 올바르게 설정되어있는지 확인하기위해 texture mapping 작업이후에 하는것이 좋다.

 

 

9.1 transitionImageLayout 코드

    void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) {
        VkCommandBuffer commandBuffer = beginSingleTimeCommands();

        VkImageMemoryBarrier barrier{};
        barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
        barrier.oldLayout = oldLayout;
        barrier.newLayout = newLayout;
        barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        barrier.image = image;
        barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        barrier.subresourceRange.baseMipLevel = 0;
        barrier.subresourceRange.levelCount = 1;
        barrier.subresourceRange.baseArrayLayer = 0;
        barrier.subresourceRange.layerCount = 1;

        VkPipelineStageFlags sourceStage;
        VkPipelineStageFlags destinationStage;

        if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
            barrier.srcAccessMask = 0;
            barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

            sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
            destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
        } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
            barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
            barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

            sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
            destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
        } else {
            throw std::invalid_argument("unsupported layout transition!");
        }

        vkCmdPipelineBarrier(
            commandBuffer,
            sourceStage, destinationStage,
            0,
            0, nullptr,
            0, nullptr,
            1, &barrier
        );

        endSingleTimeCommands(commandBuffer);
    }

 

 

 

 

 

10. Cleanup

- createTextureImage 함수를 끝마치고나면 staging buffer와 메모리를 제거해야한다.

   transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

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

- main texture image는 프로그램 끝날때까지 사용된다.

void cleanup() {
    cleanupSwapChain();

    vkDestroyImage(device, textureImage, nullptr);
    vkFreeMemory(device, textureImageMemory, nullptr);

    ...
}

- 이제 이미지는 텍스쳐를 포함하고있지만

- 이것을 graphics pipeline에서 접근하는방법이 여전히 필요하다.

- 이 방법은 다음 챕터에서 다룰것이다.

10.1 createTextureImage 코드

    void createTextureImage() {
        int texWidth, texHeight, texChannels;
        stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
        VkDeviceSize imageSize = texWidth * texHeight * 4;

        if (!pixels) {
            throw std::runtime_error("failed to load texture image!");
        }

        VkBuffer stagingBuffer;
        VkDeviceMemory stagingBufferMemory;
        createBuffer(imageSize, 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, imageSize, 0, &data);
            memcpy(data, pixels, static_cast<size_t>(imageSize));
        vkUnmapMemory(device, stagingBufferMemory);

        stbi_image_free(pixels);

        createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);

        transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
            copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
        transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

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

 

 

C++ code / Vertex shader / Fragment shader

https://vulkan-tutorial.com/Texture_mapping/Images