在之前我们使用顶点着色器以及描述符来实现绘制有颜色的几何体,还实现了旋转动画。接下来我们学习一下纹理贴图,这个将是我们实现加载绘制基本3D模型的基础。
添加纹理的基本步骤有:
我们以前已经使用过图像对象,但是这些对象是由swap chain扩展自动创建的。这次需要手动创建,创建图像并填充数据类似于创建顶点缓冲区。我们将通过创建一个暂存资源和填充它与像素数据,然后我们复制这到我们将用于渲染的最终图像对象。
可以创建一个暂存图像,不过Vulkan允许将像素从VkBuffer复制到image中,而且这个API在某些硬件上实际上更快。我们将首先创建这个缓冲区并填充像素值,然后我们将创建一个图像复制像素到。创建image与创建缓冲区并没有太大的不同。它包括查询内存需求、分配设备内存并绑定它,就像我们之前看到的那样。
图像可以有不同的布局,影响像素在内存中的存储方式。例如,由于图形硬件的工作方式,简单地逐行存储像素可能不会带来最好的性能。当对图像执行任何操作时,确保它们具有在该操作中使用的最佳布局。比如指定渲染通道时其中一些布局有:
转换图像布局的最常见方法之一是管道屏障(pipeline barrier)。管道屏障主要用于同步对资源的访问,例如确保在读取图像之前将其写入。后面我们将了解如何将管道壁垒用于转换布局。
使用VK_SHARING_MODE_EXCLUSIVE时,可以另外使用屏障来转移队列系列的所有权。
有许多库可用于加载图像,您甚至可以编写自己的代码来加载BMP和PPM等简单格式。 这里我们将使用stb集合中的stb_image库。 这样做的好处是所有代码都在一个文件中,因此不需要任何棘手的构建配置。 下载stb_image.h并将其存储在方便的位置,例如保存GLFW和GLM的目录。 将位置添加到您的包含路径。
stb_image库地址: https://github.com/nothings/stb
下载后解压,放在指定目录,然后修改我们的Makefile文件:
VULKAN_SDK_PATH = /home/jh/Program/vulkan/1.2.170.0/x86_64
STB_IMAGE_PATH = /home/jh/Program/stb-image
CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_IMAGE_PATH)
在shaders目录旁边创建一个新的目录textures来存储纹理图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9AyMXavx-1617639060029)(assert/texture.jpg)]
首先添加头文件:
#define STB_IMAGE_IMPLEMENTATION
#include
默认情况下,头文件只定义函数的原型。一个代码文件需要包含STB_IMAGE_IMPLEMENTATION定义的头文件来包含函数体,否则会有链接错误。
void initVulkan() {
...
createCommandPool();
// 因为需要使用指令缓冲,所以在创建指令池之后调用
createTextureImage();
createVertexBuffer();
...
}
void createTextureImage() {
int texWidth, texHeight, texChannels;
// 加载texture.jpg图像
stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
// 每个像素4个字节
VkDeviceSize imageSize = texWidth * texHeight * 4;
if (!pixels) {
throw std::runtime_error("failed to load texture image!");
}
}
stbi_load函数将文件路径和要加载的通道数量作为参数。STBI_rgb_alpha值会强制为图像加载Alpha通道,即使它没有通道也是如此, 与其他纹理保持一致性。中间的三个参数是输出图像中通道的宽度、高度和实际数量。返回的指针是像素值数组中的第一个元素。在STBI_rgba_alpha中,像素逐行排列,每个像素4个字节,总共texWidth * texHeight * 4个值。
现在,我们将在主机可见内存中创建一个缓冲区,以便我们可以使用vkMapMemory并将像素复制到其中。 将此临时缓冲区的变量添加到createTextureImage函数:
void createTextureImage() {
int texWidth, texHeight, texChannels;
// 加载texture.jpg图像
stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
// 每个像素4个字节
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(imageSize));
vkUnmapMemory(device, stagingBufferMemory);
// 最后释放原始像素数据
stbi_image_free(pixels);
}
尽管我们可以设置着色器来访问缓冲区中的像素值,但为此目的最好使用Vulkan中的图像对象-VkImage。 通过使用2D坐标,图像对象将使检索颜色更加容易和快捷。 图像对象中的像素称为纹理像素:
VkImage textureImage;
VkDeviceMemory textureImageMemory;
void createTextureImage() {
...
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;
// 指定图像格式,对于像素像素,使用与缓冲区中像素相同的格式,否则复制操作将失败
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
// 图像平铺模式,这里指定图像像素最佳内存拼接布局
// 与图像的布局不同,平铺模式不能在以后更改。如果希望能够直接访问图像内存中的texel,则必须使用VK_IMAGE_TILING_OPTIMAL
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
// 图像的initialLayout只有两个可能的值:VK_IMAGE_LAYOUT_UNDEFINED || VK_IMAGE_LAYOUT_PREINITIALIZED
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
// 图像将仅由一个队列族使用, 因此独占模式
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// 图像采样,每个像素都采样
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.flags = 0; // Optional
// 创建图像
if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) {
throw std::runtime_error("failed to create image!");
}
// 同样的,需要给Image分配内存空间
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);
}
对于initialLayout,很少有情况需要在第一次过渡期间保留纹理像素,但是如果想将图像与VK_IMAGE_TILING_LINEAR布局结合使用作为缓存图像。 在这种情况下,将纹理像素数据上传到其中,然后将图像转换为传输源而又不丢失数据。但是,我们首先将图像转换为传输目标,然后从缓冲区对象将纹理像素数据复制到该图像,因此使用VK_IMAGE_LAYOUT_UNDEFINED。
对于usage, 与缓冲区创建期间的含义相同。 该图像将用作缓冲区副本的目的地,因此应将其设置为传输目的地。 我们还希望能够从着色器访问图像来为网格着色,因此用法应包括VK_IMAGE_USAGE_SAMPLED_BIT。
采样标志与多重采样有关。 这仅与将用作附件的图像有关,这里使用一个样本。 对于与稀疏图像有关的图像,有一些可选的标志。 稀疏图像是其中实际上仅某些区域由内存支持的图像。 例如,如果将3D纹理用于体素地形,则可以使用它来避免分配内存来存储大量的“空”值,这里我们设置为0。
创建图像的一系列参数是在VkImageCreateInfo中指明的:
typedef struct VkImageCreateInfo {
VkStructureType sType;
const void* pNext;
VkImageCreateFlags flags;
VkImageType imageType;
VkFormat format;
VkExtent3D extent;
uint32_t mipLevels;
uint32_t arrayLayers;
VkSampleCountFlagBits samples;
VkImageTiling tiling;
VkImageUsageFlags usage;
VkSharingMode sharingMode;
uint32_t queueFamilyIndexCount;
const uint32_t* pQueueFamilyIndices;
VkImageLayout initialLayout;
} VkImageCreateInfo;
图像表示多维(最多3个)数据数组,可用于各种目的(例如附件、纹理),通过描述符集将其绑定到图形或计算管道,或直接将其指定为特定命令的参数。
VkResult vkCreateImage(
VkDevice device,
const VkImageCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkImage* pImage);
现在我们重构下createTextureImage, 将创建VkImage的部分单独做个函数:
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 = static_cast(width);
imageInfo.extent.height = static_cast(height);
imageInfo.extent.depth = 1;
// 图像的最小采样的细节级别
imageInfo.mipLevels = 1;
// 图像中的层数
imageInfo.arrayLayers = 1;
imageInfo.format = format;
// 图像平铺模式,这里指定图像像素最佳内存拼接布局
// 与图像的布局不同,平铺模式不能在以后更改。如果希望能够直接访问图像内存中的texel,则必须使用VK_IMAGE_TILING_OPTIMAL
imageInfo.tiling = tiling;
// 图像的initialLayout只有两个可能的值:VK_IMAGE_LAYOUT_UNDEFINED || VK_IMAGE_LAYOUT_PREINITIALIZED
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!");
}
// 同样的,需要给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, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate image memory!");
}
// 绑定图像和内存
vkBindImageMemory(device, image, imageMemory, 0);
}
void createTextureImage() {
int texWidth, texHeight, texChannels;
// 加载texture.jpg图像
stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
// 每个像素4个字节
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, VK_SHARING_MODE_EXCLUSIVE);
// 内存映射
void* data;
vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data);
memcpy(data, pixels, static_cast(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);
}
现在有了beginSingleTimeCommands和endSingleTimeCommands函数,可以对执行单条指令缓冲区的函数进行优化:
void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
VkCommandBuffer commandBuffer= beginSingleTimeCommands();
// 缓冲拷贝指令
VkBufferCopy copyRegion = {};
copyRegion.srcOffset = 0; // Optional
copyRegion.dstOffset = 0; // Optional
copyRegion.size = size;
// std::cout<<"copyBuffer vkCmdCopyBuffer"<
如果我们仍然使用缓冲区,那么我们现在可以编写一个函数来记录并执行vkCmdCopyBufferToImage,但是这个命令要求首先将Image置于正确的布局中。
创建一个新函数来处理布局转换:
void transitionImageLayout(VkImage image, VkFormat format,
VkImageLayout oldLayout, VkImageLayout newLayout) {
VkCommandBuffer commandBuffer = beginSingleTimeCommands();
// 使用图像内存屏障,用于同步资源访问
VkImageMemoryBarrier barrier = {};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
// 指定布局转换。如果不关心图像的现有内容,可以将VK_IMAGE_LAYOUT_UNDEFINED用作oldLayout
barrier.oldLayout = oldLayout;
barrier.newLayout = newLayout;
// 如果使用屏障来传递队列族的所有权,那么这两个字段应该是队列族的索引
// 如果不这样做,则必须将它们设置为VK_QUEUE_FAMILY_IGNORED
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
// image和subresourceRange指定受影响的图像以及图像的特定部分
barrier.image = image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
// 我们的图像不是数组,也没有mipmapping级别,因此只指定了一个级别和层
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;
// 屏障主要用于同步目的,因此必须指定哪些涉及资源的操作类型必须在屏障之前发生,哪些涉及资源的操作必须在屏障上等待
barrier.srcAccessMask = 0; // TODO
barrier.dstAccessMask = 0; // TODO
// 在管道上执行barrier指令, 所有类型的管道屏障都使用相同的函数提交
vkCmdPipelineBarrier(commandBuffer,
0 /* TODO */, 0 /* TODO */,
0,
0, nullptr,
0, nullptr,
1, &barrier
);
endSingleTimeCommands(commandBuffer);
}
执行布局转换的最常见方法之一是使用图像内存屏障。像这样的管道屏障通常用于同步对资源的访问,例如确保在从缓冲区读取之前完成对缓冲区的写入,但是当使用VK_SHARING_MODE_EXCLUSIVE时,它也可以用于转换映像布局和传输队列族所有权。对于缓冲区,有一个等效的缓冲存储器屏障来实现这一点。
图像存储器屏障仅适用于涉及特定图像子资源范围的存储器访问。也就是说,从图像存储器屏障形成的存储器依赖被限定为通过指定的图像子资源范围进行访问。图像内存屏障还可用于定义指定图像子资源范围的图像布局转换或队列族所有权转移。
typedef struct VkImageMemoryBarrier {
VkStructureType sType;
const void* pNext;
VkAccessFlags srcAccessMask;
VkAccessFlags dstAccessMask;
VkImageLayout oldLayout;
VkImageLayout newLayout;
uint32_t srcQueueFamilyIndex;
uint32_t dstQueueFamilyIndex;
VkImage image;
VkImageSubresourceRange subresourceRange;
} VkImageMemoryBarrier;
指定图像子资源范围使用结构体VkImageSubresourceRange
typedef struct VkImageSubresourceRange {
VkImageAspectFlags aspectMask;
uint32_t baseMipLevel;
uint32_t levelCount;
uint32_t baseArrayLayer;
uint32_t layerCount;
} VkImageSubresourceRange;
标识子资源的目的,指定视图中包含图像的哪些方面:
typedef enum VkImageAspectFlagBits {
VK_IMAGE_ASPECT_COLOR_BIT = 0x00000001,
VK_IMAGE_ASPECT_DEPTH_BIT = 0x00000002,
VK_IMAGE_ASPECT_STENCIL_BIT = 0x00000004,
VK_IMAGE_ASPECT_METADATA_BIT = 0x00000008,
VK_IMAGE_ASPECT_PLANE_0_BIT = 0x00000010,
VK_IMAGE_ASPECT_PLANE_1_BIT = 0x00000020,
VK_IMAGE_ASPECT_PLANE_2_BIT = 0x00000040,
VK_IMAGE_ASPECT_MEMORY_PLANE_0_BIT_EXT = 0x00000080,
VK_IMAGE_ASPECT_MEMORY_PLANE_1_BIT_EXT = 0x00000100,
VK_IMAGE_ASPECT_MEMORY_PLANE_2_BIT_EXT = 0x00000200,
VK_IMAGE_ASPECT_MEMORY_PLANE_3_BIT_EXT = 0x00000400,
VK_IMAGE_ASPECT_PLANE_0_BIT_KHR = VK_IMAGE_ASPECT_PLANE_0_BIT,
VK_IMAGE_ASPECT_PLANE_1_BIT_KHR = VK_IMAGE_ASPECT_PLANE_1_BIT,
VK_IMAGE_ASPECT_PLANE_2_BIT_KHR = VK_IMAGE_ASPECT_PLANE_2_BIT,
VK_IMAGE_ASPECT_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkImageAspectFlagBits;
vkCmdPipelineBarrier是一个同步命令,它在提交到同一队列的命令之间或同一子类中的命令之间插入依赖关系。
void vkCmdPipelineBarrier(
VkCommandBuffer commandBuffer,
VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask,
VkDependencyFlags dependencyFlags,
uint32_t memoryBarrierCount,
const VkMemoryBarrier* pMemoryBarriers,
uint32_t bufferMemoryBarrierCount,
const VkBufferMemoryBarrier* pBufferMemoryBarriers,
uint32_t imageMemoryBarrierCount,
const VkImageMemoryBarrier* pImageMemoryBarriers);
当vkCmdPipelineBarrier提交到队列时,它定义了在它之前提交的命令和在它之后提交的命令之间的内存依赖关系。
如果vkCmdPipelineBarrier是在渲染过程实例外部录制的,则第一个同步作用域将包括按提交顺序较早出现的所有命令。如果vkCmdPipelineBarrier记录在渲染过程实例中,则第一个同步作用域仅包括在同一子过程中以提交顺序较早出现的命令。在这两种情况下,第一个同步作用域仅限于由srcStageMask指定的源阶段掩码确定的管道阶段上的操作。
如果vkCmdPipelineBarrier是在渲染过程实例外部录制的,则第二个同步作用域将包括以后按提交顺序执行的所有命令。如果vkCmdPipelineBarrier记录在渲染过程实例中,则第二个同步作用域仅包括稍后在同一子过程中按提交顺序出现的命令。在任何一种情况下,第二同步作用域都限于由dstStageMask指定的目的级掩码确定的管道级上的操作。
第一个访问范围被限制为在由srcStageMask指定的源阶段掩码确定的管道阶段中进行访问。其中,第一访问作用域仅包括由pMemoryBarriers、pBufferMemoryBarriers和pImageMemoryBarriers数组的元素定义的第一访问作用域,每个元素定义一组内存屏障。如果未指定内存屏障,则第一个访问作用域不包括任何访问。
第二访问范围被限制为在由dstStageMask指定的目标阶段掩码确定的管道阶段中的访问。其中,第二访问作用域仅包括由pMemoryBarriers、pBufferMemoryBarriers和pImageMemoryBarriers数组的元素定义的第二访问作用域,它们各自定义了一组内存屏障。如果未指定内存屏障,则第二访问作用域不包括任何访问。
就像缓冲区复制一样,需要指定缓冲区的哪个部分将被复制到图像的哪个部分:
void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width,
uint32_t height) {
VkCommandBuffer commandBuffer = beginSingleTimeCommands();
// 使用VkBufferImageCopy指定缓冲区复制行为
VkBufferImageCopy region = {};
// 指定缓冲区中像素值开始的字节偏移量
region.bufferOffset = 0;
// 指定像素在内存中的布局方式, 指定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};
// 使用vkCmdCopyBufferToImage函数将缓冲区到图像的复制操作排队
// 第四个参数指示图像当前使用的布局
vkCmdCopyBufferToImage(commandBuffer, buffer, image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, ®ion);
endSingleTimeCommands(commandBuffer);
}
typedef struct VkBufferImageCopy {
VkDeviceSize bufferOffset;
uint32_t bufferRowLength;
uint32_t bufferImageHeight;
VkImageSubresourceLayers imageSubresource;
VkOffset3D imageOffset;
VkExtent3D imageExtent;
} VkBufferImageCopy;
当复制到或从深度或模具方面时,缓冲区内存中的数据使用的布局是深度或模具数据的(大部分)紧密封装的表示形式。具体地说:
由于图像副本的深度或模板方面缓冲区在某些实现上可能需要格式转换,因此不支持图形的队列不支持格式转换。
当复制到深度方面时,并且没有启用VK_EXT_depth_range_unrestricted扩展名,缓冲区内存中的数据必须在[0,1]范围内,否则结果值是未定义的。
复制从imageSubresource的图像图层baseArrayLayer成员开始一层一层地进行。layerCount层从源图像或目标图像复制。
在缓冲区和图像之间复制数据, 从buffer对象复制数据到image对象, 调用vkCmdCopyBufferToImage:
void vkCmdCopyBufferToImage(
VkCommandBuffer commandBuffer,
VkBuffer srcBuffer,
VkImage dstImage,
VkImageLayout dstImageLayout,
uint32_t regionCount,
const VkBufferImageCopy* pRegions);
区域中的每个区域从源缓冲区的指定区域复制到目标图像的指定区域。
如果dstImage的格式是一个多平面的图像格式),必须使用VkBufferImageCopy结构的pRegions成员单独指定作为拷贝目标的每个平面的区域。在本例中,imageSubresource的aspectMask必须为VK_IMAGE_ASPECT_PLANE_0_BIT、VK_IMAGE_ASPECT_PLANE_1_BIT或VK_IMAGE_ASPECT_PLANE_2_BIT。对于vkCmdCopyBufferToImage来说,多平面图像的每个平面都被视为具有由相应子资源的aspectMask标识的平面的多平面格式的兼容平面格式中列出的格式。这既适用于VkFormat,也适用于复制中使用的坐标,它对应于平面中的texel,而不是这些texel如何映射到整个图像中的坐标。
回到createTextureImage函数。我们在那里做的最后一件事是创建纹理图像。下一步是将暂存缓冲区复制到纹理图像。这包括两个步骤:
// 该图像是使用VK_IMAGE_LAYOUT_UNDEFINED布局创建的,因此在转换textureImage时应将oldLayout指定为VK_IMAGE_LAYOUT_UNDEFINED
// 在执行复制操作之前,不关心图像内容,所以可以这样做
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
// 拷贝stagingBuffer中缓存的图像数据至Image(GPU可见内存)
copyBufferToImage(stagingBuffer, textureImage,
static_cast(texWidth), static_cast(texHeight));
// 为了能够从着色器中的纹理图像开始采样,我们需要最后一个过渡来准备着色器访问(用于同步对资源的访问):
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;
// Image或缓冲区在清除或复制操作中的写访问
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) {
// Image或缓冲区在清除或复制操作中的写访问
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
// 指定对存储缓冲区、物理存储缓冲区、统一texel缓冲区、存储texel缓冲区、采样图像或存储图像的读访问
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);
传输写入必须在管道传输阶段进行。因为写操作不需要等待任何东西,所以您可以为预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。
实际上,有一种特殊的图像布局类型可以支持所有操作–VK_IMAGE_LAYOUT_GENERAL。当然,它的问题在于,它不一定能为任何操作提供最佳性能。在某些特殊情况下,例如使用图像作为输入和输出,或者在离开预初始化的布局后读取图像。到目前为止,所有提交命令的帮助程序功能都已设置为通过等待队列变为空闲状态而同步执行。对于实际应用,建议将这些操作组合在单个命令缓冲区中,并异步执行它们以提高吞吐量,尤其是createTextureImage函数中的过渡和复制。通过创建一个helper函数将命令记录到其中的setupCommandBuffer并尝试添加一个flushSetupCommands来执行到目前为止已记录的命令,来尝试进行此操作。最好在纹理贴图工作后执行此操作,以检查纹理资源是否仍正确设置。
Vulkan中的内存可以通过shader调用和管道中的一些固定函数来访问。访问类型是所使用的描述符类型的函数,或者固定函数阶段如何访问内存。每个访问类型对应于VkAccessFlagBits中的一个位标志。
一些同步命令以访问类型集作为参数来定义内存依赖项的访问范围。如果同步命令包含源访问掩码,则其第一个访问作用域仅包括通过该掩码中指定的访问类型进行的访问。类似地,如果同步命令包含目标访问掩码,则其第二个访问作用域仅包括通过该掩码中指定的访问类型进行的访问。
typedef enum VkAccessFlagBits {
VK_ACCESS_INDIRECT_COMMAND_READ_BIT = 0x00000001,
VK_ACCESS_INDEX_READ_BIT = 0x00000002,
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT = 0x00000004,
VK_ACCESS_UNIFORM_READ_BIT = 0x00000008,
VK_ACCESS_INPUT_ATTACHMENT_READ_BIT = 0x00000010,
VK_ACCESS_SHADER_READ_BIT = 0x00000020,
VK_ACCESS_SHADER_WRITE_BIT = 0x00000040,
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT = 0x00000080,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT = 0x00000100,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT = 0x00000200,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT = 0x00000400,
VK_ACCESS_TRANSFER_READ_BIT = 0x00000800,
VK_ACCESS_TRANSFER_WRITE_BIT = 0x00001000,
VK_ACCESS_HOST_READ_BIT = 0x00002000,
VK_ACCESS_HOST_WRITE_BIT = 0x00004000,
VK_ACCESS_MEMORY_READ_BIT = 0x00008000,
VK_ACCESS_MEMORY_WRITE_BIT = 0x00010000,
VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT = 0x02000000,
VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_READ_BIT_EXT = 0x04000000,
VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_WRITE_BIT_EXT = 0x08000000,
VK_ACCESS_CONDITIONAL_RENDERING_READ_BIT_EXT = 0x00100000,
VK_ACCESS_COMMAND_PROCESS_READ_BIT_NVX = 0x00020000,
VK_ACCESS_COMMAND_PROCESS_WRITE_BIT_NVX = 0x00040000,
VK_ACCESS_COLOR_ATTACHMENT_READ_NONCOHERENT_BIT_EXT = 0x00080000,
VK_ACCESS_SHADING_RATE_IMAGE_READ_BIT_NV = 0x00800000,
VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_NV = 0x00200000,
VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_NV = 0x00400000,
VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT = 0x01000000,
VK_ACCESS_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkAccessFlagBits;
操作或同步命令执行的工作由多个操作组成,这些操作作为逻辑上独立的步骤序列执行,称为管道阶段。执行的确切管道阶段取决于所使用的特定命令,以及记录命令时的当前命令缓冲区状态。绘制命令、分派命令、复制命令、清除命令和同步命令都在管道阶段的不同集合中执行。同步命令不会在已定义的管道中执行,但会执行VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT和VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT。
注意同步命令执行的操作(例如可用性和可见性操作)不是由定义的管道阶段执行的。但是,其他命令仍然可以通过VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT和VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT管道阶段与它们同步。
跨管道阶段执行操作必须遵循隐式排序保证,特别是包括管道阶段顺序。否则,与其他阶段相比,跨管道阶段的执行可能会重叠或无序执行,除非执行依赖项强制执行。
一些同步命令包括管道阶段参数,将该命令的同步范围限制在这些阶段。这允许对精确的执行依赖关系和操作命令执行的访问进行细粒度的控制。实现应该使用这些管道阶段来避免不必要的停顿或缓存刷新。
可以设置指定管道阶段通过VkPipelineStageFlags:
typedef enum VkPipelineStageFlagBits {
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT = 0x00000001,
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT = 0x00000002,
VK_PIPELINE_STAGE_VERTEX_INPUT_BIT = 0x00000004,
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT = 0x00000008,
VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT = 0x00000010,
VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT = 0x00000020,
VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT = 0x00000040,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT = 0x00000080,
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT = 0x00000100,
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT = 0x00000200,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT = 0x00000400,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT = 0x00000800,
VK_PIPELINE_STAGE_TRANSFER_BIT = 0x00001000,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT = 0x00002000,
VK_PIPELINE_STAGE_HOST_BIT = 0x00004000,
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT = 0x00008000,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT = 0x00010000,
VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT = 0x01000000,
VK_PIPELINE_STAGE_CONDITIONAL_RENDERING_BIT_EXT = 0x00040000,
VK_PIPELINE_STAGE_COMMAND_PROCESS_BIT_NVX = 0x00020000,
VK_PIPELINE_STAGE_SHADING_RATE_IMAGE_BIT_NV = 0x00400000,
VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_NV = 0x00200000,
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_NV = 0x02000000,
VK_PIPELINE_STAGE_TASK_SHADER_BIT_NV = 0x00080000,
VK_PIPELINE_STAGE_MESH_SHADER_BIT_NV = 0x00100000,
VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT = 0x00800000,
VK_PIPELINE_STAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkPipelineStageFlagBits;
创建纹理贴图后,不能忘记在必要的时候将内存释放出来:
void createTextureImage() {
...
// 通过清除过渡缓冲区及其末尾的内存来完成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);
...
}
到目前为止,我们从设备物理存储上读取了图片内容,将其转成临时缓存后又将其存储在对应GPU可见的内存中以及生成对应VkImage纹理贴图对象,接下来需要将其显示在屏幕上还需要把这个对象放入图形管道中。
在回顾下本章中的读取图像的步骤:
上面步骤中,4和5是下一章的内容。
windows平台上编译当前项目,可以使用cmake, CMakefileLists.txt文件如下(注意先安装Vulkan sdk):
cmake_minimum_required (VERSION 3.7) #最低要求的CMake版本
project(MyVulkan) # 项目名称
set(VERSION 0.0.1)
set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -g -Wall -Wno-unused-variable -pthread")
message(STATUS "This is " ${PROJECT_NAME} " version " ${VERSION})
message(STATUS "This is for windows platform")
message("Build Type:" ${CMAKE_BUILD_TYPE} ${CMAKE_CXX_FLAGS})
# Use FindVulkan module added with CMAKE 3.7
if (NOT CMAKE_VERSION VERSION_LESS 3.7.0)
message(STATUS "Using module to find Vulkan")
find_package(Vulkan)
endif()
find_library(Vulkan_LIBRARY NAMES vulkan-1 vulkan PATHS ${CMAKE_SOURCE_DIR}/libs/vulkan)
IF (Vulkan_LIBRARY)
set(Vulkan_FOUND ON)
MESSAGE("Using bundled Vulkan library version")
ENDIF()
message(STATUS "Using Vulkan lib: " ${Vulkan_LIBRARY})
# CMAKE_SOURCE_DIR 代表工程根目录CMakeLists.txt文件所在目录
set(ROOT_DIR ${CMAKE_SOURCE_DIR})
### GLFW3
set(GLFW_LIB_DIR ${ROOT_DIR}/lib/glfw3)
set(GLFW_LIBS ${GLFW_LIB_DIR}/glfw3dll.lib)
### GLM
set(GLM_INCLUDE_DIRS ${ROOT_DIR}/include/glm)
### stb-image
set(STB_IMAGE_DIRS ${ROOT_DIR}/include/stb-image)
message(STATUS "Lib path: ")
message(STATUS " GLFW3: " ${GLFW_LIBS})
message(STATUS " GLM : " ${GLM_INCLUDE_DIRS})
message(STATUS " STB_IMAGE: " ${STB_IMAGE_DIRS})
# 定义头文件搜索路径
include_directories(${ROOT_DIR}/inlcude
${GLM_INCLUDE_DIRS})
#aux_source_directory(./ SOURCE_DIR)
aux_source_directory(${ROOT_DIR}/inlcude SOURCE_DIR)
aux_source_directory(${ROOT_DIR}/src SOURCE_DIR)
# Target
add_executable(MyVulkan ${SOURCE_DIR})
####Vulkan
find_package(Vulkan REQUIRED)
# GLFW3 is dynamic link
target_link_libraries(${PROJECT_NAME} Vulkan::Vulkan ${GLFW_LIBS})