纹理图像
尽管,我们可以在着色器直接访问缓冲中的像素数据,但使用Vulkan的图像对象会更好。Vulkan的图像对象允许我们使用二维坐标来快速获取颜色数据。图像对象的像素数据也被叫做纹素。现在,让我们添加新的类成员变量:
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通过它来确定图像数据的坐标系。图像类型可以是一维,二维和三维图像。一维图像通常被用来存储数组数据或梯度数据。二维图像通常被用来存储纹理。三维图像通常被用类存储体素数据。extent成员变量用于指定图像在每个维度的范围,也就是在每个坐标轴有多少纹素。我们在这里使用的是二维图像,所以depth的值被我们设置为1,并且我们现在没有使用分级细化,所以将其设置为1。
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
Vulkan支持多种格式的图像数据,这里我们使用的是图像库解析的像素数据格式。
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
tiling成员变量可以是下面这两个值之一:
-
VK_IMAGE_TILING_LINEAR:纹素以行主序的方式排列
-
VK_IMAGE_TILING_OPTIMAL:纹素以一种对访问优化的方式排列
tiling成员变量的设置在之后不可以修改。如果读者需要直接访问图像数据,应该将tiling成员变量设置为VK_IMAGE_TILING_LINEAR。由于这里我们使用暂存缓冲而不是暂存图像来存储图像数据,设置为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成员变量的用法和创建缓冲时使用的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
samples成员变量用于设置多重采样。这一设置只对用作附着的图像对象有效,我们的图像不用于附着,将其设置为采样1次。有许多用于稀疏图像的优化标记可以使用。稀疏图像是一种离散存储图像数据的方法。比如,我们可以使用稀疏图像来存储体素地形,避免为"空气"部分分配内存。在这里,我们没有使用flags标记,将其设置为默认值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函数获取图像对象的内存需求,然后调用vkAllocateMemory函数分配内存,最后调用vkBindImageMemory函数将图像对象和内存进行关联即可。
为了简化图像对象的创建操作,我们编写了一个叫做createImage的辅助函数:
void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) {
AVkImageCreateInfo 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);
}
上面代码我们将图像宽度、高度、格式、tiling模式、使用标记、内存属性作为函数参数,之后的章节,我们将直接使用这一函数来创建图像对象。
现在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);
}