[譯]Vulkan教程(27)Image
Images
Introduction 入門
The geometry has been colored using per-vertex colors so far, which is a rather limited approach. In this part of the tutorial we're going to implement texture mapping to make the geometry look more interesting. This will also allow us to load and draw basic 3D models in a future chapter.
現在幾何體已經用逐頂點的顏色上色了,但是很初級。從本章開始我們要實現紋理映射to讓幾何體看起來更有趣。這也會讓我們能夠加載和繪制基本的3D模型-在后續章節。
Adding a texture to our application will involve the following steps:
添加紋理到我們的程序會涉及下述步驟:
- Create an image object backed by device memory 創建由設備內存烘培的image對象
- Fill it with pixels from an image file 從文件得到像素,填充image
- Create an image sampler 創建image采樣器
- Add a combined image sampler descriptor to sample colors from the texture 添加組合image采樣器描述符to從紋理采樣
We've already worked with image objects before, but those were automatically created by the swap chain extension. This time we'll have to create one by ourselves. Creating an image and filling it with data is similar to vertex buffer creation. We'll start by creating a staging resource and filling it with pixel data and then we copy this to the final image object that we'll use for rendering. 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. We'll first create this buffer and fill it with pixel values, and then we'll create an image to copy the pixels to. Creating an image is not very different from creating buffers. It involves querying the memory requirements, allocating device memory and binding it, just like we've seen before.
我們曾經用過image對象,但是那是被交換鏈自動創建的。這次我們必須自己創建image。創建image並填入數據,與頂點buffer的創建類似。我們首先創建一個暫存資源,填入像素數據,然后復制它到最終的image對象that用於渲染。盡管可以創建暫存image,Vulkan也允許你復制像素fromVkBuffer
到image,且這個API在某些硬件上更快。我們先創建這個buffer,填入像素值,然后創建image,將像素復制給它。創建image與創建buffer沒什么大的區別。它涉及查詢內存需求、分配設備內存和綁定它,就像我們見過的那些。
However, there is something extra that we'll have to take care of when working with images. Images can have different layouts that affect how the pixels are organized in memory. Due to the way graphics hardware works, simply storing the pixels row by row may not lead to the best performance, for example. When performing any operation on images, you must make sure that they have the layout that is optimal for use in that operation. We've actually already seen some of these layouts when we specified the render pass:
但是,使用image時還有一些其他的注意事項。Image可以有不同的布局that影響像素如何在內存中組織。基於圖像硬件的工作方式,簡單地一行一行地保存像素,不是性能最好的方式。當在image上實施任何操作時,你必須確保,它們有對於那個操作最優的布局。我們實際上已經看到了這樣一些布局when我們知道render pass:
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 用作附件,從Fragment shader寫入顏色,最優VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
: Optimal as source in a transfer operation, likevkCmdCopyImageToBuffer
在轉移操作中用作源(例如vkCmdCopyImageToBuffer
),最優VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
: Optimal as destination in a transfer operation, likevkCmdCopyBufferToImage
在轉移操作中用作目標(例如vkCmdCopyBufferToImage
),最優VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
: Optimal for sampling from a shader 從shader中采樣,最優
One of the most common ways to transition the layout of an image is a pipeline barrier. Pipeline barriers are primarily used for synchronizing access to resources, like making sure that an image was written to before it is read, but they can also be used to transition layouts. In this chapter we'll see how pipeline barriers are used for this purpose. Barriers can additionally be used to transfer queue family ownership when using VK_SHARING_MODE_EXCLUSIVE
.
轉換image布局的一個最常見的方式是管道屏障。管道屏障基本上用於資源的同步存取,例如確保image在寫入后才被讀取,但是它們也可以用於轉換布局。本章我們就看看管道屏障如何用於這個目的。屏障還可以用於轉移隊列家族所有權when使用VK_SHARING_MODE_EXCLUSIVE
。
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.
有許多庫可用於加載圖形,你甚至可以寫你自己的代碼來加載簡單的格式如BMP和PPM。本教程中我們用來自stb collection的stb_image。它的優點是,所有代碼都在一個文件中,所以不要求任何build和配置技巧。下載stb_image.h
保存到方便的位置,例如你保存和的位置。添加此位置到你的include路徑。
Visual Studio
Add the directory with stb_image.h
in it to the Additional Include Directories
paths.
添加包含stb_image.h
的文件夾到Additional Include Directories
路徑。
Makefile
Add the directory with stb_image.h
to the include directories for GCC:
添加含有stb_image.h
的文件夾到include目錄給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)
Loading an image 加載圖像
Include the image library like this:
像這樣include這個圖像庫:
#define STB_IMAGE_IMPLEMENTATION #include <stb_image.h>
The header only defines the prototypes of the functions by default. One code file needs to include the header with the STB_IMAGE_IMPLEMENTATION
definition to include the function bodies, otherwise we'll get linking errors.
默認的,頭文件只定義了函數的原型。代碼文件需要用STB_IMAGE_IMPLEMENTATION
宏定義來包含函數體,否則我們會收到鏈接錯誤。
void initVulkan() { ... createCommandPool(); createTextureImage(); createVertexBuffer(); ... } ... void createTextureImage() { }
Create a new function createTextureImage
where we'll load an image and upload it into a Vulkan image object. We're going to use command buffers, so it should be called after createCommandPool
.
創建新函數createTextureImage
where我們將加載圖像,上傳它到一個Vulkan的image對象。我們要用命令buffer,所以應該在createCommandPool
之后調用它。
Create a new directory textures
next to the shaders
directory to store texture images in. We're going to load an image called texture.jpg
from that directory. I've chosen to use the following CC0 licensed image resized to 512 x 512 pixels, but feel free to pick any image you want. The library supports most common image file formats, like JPEG, PNG, BMP and GIF.
創建新文件夾textures
,與文件夾shaders
並列,保存紋理圖像。我們要從那個文件夾里加載一個圖像文件texture.jpg
。我選用這個512 x 512像素的CC0 licensed 圖像,不過你想用什么圖像都隨你。庫支持大多數常見的圖像文件格式,例如JPEG、PNG、BMP和GIF。
Loading an image with this library is really easy:
用這個庫加載圖像很簡單:
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!"); } }
The stbi_load
function takes the file path and number of channels to load as arguments. The STBI_rgb_alpha
value forces the image to be loaded with an alpha channel, even if it doesn't have one, which is nice for consistency with other textures in the future. The middle three parameters are outputs for the width, height and actual number of channels in the image. The pointer that is returned is the first element in an array of pixel values. The pixels are laid out row by row with 4 bytes per pixel in the case of STBI_rgb_alpha
for a total of texWidth * texHeight * 4
values.
stbi_load
函數接收文件路徑和要加載的通道數量為參數。STBI_rgb_alpha
值強制圖像帶上alpha通道,即使它沒有這個通道,which對將來其它紋理的一致性是好事。中間3個參數是輸出for寬度、高度和圖像實際的通道數量。返回的指針是像素數組的第一個元素。像素是一行一行地排列,使用STBI_rgb_alpha
時是每4個字節一個像素,總共texWidth * texHeight * 4
個值。
Staging buffer 暫存buffer
We're now going to create a buffer in host visible memory so that we can use vkMapMemory
and copy the pixels to it. Add variables for this temporary buffer to the createTextureImage
function:
我們現在要在宿主可見的內存創建一個buffer,這樣我們可以用vkMapMemory
將像素復制給它。在createTextureImage
函數中為這個臨時buffer添加變量:
VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
The buffer should be in host visible memory so that we can map it and it should be usable as a transfer source so that we can copy it to an image later on:
這個buffer應當在宿主可見的內存,這樣我們可以映射它,它應當可用作轉移源,這樣稍后我們就可以將它復制到一個image:
createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);
We can then directly copy the pixel values that we got from the image loading library to the buffer:
我們然后就可以直接復制像素值that我們從圖像加載庫得到to這個buffer:
void* data; vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); memcpy(data, pixels, static_cast<size_t>(imageSize)); vkUnmapMemory(device, stagingBufferMemory);
Don't forget to clean up the original pixel array now:
別忘了清理原始像素數組:
stbi_image_free(pixels);
Texture Image 紋理image
Although we could set up the shader to access the pixel values in the buffer, it's better to use image objects in Vulkan for this purpose. Image objects will make it easier and faster to retrieve colors by allowing us to use 2D coordinates, for one. Pixels within an image object are known as texels and we'll use that name from this point on. Add the following new class members:
盡管我們可以設置shader去讀取buffer中的像素值,還是用Vulkan中的image對象做這件事更好。Image對象會讓檢索顏色更簡單更快速by允許我們使用2D坐標。
VkImage textureImage;
VkDeviceMemory textureImageMemory;
The parameters for an image are specified in a VkImageCreateInfo
struct:
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;
The image type, specified in the imageType
field, tells Vulkan with what kind of coordinate system the texels in the image are going to be addressed. It is possible to create 1D, 2D and 3D images. One dimensional images can be used to store an array of data or gradient, two dimensional images are mainly used for textures, and three dimensional images can be used to store voxel volumes, for example. The extent
field specifies the dimensions of the image, basically how many texels there are on each axis. That's why depth
must be 1
instead of 0
. Our texture will not be an array and we won't be using mipmapping for now.
在imageType
字段中指定的image類型,告訴了Vulkan,image中的紋素要以何種坐標系統被讀取。可以創建1D、2D和3D的image。一維image可用於保存數組或漸變色,二維image主要用於紋理,三維image可用於保存體素。extent
字段指定了image的維度,基本上就是每個坐標上有多少紋素。這就是為什么depth
必須是1
,而不是0
。我們的紋理不會是數組,我們現在也不使用mipmap。
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
Vulkan supports many possible image formats, but we should use the same format for the texels as the pixels in the buffer, otherwise the copy operation will fail.
Vulkan支持許多可能的image格式,但我們應當使用與buffer中像素格式相同的,否則復制操作會失敗。
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
The tiling
field can have one of two values:
字段可以有如下值:
VK_IMAGE_TILING_LINEAR
: Texels are laid out in row-major order like ourpixels
array 紋素像我們的數組一樣,是按行主序排布的。VK_IMAGE_TILING_OPTIMAL
: Texels are laid out in an implementation defined order for optimal access 紋素的排布由實現定義for最優的讀取。
Unlike the layout of an image, the tiling mode cannot be changed at a later time. If you want to be able to directly access texels in the memory of the image, then you must use VK_IMAGE_TILING_LINEAR
. We will be using a staging buffer instead of a staging image, so this won't be necessary. We will be using VK_IMAGE_TILING_OPTIMAL
for efficient access from the shader.
與image的布局不同,tiling模式不能在以后修改。如果你想直接在image的內存上讀取紋素,那么你必須用VK_IMAGE_TILING_LINEAR
。我們會使用一個暫存buffer,而不是暫存image,所以這就沒必要了。我們會用VK_IMAGE_TILING_OPTIMAL
for高效地shader讀取。
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
There are only two possible values for the initialLayout
of an image:
Image的只有2個可選值:
VK_IMAGE_LAYOUT_UNDEFINED
: Not usable by the GPU and the very first transition will discard the texels. 對GPU不可用,首次轉換會忽略紋素。VK_IMAGE_LAYOUT_PREINITIALIZED
: Not usable by the GPU, but the first transition will preserve the texels. 對GPU不可用,但是首次轉換會保留紋素。
There are few situations where it is necessary for the texels to be preserved during the first transition. One example, however, would be if you wanted to use an image as a staging image in combination with the VK_IMAGE_TILING_LINEAR
layout. In that case, you'd want to upload the texel data to it and then transition the image to be a transfer source without losing the data. In our case, however, we're first going to transition the image to be a transfer destination and then copy texel data to it from a buffer object, so we don't need this property and can safely use VK_IMAGE_LAYOUT_UNDEFINED
.
只有少數幾種情況下,才有必要在首次轉換時保留紋素。一個例子是,如果你想將image用作暫存image-聯合VK_IMAGE_TILING_LINEAR
布局。那時,你會想上傳紋素數據給他,然后轉換image為轉移源且不失去數據。但是在我們的案例中,我們首先要轉換image為一個轉移目標,然后從一個buffer對象復制紋素數據給它,所以我們不需要這個屬性,可以安全地使用VK_IMAGE_LAYOUT_UNDEFINED
。
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
The usage
field has the same semantics as the one during buffer creation. The image is going to be used as destination for the buffer copy, so it should be set up as a transfer destination. We also want to be able to access the image from the shader to color our mesh, so the usage should include VK_IMAGE_USAGE_SAMPLED_BIT
.
usage
字段和在創建buffer時有相同的語義。Image要被用作buffer復制的目標,所以它應當被設置為一個轉移目標。我們也想要能夠從shader讀取image來給我們的網格上色,所以用法應當包含VK_IMAGE_USAGE_SAMPLED_BIT
。
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
The image will only be used by one queue family: the one that supports graphics (and therefore also) transfer operations.
這個image只會被一個隊列家族使用:支持圖形和(因此也支持)轉移操作的那個。
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.flags = 0; // Optional
The samples
flag is related to multisampling. This is only relevant for images that will be used as attachments, so stick to one sample. There are some optional flags for images that are related to sparse images. Sparse images are images where only certain regions are actually backed by memory. If you were using a 3D texture for a voxel terrain, for example, then you could use this to avoid allocating memory to store large volumes of "air" values. We won't be using it in this tutorial, so leave it to its default value of 0
.
samples
標志與多重采樣有關。這只在image會被用作附件時才有關系,所以用1采樣即可。Image還有有些與稀疏image相關的可選標志。稀疏image是只有某些區域被內存烘培的image。如果你使用3D紋理for紋素地形,那么你可以用這個來避免分配內存to保存巨大的“空氣”值。我們不會在本教程中用它,所以讓它保持默認值0
即可。
if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { throw std::runtime_error("failed to create image!"); }
The image is created using vkCreateImage
, which doesn't have any particularly noteworthy parameters. It is possible that the VK_FORMAT_R8G8B8A8_UNORM
format is not supported by the graphics hardware. You should have a list of acceptable alternatives and go with the best one that is supported. However, support for this particular format is so widespread that we'll skip this step. Using different formats would also require annoying conversions. We will get back to this in the depth buffer chapter, where we'll implement such a system.
Image用vkCreateImage
創建,它沒有任何值得注意的參數。有可能VK_FORMAT_R8G8B8A8_UNORM
格式不被圖形硬件支持。你應當有一個可接受的選項,選擇最好的且被支持的那個。但是,對這個特定格式的支持太廣泛了,我們跳過這一步。使用不同的格式也會要求煩人的轉換。我們在深度緩存章節會回到這里,到時候我們要實現這樣一個系統。
1 VkMemoryRequirements memRequirements; 2 vkGetImageMemoryRequirements(device, textureImage, &memRequirements); 3 4 VkMemoryAllocateInfo allocInfo = {}; 5 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; 6 allocInfo.allocationSize = memRequirements.size; 7 allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); 8 9 if (vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory) != VK_SUCCESS) { 10 throw std::runtime_error("failed to allocate image memory!"); 11 } 12 13 vkBindImageMemory(device, textureImage, textureImageMemory, 0);
Allocating memory for an image works in exactly the same way as allocating memory for a buffer. Use vkGetImageMemoryRequirements
instead of vkGetBufferMemoryRequirements
, and use vkBindImageMemory
instead ofvkBindBufferMemory
.
為image分配內存,與為buffer分配內存是一樣的。用vkGetImageMemoryRequirements
代替vkGetBufferMemoryRequirements
,用vkBindImageMemory
代替ofvkBindBufferMemory
。
This function is already getting quite large and there'll be a need to create more images in later chapters, so we should abstract image creation into a createImage
function, like we did for buffers. Create the function and move the image object creation and memory allocation to it:
這個函數已經很大了,后續章節還需要創建更多的image,所以我們應當抽象出創建image的過程為一個createImage
函數-像我們為buffer做的那樣。創建函數,將image對象的創建過程和內存分配過程放進去:
1 void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { 2 VkImageCreateInfo imageInfo = {}; 3 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; 4 imageInfo.imageType = VK_IMAGE_TYPE_2D; 5 imageInfo.extent.width = width; 6 imageInfo.extent.height = height; 7 imageInfo.extent.depth = 1; 8 imageInfo.mipLevels = 1; 9 imageInfo.arrayLayers = 1; 10 imageInfo.format = format; 11 imageInfo.tiling = tiling; 12 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; 13 imageInfo.usage = usage; 14 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; 15 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; 16 17 if (vkCreateImage(device, &imageInfo, nullptr, &image) != VK_SUCCESS) { 18 throw std::runtime_error("failed to create image!"); 19 } 20 21 VkMemoryRequirements memRequirements; 22 vkGetImageMemoryRequirements(device, image, &memRequirements); 23 24 VkMemoryAllocateInfo allocInfo = {}; 25 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; 26 allocInfo.allocationSize = memRequirements.size; 27 allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); 28 29 if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) { 30 throw std::runtime_error("failed to allocate image memory!"); 31 } 32 33 vkBindImageMemory(device, image, imageMemory, 0); 34 }
I've made the width, height, format, tiling mode, usage, and memory properties parameters, because these will all vary between the images we'll be creating throughout this tutorial.
我讓寬度、高度、格式、tiling模式、用法和內存屬性作為參數,因為這些會隨着我們本教程創建image的不同而變化。
The createTextureImage
function can now be simplified to:
createTextureImage
函數現在可以簡化為:
1 void createTextureImage() { 2 int texWidth, texHeight, texChannels; 3 stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); 4 VkDeviceSize imageSize = texWidth * texHeight * 4; 5 6 if (!pixels) { 7 throw std::runtime_error("failed to load texture image!"); 8 } 9 10 VkBuffer stagingBuffer; 11 VkDeviceMemory stagingBufferMemory; 12 createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); 13 14 void* data; 15 vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); 16 memcpy(data, pixels, static_cast<size_t>(imageSize)); 17 vkUnmapMemory(device, stagingBufferMemory); 18 19 stbi_image_free(pixels); 20 21 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); 22 }
Layout transitions 布局轉換
The function we're going to write now involves recording and executing a command buffer again, so now's a good time to move that logic into a helper function or two:
現在我們要寫的函數涉及錄制和執行命令buffer,所以現在是個好時候to移動這塊擴及到一兩個輔助函數:
1 VkCommandBuffer beginSingleTimeCommands() { 2 VkCommandBufferAllocateInfo allocInfo = {}; 3 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; 4 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; 5 allocInfo.commandPool = commandPool; 6 allocInfo.commandBufferCount = 1; 7 8 VkCommandBuffer commandBuffer; 9 vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); 10 11 VkCommandBufferBeginInfo beginInfo = {}; 12 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; 13 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; 14 15 vkBeginCommandBuffer(commandBuffer, &beginInfo); 16 17 return commandBuffer; 18 } 19 20 void endSingleTimeCommands(VkCommandBuffer commandBuffer) { 21 vkEndCommandBuffer(commandBuffer); 22 23 VkSubmitInfo submitInfo = {}; 24 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; 25 submitInfo.commandBufferCount = 1; 26 submitInfo.pCommandBuffers = &commandBuffer; 27 28 vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); 29 vkQueueWaitIdle(graphicsQueue); 30 31 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); 32 }
The code for these functions is based on the existing code in copyBuffer
. You can now simplify that function to:
這些函數的代碼是基於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); }
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. Create a new function to handle layout transitions:
如果我們還在用buffer,那么我們現在就可以寫個函數to錄制和執行vkCmdCopyBufferToImage
to to完成這個工作,但是這個命令要求image首先已經處於正確的布局了。創建新函數來處理布局轉換:
void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) { VkCommandBuffer commandBuffer = beginSingleTimeCommands(); endSingleTimeCommands(commandBuffer); }
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.
實施布局變換的最常用方式是用image內存屏障。管道屏障一般用於同步對資源的存取,例如確保先寫入buffer完成后再對其進行讀取,但是它也可以用於變換image布局或轉移隊列家族所有權when使用VK_SHARING_MODE_EXCLUSIVE
。有個等價的buffer內存屏障來為buffer做這件事。
VkImageMemoryBarrier barrier = {}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; barrier.oldLayout = oldLayout; barrier.newLayout = newLayout;
The first two fields specify layout transition. It is possible to use VK_IMAGE_LAYOUT_UNDEFINED
as oldLayout
if you don't care about the existing contents of the image.
前2個參數指定了布局變換。可以用VK_IMAGE_LAYOUT_UNDEFINED
作為oldLayout
if你不關心image現存的內容。
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
If you are using the barrier to transfer queue family ownership, then these two fields should be the indices of the queue families. They must be set to VK_QUEUE_FAMILY_IGNORED
if you don't want to do this (not the default value!).
如果你用屏障來轉移隊列家族的所有權,那么這2個字段應當是隊列家族的索引。它們必須背后設置為VK_QUEUE_FAMILY_IGNORED
if你不想這樣做(不是默認值!)。
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;
The image
and subresourceRange
specify the image that is affected and the specific part of the image. Our image is not an array and does not have mipmapping levels, so only one level and layer are specified.
和指定了受影響的image及其區域。我們的image不是數組,沒有mipmap層,所以只指定了1個level和layer。
barrier.srcAccessMask = 0; // TODO barrier.dstAccessMask = 0; // TODO
Barriers are primarily used for synchronization purposes, so you must specify which types of operations that involve the resource must happen before the barrier, and which operations that involve the resource must wait on the barrier. We need to do that despite already using vkQueueWaitIdle
to manually synchronize. The right values depend on the old and new layout, so we'll get back to this once we've figured out which transitions we're going to use.
屏障基本用於同步,所以你必須指定哪種類型的操作that涉及到的資源-必須在屏障之前發生,哪種操作that涉及到的資源-必須等待屏障。即使已經使用vkQueueWaitIdle
to人工地同步了,我們也需要做這些。正確的值依賴與新舊布局,所以我們會回到這里-一旦我們判斷出了我們要用哪種轉換。
vkCmdPipelineBarrier( commandBuffer, 0 /* TODO */, 0 /* TODO */, 0, 0, nullptr, 0, nullptr, 1, &barrier );
All types of pipeline barriers are submitted using the same function. The first parameter after the command buffer specifies in which pipeline stage the operations occur that should happen before the barrier. The second parameter specifies the pipeline stage in which operations will wait on the barrier. The pipeline stages that you are allowed to specify before and after the barrier depend on how you use the resource before and after the barrier. The allowed values are listed in this table of the specification. For example, if you're going to read from a uniform after the barrier, you would specify a usage of VK_ACCESS_UNIFORM_READ_BIT
and the earliest shader that will read from the uniform as pipeline stage, for example VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT
. It would not make sense to specify a non-shader pipeline stage for this type of usage and the validation layers will warn you when you specify a pipeline stage that does not match the type of usage.
所有類型的管道屏障都用同一個函數提交。命令buffer之后的第一個參數指定了操作發生的哪個管道階段應當在屏障之前發生。第二個參數指定了要等待屏障的管道階段。管道階段that你被允許指定為之前和之后的-依賴於你如何在屏障之前和之后使用資源。被允許的值陳列在說明書的這個表。例如,如果你要在屏障之后從一個uniform里讀數據,你得指定VK_ACCESS_UNIFORM_READ_BIT
用法和最早的shader-that會從uniform里讀數據-以管道階段的形式-例如VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT
。那不合理to指定一個無shader的管道階段for這個類型的用法,驗證層也會警告你when你指定一個與用法類型不匹配的管道階段。
The third parameter is either 0
or VK_DEPENDENCY_BY_REGION_BIT
. The latter turns the barrier into a per-region condition. That means that the implementation is allowed to already begin reading from the parts of a resource that were written so far, for example.
第三個參數是0
或VK_DEPENDENCY_BY_REGION_BIT
。后者將瓶臟轉換為一個逐區域的條件。這意味着實現被允許從資源的一部分開始讀取that之前被寫入的。
The last three pairs of parameters reference arrays of pipeline barriers of the three available types: memory barriers, buffer memory barriers, and image memory barriers like the one we're using here. Note that we're not using the VkFormat
parameter yet, but we'll be using that one for special transitions in the depth buffer chapter.
最后3對參數指向3種可用類型的管道屏障:內存屏障,buffer內存屏障和image內存屏障-例如我們這里使用的那個。注意,我們暫時不使用VkFormat
參數,但是我們戶在后續的深度緩存章節把它用於特別的轉換。
Copying buffer to image 復制buffer到image
Before we get back to createTextureImage
, we're going to write one more helper function: copyBufferToImage
:
回到createTextureImage
之前,我們在寫一個輔助函數copyBufferToImage
:
void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) { VkCommandBuffer commandBuffer = beginSingleTimeCommands(); endSingleTimeCommands(commandBuffer); }
Just like with buffer copies, you need to specify which part of the buffer is going to be copied to which part of the image. This happens through VkBufferImageCopy
structs:
像buffer的復制一樣,你需要指定buffer的哪部分要被復制到image 的哪部分。這通過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 };
Most of these fields are self-explanatory. The bufferOffset
specifies the byte offset in the buffer at which the pixel values start. The bufferRowLength
and bufferImageHeight
fields specify how the pixels are laid out in memory. For example, you could have some padding bytes between rows of the image. Specifying 0
for both indicates that the pixels are simply tightly packed like they are in our case. The imageSubresource
, imageOffset
and imageExtent
fields indicate to which part of the image we want to copy the pixels.
這些字段大多數是一目了然的。bufferOffset
指定buffer中的字節偏移量where像素值開始。bufferRowLength
和bufferImageHeight
字段指定像素在內存中的排列方式。例如,你可以在圖像的各個行之間有一些空白字節。指定兩個都為0
就表示像素是緊密地排列在一起的,就像我們的例子一樣。imageSubresource
、imageOffset
and 和imageExtent
字段表示我們想從image的哪部分復制像素。
Buffer to image copy operations are enqueued using the vkCmdCopyBufferToImage
function:
Buffer到image的復制操作用vkCmdCopyBufferToImage
函數入隊:
vkCmdCopyBufferToImage( commandBuffer, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion );
The fourth parameter indicates which layout the image is currently using. I'm assuming here that the image has already been transitioned to the layout that is optimal for copying pixels to. Right now we're only copying one chunk of pixels to the whole image, but it's possible to specify an array of VkBufferImageCopy
to perform many different copies from this buffer to the image in one operation.
第4個參數表示image目前在用哪種布局。我這里假設image已經被轉換為對復制像素操作最優的布局了。現在我們只復制一塊像素到全部image,但是可以指定一個VkBufferImageCopy
數組to在一次操作中實施許多不同的復制-這個buffer到image。
Preparing the texture image 准備紋理image
We now have all of the tools we need to finish setting up the texture image, so we're going back to the createTextureImage
function. The last thing we did there was creating the texture image. The next step is to copy the staging buffer to the texture image. This involves two steps:
我們現在有所有需要的工具to完成設置紋理image,所以我們要回到createTextureImage
函數。我們在那里做的最后一件事是創建紋理image。下一步是復制暫存buffer到紋理image。這涉及2步:
- Transition the texture image to
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
轉換紋理image為VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
。 - Execute the buffer to image copy operation 執行buffer to image的復制操作。
This is easy to do with the functions we just created:
用我們剛剛創建的函數,這很容易做:
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
The image was created with the VK_IMAGE_LAYOUT_UNDEFINED
layout, so that one should be specified as old layout when transitioning textureImage
. Remember that we can do this because we don't care about its contents before performing the copy operation.
Image用VK_IMAGE_LAYOUT_UNDEFINED
布局創建,這樣當轉換textureImage
時應當被指定位舊布局。記住,我們可以這樣做,因為我們不關心在實施復制操作之前的它的內容。
To be able to start sampling from the texture image in the shader, we need one last transition to prepare it for shader access:
為了能夠在shader中開始從紋理image采樣,我們需要最后一個轉換to讓它被shader讀取:
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
Transition barrier masks 轉換屏障mask
If you run your application with validation layers enabled now, then you'll see that it complains about the access masks and pipeline stages in transitionImageLayout
being invalid. We still need to set those based on the layouts in the transition.
如果你現在啟用驗證層,運行程序,那么你會看到它抱怨說在transitionImageLayout
中的存取mask和管道階段無效。我們需要設置這些-基於轉換中的布局。
There are two transitions we need to handle:
有2個轉換需要我們處理:
- Undefined → transfer destination: transfer writes that don't need to wait on anything 未定義→轉移目標:轉移寫入that不需要等待任何東西
- 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 轉移目標→shader讀取:shader讀取需要等待轉移寫入,特別是在Fragment shader中的shader讀取,因為那是我們要用紋理的地方。
These rules are specified using the following access masks and pipeline stages:
這些規則使用下述存取mask和管道階段來指定:
1 VkPipelineStageFlags sourceStage; 2 VkPipelineStageFlags destinationStage; 3 4 if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { 5 barrier.srcAccessMask = 0; 6 barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; 7 8 sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; 9 destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; 10 } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { 11 barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; 12 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; 13 14 sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; 15 destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; 16 } else { 17 throw std::invalid_argument("unsupported layout transition!"); 18 } 19 20 vkCmdPipelineBarrier( 21 commandBuffer, 22 sourceStage, destinationStage, 23 0, 24 0, nullptr, 25 0, nullptr, 26 1, &barrier 27 );
As you can see in the aforementioned table, transfer writes must occur in the pipeline transfer stage. Since the writes don't have to wait on anything, you may specify an empty access mask and the earliest possible pipeline stage VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT
for the pre-barrier operations. It should be noted that VK_PIPELINE_STAGE_TRANSFER_BIT
is not a real stage within the graphics and compute pipelines. It is more of a pseudo-stage where transfers happen. See the documentation for more information and other examples of pseudo-stages.
如你所見,在前述表格中,轉移寫入必須發生在管道的轉移階段。既然寫入不需等待任何東西,你可以指定一個空的存取mask和最早的可能的管道階段VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT
for預屏障操作。要注意,VK_PIPELINE_STAGE_TRANSFER_BIT
不是真的圖形或計算管道的階段。它是個轉移發生的偽階段。查看the documentationfor更多信息和其他偽階段的示例。
The image will be written in the same pipeline stage and subsequently read by the fragment shader, which is why we specify shader reading access in the fragment shader pipeline stage.
Image會在同樣的管道階段被寫入,然后被Fragment shader讀取,這就是為什么我們指定在Fragment shader的shader讀取功能。
If we need to do more transitions in the future, then we'll extend the function. The application should now run successfully, although there are of course no visual changes yet.
如果我們需要在將來做更多的轉換,那么我們要擴展這個函數。程序現在應該能成功運行了,盡管當然不會有可見的改變。
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.
要注意的一件事是,提交命令buffer會在開始導致隱式的VK_ACCESS_HOST_WRITE_BIT
同步。由於函數執行只有一個命令的命令buffer,你可以用這個隱式的轉換,設置srcAccessMask
為0
if你在布局轉換中需要VK_ACCESS_HOST_WRITE_BIT
依賴。你自己決定十分想顯式地做,但我個人不是依賴這些像OpenGL的“隱藏”操作。
There is actually a special type of image layout that supports all operations, VK_IMAGE_LAYOUT_GENERAL
. The problem with it, of course, is that it doesn't necessarily offer the best performance for any operation. 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.
實際上有一個特殊的image布局類型VK_IMAGE_LAYOUT_GENERAL
,它支持所有的操作。當然,它的問題是,它不會給任何操作提供最好的性能。某些情況下就得用它,例如使用一個image同時作為輸入和輸出,或者在image留下了預初始化布局后讀取image。
All of the helper functions that submit commands so far have been set up to execute synchronously by waiting for the queue to become idle. For practical applications it is recommended to combine these operations in a single command buffer and execute them asynchronously for higher throughput, especially the transitions and copy in the createTextureImage
function. Try to experiment with this by creating a setupCommandBuffer
that the helper functions record commands into, and add a flushSetupCommands
to execute the commands that have been recorded so far. It's best to do this after the texture mapping works to check if the texture resources are still set up correctly.
所有的輔助函數that提交命令都已經被設置為同步執行by等待隊列為空閑狀態。對於實際的應用程序,推薦你將這些操作組合到一個命令buffer並異步地執行它們for更高的通量,特別是createTextureImage
函數中的轉換和復制操作。嘗試這種方式by創建一個setupCommandBuffer
that輔助函數錄制命令進去,添加一個flushSetupCommands
to執行添加的命令。最后在紋理映射正常工作后再做這些to檢查紋理資源是否仍舊被設置得正確。
Cleanup 清理
Finish the createTextureImage
function by cleaning up the staging buffer and its memory at the end:
完成函數createTextureImage
by在最后清理暫存buffer及其內存:
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);
}
The main texture image is used until the end of the program:
主紋理image一直被用到了程序最后:
void cleanup() { cleanupSwapChain(); vkDestroyImage(device, textureImage, nullptr); vkFreeMemory(device, textureImageMemory, nullptr); ... }
The image now contains the texture, but we still need a way to access it from the graphics pipeline. We'll work on that in the next chapter.
現在image包含了紋理,但是我們還需要一個方法to從圖像管道存取它。我們將在下一章解決這個問題。
C++ code / Vertex shader / Fragment shader