Vulkan Tutorial 7 纹理贴图

news2024/10/7 8:22:16

目录

23 图像

图片库

 暂存缓冲区

        纹理图像

布局转换

将缓冲区复制到图像上

准备纹理图像

传输屏障掩码

清除

24  图像视图和采样器

纹理图像视图

采样器

Anisotropy 设备特征

 25 组合图像采样器

更新描述符

纹理坐标系

着色器


23 图像

添加纹理将涉及以下步骤:

  • 创建一个由设备内存支持的图像对象
  • 用图像文件中的像素填充它
  • 创建图像采样器
  • 添加组合图像采样器描述符以从纹理中采样颜色

我们之前已经使用过图像对象,但它们是由交换链扩展自动创建的。这次我们必须自己创建一个。创建图像并用数据填充它类似于创建顶点缓冲区。我们将从创建暂存资源并用像素数据填充它开始,然后将其复制到我们将用于渲染的最终图像对象。我们将首先创建这个缓冲区并用像素值填充它,然后我们将创建一个图像来将像素复制到。

图像可以有不同的布局,这些布局会影响像素在内存中的组织方式。例如,由于图形硬件的工作方式,简单地逐行存储像素可能不会带来最佳性能。在对图像执行任何操作时,您必须确保它们具有最适合该操作的布局。

  • 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:最适合从着色器采样

图片库

有许多库可用于加载图像,您甚至可以编写自己的代码来加载 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_LINEARpixels: 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, &copyRegion);

    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,
    &region
);

    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,01,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函数进行采样的。它需要一个 “采样器”和坐标作为参数。采样器会在后台自动处理过滤和转换的问题。现在当你运行应用程序时,你应该看到广场上的纹理了。

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/588888.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何快速手撕单例类?一文教会你

&#x1f468;‍&#x1f393;作者&#xff1a;bug菌 ✏️博客&#xff1a;CSDN、掘金、infoQ、51CTO等 &#x1f389;简介&#xff1a;CSDN|阿里云|华为云|51CTO等社区博客专家&#xff0c;历届博客之星Top30&#xff0c;掘金年度人气作者Top40&#xff0c;51CTO年度博主Top12…

数据结构与算法基础-学习-24-遍历之DFS(深度优先搜索)和BFS(广度优先搜索)

目录 一、遍历定义 二、遍历实质 三、DFS 四、BFS 五、宏定义 六、自定义类型 七、函数实现 1、DFS&#xff08;邻接矩阵实现&#xff09; 2、DFS&#xff08;邻接表实现&#xff09; 3、BFS&#xff08;邻接矩阵实现&#xff09; 4、BFS&#xff08;邻接表实现&…

在idea中创建maven

说明&#xff1a;maven是一款管理和构建java项目的工具&#xff0c;使用maven&#xff0c;可规范开发&#xff0c;提高开发效率&#xff1b;maven的安装参考&#xff1a;http://t.csdn.cn/623Ah 配置Maven环境 创建maven&#xff0c;先要做准备工作&#xff0c;把idea中的环境…

带你开发一个远程控制项目---->STM32+标准库+阿里云平台+传感器模块+远程显示-------之 MQTT连接阿里云平台

第一篇&#xff1a; (13条消息) 带你开发一个远程控制项目----&#xff1e;STM32标准库阿里云平台传感器模块远程显示。_海口飞鹏岛科技有限公司的博客-CSDN博客 第二篇&#xff1a; (13条消息) 带你开发一个远程控制项目----&#xff1e;STM32标准库阿里云平台传感器模块远程…

C语言---数组

1、一维数组的创建和初始化 1.1、数组的创建 数组是一组相类型元素的集合。 数组的创建方式&#xff1a; type_t arr_name [const_n];//type_t 是指数组的元素类型 //const_n 是一个常量表达式&#xff0c;用来指定数组的大小。1.2、数组的初始化 数组的初始化是指&#x…

国产替代10BASE-T ST7010QNL 应用局域网的以太网变压器/扼流器

Hqst华强盛导读&#xff1a; 华强盛是电子产品国产替代大军中的一员&#xff0c;随着中国电子产业的快速发展&#xff0c;越来越多的电子产品开始出现了国产替代品。这些国产替代品在性能、品质和价格等方面都有了显著的提升&#xff0c;成为了工厂用户的首选。 国产替代10BAS…

DERT(DEtection TRansformer) ONNX直接推理!!

目录 1.前言 2. ONNX模型 (1) backbone使用的是resnet50 (2) Transformer结构 (3)模型输出 3.代码展示(不收费&#xff01;&#xff01;&#xff01;) 4.结果展示 5.源代码地址 1.前言 DETR DETR的全称是DEtection TRansformer&#xff0c;是Facebook提出的基于…

c++实现【典型的旅行商问题(TSP)】实现配送中心最多可以用2辆车对8个客户进行运输配送

假定配送中心最多可以用2辆车对8个客户进行运输配送。每个车辆载重均 为8吨,车辆每次配送的最大行驶距离为50km,配送中心(编号0)与8个客 户之间及8个客户相互之间的距离d; (i, j= 1, 2, ... 8)、8个客户的货物需 求r;(j= 1, 2... 8)如表1所示。要求寻找一条路径, 使得配送总…

Codeforces Div.2 1798B Three Sevens题解

题目&#xff1a; 传送门 B. Three Sevens time limit per test 2 seconds memory limit per test 256 megabytes input standard input output standard output Lottery "Three Sevens" was held for m days. On day i, ni people with the numbers ai,1…

生态系统NPP及碳源、碳汇模拟——土地利用变化、未来气候变化、空间动态模拟

由于全球变暖、大气中温室气体浓度逐年增加等问题的出现&#xff0c;“双碳”行动特别是碳中和已经在世界范围形成广泛影响。碳中和可以从碳排放&#xff08;碳源&#xff09;和碳固定&#xff08;碳汇&#xff09;这两个侧面来理解。陆地生态系统在全球碳循环过程中有着重要作…

手动计算校正年龄、性别后的标准化死亡率 (SMR)

分析队列人群有无死亡人数超额&#xff0c;通常应用标准人群死亡率来校正&#xff0c;即刻观察到中的实际死亡数&#xff08;D&#xff09;与定一个标准的死亡人数&#xff08;E&#xff09;&#xff0c;D与E之比称为死亡比&#xff08;standarized Mortality ratio&#xff0c…

运筹学-单纯形法

一、单纯形法的求解思路 单纯形法求解线性规划的思路&#xff1a;在高斯消去法的基础上&#xff0c;发展为求解变量数大于方程数&#xff0c;并且使目标函数值优化的方法。从线性方程中找到一个个的单纯形&#xff0c;每个单纯形&#xff08;图形的顶点&#xff09;可以得到一组…

支付宝 小程序 抽奖组件 大转盘

介绍 使用支付宝原有的大转盘营销组件进行改造的&#xff0c;由于背景使用的图片&#xff0c;目前只支持 6 个奖品&#xff0c;一般情况下的大转盘都是这个规格。 转盘停止&#xff1a;之前使用的是计算角度来完成的&#xff0c;没有那种缓慢停止的动画。现在加了一个缓慢停止…

Android实现皮肤主题修改

最近在做App内皮肤切换功能&#xff0c;想了很久方案&#xff0c;写了个皮肤更换工具类&#xff0c;适配N皮肤种类。 话不多说&#xff0c;直接捋一下我的设计思路&#xff0c;因为我的App默认为黑色主题&#xff0c;因此在做其他皮肤主题时&#xff0c;我的图片命名方式是直接…

Fastjson核心解析器DefaultJSONParser,解析算法递归下降算法,实例解析json的步骤

先恭喜热火没有在3-0的情况下被凯尔特人翻盘&#xff0c;抢七获胜成功晋级总决赛~ 最近的项目用到了fastjson&#xff0c;因为源码比较容易搞到&#xff0c;所以就拿来简单的了解了一下&#xff0c;json的主要功能就是解析json和生成json字符串&#xff0c;今天主要是从解析jso…

基于vue3.0简单的页面使用

基于vue3.0简单的页面使用 项目效果图项目文件图package.jsonmain.jsApp.vueviews/Tutorial.vueviews/TS.vueviews/Docs.vueviews/Community.vueviews/Blog.vueviews/About.vueutils/create.jsxutils/defineCom.jsutils/DragIcon.jsutils/someName.tsutils/TS.tsstores/client.…

win11任务栏时间改成12时制

需求&#xff1a;默认24小时值&#xff0c;想改成12小时3:49 方法&#xff1a;设置-时间和语言-语言和区域-管理语言设置-格式 将时间格式改成带tt的

2022年长三角高校数学建模竞赛C题隧道的升级改造与设计解题全过程文档及程序

2022年长三角高校数学建模竞赛 C题 隧道的升级改造与设计 原题再现&#xff1a; 某地现存一旧式双洞隧道&#xff0c;现计划将该隧道在旧貌基础上升级改造。在升级改造前&#xff0c;需进行定标与设计。考虑到该隧道洞壁附着特殊涂料&#xff0c;无人机在洞内通信信号较差&am…

网络面试题:什么是 TCP/IP?

目录标题 什么是 TCP/IP?1) 网络接口层:2) 网络层:3) 传输层:4) 应用层: 2.数据包3.网络接口层4.网络层1) IP:2)地址解析协议 ARP3)子网 5 传输层1&#xff09;UDP&#xff1a;2&#xff09;TCP&#xff1a; 6 应用层运行在TCP协议上的协议&#xff1a;运行在UDP协议上的协议&…

Netty 实现百万级连接服务的难点和优点分析总结

推送服务 还记得一年半前&#xff0c;做的一个项目需要用到 Android 推送服务。和 iOS 不同&#xff0c;Android 生态中没有统一的推送服务。Google 虽然有 Google Cloud Messaging &#xff0c;但是连国外都没统一&#xff0c;更别说国内了&#xff0c;直接被墙。 所以之前在…