Vulkan填坑學習Day24—紋理圖像(Images)

Vulkan 紋理圖像(Images)

Vulkan 圖像(Images),到目前爲止,幾何圖形使用每個頂點顏色進行着色處理,這是一個侷限性比較大的方式。在本教程的一部分內容中,我們實現紋理映射,使得幾何圖形看起來更加生動有趣。這也會允許我們在未來的章節中加載和繪製基本的3D模型。

一、介紹

添加一個紋理貼圖到應用程序需要以下幾個步驟:

  • 創建設備內存支持的圖像對象
  • 從圖像文件填充像素
  • 創建圖像採樣器
  • 添加組合的圖像採樣器描述符,並從紋理採樣顏色信息

我們之前已經使用過圖像對象,但是它們都是由交換鏈擴展自動創建的。這次我們將要自己創建。創建一個圖像及填充數據與之前的頂點緩衝區創建類似。我們開始使用暫存資源並使用像素數據進行填充,接着將其拷貝到最終用於渲染使用的圖像對象中。儘管可以爲此創建一個暫存圖像,Vulkan也允許從VkBuffer中拷貝像素到圖像中,這部分API在一些硬件上非常有效率 faster on some hardware。我們首先會創建緩衝區並通過像素進行填充,接着創建一個圖像對象拷貝像素。創建一個圖像與創建緩衝區類似。就像我們之前看到的那樣,它需要查詢內存需求,分配設備內存並進行綁定。

然而,仍然有一些額外的工作需要面對,當我們使用圖像的時候。我們知道圖像可以有不同的佈局,它影響實際像素在內存中的組織。由於圖形硬件的工作原理,簡單的逐行存儲像素可能不是最佳的性能選擇。對圖像執行任何操作時,必須確保它們有最佳的佈局,以便在該操作中使用。實際上我們已經在指定渲染通道的時候看過這些佈局類型:

  • VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: 用於呈現,使用最佳
  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: 當使用附件從片段着色器進行寫入時候,使用最佳
  • VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: 作爲傳送源操作的時候,使用最佳,比如vkCmdCopyImageToBuffer
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: 作爲傳輸目的地的時候,使用最佳,比如vkCmdCopyBufferToImage
  • VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: 着色器中用於採樣,使用最佳

變換圖像佈局的最常見方式之一是管線屏障 pipeline barrier。管線屏障主要用於同步訪問資源,諸如確保圖像在讀之前寫入,但是也可以用於佈局變換。在本章節中我們將會看到如何使用管線屏障完成此任務。除此之外,屏障也可以用於VK_SHARING_MODE_EXCLUSIVE模式下隊列簇宿主的變換。

二、圖像庫

用於加載圖片的庫有很多,甚至可以自己編寫代碼加載簡單格式的圖片比如BMP和PPM。在本教程中我們將會使用stb_image庫。優勢是所有的代碼都在單一的文件中,所以它不需要任何棘手的構建配置。下載stb_image.h頭文件並將它保存在方便的位置,在這裏我們存放與GLFW、GLM、vulkan頭文件的相同的目錄中 Third-Party\Include 下,如圖所示:
在這裏插入圖片描述
Visual Studio,確認$(SolutionDir)\Third-Party\Include添加到 Additional Include Directories 路徑中

三、加載圖像

包含image庫的頭文件:

#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>

默認情況下頭文件僅僅定義了函數的原型。一個代碼文件需要使用STB_IMAGE_IMPLEMENTATION定義包含頭文件中定義的函數體,否則會收到鏈接錯誤。

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

...

void createTextureImage() {

}

創建新的函數createTextureImage用於加載圖片和提交到Vulkan圖像對象中。我們也會使用命令緩衝區,所以需要在createCommandPool之後調用。

在shaders目錄下新增新的textures目錄,用於存放貼圖資源。我們將會從目錄中加載名爲texture.jpg的圖像。這裏選擇 CC0 licensed image 並調整爲512 x 512像素大小,但是在這裏可以使用任何你期望的圖片。庫支持很多主流的圖片文件格式,比如JPEG,PNG,BMP和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通道,即使它本身沒有alpha,但是這樣做對於將來加載其他的紋理的一致性非常友好。中間三個參數用於輸出width, height 和實際的圖片通道數量。返回的指針是像素數組的第一個元素地址。總共 texWidth * texHeight * 4 個像素值,像素在STBI_rgba_alpha的情況下逐行排列,每個像素4個字節。

四、臨時緩衝區

我們現在要在host visible內存中創建一個緩衝區,以便我們可以使用vkMapMemory並將像素複製給它。在createTextureImage函數中添臨時緩衝區變量。

VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;

緩衝區必須對於host visible內存可見,爲此我們對它進行映射,之後使用它作爲傳輸源拷貝像素到圖像對象中。

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);

五、紋理圖像

雖然我們可以通過設置着色器訪問緩衝區中的像素值,但是在Vulkan中最好使用image對象完成該操作。圖像對象可以允許我們使用二維座標,從而更容易的快速的檢索顏色。圖像中的像素被成爲紋素即紋理元素,我們將從此處開始使用該名稱。添加以下新的類成員:

VkImage textureImage;
VkDeviceMemory textureImageMemory;

對於圖像的參數通過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採用什麼樣的座標系在圖像中採集紋素。它可以是1D,2D和3D圖像。1D圖像用於存儲數組數據或者灰度圖,2D圖像主要用於紋理貼圖,3D圖像用於存儲立體紋素。extent字段指定圖像的尺寸,基本上每個軸上有多少紋素。這就是爲什麼深度必須是1而不是0。我們的紋理不會是一個數組,而現在我們不會使用mipmapping功能。

imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;

Vulkan支持多種圖像格式,但無論如何我們要在緩衝區中爲紋素應用與像素一致的格式,否則拷貝操作會失敗。

imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;

tiling字段可以設定兩者之一:

  • VK_IMAGE_TILING_LINEAR: 紋素基於行主序的佈局,如pixels數組
  • VK_IMAGE_TILING_OPTIMAL: 紋素基於具體的實現來定義佈局,以實現最佳訪問

與圖像佈局不同的是,tiling模式不能在之後修改。如果需要在內存圖像中直接訪問紋素,必須使用VK_IMAGE_TILING_LINEAR。我們將會使用暫存緩衝區代替暫存圖像,所以這部分不是很有必要。爲了更有效的從shader中訪問紋素,我們將會使用VK_IMAGE_TILING_OPTIMAL。

imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

對於圖像的initialLayout字段,僅有兩個可選的值:

  • VK_IMAGE_LAYOUT_UNDEFINED: GPU不能使用,第一個變換將丟棄紋素。
  • VK_IMAGE_LAYOUT_PREINITIALIZED: GPU不能使用,但是第一次變換將會保存紋素。

幾乎沒有必要在第一次轉換時候保留紋素。然而,一個例子是,如果您想將圖像用作與 VK_IMAGE_TILING_LINEAR 佈局相結合的暫存圖像。在這種情況下,您需要將紋素數據上傳到它,然後將圖像轉換爲傳輸源,而不會導致丟失數據。但是,在我們的例子中,我們首先將圖像轉換爲傳輸目標,然後從緩衝區對象複製紋理數據,因此我們不需要此屬性,可以安全地使用 VK_IMAGE_LAYOUT_UNDEFINED 。

imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;

這裏的** usage字段與緩衝區創建過程中使用的 **usage 字段有相同的語意。圖像將會被用作緩衝區拷貝的目標,所以應該設置作爲傳輸目的地。我們還希望從着色器中訪問圖像對我們的mesh進行着色,因此具體的usage還要包括VK_IMAGE_USAGE_SAMPLED_BIT。

imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

因爲圖像會在一個隊列簇中使用:支持圖形或者傳輸操作。

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

samples標誌位與多重採樣相關。這僅僅適用於作爲附件的圖像,所以我們堅持一個採樣數值。與稀疏圖像相關的圖像有一些可選的標誌。稀疏圖像是僅僅某些區域實際上被存儲器支持的圖像。例如,如果使用3D紋理進行立體地形,則可以使用此方法來避免分配內存來存儲大量“空氣”值。我們不會在本教程中使用,所以設置默認值0。

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

使用vkCreateImage創建圖像,這裏沒有任何特殊的參數設置。可能圖形硬件不支持VK_FORMAT_R8G8B8A8_UNORM格式。我們應該持有一個可以替代的可以接受的列表。然而對這種特定格式的支持是非常普遍的,我們將會跳過這一步。使用不同的格式也需要繁瑣的轉換過程。我們會回到深度緩衝區章節,實現類似的系統。

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);

爲圖像工作分配內存與爲緩衝區分配內存是類似的,使用vkGetImageMemoryRequirements代替vkGetBufferMemoryRequirements,並使用vkBindImageMemory代替vkBindBufferMemory。

這個函數已經變得比較龐大臃腫了,而且需要在後面的章節中創建更多的圖像,所以我們應該將圖像創建抽象成一個createImage函數,就像之前爲buffers緩衝區做的事情一樣。創建函數並將圖像對象的創建和內存分配移動過來:

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);
}

這裏使用了width, height, format, tiling mode, usage和memory properties參數,因爲這些參數根據教程中創建的圖像而不同。

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_UNORM, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);
}

六、佈局轉換

我們將要編寫的函數會涉及到記錄和執行命令緩衝區,所以現在適當的移除一些邏輯到輔助函數中去:

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中已經存在的代碼。現在可以簡化函數如下:

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

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

    endSingleTimeCommands(commandBuffer);
}

如果仍然繼續使用緩衝區,我們可以編寫一個函數記錄和執行vkCmdCopyBuffeToImage來完成這個工作,但首先命令要求圖像在正確的佈局中。創建一個新的函數處理佈局變換:

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

    endSingleTimeCommands(commandBuffer);
}

通常主流的做法用於處理圖像變換是使用 image memory barrier。一個管線的屏障通常用於訪問資源的時候進行同步,也類似緩衝區在讀操作之前完成寫入操作,當然也可以用於圖像佈局的變換以及在使用VK_SHARING_MODE_EXCLUSIVE模式情況下,傳輸隊列簇宿主的變換。緩衝區有一個等價的 buffer memory barrier。

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

前兩個參數指定佈局變換。可以使用VK_IMAGE_LAYOUT_UNDEFINED作爲oldLayout,如果不關心已經存在與圖像中的內容。

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

如果針對傳輸隊列簇的宿主使用屏障,這兩個參數需要設置隊列簇的索引。如果不關心,則必須設置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指定受到影響的圖像和圖像的特定區域。我們的圖像不是數組,也沒有使用mipmapping levels,所以只指定一級,並且一個層。

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

屏障主要用於同步目的,所以必須在應用屏障前指定哪一種操作類型及涉及到的資源,同時要指定哪一種操作及資源必須等待屏障。我們必須這樣做盡管我們使用vkQueueWaitIdle人爲的控制同步。正確的值取決於舊的和新的佈局,所以我們一旦我們知道了要使用的變換,就可以回到佈局部分。

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

所有類型的管線屏障都使用同樣的函數提交。命令緩衝區參數後的第一個參數指定管線的哪個階段,應用屏障同步之前要執行的前置操作。第二個參數指定操作將在屏障上等待的管線階段。在屏障之前和之後允許指定管線階段取決於在屏障之前和之後如何使用資源。允許的值列在規範的 table 表格中。比如,要在屏障之後從 uniform 中讀取,您將指定使用VK_ACCESS_UNIFORM_READ_BIT以及初始着色器從 uniform 中讀取作爲管線階段,例如 VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT。爲這種類型的指定非着色器管線階段是沒有意義的,並且當指定和使用類型不匹配的管線階段時候,validation layer 將會提示警告信息。

第三個參數可以設置爲0或者VK_DEPENDENCY_BY_REGION_BIT。後者將屏障變換爲每個區域的狀態。這意味着,例如,允許已經寫完資源的區域開始讀的操作,更加細的粒度。

最後三個參數引用管線屏障的數組,有三種類型,第一種 memory barriers,第二種, buffer memory barriers, 和 image memory barriers。第一種就是我們使用的。需要注意的是我們沒有使用VkFormat參數,但是我們會在深度緩衝區章節中使用它做一些特殊的變換。

七、緩衝區拷貝到圖像

現在回到createTextureImage函數,我們編寫新的輔助函數copyBufferToImage:

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

    endSingleTimeCommands(commandBuffer);
}

就像緩衝區拷貝一樣,我們需要指定拷貝具體哪一部分到圖像的區域。這部分通過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字段指定緩衝區中的byte偏移量,代表像素值起始的位置。bufferRowLength和bufferImageHeight字段指定像素在內存中的佈局。比如可能在圖像的行與行之間填充一些空字節。爲兩者指定0表示像素緊密排列,這也是我們使用的設置。imageSubresource,imageOffset 和 imageExtent字段指定我們將要拷貝圖像的哪一部分像素。

緩衝區拷貝到圖像的操作將會使用vkCmdCopyBufferToImage函數到隊列中:

vkCmdCopyBufferToImage(
    commandBuffer,
    buffer,
    image,
    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
    1,
    ®ion
);

四個參數指定當前圖像使用的佈局。我們假設圖像爲了像素拷貝已經變換爲optimal最佳的佈局。現在我們僅拷貝像素快到一個完整的圖像中,但是也可以指定VkBufferImageCopy數組,以便在一個操作中執行從緩衝區到圖像的不同的拷貝操作。

八、準備紋理圖像

我們已經完成了使用貼圖圖像的所有工作,現在回到createTextureImage函數。最後一個事情是創建貼圖圖像texture image。下一步copy暫存緩衝區到貼圖圖像。這需要涉及兩個步驟:

  • 變換貼圖圖像到 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
  • 執行緩衝區到圖像的拷貝操作

這部分比較容易,如函數中所示:

transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_PREINITIALIZED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));

圖像是使用 VK_IMAGE_LAYOUT_UNDEFINED 佈局創建的,因此在轉換 textureImage 時候應該指定wield舊佈局。請記住,我們可以這樣做,因爲我們在執行復制操作之前不關心它的內容。

在shader着色器中開始從貼圖圖像的採樣,我們需要最後一個變換來準備着色器訪問:

transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

九、預屏障

如果應用程序開啓validation layers運行,你將會看到它提示 transitionImageLayout 中的訪問掩碼和管線階段無效。我們仍然需要根據變換中的佈局設置它們。

有兩種變換需要處理:

  • Undefined → transfer destination: 傳輸寫入操作不需要等待任何事情
  • Transfer destination→ shader reading: 着色器讀取操作應該等待傳輸寫入,特別是 fragment shader進行讀取,因爲這是我們要使用紋理的地方。

這些規則使用以下訪問掩碼和管線階段進行指定:

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
);

如上所示,傳輸寫入必須在管線傳輸階段進行。由於寫入不必等待任何事情,您可以指定一個空的訪問掩碼和最早的可能的管線階段 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT 作爲預屏障操作。

圖像將被寫入相同的流水線階段,隨後由片段着色器讀取,這就是爲什麼我們在片段着色器管線階段中指定着色器讀取訪問的原因。

如果將來我們需要做更多的轉換,那麼我們將擴展這個功能。應用程序現在應該可以成功運行,儘管當前沒有任何可視化的變化。

需要注意的是,命令緩衝區提交會在開始時導致隱式 VK_ACCESS_HOST_WRITE_BIT 同步。由於 transitionImageLayout 函數只使用單個命令執行命令緩衝區,因此如果在佈局轉換中需要 VK_ACCESS_HOST_WRITE_BIT 依賴關係,則可以使用此隱式同步將 srcAccessMask 設置爲 0 。如果你想要明確的話,這取決於你,但我個人並不是依賴這些OpenGL類似的 “隱式” 操作的粉絲。

實際上也有一種通用的圖像佈局類型支持所有的操作,VK_IMAGE_LAYOUT_GENERAL。問題是,它沒有爲任何操作提供最佳的性能表現。在某些特殊的情況下需要使用,例如使用圖像作爲輸入和輸出,或者在離開預初始化佈局後讀取圖像。

到目前爲止,所有用於提交命令的輔助函數已經被設置爲通過等待隊列變爲空閒來同步執行。對於實際應用,建議在單個命令緩衝區中組合這些操作,並異步方式執行它們獲得更高的吞吐量,尤其在createTextureImage函數中的轉換和拷貝操作。嘗試通過創建一個setupCommandBuffer輔助函數記錄命令,並添加一個flushSetupCommands函數來執行所以已經目錄的命令。最好在紋理貼圖映射工作後進行,以檢查紋理資源是否仍然正確設置。

十、清理緩衝區

在createTextureImage函數最後清理暫存緩衝區和分配的內存:

transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

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

程序中使用的貼圖圖像直到退出的時候在清理:

void cleanup() {
    cleanupSwapChain();

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

    ...
}

現在圖像包含了貼圖,但是圖形管線需要一個途徑訪問它。我們會在下一章討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章