Vulkan Tutorial 7 纹理贴图
目录
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; // Optionalif (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 450layout(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
函数进行采样的。它需要一个 “采样器”和坐标作为参数。采样器会在后台自动处理过滤和转换的问题。现在当你运行应用程序时,你应该看到广场上的纹理了。
相关文章:

Vulkan Tutorial 7 纹理贴图
目录 23 图像 图片库 暂存缓冲区 纹理图像 布局转换 将缓冲区复制到图像上 准备纹理图像 传输屏障掩码 清除 24 图像视图和采样器 纹理图像视图 采样器 Anisotropy 设备特征 25 组合图像采样器 更新描述符 纹理坐标系 着色器 23 图像 添加纹理将涉及以下步骤&am…...
LinkedBlockingQueue阻塞队列
➢ LinkedBlockingQueue阻塞队列 LinkedBlockingQueue类图 LinkedBlockingQueue 中也有两个 Node 分别用来存放首尾节点,并且里面有个初始值为 0 的原子变量 count 用来记录队列元素个数,另外里面有两个ReentrantLock的独占锁,分别用来控制…...
面试-Redis 常见问题,后续面试遇到新的在补充
面试-Redis 1.谈谈Redis 缓存穿透,击穿,雪崩及如何避免 缓存穿透:是指大量访问请求在访问一个不存在的key,由于key 不存在,就会去查询数据库,数据库中也不存在该数据,无法将数据存储到redis 中…...

2023年上半年数据库系统工程师上午真题及答案解析
1.计算机中, 系统总线用于( )连接。 A.接口和外设 B.运算器、控制器和寄存器 C.主存及外设部件 D.DMA控制器和中断控制器 2.在由高速缓存、主存和硬盘构成的三级存储体系中,CPU执行指令时需要读取数据,那么DMA控制器和中断CPU发出的数据地…...
设计模式概念
设计模式是软件工程领域中常用的解决问题的经验总结和最佳实践。它们提供了一套被广泛接受的解决方案,用于处理常见的设计问题,并促进可重用、可扩展和易于维护的代码。 设计模式的主要目标是提高软件的可重用性、可扩展性和灵活性,同时降低…...
arcpy批量对EXCE经纬度L进行投点,设置为wgs84坐标系,并利用该点计算每个区域内的核密度
以下是在 ArcPy 中批量对 Excel 经纬度 L 进行投点,设置为 WGS84 坐标系,并利用该点计算每个区域内的核密度的详细步骤: 1. 准备数据: 准备包含经纬度信息的 Excel 数据表格,我们假设文件路径为 "C:/Data/locations.xlsx&qu…...

Yolov5训练自己的数据集
先看下模型pt说明 YOLOv5s:这是 YOLOv5 系列中最小的模型。“s” 代表 “small”(小)。该模型在计算资源有限的设备上表现最佳,如移动设备或边缘设备。YOLOv5s 的检测速度最快,但准确度相对较低。 YOLOv5m࿱…...

Bert+FGSM中文文本分类
我上一篇博客已经分别用BertFGSM和BertPGD实现了中文文本分类,这篇文章与我上一篇文章BertFGSM/PGD实现中文文本分类(Loss0.5L10.5L2)_Dr.sky_的博客-CSDN博客的不同之处在于主要在对抗训练函数和embedding添加扰动部分、模型定义部分、Loss函数传到部分…...
爬楼梯问题-从暴力递归到动态规划(java)
爬楼梯,每次只能爬一阶或者两阶,计算有多少种爬楼的情况 爬楼梯--题目描述暴力递归递归缓存动态规划暴力递归到动态规划专题 爬楼梯–题目描述 一个总共N 阶的楼梯(N > 0) 每次只能上一阶或者两阶。问总共有多少种爬楼方式。 示…...
浏览器如何验证SSL证书?
浏览器如何验证SSL证书?当前SSL证书应用越来越广泛,我们看见的HTTPS网站也越来越多。点击HTTPS链接签名的绿色小锁,我们可以看见SSL证书的详细信息。那么浏览器是如何验证SSL证书的呢? 浏览器如何验证SSL证书? 在浏览器的菜单中…...
Linux :: 【基础指令篇 :: 文件及目录操作:(10)】:: ll 指令 :: 查看指定目录下的文件详细信息
前言:本篇是 Linux 基本操作篇章的内容! 笔者使用的环境是基于腾讯云服务器:CentOS 7.6 64bit。 学习集: C 入门到入土!!!学习合集Linux 从命令到网络再到内核!学习合集 目录索引&am…...
Java字符集/编码集
1 字符集/编码集 基础知识 计算机中储存的信息都是用二进制数表示的;我们在屏幕上看到的英文、汉字等字符是二进制数转换之后的结果 按照某种规则, 将字符存储到计算机中,称为编码。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码。这里强调一下: 按照…...

Apache配置与应用
目录 虚拟web主机httpd服务支持的虚拟主机类型基于域名配置方法基于IP配置方法基于端口配置方法 apache连接保持构建Web虚拟目录与用户授权限制Apache日志分割 虚拟web主机 虚拟Web主机指的是在同一台服务器中运行多个Web站点,其中每一个站点实际上并不独立占用整个…...

API自动化测试【postman生成报告】
PostMan生成测试报告有两种: 1、控制台的模式 2、HTML的测试报告 使用到一个工具newman Node.js是前端的一个组件,主要可以使用它来开发异步的程序。 一、控制台的模式 1、安装node.js 双击node.js进行安装,安装成功后在控制台输入node …...
探索OpenAI插件:ChatWithGit,memecreator,boolio
引言 在当今的技术世界中,插件扮演着至关重要的角色,它们提供了一种简单有效的方式来扩展和增强现有的软件功能。在本文中,我们将探索三个OpenAI的插件:ChatWithGit,memecreator,和boolio,它们…...
linux irq
中断上下部 软中断、tasklet、工作对列 软中断优点:运行在软中断上下文,优先级比普通进程高,调度速度快。 缺点:由于处于中断上下文,所以不能睡眠。 相对于软中断/tasklet,工作对列运行在进程上下文 h…...
串口流控(CTS/RTS)使用详解
1.流控概念 在两个设备正常通信时,由于处理速度不同,就存在这样一个问题,有的快,有的慢,在某些情况下,就可能导致丢失数据的情况。 如台式机与单片机之间的通讯,接收端数据缓冲区已满࿰…...

kube-proxy模式详解
1 kube-proxy概述 kubernetes里kube-proxy支持三种模式,在v1.8之前我们使用的是iptables 以及 userspace两种模式,在kubernetes 1.8之后引入了ipvs模式,并且在v1.11中正式使用,其中iptables和ipvs都是内核态也就是基于netfilter&…...

汽车EDI:如何与Stellantis建立EDI连接?
Stellantis 是一家实力雄厚的汽车制造公司,由法国标致雪铁龙集团(PSA集团)和意大利菲亚特克莱斯勒汽车集团(FCA集团)合并而成,是世界上第四大汽车制造商,拥有包括标致、雪铁龙、菲亚特、克莱斯勒…...
【SCI征稿】1区计算机科学类SCI, 自引率低,对国人友好~
一、【期刊简介】 JCR1区计算机科学类SCI&EI 【期刊概况】IF: 7.0-8.0,JCR1区,中科院2区; 【终审周期】走期刊系统,3-5个月左右录用; 【检索情况】SCI&EI双检; 【自引率】1.30% 【征稿领域】发表人工智能…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
React hook之useRef
React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...

CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
基础测试工具使用经验
背景 vtune,perf, nsight system等基础测试工具,都是用过的,但是没有记录,都逐渐忘了。所以写这篇博客总结记录一下,只要以后发现新的用法,就记得来编辑补充一下 perf 比较基础的用法: 先改这…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...

Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...
IP如何挑?2025年海外专线IP如何购买?
你花了时间和预算买了IP,结果IP质量不佳,项目效率低下不说,还可能带来莫名的网络问题,是不是太闹心了?尤其是在面对海外专线IP时,到底怎么才能买到适合自己的呢?所以,挑IP绝对是个技…...