现在逐个顶点加上颜色,几何体已经是彩色的了,但是这其实是不太好的办法。本章我们要实现贴图映射,这会让几何体看起来更有趣,也能够让我们加载和绘制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的时候,它还能用于转移队列族所属。
加载图像有很多可用的库,你甚至可以自己写代码来加载简单的格式如BMP和PPM。本章我们使用stb_image库。它的优势是所有的代码都在一个文件中,所以不用复杂的编译配置。下载stb_image.h并放在一个合适的位置,并添加到包含路径中。
在代码中包含该库:
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
头文件默认只定义了方法的原型,代码文件需要在包括该头文件的同时加上STB_IMAGE_IMPLEMENTATION定义来包括方法体,否则会有链接错误。
创建一个新的方法叫做createTextureImage,这里我们会加载一个图像并上传给Vulkan图像对象。我们要使用命令缓冲,所以这个要放在命令池创建之后。
在shaders目录旁边新建一个textures目录,存储贴图图像。我们要从那个目录的texture.jpg加载图像,我选择使用下面的CC0协议授权的图像,调整大小到512*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!");
}
}
我们要创建一个缓冲,放在主机可见内存中,以便我们使用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);
尽管我们可以建立着色器来访问缓冲中的像素值,但是Vulkan中还是用图像对象的方式更好。图像对象会让该任务更简单更快地获取颜色,因为它能让我们使用2D数组。图像对象中的像素就是纹素,之后我们就这么叫。添加下面的新的类成员:
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类型的图像。一维图像可以用于存储一组数据或者梯度,二维图像主要用于贴图,三维图像可以用于存储体素块。extent字段指定了图像维度,以及每个轴上基本有多少纹素。这也是depth为1而不是0的原因。我们的贴图不会是一个数组,且暂时也不用Mip贴图。
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
Vulkan支持许多图像格式,但是我们应该对纹素和像素使用同样的格式,否则复制操作会失败。
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
tiling字段可以有以下值:
VK_IMAGE_TILING_LINEAR:纹素布局以行为主顺序,就和像素数组一样;
VK_IMAGE_TILING_OPTIMAL:纹素布局以实现定义好的顺序进行最优访问。
和图像布局不一样,平铺模式不能在后面继续更改。如果你想要直接访问图像内存中的纹素,那就必须用VK_IMAGE_TILING_LINEAR。我们会用临时缓冲而不是临时图像,所以这点就不必要了。我们会用VK_IMAGE_TILING_OPTIMAL以便从着色器中高效访问:
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
图像初始布局只有两个可选的值:
VK_IMAGE_LAYOUT_UNDEFINED:不能被GPU使用,且第一次转移会丢弃纹素;
VK_IMAGE_LAYOUT_PREINITIALIZED:不能被GPU使用,但是第一次转移会保存纹素。
有一些情况下,纹素需要在第一次转移的时候进行保存。比如,如果你使用VK_IMAGE_TILING_LINEAR布局的时候,想要使用图像作为临时图像。这样你想要上传纹素数据到它里面,然后转移该图像的时候将其作为转移源。我们这里先要转移该图像将其作为转移目的地,然后从一个缓冲对象中拷贝纹素数据到它里面,因此我们不需要该属性,就直接留空即可。
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
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方法,就像对待缓冲的做法一样。创建该方法,将图像对象创建和内存分配移动到它里面:
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.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.flags = 0; // Optional
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_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,现在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作为旧布局。
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
如果你使用该屏障来转移队列族所属,这两个字段应该是队列族的索引。如果你不想这么做,它们必须设置为VK_QUEUE_FAMILY_IGNORED。
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;
image和subresourceRange字段指定了受影响的图像及图像的特定部分。我们的图像不是一个数组,没有Mip贴图等级,所以仅仅就是指定一个等级和层。
屏障主要用于同步,所以你一定要指定资源涉及的哪种操作类型必须在屏障之前发生,以及资源涉及的哪种操作必须在屏障上等待。尽管已经使用了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参数,但是我们会在深度缓冲章节的特殊转移的时候用到。
在我们回到createTextureImage之前,还要写一个助手方法:
void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) {
VkCommandBuffer commandBuffer = beginSingleTimeCommands();
endSingleTimeCommands(commandBuffer);
}
就和带缓冲拷贝那样,你要指定要被拷贝的是哪个部分,以及要拷贝到图像的哪个部分:
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来一次性从该缓冲执行许多不同的拷贝到一个图像。
我们现在有了建立贴图图像的所有工具,所以我们回到createTextureImage方法。最后一件事就是创建贴图图像。下一步是拷贝临时缓冲到贴图图像。这涉及到两个步骤:
转移贴图图像到VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
执行缓冲到图像的复制操作。
很容易用我们创建的函数来做这些操作:
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));
图像使用VK_IMAGE_LAYOUT_UNDEFINED布局创建,为的是转移贴图图像的时候有一个指定为旧布局。记住我们可以这么做,因为我们不关心执行复制操作之前它的内容。
为了能从着色器贴图图像开始采样,我们需要最后一个转移来为着色器访问做准备:
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
现在运行代码会发现报错,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方法执行一个只有一个命令的命令缓冲,你可以使用该隐式同步并设置srcAccessMask为0,如果你在布局转移的时候需要VK_ACCESS_HOST_WRITE_BIT依赖的话。这由你确定是否要显式进行与否,但是我个人不喜欢依赖于这些类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);
...
}
现在图像包含了贴图,但是我们还要从图形管线访问它才行,下一章会继续该工作。
暂无评论内容