目录
Introduction
到目前为止,几何体已使用逐顶点颜色着色,这是一种相当有限的方法。在本部分教程中,我们将实现纹理映射,以使几何体看起来更有趣。这也将允许我们在未来的章节中加载和绘制基本的3D模型。
将纹理添加到我们的应用程序将涉及以下步骤:
- 创建由设备内存支持的图像对象
- 用图像文件中的像素填充
- 创建图像采样器
- 添加组合图像采样器描述符以从纹理中采样颜色
我们以前已经使用过图像对象,但这些对象是由交换链扩展自动创建的。这次我们必须自己创造一个。创建图像并用数据填充图像与创建顶点缓冲区类似。我们将首先创建一个暂存资源,并用像素数据填充它,然后将其复制到用于渲染的最终图像对象。尽管可以为此目的创建暂存图像,但Vulkan还允许您将像素从VkBuffer复制到图像,而且在某些硬件上,用于此目的的API实际上更快。我们将首先创建这个缓冲区并用像素值填充它,然后创建一个图像将像素复制到其中。创建图像与创建缓冲区没有太大区别。它包括查询内存需求、分配设备内存和绑定它,就像我们以前看到的那样。
然而,在处理图像时,我们需要注意一些额外的问题。图像可以有不同的布局,这些布局会影响像素在内存中的组织方式。例如,由于图形硬件的工作方式,简单地逐行存储像素可能无法获得最佳性能。在对图像执行任何操作时,必须确保图像的布局适合该操作。当我们指定渲染过程时,我们实际上已经看到了一些布局:
- 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:最适合从着色器采样
转换图像布局的最常见方法之一是管道屏障。管道屏障主要用于同步对资源的访问,例如确保在读取图像之前写入图像,但也可以用于转换布局。在本章中,我们将了解管道屏障是如何用于此目的的。当使用VK_SHARING_MODE_EXCLUSIVE时,还可以使用屏障来转移队列系列所有权。
Image library
有许多库可用于加载图像,您甚至可以编写自己的代码来加载BMP和PPM等简单格式。在本教程中,我们将使用stb集合中的stb_image库。它的优点是所有代码都在一个文件中,因此不需要任何复杂的构建配置。下载stb_image.h并将其存储在一个方便的位置,例如保存GLFW和GLM的目录。将位置添加到包含路径。
Visual Studio
将包含stb_image.h的目录添加到Additional Include Directories路径。
Makefile
将stb_image.h目录添加到GCC的include目录中:
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
包括图像库,如下所示:
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
默认情况下,标头仅定义函数的原型。一个代码文件需要将头与STB_IMAGE_EXINCEMENT定义一起包含,以包含函数体,否则将出现链接错误。
void initVulkan() {
...
createCommandPool();
createTextureImage();
createVertexBuffer();
...
}
...
void createTextureImage() {
}
创建一个新函数createTextureImage,我们将在其中加载图像并将其上载到Vulkan图像对象中。我们将使用命令缓冲区,因此应该在createCommandPool之后调用它。
在着色器目录旁边创建一个新的目录textures以存储纹理图像。我们将从该目录加载一个名为texture.jpg的图像。我选择使用以下CC0许可的图像,将其大小调整为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通道,这对于将来与其他纹理的一致性很好。中间的三个参数是图像中通道的宽度、高度和实际数量的输出。返回的指针是像素值数组中的第一个元素。对于总共texWidth*texHeight*4个值,在STBI_rgb_alpha的情况下,像素以每像素4个字节逐行布局。
Staging buffer
现在我们将在主机可见内存中创建一个缓冲区,以便我们可以使用vkMapMemory并将像素复制到其中。将此临时缓冲区的变量添加到createTextureImage函数:
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);
Texture Image
尽管我们可以设置着色器以访问缓冲区中的像素值,但最好使用Vulkan中的图像对象。通过允许我们使用2D坐标,图像对象将使检索颜色变得更容易和更快。图像对象中的像素称为texels,从现在起我们将使用该名称。添加以下新类成员:
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图像。例如,一维图像可用于存储数据或梯度阵列,二维图像主要用于纹理,而三维图像可用于储存体素体积。范围字段指定图像的尺寸,基本上每个轴上有多少纹素。这就是为什么深度必须是1而不是0。我们的纹理不会是数组,现在也不会使用mipmapping。
imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
Vulkan支持许多可能的图像格式,但我们应该使用与缓冲区中像素相同的纹理像素格式,否则复制操作将失败。
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
tiling字段可以具有以下两个值之一:
- VK_IMAGE_TILING_LINEAR:像素按行主要顺序排列,就像我们的像素阵列一样
- VK_IMAGE_TTILING_OPTIMAL:按实现定义的顺序排列纹理,以实现最佳访问
与图像布局不同,平铺模式稍后无法更改。如果希望能够直接访问图像内存中的纹素,则必须使用VK_IMAGE_TILING_LINEAR。我们将使用暂存缓冲区而不是暂存映像,因此这不是必需的。我们将使用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字段具有与创建缓冲区期间相同的语义。图像将用作缓冲区副本的目标,因此应将其设置为传输目标。我们还希望能够从着色器访问图像来为网格着色,因此使用应包括VK_IMAGE_USAGE_SAMPLED_BIT。
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
图像将仅由一个队列系列使用:支持图形(因此也支持)传输操作的队列系列。
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.flags = 0; // Optional
采样标志与多重采样相关。这仅适用于将用作附件的图像,因此请坚持使用一个示例。对于与稀疏图像相关的图像,有一些可选标志。稀疏图像是指只有特定区域实际上由内存支持的图像。例如,如果对体素地形使用3D纹理,则可以使用该纹理来避免分配内存来存储大量“空气”值。我们不会在本教程中使用它,所以将其保留为默认值0。
if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) {
throw std::runtime_error("failed to create image!");
}
图像是使用vkCreateImage创建的,它没有任何特别值得注意的参数。图形硬件可能不支持VK_FORMAT_R8G8B8A8_SRGB格式。你应该有一个可接受的备选方案列表,并选择支持的最佳方案。然而,对这种特定格式的支持非常广泛,我们将跳过这一步。使用不同的格式也需要烦人的转换。我们将在深度缓冲区一章中回到这一点,在那里我们将实现这样的系统。
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代替vkGetBufferMemoryRequirement,使用vkBindImageMemory代替vkBindBufferMemory。
这个函数已经非常大了,在后面的章节中需要创建更多的图像,所以我们应该将图像创建抽象为createImage函数,就像我们对缓冲区所做的那样。创建函数并将图像对象创建和内存分配移动到该函数:
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);
}
我已经设置了宽度、高度、格式、平铺模式、使用情况和内存特性参数,因为这些参数都会因我们在本教程中创建的图像而异。
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);
}
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中的现有代码。现在,您可以将该函数简化为:
void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
VkCommandBuffer commandBuffer = beginSingleTimeCommands();
VkBufferCopy copyRegion{};
copyRegion.size = size;
vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region);
endSingleTimeCommands(commandBuffer);
}
如果我们仍然在使用缓冲区,那么我们现在可以编写一个函数来记录并执行vkCmdCopyBufferToImage以完成任务,但是这个命令要求图像首先处于正确的布局中。创建一个新函数来处理布局转换:
void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) {
VkCommandBuffer commandBuffer = beginSingleTimeCommands();
endSingleTimeCommands(commandBuffer);
}
执行布局转换的最常见方法之一是使用图像存储器屏障。这样的管道屏障通常用于同步对资源的访问,例如确保在读取缓冲区之前完成对缓冲区的写入,但当使用VK_SHARING_MODE_EXCLUSIVE时,它也可以用于转换图像布局和转移队列族所有权。对于缓冲区,有一个等效的缓冲存储器屏障来实现这一点。
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;
图像和子资源范围指定受影响的图像和图像的特定部分。我们的图像不是数组,也没有mipmapping级别,因此只指定了一个级别和层。
barrier.srcAccessMask = 0; // TODO
barrier.dstAccessMask = 0; // TODO
屏障主要用于同步目的,因此您必须指定哪些类型的涉及资源的操作必须在屏障之前发生,哪些类型的操作涉及资源必须在屏障上等待。尽管已经使用vkQueueWaitIdle手动同步,我们仍需要这样做。正确的值取决于旧的和新的布局,因此,一旦我们确定了要使用的转换,我们将返回到这里。
vkCmdPipelineBarrier(
commandBuffer,
0 /* TODO */, 0 /* TODO */,
0,
0, nullptr,
0, nullptr,
1, &barrier
);
使用相同的功能提交所有类型的管道屏障。命令缓冲区后的第一个参数指定应在屏障之前发生的操作发生在哪个管道阶段。第二个参数指定操作将在屏障上等待的管道阶段。允许您在屏障前后指定的管道阶段取决于您在屏障之前和之后如何使用资源。本规范表中列出了允许值。例如,如果要在屏障之后从制服中读取,则需要指定VK_ACCESS_UNIFORM_READ_BIT的用法以及将从制服中作为管道阶段读取的最早着色器,例如VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT。为这种类型的使用指定非着色器管道阶段是没有意义的,当您指定与使用类型不匹配的管道阶段时,验证层将警告您。
第三个参数是0或VK_DEPENDENCY_BY_REGION_BIT。后者将屏障变为按区域条件。例如,这意味着允许实现已经开始读取到目前为止编写的资源部分。
最后三对参数引用了三种可用类型的管道屏障阵列:内存屏障、缓冲内存屏障和图像内存屏障,就像我们在这里使用的那样。注意,我们还没有使用VkFormat参数,但我们将在深度缓冲区一章中使用该参数进行特殊转换。
Copying buffer to image
在我们返回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指定缓冲区中像素值开始的字节偏移。bufferRowLength和bufferImageHeight字段指定像素在内存中的布局方式。例如,可以在图像的行之间添加一些填充字节。为这两个值指定0表示像素像我们的情况一样被简单地压缩。imageSubresource、imageOffset和imageExtent字段指示要将像素复制到图像的哪个部分。
缓冲区到图像复制操作使用vkCmdCopyBufferToImage函数排队:
vkCmdCopyBufferToImage(
commandBuffer,
buffer,
image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
®ion
);
第四个参数指示图像当前使用的布局。我在这里假设图像已经转换为最适合将像素复制到的布局。现在我们只将一块像素复制到整个图像,但可以指定一个VkBufferImageCopy数组,以在一次操作中执行从该缓冲区到图像的许多不同复制。
Preparing the texture image
现在我们有了完成纹理图像设置所需的所有工具,因此我们将返回createTextureImage函数。我们在那里做的最后一件事是创建纹理图像。下一步是将暂存缓冲区复制到纹理图像。这包括两个步骤:
- 将纹理图像转换为VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
- 执行缓冲区到图像复制操作
这很容易使用我们刚刚创建的函数:
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时应将其指定为旧布局。请记住,我们可以这样做,因为在执行复制操作之前,我们不关心它的内容。
为了能够从着色器中的纹理图像开始采样,我们需要最后一个过渡来为着色器访问做好准备:
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
Transition barrier masks
如果您现在在启用验证层的情况下运行应用程序,那么您将看到它抱怨transitionImageLayout中的访问掩码和管道阶段无效。我们仍然需要根据过渡中的布局来设置它们。
我们需要处理两个转换:
- 未定义→ 传输目的地:不需要等待任何内容的传输写入
- 转移目的地→ 着色器读取:着色器读取应该等待传输写入,特别是片段着色器中的着色器读取,因为这是我们要使用纹理的地方
使用以下访问掩码和管道阶段指定这些规则:
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_PIPELINE_STAGE_TRANSFER_BIT不是图形和计算管道内的真实阶段。这更像是一个转移发生的伪阶段。有关伪阶段的更多信息和其他示例,请参阅文档。
图像将在同一管道阶段写入,随后由片段着色器读取,这就是为什么我们在片段着色器管道阶段中指定着色器读取访问。
如果我们将来需要做更多的转换,那么我们将扩展该函数。应用程序现在应该可以成功运行了,当然还没有任何可视更改。
需要注意的一点是,命令缓冲区提交在开始时会导致隐式VK_ACCESS_HOST_WRITE_BIT同步。由于transitionImageLayout函数仅使用单个命令执行命令缓冲区,因此如果在布局转换中需要VK_ACCESS_HOST_WRITE_BIT依赖项,则可以使用此隐式同步并将srcAccessMask设置为0。这取决于你是否想明确表示,但我个人并不喜欢依赖这些类似OpenGL的“隐藏”操作。
实际上,有一种特殊类型的图像布局支持所有操作,即VK_IMAGE_LAYOUT_GENERAL。当然,它的问题是它不一定能为任何操作提供最佳性能。在某些特殊情况下,如使用图像作为输入和输出,或在图像离开预先初始化的布局后读取图像时,都需要使用它。
到目前为止,提交命令的所有函数都已设置为通过等待队列空闲来同步执行。对于实际应用程序,建议将这些操作组合在一个命令缓冲区中,并异步执行,以提高吞吐量,尤其是createTextureImage函数中的转换和复制。尝试通过创建函数将命令记录到其中的setupCommandBuffer来进行实验,并添加flushSetupCommands来执行迄今为止已记录的命令。最好在纹理映射工作后执行此操作,以检查纹理资源是否仍然正确设置。
Cleanup
最后清理暂存缓冲区及其内存,完成createTextureImage函数:
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);
}
主纹理图像将一直使用到程序结束:
void cleanup() {
cleanupSwapChain();
vkDestroyImage(device, textureImage, nullptr);
vkFreeMemory(device, textureImageMemory, nullptr);
...
}
图像现在包含纹理,但我们仍然需要一种从图形管道访问它的方法。我们将在下一章对此进行研究。