没有任何秘密的 API:Vulkan* 简介第 6 部分

下载

查看 PDF [736 KB]

Back to: 第 5 部分分期资源


教程 6:描述符集 – 在着色器中使用纹理

我们知道如何创建显卡管线以及如何使用着色器在屏幕上绘制几何体。我们也学会了如何创建缓冲区并将它们用作顶点数据的来源(顶点缓冲区)。现在我们需要了解如何为着色器提供数据 — 我们将学习如何使用资源,比如着色器源代码中的采样器和图像,以及如何设置应用与可编程着色器阶段之间的界面。

在本教程中,我们将重点介绍类似于 OpenGL* 纹理的功能。但 Vulkan* 中没有此类对象。只有两种可以保存数据的资源:缓冲区和图像(还有 push constant,我们将通过单独的教程进行介绍)。它们均可提供给着色器,但需要调用资源描述符,不能直接提供给着色器。事实上,它们聚集在包装程序或称为描述符集的容器对象中。我们可在单个描述符集中放置多个资源,但需要按照这种集合的预定义结构。这种结构定义单个描述符集的内容 — 其中的资源类型、每种资源的数量,以及它们的顺序。在名为描述符集布局的对象中指定这类描述。编写着色器指示时需要指定类似的描述。它们共同组成 API(我们的应用)和可编程管线(着色器)之间的界面。

准备好布局和创建描述符集后,我们可以对其进行填充;这样可定义我们希望在着色器中使用的具体对象(缓冲区和/或图像)。之后,在发布命令缓冲区中的绘制命令之前,我们需要绑定此集合与命令缓冲区。这样我们可以使用着色器源代码中的资源;例如,从采样的图像(纹理)提取数据,或读取保存在统一缓冲区中的统一变量值。

在本部分教程中,我们将了解如何创建描述符集布局和描述符集本身。还将准备采样器和图像,以便将其制作成着色器中的纹理。我们还将了解如何在着色器中使用它们。

如前所述,本教程根据没有任何秘密的 API:Vulkan 简介教程的前面几部分所介绍的知识,仅介绍与所述主题的不同之处及重要之处。

创建图像

我们首先创建将来用作纹理的图像。图像代表连续内存区,将根据图像创建期间定义的规则进行编译。Vulkan 中仅有三种基本图像:1D、2D 和 3D。图像可以有 mipmap(细节层)、多个阵列层(要求至少一个),或每帧采样数。所有这些参数均在图像创建期间指定。在代码示例中,我们创建最常用的 2D 图像,包含每像素一个样本和 4 个 RGBA 组件。

VkImageCreateInfo image_create_info = {
  VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,  // VkStructureType        sType;
  nullptr,                              // const void            *pNext
  0,                                    // VkImageCreateFlags     flags
  VK_IMAGE_TYPE_2D,                     // VkImageType            imageType
  VK_FORMAT_R8G8B8A8_UNORM,             // VkFormat               format
  {                                     // VkExtent3D             extent
    width,                                // uint32_t               width
    height,                               // uint32_t               height
    1                                     // uint32_t               depth
  },
  1,                                    // uint32_t               mipLevels
  1,                                    // uint32_t               arrayLayers
  VK_SAMPLE_COUNT_1_BIT,                // VkSampleCountFlagBits  samples
  VK_IMAGE_TILING_OPTIMAL,              // VkImageTiling          tiling
  VK_IMAGE_USAGE_TRANSFER_DST_BIT |     // VkImageUsageFlags      usage
  VK_IMAGE_USAGE_SAMPLED_BIT,
  VK_SHARING_MODE_EXCLUSIVE,            // VkSharingMode          sharingMode
  0,                                    // uint32_t               queueFamilyIndexCount
  nullptr,                              // const uint32_t        *pQueueFamilyIndices
  VK_IMAGE_LAYOUT_UNDEFINED             // VkImageLayout          initialLayout
};

return vkCreateImage( GetDevice(), &image_create_info, nullptr, image ) == VK_SUCCESS;

1.Tutorial06.cpp, function CreateImage()

创建图像时我们需要准备 VkImageCreateInfo 类型的结构。该结构包含创建图像所需的基本参数集。这个参数可通过以下成分来指定:

  • sType – 结构类型。必须等于 VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO 的值。
  • pNext – 为扩展功能预留的指示器。
  • flags – 描述图像其他属性的参数。通过该参数,我们可规定通过稀疏内存备份该图像。但有一个更有趣的值:VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT,支持我们将图像用作立方图。如果没有其他要求,可将该参数设为 0。
  • imageType – 图像的基本类型(维数):1D、2D 或 3D。
  • format – 图像格式:组件数量、每个组件的位数,以及数据类型。
  • extent – 每种维度下的图像大小(纹素/像素数量)。
  • mipLevels – 细节层数量 (mipmap)。
  • arrayLayers – 阵列层数量。
  • samples – 每纹数样本数量(正常图像为 1,多样本图像大于 1)。
  • tiling – 定义图像的内层内存结构:线性或最佳。
  • usage – 定义我们希望该图像在整个生命周期中的所有用途。
  • sharingMode – 规定是否每次从多个家族中排队访问该图像(与创建交换链或缓冲区时所使用的的 sharingMode 参数相同。)
  • queueFamilyIndexCount – pQueueFamilyIndices 阵列中的元素数量(仅指定并发共享模式时使用)。
  • pQueueFamilyIndices – 所有队列的索引阵列(队列通过它访问图像)(仅指定并发共享模式时使用)。
  • initialLayout – 用于创建图像的内存布局。我们仅提供未定义或预初始化布局。在命令缓冲区中使用图像之前,我们还需要进行布局过渡。

图像创建期间定义的大部分参数都具备自解释性,或类似于创建其他资源时所使用的参数。但这些参数还需要进一步的解释。

区块指图像的内层内存结构(但不可与布局混淆)。图像可能包含线性或最佳区块(缓冲区通常包含线性区块)。包含线性区块的图像以线性的方式布局纹素,一个接一个,一排接一排。我们可以查询所有相关图像的内存参数(偏移和大小、行、阵列和深度步长)。这样我们可知道图像内容如何保存在内存之中。此类区块可用于(通过映射图像内存)直接将数据拷贝至图像。遗憾的是,包含线性区块的图像存在多种限制。例如,Vulkan 规格规定仅 2D 图像必须支持线性区块。硬件厂商可能在其他类型的图形中实施线性区块支持,但不是强制性的,所以我们不依赖此类支持。但更重要的是,线性区块图像的性能不及其他同类最佳区块图像。

当我们指定图像最佳区块,意味着我们不了解内存的结构。执行应用的每种平台都可能以完全不同的方式保存图像内容,在但实际应用中并不能映射图像内存,也不能直接将图像拷贝至 CPU 或从 CPU 拷贝图像(需要使用分期资源,缓冲区或图像)。但这样我们可以创建任何想要的图像(不存在类似线性区块图像的限制),而且应用也能实现更高的性能。因此强烈建议经常指定图像最佳区块。

现在我们重点介绍 initialLayout 参数。如前关于交换链的教程所述,布局规定图像的内存布局,并与我们使用图像的方式有着密切的关系。每种特定用途都有其自己的内存布局。以特定方式使用图像之前,需要执行布局过渡。例如,交换链图像仅以 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 的布局在屏幕上显示。如果想渲染成图像,需要将其内存布局设为 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL。还有一种通用布局,允许我们以任何方式使用图像,但会影响性能,不建议使用这种布局(仅在必要时使用)。

现在,如果我们想更换图像的使用方式,需要执行上面所说的布局过渡。必须指定当前的(旧)布局和新布局。旧布局包含 1-2 个值:当前图像布局或未定义布局。指定当前图像布局的值时,图像内容在过渡期间保存。但如果不需要图像内容,我们可以提供未定义布局。这样布局过渡的速度更快。

此时将用到 initialLayout 参数。我们指定 1-2 个值 — 未定义或预初始化。预初始化布局值帮助我们在图像的第一次布局过渡期间保存图像内容。这样我们可以通过内存映射将数据拷贝至图像,但这一做法并不实用。可以直接(通过内存映射)将数据拷贝至线性区块图像,如前所述这样存在诸多限制。实际上来说,这些图像只能用作分期资源 — 在 GPU 和 CPU 之间传输数据。我们也可以使用缓冲区传输数据;因为使用缓冲区拷贝数据比使用线性区块图像更简单。

总而言之,在大部分情况下,未定义布局可用于 initialLayout 参数。在这种情况下,图像内容不能直接(通过映射内存)初始化。但如果我们想初始化,可以使用临时缓冲区将数据拷贝至图像。本教程将介绍这种方法。

最后一点是记住这种用法。与缓冲区类似,创建图像时,需要指定用于使用图像的所有方法。之后不能更改,也不能以创建过程中没有指定的方法使用图像。这里我们想在着色器中将图像用作纹理。为此我们指定 VK_IMAGE_USAGE_SAMPLED_BIT 用途。还需要将数据上传至图像的方法。我们将从图像文件中读取数据,并将其拷贝至图像对象。具体方法是使用分期资源传输数据。在这种情况下,图像将成为传输操作对象,因此我们指定 VK_IMAGE_USAGE_TRANSFER_DST_BIT 用途。

现在,如果我们有所有参数的值,将可以创建图像。具体做法是,调用 vkCreateImage() 函数,我们需要为该函数提供逻辑设备句柄、上述结构指示器,以及 VkImage 类型变量指示器(其中保存有已创建图像的句柄)。

分配图像内存

与缓冲区类似,图像没有自己的内存,因此使用之前,需要绑定内存和图像。为此,我们首先需要了解待绑定至图像的内存的属性。为此我们调用 vkGetImageMemoryRequirements() 函数。

VkMemoryRequirements image_memory_requirements;
vkGetImageMemoryRequirements( GetDevice(), Vulkan.Image.Handle, &image_memory_requirements );

2.Tutorial06.cpp, function AllocateImageMemory()

上述调用将所需内存参数保存在 image_memory_requirements 变量中。它将告诉我们所需的内存量以及特定物理设备支持的哪种内存可用于图像内存分配。如果不知道特定物理设备支持的内存类型,可调用vkGetPhysicalDeviceMemoryProperties() 函数了解相关信息。之前的教程对此进行了介绍,当时我们为缓冲区分配内存。接下来,迭代可用内存类型并检查哪些兼容我们的图像。

for( uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i ) {
  if( (image_memory_requirements.memoryTypeBits & (1 << i)) &&
    (memory_properties.memoryTypes[i].propertyFlags & property) ) {

    VkMemoryAllocateInfo memory_allocate_info = {
      VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, // VkStructureType  sType
      nullptr,                                // const void      *pNext
      image_memory_requirements.size,         // VkDeviceSize     allocationSize
      i                                       // uint32_t         memoryTypeIndex
    };

    if( vkAllocateMemory( GetDevice(), &memory_allocate_info, nullptr, memory ) == VK_SUCCESS ) {
      return true;
    }
  }
}
return false;

3.Tutorial06.cpp, function AllocateImageMemory()

每种类型都有特定的属性集。如果想绑定内存和图像,也有我们自己的具体要求。例如,我们可能需要通过映射直接访问内存,因此该内存必须主机可见。如果有其他要求,可对比每种可用内存的属性。如果发现有匹配的属性,可以使用特定内存类型并通过调用 vkAllocateMemory() 函数分配内存对象。

然后需要绑定内存和图像。为此我们调用 vkBindImageMemory() 函数并提供与内存绑定的图像的句柄、内存对象的句柄,以及从内存对象开始的偏移,比如:

if( vkBindImageMemory( GetDevice(), Vulkan.Image.Handle, Vulkan.Image.Memory, 0 ) != VK_SUCCESS ) {
  std::cout << "Could not bind memory to an image!"<< std::endl;
  return false;
}

4.Tutorial06.cpp, function CreateTexture()

绑定内存和对象时偏移值非常重要。Vulkan 中的资源对内存偏移对齐有特定的要求。image_memory_requirements 变量也提供了具体的要求。绑定内存时提供的偏移必须是变量的对齐成分的倍数。0 通常是有效的偏移值。

当然,如果我们想绑定内存和图像,不需要每次创建新的内存对象。最好创建一少部分比较大的内存对象,并通过提供相应的偏移值绑定其中的一部分。

创建图像视图

如果想在应用中使用图像,我们很少提供图像的句柄,而通常使用图像视图。它们提供一个额外层以解析将在特定环境中使用的图像内容。例如,我们有一个多层图像(2D 阵列),想仅渲染至特定的阵列层。为此我们创建一个图像视图,以便定义我们想使用的那层。另外一个例子是包含 6 个阵列层的图像。我们可以使用图像视图,将其解析成立方图。

Vulkan 简介第 3 部分:第一个三角形介绍过如何创建图像视图,因此这部分仅提供所使用的源代码。

VkImageViewCreateInfo image_view_create_info = {
  VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, // VkStructureType          sType
  nullptr,                                  // const void              *pNext
  0,                                        // VkImageViewCreateFlags   flags
  image_parameters.Handle,                  // VkImage                  image
  VK_IMAGE_VIEW_TYPE_2D,                    // VkImageViewType          viewType
  VK_FORMAT_R8G8B8A8_UNORM,                 // VkFormat                 format
  {                                         // VkComponentMapping       components
    VK_COMPONENT_SWIZZLE_IDENTITY,            // VkComponentSwizzle       r
    VK_COMPONENT_SWIZZLE_IDENTITY,            // VkComponentSwizzle       g
    VK_COMPONENT_SWIZZLE_IDENTITY,            // VkComponentSwizzle       b
    VK_COMPONENT_SWIZZLE_IDENTITY             // VkComponentSwizzle       a
  },
  {                                         // VkImageSubresourceRange  subresourceRange
    VK_IMAGE_ASPECT_COLOR_BIT,                // VkImageAspectFlags       aspectMask
    0,                                        // uint32_t                 baseMipLevel
    1,                                        // uint32_t                 levelCount
    0,                                        // uint32_t                 baseArrayLayer
    1                                         // uint32_t                 layerCount
  }
};

return vkCreateImageView( GetDevice(), &image_view_create_info, nullptr, &image_parameters.View ) == VK_SUCCESS;

5.Tutorial06.cpp, function CreateImageView()

将数据拷贝至图像

现在我们需要将数据拷贝至图像。我们使用临时缓冲区来完成这一步骤。首先创建一个足以保存图像数据的缓冲区。接下来分配主机可见(可映射)的内存,并将其绑定至缓冲区。然后将数据拷贝至缓冲区的内存,如下所示:

// Prepare data in staging buffer
void *staging_buffer_memory_pointer;
if( vkMapMemory( GetDevice(), Vulkan.StagingBuffer.Memory, 0, data_size, 0, &staging_buffer_memory_pointer ) != VK_SUCCESS ) {
  std::cout << "Could not map memory and upload texture data to a staging buffer!"<< std::endl;
  return false;
}

memcpy( staging_buffer_memory_pointer, texture_data, data_size );

VkMappedMemoryRange flush_range = {
  VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,  // VkStructureType   sType
  nullptr,                                // const void       *pNext
  Vulkan.StagingBuffer.Memory,            // VkDeviceMemory    memory
  0,                                      // VkDeviceSize      offset
  data_size                               // VkDeviceSize      size
};
vkFlushMappedMemoryRanges( GetDevice(), 1, &flush_range );

vkUnmapMemory( GetDevice(), Vulkan.StagingBuffer.Memory );

6.Tutorial06.cpp, function CopyTextureData()

我们映射缓冲区的内存。此操作为我们提供一个指示器,其用法与其他 C++ 指示器相同。我们为其拷贝纹理数据,并告知驱动程序在该操作中(我们刷新内存)哪部分缓冲区的内存有所变化。最后,取消内存映射,但这不是必需的步骤。

通过以下代码从文件中读取图像数据:

int width = 0, height = 0, data_size = 0;
std::vector texture_data = Tools::GetImageData( "Data06/texture.png", 4, &width, &height, nullptr, &data_size );
if( texture_data.size() == 0 ) {
  return false;
}

if( !CopyTextureData( &texture_data[0], data_size, width, height ) ) {
  std::cout << "Could not upload texture data to device memory!"<< std::endl;
  return false;
}

7.Tutorial06.cpp, function CreateTexture()

为实现本教程的目的,我们将下图用作纹理:

Image of a large semi truck with intel logo on the side, speeding down the road

 

将数据从缓冲区拷贝至图像这一操作要求记录命令缓冲区并将其提交至队列。调用 vkBeginCommandBuffer() 函数开始记录:

// Prepare command buffer to copy data from staging buffer to a vertex buffer
VkCommandBufferBeginInfo command_buffer_begin_info = {
  VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,  // VkStructureType                        sType
  nullptr,                                      // const void                            *pNext
  VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,  // VkCommandBufferUsageFlags              flags
  nullptr                                       // const VkCommandBufferInheritanceInfo  *pInheritanceInfo
};

VkCommandBuffer command_buffer = Vulkan.RenderingResources[0].CommandBuffer;

vkBeginCommandBuffer( command_buffer, &command_buffer_begin_info);

8.Tutorial06.cpp, function CopyTextureData()

在记录命令缓冲区一开始,需要对图像执行布局过渡。我们想将数据拷贝至图像,因此要将布局改成 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL。我们需明确执行这一操作,具体做法是使用图像内存壁垒并调用 vkCmdPipelineBarrier() 函数:

VkImageSubresourceRange image_subresource_range = {
  VK_IMAGE_ASPECT_COLOR_BIT,              // VkImageAspectFlags        aspectMask
  0,                                      // uint32_t                  baseMipLevel
  1,                                      // uint32_t                  levelCount
  0,                                      // uint32_t                  baseArrayLayer
  1                                       // uint32_t                  layerCount
};

VkImageMemoryBarrier image_memory_barrier_from_undefined_to_transfer_dst = {
  VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, // VkStructureType           sType
  nullptr,                                // const void               *pNext
  0,                                      // VkAccessFlags             srcAccessMask
  VK_ACCESS_TRANSFER_WRITE_BIT,           // VkAccessFlags             dstAccessMask
  VK_IMAGE_LAYOUT_UNDEFINED,              // VkImageLayout             oldLayout
  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,   // VkImageLayout             newLayout
  VK_QUEUE_FAMILY_IGNORED,                // uint32_t                  srcQueueFamilyIndex
  VK_QUEUE_FAMILY_IGNORED,                // uint32_t                  dstQueueFamilyIndex
  Vulkan.Image.Handle,                    // VkImage                   image
  image_subresource_range                 // VkImageSubresourceRange   subresourceRange
};
vkCmdPipelineBarrier( command_buffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier_from_undefined_to_transfer_dst);

9.Tutorial06.cpp, function CopyTextureData()

接下来拷贝数据。为此我们需要提供描述数据来源和目的地的参数:希望更新哪部分图像 (imageSubresource member)、所提供部分中的特定区域 (imageOffset),以及图像的大小。关于数据来源,我们需要提供启动数据的缓冲区内存一开始的偏移、数据的结构,以及缓冲区内想象图像的大小(行与列的大小)。幸运的是,我们能够以适合图像的方式保存数据。这样我们可将两个参数(bufferRowLength 和 bufferImageHeight)都设为 0,规定按照图像大小紧密封装数据。

VkBufferImageCopy buffer_image_copy_info = {
  0,                                  // VkDeviceSize               bufferOffset
  0,                                  // uint32_t                   bufferRowLength
  0,                                  // uint32_t                   bufferImageHeight
  {                                   // VkImageSubresourceLayers   imageSubresource
    VK_IMAGE_ASPECT_COLOR_BIT,          // VkImageAspectFlags         aspectMask
    0,                                  // uint32_t                   mipLevel
    0,                                  // uint32_t                   baseArrayLayer
    1                                   // uint32_t                   layerCount
  },
  {                                   // VkOffset3D                 imageOffset
    0,                                  // int32_t                    x
    0,                                  // int32_t                    y
    0                                   // int32_t                    z
  },
  {                                   // VkExtent3D                 imageExtent
    width,                              // uint32_t                   width
    height,                             // uint32_t                   height
    1                                   // uint32_t                   depth
  }
};
vkCmdCopyBufferToImage( command_buffer, Vulkan.StagingBuffer.Handle, Vulkan.Image.Handle, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &buffer_image_copy_info );

10.Tutorial06.cpp, function CopyTextureData()

最后是执行另外一种布局过渡。我们的图像将在着色器中用作纹理,因此需要将其过渡至 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL 布局。之后可以结束命令缓冲区,将其提交至队列,并等待传输完成(在真实应用中,我们应该等待并用其他的方法(例如使用旗语)同步操作,以避免不必要的管线停顿)。

VkImageMemoryBarrier image_memory_barrier_from_transfer_to_shader_read = {
  VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,   // VkStructureType              sType
  nullptr,                                  // const void                  *pNext
  VK_ACCESS_TRANSFER_WRITE_BIT,             // VkAccessFlags                srcAccessMask
  VK_ACCESS_SHADER_READ_BIT,                // VkAccessFlags                dstAccessMask
  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,     // VkImageLayout                oldLayout
  VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // VkImageLayout                newLayout
  VK_QUEUE_FAMILY_IGNORED,                  // uint32_t                     srcQueueFamilyIndex
  VK_QUEUE_FAMILY_IGNORED,                  // uint32_t                     dstQueueFamilyIndex
  Vulkan.Image.Handle,                      // VkImage                      image
  image_subresource_range                   // VkImageSubresourceRange      subresourceRange
};
vkCmdPipelineBarrier( command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier_from_transfer_to_shader_read);

vkEndCommandBuffer( command_buffer );

// Submit command buffer and copy data from staging buffer to a vertex buffer
VkSubmitInfo submit_info = {
  VK_STRUCTURE_TYPE_SUBMIT_INFO,            // VkStructureType              sType
  nullptr,                                  // const void                  *pNext
  0,                                        // uint32_t                     waitSemaphoreCount
  nullptr,                                  // const VkSemaphore           *pWaitSemaphores
  nullptr,                                  // const VkPipelineStageFlags  *pWaitDstStageMask;
  1,                                        // uint32_t                     commandBufferCount
  &command_buffer,                          // const VkCommandBuffer       *pCommandBuffers
  0,                                        // uint32_t                     signalSemaphoreCount
  nullptr                                   // const VkSemaphore           *pSignalSemaphores
};

if( vkQueueSubmit( GetGraphicsQueue().Handle, 1, &submit_info, VK_NULL_HANDLE ) != VK_SUCCESS ) {
  return false;
}

vkDeviceWaitIdle( GetDevice() );

11.Tutorial06.cpp, function CopyTextureData()

现在我们的图像已创建并完全初始化(包含相应的数据)。但还未准备好纹理。

创建采样器

在 OpenGL 中创建纹理时,图像和采样参数都已指定。在之后的 OpenGL 版本中,我们还可创建独立的采样器对象。在着色器中,我们通常创建 sampler2D 类型的变量,它还结合了图像及其采样参数(采样器)。在 Vulkan 中,我们需要单独创建图像和采样器。

采样器规定如何在着色器中读取图像数据:是否启用过滤、是否想使用 mipmap(或 mipmap 的特定子界),或我们想使用的寻址模式(锁定或包装)。

VkSamplerCreateInfo sampler_create_info = {
  VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,  // VkStructureType        sType
  nullptr,                                // const void*            pNext
  0,                                      // VkSamplerCreateFlags   flags
  VK_FILTER_LINEAR,                       // VkFilter               magFilter
  VK_FILTER_LINEAR,                       // VkFilter               minFilter
  VK_SAMPLER_MIPMAP_MODE_NEAREST,         // VkSamplerMipmapMode    mipmapMode
  VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,  // VkSamplerAddressMode   addressModeU
  VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,  // VkSamplerAddressMode   addressModeV
  VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,  // VkSamplerAddressMode   addressModeW
  0.0f,                                   // float                  mipLodBias
  VK_FALSE,                               // VkBool32               anisotropyEnable
  1.0f,                                   // float                  maxAnisotropy
  VK_FALSE,                               // VkBool32               compareEnable
  VK_COMPARE_OP_ALWAYS,                   // VkCompareOp            compareOp
  0.0f,                                   // float                  minLod
  0.0f,                                   // float                  maxLod
  VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,// VkBorderColor          borderColor
  VK_FALSE                                // VkBool32               unnormalizedCoordinates
};

return vkCreateSampler( GetDevice(), &sampler_create_info, nullptr, sampler ) == VK_SUCCESS;

12.Tutorial06.cpp, function CreateSampler()

上述所有参数均通过 VkSamplerCreateInfo 类型的变量来指定。它包含许多成分:

  • sType – 结构类型。必须等于 VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO 的值。
  • pNext – 为扩展功能预留的指示器。
  • flags – 必须设为 0。改参数留作以后使用。
  • magFilter – 用于放大的过滤类型(最近或线性)。
  • minFilter – 用于缩小的过滤类型(最近或线性)。
  • mipmapMode – 用于查找 mipmap 的过滤类型(最近或线性)。
  • addressModeU - <0.0; 1.0> 范围外的 U 坐标寻址模式。
  • addressModeV - <0.0; 1.0> 范围外的 V 坐标寻址模式。
  • addressModeW - <0.0; 1.0> 范围外的 W 坐标寻址模式。
  • mipLodBias - 添加至 mipmap 细节层计算的偏差值。如果想抵消从特定 mipmap 提取数据,可以提供 0.0 之外的值。
  • anisotropyEnable - 定义是否使用异性过滤的参数。
  • maxAnisotropy - 用于异性过滤的最大容许值(锁定值)。
  • compareEnable - 在纹理查找过程中支持对比参考值。
  • compareOp - compareEnable 参数设为真值时在查找期间执行的对比类型。
  • minLod - 数据提取过程中使用的最小容许细节层。如果计算出的细节层(mipmap 层)小于该值,它将被锁定。
  • maxLod - 数据提取过程中使用的最大容许细节层。如果计算出的细节层(mipmap 层)大于该值,它将被锁定。
  • borderColor - 规定边框像素的预定义颜色。边框颜色在寻址模式包含边框颜色锁定时使用。
  • unnormalizedCoordinates - 通常(当该参数设为假值)我们使用标准化 <0.0; 1.0> 范围提供纹理坐标。当设为真值时,该参数允许我们指定我们想使用标准化坐标并通过纹素寻址纹理(在 <0; texture dimension> 范围中,类似于 OpenGL 的矩形纹理)。

采样器对象通过调用 vkCreateSampler() 函数创建,我们为它提供一个针对上述结构的指示器。

使用描述符集

我们创建了图像、绑定了内存,甚至将数据上传至图像。还创建了采样器以为纹理设置采样参数。现在我们想使用该纹理。我们如何使用?通过描述符集。

我们在教程开头说过,着色器中使用的资源被称为描述符。Vulkan 中有 11 种描述符:

  • 采样器 - 定义读取图像数据的方式。在着色器中,采样器可用于多个图像。
  • 采样图像 - 定义在着色器中用于读取数据的图像。我们使用不同的采样器从单个图像中读取数据。
  • 合并图像采样器 - 这些描述符将采样器和采样图像合成一个对象。从 API(我们的应用)的角度来说,我们仍然需要创建采样器和图像,但在着色器中它们以单个对象的形式出现。使用它们要优于(性能更高)使用单独的采样器和采样图像。
  • 存储图像 - 该描述符支持我们读取和保存图像中的数据。
  • 输入附件 - 这是渲染通道附件的特定用法。如果想从在相同渲染通道中用作附件的图像读取数据,只能通过输入附件进行。使用这种方法时不需要结束渲染通道并启动另一个渲染通道,但仅限于片段着色器和每个片段着色器的单个位置(片段着色器的特定实例可从与片段着色器坐标相关的坐标读取数据)。
  • 统一缓冲区(及其动态变体) - 统一缓冲区支持我们读取统一变量的数据。在 Vulkan 中,这种变量放在全局范围内,我们需要使用统一缓冲区。
  • 存储缓冲区(及其动态变体) - 存储缓冲区支持我们读取和保存变量中的数据。
  • 统一纹素缓冲区 - 它们支持将缓冲区内容处理成包含纹理数据,解析成带有指定数量的组件和格式的纹素。这样我们可访问大型数据阵列(比统一缓冲区大)。
  • 存储纹素缓冲区 - 类似于统一纹素缓冲区。不仅用于读取数据,还可用于保存数据。

上述所有描述符均通过采样器、图像或缓冲区创建。但我们使用并在着色器中访问它们的方式不同。此类访问的所有额外参数都会对性能产生影响。例如,使用存储缓冲区只能读取数据,但读取数据的速度可能比将数据保存在存储缓冲区快。同样,纹素缓冲区支持我们访问的元素比统一缓冲区多,但这样可能会降低性能。我们应该记住,要选择适合需求的描述符。

在本教程中我们想使用纹理。为此我们创建了图像和采样器。我们还将使用它们准备合并图像采样器描述符。

创建描述符集布局

准备着色器在创建描述符集布局开始时使用的资源。描述符集是不透明的对象,其中保存了资源的句柄。布局规定了描述符集的结构 — 包含哪种类型的描述符、每种类型的描述符有多少,以及它们的顺序如何。

image of a diagram

创建描述符集布局,首先定义特定集中的所有可用描述符的参数。具体做法是填充 VkDescriptorSetLayoutBinding 类型变量结构:

VkDescriptorSetLayoutBinding layout_binding = {
  0,                                          // uint32_t             binding
  VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,  // VkDescriptorType     descriptorType
  1,                                          // uint32_t             descriptorCount
  VK_SHADER_STAGE_FRAGMENT_BIT,               // VkShaderStageFlags   stageFlags
  nullptr                                     // const VkSampler     *pImmutableSamplers
};

13.Tutorial06.cpp, function CreateDescriptorSetLayout()

上述描述包含以下成分:

  • binding – 特定集的描述符索引。单个布局(和集)的所有描述符必须唯一绑定。相同的绑定可在着色器中用于访问描述符。
  • descriptorType – 描述符(采样器、统一缓冲区等)的类型
  • descriptorCount – 指定类型以阵列形式访问的描述符数量。单个描述符应该使用的值为 1。
  • stageFlags – 定义所有将访问特定描述符的着色器阶段的一套标记。为了提高性能,我们应仅规定将访问特定资源的阶段。
  • pImmutableSamplers – 影响仅在布局中永久绑定(以后不能更改)的采样器。但我们不必担心这个参数,可通过将该参数设为 null,以其他描述符的形式绑定采样器。

在示例中,我们想仅使用合并图像采样器的一个描述符,仅片段着色器访问该描述符。它将成为特定布局中的第一个(绑定 0)描述符。为避免浪费内存,应该绑定地尽可能紧凑(尽量接近 0),因为驱动程序可能会为描述符插槽分配内存,即使不用。

我们可以为从单个集访问的其他描述符准备类似的参数。然后为 VkDescriptorSetLayoutCreateInfo 类型的变量提供变量指示器:

VkDescriptorSetLayoutCreateInfo descriptor_set_layout_create_info = {
  VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,  // VkStructureType                      sType
  nullptr,                                              // const void                          *pNext
  0,                                                    // VkDescriptorSetLayoutCreateFlags     flags
  1,                                                    // uint32_t                             bindingCount
  &layout_binding                                       // const VkDescriptorSetLayoutBinding  *pBindings
};

if( vkCreateDescriptorSetLayout( GetDevice(), &descriptor_set_layout_create_info, nullptr, &Vulkan.DescriptorSet.Layout ) != VK_SUCCESS ) {
  std::cout << "Could not create descriptor set layout!"<< std::endl;
  return false;
}

14.Tutorial06.cpp, function CreateDescriptorSetLayout()

该结构仅包含几个成分:

  • sType – 结构类型。必须等于 VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO 的值。
  • pNext – 为扩展功能预留的指示器。
  • flags – 该参数支持我们为创建布局提供另外的选项。但因为它们通过扩展连接,所以我们将该参数设为 0。
  • bindingCount – 绑定的数量,pBindings 阵列中的元素。
  • pBindings – 特定布局中所有资源的描述阵列指示器。该阵列必须大于 bindingCount 参数的值。

填充完该结构后,我们可调用 vkCreateDescriptorSetLayout() 函数创建描述符集布局。稍后我们需要多次使用该布局。

创建描述符池

下一步是准备描述符集。描述符集与命令缓冲区类似,不能直接创建,而是从池中分配。分配描述符集之前,需要创建一个描述符池。

VkDescriptorPoolSize pool_size = {
  VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,      // VkDescriptorType               type
  1                                               // uint32_t                       descriptorCount
};

VkDescriptorPoolCreateInfo descriptor_pool_create_info = {
  VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,  // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkDescriptorPoolCreateFlags    flags
  1,                                              // uint32_t                       maxSets
  1,                                              // uint32_t                       poolSizeCount
  &pool_size                                      // const VkDescriptorPoolSize    *pPoolSizes
};

if( vkCreateDescriptorPool( GetDevice(), &descriptor_pool_create_info, nullptr, &Vulkan.DescriptorSet.Pool ) != VK_SUCCESS ) {
  std::cout << "Could not create descriptor pool!"<< std::endl;
  return false;
}

15.Tutorial06.cpp, function CreateDescriptorPool()

创建描述符池涉及到规定从中分配的描述符集数量。同时我们还需要规定描述符的类型,以及跨所有描述符集从池中分配多少描述符集。例如,如果想从特定池中分配一个采样图像和一个存储缓冲区,我们可从该池中分配两个描述符集。进行该操作时,如果分配一个带有采样图像的描述符集,第二个描述符可以仅包含一个存储缓冲区。如果从该池分配的单个描述符集包含两种资源,我们将不能分配另一个描述符集,因为它必须是空的。在描述符池创建期间,我们定义可分配的描述符总数和描述符集总数。该操作包含两个步骤。

首先准备 VkDescriptorPoolSize 类型的变量,它规定描述符类型以及可从池中分类的指定类型的描述符总数。接下来向 VkDescriptorPoolCreateInfo 类型的变量提供此类变量阵列。它包含以下成分:

  • sType – 结构类型。在本情况下应设为 VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO。
  • pNext – 为扩展功能预留的指示器。
  • flags – 该参数定义(当使用 VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT 标记)从池中分配的描述符集是否单独释放或重设。如果该参数设为 0,从池中分配的所有描述符集只能通过重设整个池来一次性(批量)完成重设。
  • maxSets – 从池中分配的描述符集总数。
  • poolSizeCount – 定义 pPoolSizes 阵列中元素的数量。
  • pPoolSizes – 包含与 poolSizeCount 一样多的描述符类型的阵列,以及可从池中分配的类型的描述符总数的指示器。

在本示例中,我们想仅分配一个描述符集,它仅包含一个合并图像采样器类型的描述符。我们根据示例准备参数并通过调用 vkCreateDescriptorPool() 函数创建描述符池。

分配描述符集

现在我们准备分配描述符集。所用的代码很短:

VkDescriptorSetAllocateInfo descriptor_set_allocate_info = {
  VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  Vulkan.DescriptorSet.Pool,                      // VkDescriptorPool               descriptorPool
  1,                                              // uint32_t                       descriptorSetCount
  &Vulkan.DescriptorSet.Layout                    // const VkDescriptorSetLayout   *pSetLayouts
};

if( vkAllocateDescriptorSets( GetDevice(), &descriptor_set_allocate_info, &Vulkan.DescriptorSet.Handle ) != VK_SUCCESS ) {
  std::cout << "Could not allocate descriptor set!"<< std::endl;
  return false;
}

16.Tutorial06.cpp, function AllocateDescriptorSet()

为分配描述符集,我们需要准备 VkDescriptorSetAllocateInfo 类型的变量,它包含以下成分:

  • sType – 标准结构类型。为达到分配描述符集的目的,我们需要将它设为 VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO 的值。
  • pNext – 为扩展功能预留的指示器。
  • descriptorPool – 分配命令缓冲区的描述符池的句柄。
  • descriptorSetCount – 希望分配的描述符集数量(以及 pSetLayouts 成分中的元素数量)。
  • pSetLayouts – 至少包含 descriptorSetCount 元素的阵列指示器。该阵列的每个元素都必须包含定义已分配描述符集内部结构的描述符集布局(例如元素可能重复,我们可一次分配 5 个描述符集,均包含相同的布局)。

从上述结构中可看出,我们需要提供描述符集布局。因此我们才需要提前创建它们。为了从提供的池中分配指定数量的描述符集,我们需要向 vkAllocateDescriptorSets() 函数提供上述结构的指示器。

更新描述符集

我们准备了一个描述符集,但它是空的,未完成初始化。现在我们需要填充或更新它。这意味着我们将告诉驱动程序哪些资源应用于该集中的描述符。

可通过以下两种方法更新描述符集:

  • 写入描述符集 — 使用这种这种方法时我们提供新资源。
  • 拷贝其他描述符集的数据 — 如果有之前更新过的描述符集,并想在另一描述符集中使用一部分描述符,我们可以拷贝它们;这种方法比直接从 CPU 编写描述符集更快。

因为我们没有其他描述符集,所以需要直接写入一个描述符集。我们需要为每种描述符准备两种结构。第一种是所有描述符通用的 VkWriteDescriptorSet 结构。它包含以下成分:

  • sType – 结构类型。需要使用 VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET 值。
  • pNext – 为扩展功能预留的指示器。
  • dstSet – 我们希望更新(填充特定资源)的描述符集的句柄。
  • dstBinding – 我们希望更新的描述符集中的索引。必须提供在创建描述符集布局期间规定的绑定。另外,指定的绑定必须对应所提供的描述符类型。
  • dstArrayElement – 规定我们希望更新的第一个阵列索引。使用一种 VkWriteDescriptorSet 结构可更新一个阵列的多个元素。例如,我们有一个包含 4 个元素的采样器阵列,并且我们希望更新最后 2 个(索引为 2 和 3);我们可提供两个采样器并从索引 2 开始更新该阵列。
  • descriptorCount – 我们希望更新的描述符数量(pImageInfo, pBufferInfo 或 pTexelBufferView 阵列中的元素数量)。对于普通描述符,我们将该值设为 1。但对于阵列,我们可提供更大的值。
  • descriptorType – 即将更新的描述符类型。必须与在描述符集布局创建期间提供的且包含相同绑定(描述符集中的索引)的描述符类型相同。
  • pImageInfo – 至少包含 VkDescriptorImageInfo 类型的 descriptorCount 元素的阵列指示器。如果想更新 VK_DESCRIPTOR_TYPE_SAMPLER、VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE、VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER、VK_DESCRIPTOR_TYPE_STORAGE_IMAGE 或 VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT 描述符,每种元素都必须包含特定资源的句柄。
  • pBufferInfo – 至少包含 VkDescriptorBufferInfo 类型的 descriptorCount 元素的阵列指示器。如果想更新 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER、VK_DESCRIPTOR_TYPE_STORAGE_BUFFER、VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC或 VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC 描述符,每种元素都必须包含特定资源的句柄。
  • pTexelBufferView – 至少包含 descriptorCount VkBufferView 句柄的阵列。该阵列用于更新 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER、VK_DESCRIPTOR_TYPE_STORAGE_BUFFER、VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC 或 VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC 描述符。

根据我们希望更新的描述符类型,我们需要准备 VkDescriptorImageInfo、VkDescriptorBufferInfo 或 VkBufferView 类型的变量(或变量阵列)。这里,我们想更新合并图像采样器描述符,因此我们需要准备 VkDescriptorImageInfo 类型的变量。它包含以下成分:

  • sampler – 采样器对象的句柄。
  • imageView – 图像视图的句柄。
  • imageLayout – 这里我们提供在着色器中访问描述符时图像将呈现的布局。

在该结构中,我们提供特定资源的参数;指向已创建的并希望在着色器中使用的有效资源。结构成分将根据描述符的类型初始化。例如,如果更新采样器,我们仅需提供一个采样器的句柄。如果更新采样图像,我们需要提供图像视图的句柄以及图像的布局。但图像不会(像在渲染通道中那样)自动过渡到这种布局。我们需要执行布局过渡,通过管线壁垒明确进行或在有输入附件的情况下通过渲染通道进行。此外,我们需要提供对应于特定用途的布局。

在本示例中我们想使用纹理。可通过两种方法进行,一种是使用单独的采样器和采用图像描述符,另一种是使用合并图像采样器描述符(像在常见 OpenGL 应用中那样)。后一种方法更好(部分硬件平台从合并图像采样器抽样数据的速度比从单个采样器和采样图像抽样快),我们下面来介绍这种方法。如果想更新合并图像采样器,需要提供 VkDescriptorImageInfo 结构的所有成分:

VkDescriptorImageInfo image_info = {
  Vulkan.Image.Sampler,                       // VkSampler                      sampler
  Vulkan.Image.View,                          // VkImageView                    imageView
  VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL    // VkImageLayout                  imageLayout
};

VkWriteDescriptorSet descriptor_writes = {
  VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,     // VkStructureType                sType
  nullptr,                                    // const void                    *pNext
  Vulkan.DescriptorSet.Handle,                // VkDescriptorSet                dstSet
  0,                                          // uint32_t                       dstBinding
  0,                                          // uint32_t                       dstArrayElement
  1,                                          // uint32_t                       descriptorCount
  VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,  // VkDescriptorType               descriptorType
  &image_info,                                // const VkDescriptorImageInfo   *pImageInfo
  nullptr,                                    // const VkDescriptorBufferInfo  *pBufferInfo
  nullptr                                     // const VkBufferView            *pTexelBufferView
};

vkUpdateDescriptorSets( GetDevice(), 1, &descriptor_writes, 0, nullptr );

17.Tutorial06.cpp, function UpdateDescriptorSet()

VkDescriptorImageInfo 类型的变量指示器稍后将在 VkWriteDescriptorSet 类型的变量中提供。由于我们仅更新一个描述符,因此只需要两种结构的一个实例。不过我们也可以一次更新多个描述符,这样我们需要准备多个变量,之后提供给 vkUpdateDescriptorSets() 函数。

创建管线布局

我们没有创建管线布局。如果想使用描述符,不仅需要分配和更新描述符集。我们准备的特定资源大部分是为了在着色器中使用,但描述符集可用于保存特定资源的句柄。这些句柄将在记录命令缓冲区时提供。我们需要为围挡的另一侧准备信息:驱动程序还需知道特定管线需要访问哪类资源。这种信息在创建管线时将起关键作用,因为它可能影响其内部结构或着色器编译。而且这种信息在管线布局中提供。

管线布局保存有关特定管线将访问的资源类型的信息。这些资源涉及描述符和 push constant 范围。现在我们跳过 push constant,重点介绍描述符。

为创建管线布局和准备有关该管线访问的资源类型信息,我们需要提供描述符集布局阵列。这可通过 VkPipelineLayoutCreateInfo 类型的变量的以下成分来完成:

  • sType – 为扩展功能预留的指示器。
  • flags – 该参数留作将来使用。
  • setLayoutCount – pSetLayouts 成分中的元素数量,以及管线可使用的描述符数量。
  • pSetLayouts – 包含描述符集布局的阵列。
  • pushConstantRangeCount – push constant 范围的数量。
  • pPushConstantRanges – 描述 push constant 范围的元素阵列。

这里将再次用到描述符集布局。单个描述符集布局定义单个描述符集所包含的资源类型。而且这些布局的阵列定义特定管线需访问的资源类型。

如果创建管线布局,我们只需调用 vkCreatePipelineLayout() 函数即可。我们在 Vulkan 简介第 3 部分:第一个三角形中已创建过管线布局。但我们创建的是空白布局(没有 push constants,也不访问描述符资源)。这里我们创建一种比较典型的管线布局。

VkPipelineLayoutCreateInfo layout_create_info = {
  VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,  // VkStructureType                sType
  nullptr,                                        // const void                    *pNext
  0,                                              // VkPipelineLayoutCreateFlags    flags
  1,                                              // uint32_t                       setLayoutCount
  &Vulkan.DescriptorSet.Layout,                   // const VkDescriptorSetLayout   *pSetLayouts
  0,                                              // uint32_t                       pushConstantRangeCount
  nullptr                                         // const VkPushConstantRange     *pPushConstantRanges
};

if( vkCreatePipelineLayout( GetDevice(), &layout_create_info, nullptr, &Vulkan.PipelineLayout ) != VK_SUCCESS ) {
  std::cout << "Could not create pipeline layout!"<< std::endl;
  return false;
}
return true;

18.Tutorial06.cpp, function CreatePipelineLayout()

此布局稍后将在管线创建期间提供。我们还需要在记录命令缓冲区期间绑定描述符集时使用该布局。因此我们需要保存管线布局句柄。

绑定描述符集

最后一件事是在记录期间将描述符集绑定至命令缓冲区。我们可有多个不同的描述符集或多个类似的描述符集(布局相同),但它们可能包含不同的资源句柄。在渲染期间使用哪些描述符将在命令缓冲区记录期间指定。进行绘制之前,我们需要(根据绘制参数)设置有效状态。对每个记录的命令缓冲区都需要重新设置。

绘制操作要求使用渲染通道和管线。如果管线使用描述符资源(当着色器访问图像或缓冲区),我们需要调用 vkCmdBindDescriptorSets() 函数,绑定描述符集。我们必须为该函数提供管线布局句柄以及描述符集阵列句柄。将描述符集绑定至特定索引。绑定描述符集的特定索引必须对应管线创建期间索引相同的布局。

vkCmdBeginRenderPass( command_buffer, &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE );

vkCmdBindPipeline( command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, Vulkan.GraphicsPipeline );

// ...

vkCmdBindDescriptorSets( command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, Vulkan.PipelineLayout, 0, 1, &Vulkan.DescriptorSet.Handle, 0, nullptr );

vkCmdDraw( command_buffer, 4, 1, 0, 0 );

vkCmdEndRenderPass( command_buffer );

19.Tutorial06.cpp, function PrepareFrame()

在着色器中访问描述符

另外,我们需要编写合适的着色器。在本示例中,我们仅在片段着色器中访问纹理,因此只需呈现片段着色器。

本教程的开头部分涉及到了描述符集、描述符集中的绑定以及绑定描述符集。可同时将多个描述符集绑定至一个命令缓冲区。每个描述符集可包含多种资源。该数据与我们在着色器中使用的特定地址是相对应的。可通过如下所示的 layout() 说明符定义该地址:

layout(set=S, binding=B) uniform <variable type> <variable name>

Set 定义通过 vkCmdBindDescriptorSets() 函数绑定的描述符集的索引。Binding 规定所提供的描述符集中的资源的索引,对应创建描述符集布局期间所定义的绑定。在本示例中只有一个描述符集,索引为 0,包含一个绑定 0 的合并图像采样器。在着色器中通过 sampler1D、sampler2D 或 sampler 3D 变量访问合并图像采样器。因此片段着色器的源代码如下所示:

#version 450

layout(set=0, binding=0) uniform sampler2D u_Texture;

layout(location = 0) in vec2 v_Texcoord;

layout(location = 0) out vec4 o_Color;

void main() {
  o_Color = texture( u_Texture, v_Texcoord );
}

20.shader.frag, -

Tutorial06 执行

下方所示为该示例项目生成的最终图像:

Image of a large semi truck with intel logo on the side, speeding down the road

我们渲染纹理已应用于表面的四边形。该四边形应调整大小(和方位)以匹配窗口的大小和形状(如果拉伸窗口,该四边形和图像也会被拉伸)。

清理

结束应用之前应进行清理。

// ...

if( Vulkan.GraphicsPipeline != VK_NULL_HANDLE ) {
  vkDestroyPipeline( GetDevice(), Vulkan.GraphicsPipeline, nullptr );
  Vulkan.GraphicsPipeline = VK_NULL_HANDLE;
}

if( Vulkan.PipelineLayout != VK_NULL_HANDLE ) {
  vkDestroyPipelineLayout( GetDevice(), Vulkan.PipelineLayout, nullptr );
  Vulkan.PipelineLayout = VK_NULL_HANDLE;
}

// ...

if( Vulkan.DescriptorSet.Pool != VK_NULL_HANDLE ) {
  vkDestroyDescriptorPool( GetDevice(), Vulkan.DescriptorSet.Pool, nullptr );
  Vulkan.DescriptorSet.Pool = VK_NULL_HANDLE;
}

if( Vulkan.DescriptorSet.Layout != VK_NULL_HANDLE ) {
  vkDestroyDescriptorSetLayout( GetDevice(), Vulkan.DescriptorSet.Layout, nullptr );
  Vulkan.DescriptorSet.Layout = VK_NULL_HANDLE;
}

if( Vulkan.Image.Sampler != VK_NULL_HANDLE ) {
  vkDestroySampler( GetDevice(), Vulkan.Image.Sampler, nullptr );
  Vulkan.Image.Sampler = VK_NULL_HANDLE;
}

if( Vulkan.Image.View != VK_NULL_HANDLE ) {
  vkDestroyImageView( GetDevice(), Vulkan.Image.View, nullptr );
  Vulkan.Image.View = VK_NULL_HANDLE;
}

if( Vulkan.Image.Handle != VK_NULL_HANDLE ) {
  vkDestroyImage( GetDevice(), Vulkan.Image.Handle, nullptr );
  Vulkan.Image.Handle = VK_NULL_HANDLE;
}

if( Vulkan.Image.Memory != VK_NULL_HANDLE ) {
  vkFreeMemory( GetDevice(), Vulkan.Image.Memory, nullptr );
  Vulkan.Image.Memory = VK_NULL_HANDLE;
}

21.Tutorial06.cpp, function destructor

我们调用 vkDestroyPipeline()vkDestroyPipelineLayout() 函数,破坏管线及其布局。接下来分别调用 vkDestroyDescriptorPool()vkDestroyDescriptorSetLayout() 函数破坏描述符池和描述符集布局。肯定会破坏其他资源,但我们知道如何操作。注意,我们不释放描述符集。如果在描述符池创建期间提供有相应的标记,可以单独释放各描述符集。但没有必要 — 破坏描述符池时,所有从该池中分配的集均已释放。

结论

本教程主要介绍了如何在着色器中使用纹理(实际上是合并图像采样器)。为此我们创建了图像,并为此分配和绑定内存。还创建了图像视图。然后将数据从缓冲区拷贝至图像,以对其内容进行初始化。我们还创建了采样器对象,以定义如何在着色器中读取图像数据。

然后准备描述符集。首先创建描述符集布局。之后创建描述符池,以从中分配单个描述符集。我们通过采样器和图像视图的句柄更新该描述符集。

描述符集布局还用于定义显卡管线访问的资源。该操作在管线布局创建期间完成。然后在将描述符集绑定至命令缓冲区时使用该布局。

我们还学习了如何准备着色器代码,以便访问合并图像采样器以读取数据(以纹理的形式采样)。这一操作在渲染简单几何体期间所使用的片段着色器中完成。这样我们将纹理应用于几何体表面。

在下一教程中我们将学习如何在着色器中使用统一缓冲区。


该示例源代码根据“英特尔示例源代码许可协议”发布

Для получения подробной информации о возможностях оптимизации компилятора обратитесь к нашему Уведомлению об оптимизации.
Возможность комментирования русскоязычного контента была отключена. Узнать подробнее.