目录
23 图像
图片库
暂存缓冲区
纹理图像
布局转换
将缓冲区复制到图像上
准备纹理图像
传输屏障掩码
清除
24 图像视图和采样器
纹理图像视图
采样器
Anisotropy 设备特征
25 组合图像采样器
更新描述符
纹理坐标系
着色器
23 图像
添加纹理将涉及以下步骤:
- 创建一个由设备内存支持的图像对象
- 用图像文件中的像素填充它
- 创建图像采样器
- 添加组合图像采样器描述符以从纹理中采样颜色
我们之前已经使用过图像对象,但它们是由交换链扩展自动创建的。这次我们必须自己创建一个。创建图像并用数据填充它类似于创建顶点缓冲区。我们将从创建暂存资源并用像素数据填充它开始,然后将其复制到我们将用于渲染的最终图像对象。我们将首先创建这个缓冲区并用像素值填充它,然后我们将创建一个图像来将像素复制到。
图像可以有不同的布局,这些布局会影响像素在内存中的组织方式。例如,由于图形硬件的工作方式,简单地逐行存储像素可能不会带来最佳性能。在对图像执行任何操作时,您必须确保它们具有最适合该操作的布局。
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
: 最适合展示VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
: 最适合作为从片段着色器写入颜色的附件VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
:最适合作为传输操作中的源,例如vkCmdCopyImageToBufferVK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
:最适合作为传输操作中的目的地,例如vkCmdCopyBufferToImageVK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
:最适合从着色器采样
图片库
有许多库可用于加载图像,您甚至可以编写自己的代码来加载 BMP 和 PPM 等简单格式。 stb_image 库。stb_image库实现都写在头文件中,不需要编译成库,项目中直接引用头文件目录即可。
GitHub - nothings/stb: stb single-file public domain libraries for C/C++
项目属性 ----> C/C++ —> 附加包含目录 —> your_path\stb-master
新建文件夹textures,放入图像texture.jpg,将其大小调整为 512 x 512 像素,但您可以随意选择您想要的任何图像。该库支持最常见的图像文件格式,如 JPEG、PNG、BMP 和 GIF。
包含图像库:
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
void createTextureImage()
{//创建一个新函数createTextureImage,我们将在其中加载图像并将其上传到 Vulkan 图像对象中
int texWidth, texHeight, texChannels;
stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
VkDeviceSize imageSize = texWidth * texHeight * 4;
//stbi_load函数将要加载的文件路径和通道数作为参数。该STBI_rgb_alpha值强制使用 alpha 通道加载图像
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);
纹理图像
虽然我们可以设置着色器来访问缓冲区中的像素值,但最好使用 Vulkan 中的图像对象来实现此目的。通过允许我们使用 2D 坐标,图像对象将使检索颜色变得更容易和更快。图像对象中的像素称为纹素,添加以下新类成员:
VkImage textureImage;
VkDeviceMemory textureImageMemory;
//图像的参数在VkImageCreateInfo结构中指定:
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
//imageType字段中指定的图像类型告诉 Vulkan 使用哪种坐标系来处理图像中的纹素
//创建 1D、2D 和 3D 图像
imageInfo.imageType = VK_IMAGE_TYPE_2D;
//该extent字段指定图像的尺寸,基本上是每个轴上有多少纹素
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;
imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
//该usage字段与缓冲区创建期间的语义相同。该图像将用作缓冲区副本的目标,因此应将其设置为传输目标。
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;//samples标志与多重采样有关
imageInfo.flags = 0; // Optional
if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) {
throw std::runtime_error("failed to create image!");
}
该tiling
字段可以具有以下两个值之一:
VK_IMAGE_TILING_LINEAR
pixels
: Texels 像我们的数组一样按行优先顺序排列VK_IMAGE_TILING_OPTIMAL
:纹理元素按照实现定义的顺序排列,以实现最佳访问- 如果您希望能够直接访问图像内存中的纹素,那么您必须使用
VK_IMAGE_TILING_LINEAR
initialLayout
图像的的 只有两个可能的值:
VK_IMAGE_LAYOUT_UNDEFINED
: GPU 不可用,第一个转换将丢弃纹素。VK_IMAGE_LAYOUT_PREINITIALIZED
: GPU 不可用,但第一个转换将保留纹素。
图像是用vkCreateImage创建的,它没有任何特别值得注意的参数。
VK_FORMAT_R8G8B8A8_SRGB
格式有可能不被图形硬件所支持。
为图像分配内存的方法与为缓冲区分配内存的方法完全相同。使用vkGetImageMemoryRequirements代替vkGetBufferMemoryRequirements,使用vkBindImageMemory代替vkBindBufferMemory 。
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);
这个函数已经变得相当大了,而且在后面的章节中还需要创建更多的图像,所以我们应该把图像创建抽象成一个createImage
函数,就像我们对缓冲区所做的那样。创建这个函数,并将图像对象的创建和内存分配移到它上面。
布局转换
我们现在要写的函数涉及到再次记录和执行一个命令缓冲区,所以现在是把这个逻辑移到一两个辅助函数中的好时机:
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);
}
创建一个新的函数来处理布局的转换:
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;
//如果你使用屏障来转移队列家族的所有权,那么这两个字段应该是队列家族的索引
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
//image和subresourceRange指定受影响的图像和图像的具体部分。
//我们的图像不是一个数组,也没有MIP映射层,所以只指定了一个level 和layer
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;
vkCmdPipelineBarrier(
commandBuffer,
0 /* TODO */, 0 /* TODO */,
0,
0, nullptr,
0, nullptr,
1, &barrier
);
//指定哪些涉及资源的操作类型必须在障碍物之前发生,哪些涉及资源的操作必须在障碍物上等待
endSingleTimeCommands(commandBuffer);
}
将缓冲区复制到图像上
就像缓冲区拷贝一样,你需要指定缓冲区的哪一部分将被拷贝到图像的哪一部分。这是通过VkBufferImageCopy结构实现的
void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) {
VkCommandBuffer commandBuffer = beginSingleTimeCommands();
VkBufferImageCopy region{};
region.bufferOffset = 0;
//bufferOffset “指定了像素值开始在缓冲区中的字节偏移。
//bufferRowLength”和 “bufferImageHeight”字段指定像素在内存中的排列方式
region.bufferRowLength = 0;
region.bufferImageHeight = 0;
//这两个字段中指定 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);
}
准备纹理图像
我们现在拥有完成设置纹理图像所需的所有工具,所以我们要回到createTextureImage
函数。我们在那里做的最后一件事是创建纹理图像。下一步是将暂存缓冲区复制到纹理图像上。这包括两个步骤。
- 将纹理图像过渡到
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
。 - 执行缓冲区到图像的复制操作
传输屏障掩码
如果你现在启用了验证层来运行你的应用程序,那么你会看到它抱怨transitionImageLayout
中的访问掩码和管道阶段是无效的。我们仍然需要根据过渡中的布局来设置这些。
有两个过渡我们需要处理。
- Undefined → transfer destination:传输写入,不需要等待任何东西
- Transfer destination → shader reading:着色器读取应该等待传输写入,特别是片段着色器中的着色器读取,因为那是我们要使用纹理的地方。
这些规则是用以下访问掩码和管线阶段指定的:
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;
//传输写入必须发生在流水线传输阶段。由于写操作不需要等待任何东西,
//你可以指定一个空的访问掩码和最早的流水线阶段VK_PIPELINE_STAGE_TOP_OF_PIPE_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;
//VK_PIPELINE_STAGE_TRANSFER_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
);
清除
完成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);
...
}
24 图像视图和采样器
纹理图像视图
我们之前已经看到,在交换链图像和帧缓冲区中,图像是通过图像视图而不是直接访问。我们也需要为纹理图像创建这样一个图像视图。
添加一个类成员,为纹理图像保存一个VkImageView,并创建一个新的函数createTextureImageView
,我们将在这里创建它:
VkImageView textureImageView;
//因为很多逻辑与createImageViews重复,你可能希望将其抽象为一个新的createImageView函数:
VkImageView createImageView(VkImage image, VkFormat format) {
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = format;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
VkImageView imageView;
if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
throw std::runtime_error("failed to create texture image view!");
}
return imageView;
}
采样器
着色器有可能直接从图像中读取纹理,但当它们被用作纹理时,这并不是很常见。纹理通常是通过采样器访问的,它将应用滤波和变换来计算被检索的最终颜色。
这些过滤器有助于处理诸如过采样的问题。考虑一个被映射到几何体上的纹理,它的碎片多于纹理。如果你只是在每个片段的纹理坐标中选择最接近的texel,那么你会得到像第一张图片那样的结果。如果你通过线性插值将4个最接近的texel结合起来,那么你会得到一个更平滑的结果
欠采样是一个相反的问题,即你的纹理比片段多。这将导致在对高频图案进行采样时出现伪影,比如在一个尖锐的角度对棋盘纹理进行采样。
除了这些滤镜之外,采样器还可以处理变换问题。它决定了当你试图通过它的addressing模式读取图像外的文本时会发生什么。下面的图片显示了一些可能性。
创建一个函数createTextureSampler
来设置这样一个采样器对象。稍后我们将使用这个采样器从着色器的纹理中读取颜色。
void createTextureSampler() {
//采样器是通过VkSamplerCreateInfo结构配置的,该结构指定了它应该应用的所有过滤器和转换。
VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
//magFilter和minFilter字段指定了如何插值被放大或缩小的文本。
//放大涉及到上面描述的过度取样问题,缩小涉及到欠取样问题
samplerInfo.magFilter = VK_FILTER_LINEAR;
samplerInfo.minFilter = VK_FILTER_LINEAR;
//寻址模式可以通过 addressMode 字段指定每个轴
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
//这两个字段指定是否应该使用各向异性过滤
samplerInfo.anisotropyEnable = VK_TRUE;
//maxAnisotropy “字段限制了可用于计算最终颜色的texel样本量。
//samplerInfo.maxAnisotropy = ???;
//一个较低的值会带来更好的性能,但质量较低。
//为了弄清我们可以使用哪个值,我们需要检索物理设备的属性:
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(physicalDevice, &properties);
samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy;
samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
//borderColor 字段指定了当用钳制边界寻址模式在图像之外取样时返回哪种颜色。
//可以返回黑色、白色或透明的浮点数或英寸格式
//unnormalizedCoordinates字段指定了你想用哪种坐标系统来处理图像中的纹理。
//如果这个字段是VK_TRUE,那么你可以简单地使用[0, texWidth)和[0, texHeight)范围内的坐标。
//如果它是VK_FALSE,那么在所有轴上都使用[0, 1)范围来处理texels。
//
samplerInfo.unnormalizedCoordinates = VK_FALSE;
samplerInfo.compareEnable = VK_FALSE;
samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
//如果启用了比较功能,那么texels将首先与一个值进行比较,而比较的结果将用于过滤操作
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
samplerInfo.mipLodBias = 0.0f;
samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = 0.0f;
}
注意,轴被称为U、V和W,而不是X、Y和Z,这是纹理空间坐标的惯例。
vk_sampler_address_mode_repeat
。当超出图像尺寸时重复纹理。vk_sampler_address_mode_mirrored_repeat
。和重复一样,但当超出尺寸时,将坐标倒置以镜像图像。vk_sampler_address_mode_clamp_to_edge
: 取最接近坐标的边缘的颜色,超出图像尺寸。vk_sampler_address_mode_mirror_clamp_to_edge
: 和钳制边缘一样,但使用与最接近的边缘相反的边缘。vk_sampler_address_mode_clamp_to_border
: 当取样超出图像的尺寸时,返回一个纯色。
采样器的功能现在已经完全定义了。添加一个类成员来保存采样器对象的句柄,用vkCreateSampler创建采样器:
VkSampler textureSampler;
if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) {
throw std::runtime_error("failed to create texture sampler!");
}
Anisotropy 设备特征
anisotropic filtering实际上是一个可选的设备功能
VkPhysicalDeviceFeatures deviceFeatures{};
deviceFeatures.samplerAnisotropy = VK_TRUE;
//而即使现代显卡不支持它的可能性很小,我们也应该更新isDeviceSuitable来检查它是否可用:
bool isDeviceSuitable(VkPhysicalDevice device) {
...
VkPhysicalDeviceFeatures supportedFeatures;
//vkGetPhysicalDeviceFeatures重新利用了VkPhysicalDeviceFeatures结构,通过设置布尔值来指示支持哪些功能,而不是要求哪些功能。
vkGetPhysicalDeviceFeatures(device, &supportedFeatures);
return indices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy;
}
25 组合图像采样器
组合图像采样器使得着色器可以通过采样器对象来访问图像资源。我们将首先修改描述符布局、描述符池和描述符集,以包括这样一个组合图像取样器描述符。之后,我们将为Vertex
添加纹理坐标,并修改片段着色器以从纹理中读取颜色,而不是仅仅插值顶点的颜色。
更新描述符
浏览createDescriptorSetLayout
函数,为组合图像采样器添加一个VkDescriptorSetLayoutBinding。我们将简单地把它放在统一缓冲区之后的绑定中:
VkDescriptorSetLayoutBinding samplerLayoutBinding{};
samplerLayoutBinding.binding = 1;
samplerLayoutBinding.descriptorCount = 1;
samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
samplerLayoutBinding.pImmutableSamplers = nullptr;
samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
//确保设置`stageFlags’以表明我们打算在片段着色器中使用组合图像采样器描述符
std::array<VkDescriptorSetLayoutBinding, 2> bindings = {uboLayoutBinding, samplerLayoutBinding};
VkDescriptorSetLayoutCreateInfo layoutInfo{};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
layoutInfo.pBindings = bindings.data();
我们还必须创建一个更大的描述符池,为组合图像采样器的分配腾出空间,在VkDescriptorPoolCreateInfo中添加另一个类型为VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
的VkPoolSize 。转到createDescriptorPool
函数,并修改它,为这个描述符包括一个VkDescriptorPoolSize:
std::array<VkDescriptorPoolSize, 2> poolSizes{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[0].descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
poolInfo.pPoolSizes = poolSizes.data();
poolInfo.maxSets = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
//最后一步是将实际的图像和采样器资源与描述符集中的描述符绑定。转到createDescriptorSets函数。
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffers[i];
bufferInfo.offset = 0;
bufferInfo.range = sizeof(UniformBufferObject);
VkDescriptorImageInfo imageInfo{};
imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
imageInfo.imageView = textureImageView;
imageInfo.sampler = textureSampler;
...
}
组合图像采样器结构的资源必须在VkDescriptorImageInfo结构中指定,就像统一缓冲区描述符的缓冲区资源在VkDescriptorBufferInfo结构中指定。
纹理坐标系
纹理映射还缺少一个重要成分,那就是每个顶点的实际坐标。坐标决定了图像是如何实际映射到几何体上的。
glm::vec2 texCoord;
const std::vector<Vertex> vertices = {
{{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}},
{{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
{{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
{{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}}
};
将通过使用左上角的 0, 0
到右下角的 1, 1
的坐标来简单地用纹理填充正方形。请随意尝试不同的坐标。试着使用低于0
或高于1
的坐标,看看寻址模式是如何运作的!
着色器
最后一步是修改着色器以从纹理中提取颜色。我们首先需要修改顶点着色器,将纹理坐标传递给片段着色器:不要忘记重新编译着色器!
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec2 inTexCoord;
layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec2 fragTexCoord;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
}
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec2 fragTexCoord;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragTexCoord, 0.0, 1.0);
}
绿色通道代表水平坐标,红色通道代表垂直坐标。黑角和黄角确认纹理坐标在整个广场上从 0,0
到1,1
正确插值。
一个组合的图像采样器描述符在GLSL中由一个采样器统一表示。在片段着色器中添加一个对它的引用:
layout(binding = 1) uniform sampler2D texSampler;
void main() {
outColor = texture(texSampler, fragTexCoord);
outColor = texture(texSampler, fragTexCoord * 2.0);
outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0);
}
纹理是使用内置的texture
函数进行采样的。它需要一个 “采样器”和坐标作为参数。采样器会在后台自动处理过滤和转换的问题。现在当你运行应用程序时,你应该看到广场上的纹理了。