教程 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() 函数。


查看原文


了解更多相关内容,请关注CSDN英特尔开发专区

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐