[译]Vulkan教程(27)Image
[译]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
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(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(texWidth); imageInfo.extent.height = static_cast (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(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(texWidth), static_cast (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
- Previous
- Next