第 11 章. Drawing Textures 绘制纹理

原文链接:https://zhuanlan.zhihu.com/p/55308762 第 11 章. Drawing Textures 绘制纹理首发于Vulkan 学习指南关注专栏写文章第 11 章. Drawing Textures 绘制纹理

第 11 章. Drawing Textures 绘制纹理

第 11 章. Drawing Textures 绘制纹理SnowFox图形学 AI 分布式 操作系统 编译器 架构 后端 多线程4 人赞同了该文章

第 11 章. Drawing Textures 绘制纹理

在前一章中,我们学习了如何更新资源的内容并使用描述符在着色器阶段读取它们。 我们还介绍了 push 常量,它是在着色器阶段使用命令缓冲区更新常量数据的优化方式。 另外,通过使用描述符,我们对渲染的图元添加了 3D 变换,并且还演示了一个示例来学习 push 常量。

在本章中,我们会学习和实现纹理;我们会将它们缠绕在几何表面上,以便增强现实感。 纹理是使用 Vulkan 的图像资源创建的;其数据可以以线性或最佳布局存储。 我们将实现这两种布局 - 后者布局使用暂存。 在暂存中,物理的分配过程使用了两个不同的存储区域。 主机可能无法看到资源的理想内存布局。 在这种情况下,应用程序必须首先将资源填充到主机可见的暂存缓冲区中,然后将其转移到理想的位置。

在本章中,我们会介绍以下主题:

  • 图像资源 Image resource - 快速回顾
  • 纹理绘制 texture drawing 的先决条件
  • 用线性平铺 linear tiling 实现图像资源
  • 用优化平铺 optimal tiling 实现图像资源
  • 在图像和缓冲区之间复制数据内容
  • 更新描述符集 descriptor set

Image resource - a quick recap

图像资源 - 快速回顾

图像是以 1D,2D 或 3D 形式存储的连续字节数组。 与缓冲区资源不同,图像是存储在内存中格式化的信息。

Vulkan 中的图像资源由 VkImage 对象表示,并使用 vkCreateImage API 创建。 这个对象的创建还没有实际的图像内容作为后端支持。 这必须单独完成,其中需要分配设备内存并将图像内容存储到其中。 然后把该内存绑定到创建的对象。

为了在着色器阶段使用创建的图像对象,必须将它们转换为图像视图 -VkImageView。 在将图像转换为图像视图之前,必须使用图像布局使其与底层实现兼容。

使用 VkImageLayout 将图像转换为实现相关的布局。 对于给定的图像资源,可以创建并使用多个图像布局。 不同的布局可能会暴露不同的性能特征,因为它们非常专注于 usage 类型。 因此指明正确的用法 usage 可让驱动程序选择一个特定的存储位置或部分区域,适合用来提供最佳的性能。

如果您想了解图像资源的详细介绍,请参阅第 6 章“分配图像资源以及使用 WSI 创建交换链接”中的第一部分,即“图像资源入门”。 在同一章节中,您可以参考“了解图像资源”部分以获取有关图像,图像视图和图像布局的详细信息。

创建一个图像资源很简单,由以下步骤组成:

  1. 图像对象的创建:Image object creation,首先,创建 VkImage 对象。 此对象不包含图像数据,但它存储了图像资源各种重要的对象状态,例如格式,尺寸,图像类型,图像的 usage 类型,平铺样式等。 一个给定的图像可以具有多个子图像资源,例如 mipmap。 以下是创建图像对象的步骤:
    1. 平铺:Tiling,可以指定图像平铺的两种方式:线性和最佳。 在线性布局中,图像数据会被映射到设备上的连续内存,以线性方式排列。 然而,在最佳布局中,图像以贴片的形式存储,并且每个贴片内的纹理元素可以以线性或某种专有格式排列以提供最佳的性能。 有关线性和最佳布局的详细介绍,请参阅第 6 章“分配图像资源以及使用 WSI 创建交换链”中的“平铺简介”一节。
    2. 分配和赋值图像数据:Allocating and assigning image data,读取图像内容并将所需的内存分配给图像资源。 使用图像通道的内容填充分配的设备内存。
    3. 设置正确的布局:Setting the correct layout,创建一个实现兼容的图像布局。 单个图像及其子资源可以用多个布局来指定。

  2. 图像采样器:Image sampler,创建采样器(VkSampler)来控制纹理的过滤。
  3. 图像视图创建:Image view creation,图像资源只能在着色器中以图像视图(VkImageView)的形式访问。

Prerequisites for texture drawing

纹理绘制的先决条件

实现纹理很简单,只需要几个步骤。 我们首先快速浏览一下,然后我们会对其进行深入的探讨:

  1. 纹理坐标:Texture coordinates,使用纹理坐标将纹理粘贴到几何表面。 对于每个顶点,都附有相应的纹理坐标。 在我们的实现中,我们以交错的形式指定了顶点和纹理坐标。
  2. 着色器阶段:The shader stage,修改顶点和片段着色器以便约束纹理资源。 着色器阶段允许片段着色器访问纹理资源以及绘制片段。 在着色器阶段,纹理会以采样器的形式共享。
  3. 加载图像文件:Loading the image files,解析图像文件并将原始图像数据加载到本地的数据结构中。 这将有助于生成 Vulkan 图像资源并在着色器阶段共享它们。
  4. 本地图像数据结构:Local image data structure,TextureData 本地数据结构存储了所有图像特定的属性。

Specifying the texture coordinates 指定纹理坐标

几何坐标(x,y,z,w)与纹理坐标(u,v)以交错的形式定义在 MeshData.h 文件内的 VertexWithUV 结构中:

struct VertexWithUV
{
float x, y, z, w; // Vertex Position
float u, v; // Texture format U,V
};

在本示例中,我们会渲染用纹理面绘制的立方体。 以下代码显示了立方体的一个面,具有四个顶点位置,后面跟着两个纹理坐标。 完整的代码请参考 MeshData.h:

static const VertexWithUV geometryData[] = {
{ -1.0f,-1.0f,-1.0f, 1.0f, 0.0f, 1.0f },  // - X side
{ -1.0f,-1.0f, 1.0f, 1.0f, 1.0f, 1.0f },
{ -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f },
{ -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f },
{ -1.0f, 1.0f,-1.0f, 1.0f, 0.0f, 0.0f },
{ -1.0f,-1.0f,-1.0f, 1.0f, 0.0f, 1.0f },
. . . .
// Similar, specify +X, - Y, +Y, - Z, +Z faces
}

Updating the shader program 更新着色器程序

除了顶点坐标之外,现在我们的顶点着色器也会考虑纹理坐标。 输入的纹理坐标在布局位置 1 的 inUV 属性中进行接收。 然后这些坐标会被传递到片段着色器阶段并被接收到 outUV 中。 以下代码以粗体显示在现有顶点着色器中所做的修改:

// Vertex Shader
#version 450
layout (std140, binding = 0) uniform bufferVals { mat4 mvp;
} myBufferVals;

layout (location = 0) in vec4 pos;
layout (location = 1) in vec2 inUV; layout (location = 0) out vec2 outUV;
void main() {
outUV = inUV;
gl_Position = myBufferVals.mvp * pos; gl_Position.z = (gl_Position.z + gl_Position.w) / 2.0;
}


以下代码实现了片段着色器,其中在布局绑定索引 1 处接收了采样过的纹理。接收到的纹理与输入的纹理坐标一起使用,以便获取片段的最终颜色:

// Fragment Shader
#version 450
layout(binding = 1) uniform sampler2D tex;

layout (location = 0) in vec2 uv;
layout (location = 0) out vec4 outColor;

void main() {
outColor = texture(tex, uv);
}

Loading the image files 加载图像文件

图像文件使用 GLI 库加载到我们的示例应用程序中。 OpenGL Image(GLI)是仅只有头文件的 C ++ 图像库,支持为图形软件应用程序加载 KTX 和 DDS 图像文件。 它提供了各种功能,如纹理加载和创建,纹理压缩,纹理纹素的访问,样本纹理,纹理转换,mipmap 等。

你可以从 http://gli.g-truc.net/0.8.1/index.html 下载这个库。 为了使用这个库,请执行以下更改: - CMakeLists.txt:通过将以下行添加到项目的 CMakeLists.txt 文件来增加 GLI 支持:

# GLI SETUP
set (EXTDIR “CMAKESOURCEDIR/../../external/gli")set(GLIINCLUDES"{CMAKE_SOURCE_DIR}/../../external/gli") set (GLIINCLUDES "CMAKES​OURCED​IR/../../external/gli")set(GLIINCLUDES"{EXTDIR}”)
get_filename_component(GLIINC_PREFIX “${GLIINCLUDES}” ABSOLUTE) if(NOT EXISTS ${GLIINC_PREFIX})
message(FATAL_ERROR "Necessary gli headers do not exist: " ${GLIINC_PREFIX})
endif()
include_directories( ${GLIINC_PREFIX} )
  • 头文件:需要在 Headers.h 文件中包括 GLI 的头文件:
/*********** GLI HEADER FILES **********/
#include <gli/gli.hpp>

Using the GLI library 使用 GLI 库

以下代码是我们应用程序中 GLI 库的最小用法。 此代码演示了图像加载,尺寸查询,mipmap 级别以及图像数据的检索:

// Load the image
const char filename = “…/VulkanEssentials.ktx”; gli::texture2D image2D(gli::load(filename)); assert(!image2D.empty());

// Get the image dimensions at ith sub- resource uint32_t textureWidth = image2D[i].dimensions().x; uint32_t textureHeight = image2D[i].dimensions().y;

// Get number of mip- map levels
uint32_t mipMapLevels = image2D.levels();

// Retrieve the raw image data
void* rawData = image2D.data();

Local image data structure 本地图像数据结构

wrapper.h 包含一个用户定义的 TextureData 结构,用于保存应用程序中的图像属性以及各种特定于图像的信息。 以下是每个字段的语法和描述:

struct TextureData{
VkSampler sampler;
VkImage image;
VkImageLayout imageLayout; VkMemoryAllocateInfo memoryAlloc; VkDeviceMemory mem;
VkImageView view;
uint32_t mipMapLevels;
uint32_t layerCount;
uint32_t textureWidth, textureHeight; VkDescriptorImageInfo descsImgInfo;
};


下表描述了用户定义结构 TextureData 的各个字段:

字段 | 描述

—|---

sampler | 这是与图像对象关联的 VkSampler 对象。

image | 这是 VkImage 对象。

imageLayout | 这个字段包含图像资源对象特定的依赖实现的布局信息。

memoryAlloc | 这个字段存储了与关联的图像对象(VkImage)绑定的内存分配信息。 mem | 这是指为此图像资源分配的物理设备内存。

view | 这是图像 image 的 ImageView 对象。

mipMapLevels | 这是指图像资源中的 mipmap 级别的数量。 layerCount | 这是指图像资源中层计数的数量。 textureWidth
textureHeight | 这些是图像资源的尺寸。

descsImgInfo | 这个字段是描述符图像信息,其中包含使用适当图像布局 usage 类型的图像视图和样本信息。

在下一节中,我们就开始实现图像资源并将其发挥作用。

Implementing the image resource with linear tiling

用线性平铺实现图像资源

在本节中,我们将使用线性平铺实现图像资源,并在我们上一章中实现的渲染立方体的表面上显示纹理图像。

正如我们在第 6 章“分配图像资源以及使用 WSI 构建交换链”中的“平铺简介”一节中所了解的那样,有两种类型的图像平铺 - 线性和最佳:

  • 线性平铺:Linear tiling,在这种类型的平铺排列中,图像纹素以逐行的方式(行主序)排列,这可能需要一些填充以匹配行间距。 行间距定义了行的宽度;因此,如果排列的纹理元素行小于行间距,则需要使用填充。 VkImageCreateInfo 通过 tiling 字段(VkImageTiling 类型)指示线性平铺。 该参数必须指定为 VK_IMAGE_TILING_LINEAR。
  • 最佳平铺:Optimal tiling,顾名思义,图像的纹理元素以特定于实现的方式进行排列,旨在通过优化内存访问来提供更好的性能。 这里,tiling 字段必须指定为 VK_IMAGE_TILING_OPTIMAL。

线性图像平铺在 VulkanRenderer 类的 createTextureLinear()函数中实现。 函数需要四个参数。 第一个参数(filename)指定要加载哪个图像文件。 第二个参数是一个 TextureData 数据结构,应该在其中存储创建的图像和属性。 第三个参数 imageUsageFlags 指示图像资源的提示,指定使用它的目的。 最后一个参数 format 指定创建图像对象必须使用的图像格式。 这是它的语法:

void VulkanRenderer::createTextureLinear(const char* filename, TextureData
*texture, VkImageUsageFlags imageUsageFlags, VkFormat format);


让我们一步一步了解并实现这些功能。

Loading the image file 加载图像文件

使用 GLI 库并加载图像文件:

// Load the image
gli::texture2D image2D(gli::load(filename)); assert(!image2D.empty());

// Get the image dimensions
texture->textureWidth = uint32_t(image2D[0].dimensions().x); texture->textureHeight = uint32_t(image2D[0].dimensions().y);

// Get number of mip- map levels
texture->mipMapLevels = uint32_t(image2D.levels());

Creating the image object 创建图像对象

使用 vkCreateImage API 创建图像对象(VkImage)。 vkCreateImage()API 使用了 VkImageCreateInfo, 该结构指定图像资源的元数据。 VkImageCreateInfo 的 usage 字段必须使用 VK_IMAGE_USAGE_SAMPLED_BIT 按位标志来传递,因为在着色器阶段,纹理会以采样器的形式使用:

// Create image resource states using VkImageCreateInfo
VkImageCreateInfo imageCreateInfo = {};
imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageCreateInfo.pNext = NULL;
imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
imageCreateInfo.format = format;
imageCreateInfo.extent.width = image2D[0].dimensions().x;
imageCreateInfo.extent.height = image2D[0].dimensions().y;
imageCreateInfo.extent.depth = 1; imageCreateInfo.mipLevels = texture->mipMapLevels;
imageCreateInfo.arrayLayers = 1;
imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageCreateInfo.queueFamilyIndexCount = 0;
imageCreateInfo.pQueueFamilyIndices = NULL;
imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageCreateInfo.usage = imageUsageFlags;
imageCreateInfo.flags = 0;
imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED, imageCreateInfo.tiling = VK_IMAGE_TILING_LINEAR;

// Use create image info and create the image objects
vkCreateImage(deviceObj->device, &imageCreateInfo, NULL, &texture->image);


创建信息结构 VkImageCreateInfo 的 usage 位字段指定如何使用图像资源,例如,用于显示的彩色图像类型,用于深度测试的深度图像类型,用于传输命令的源和目标图像类型等等。 预先提供的这些信息有助于优化资源的管理。

VkImageCreateInfo 控制结构的图像 usage 标志使用 VkImageUsageFlagBits 枚举标志来表示。 以下是每种类型的语法和描述信息:

typedef enum VkImageUsageFlagBits {
VK_IMAGE_USAGE_TRANSFER_SRC_BIT = 0x00000001,
VK_IMAGE_USAGE_TRANSFER_DST_BIT = 0x00000002,
VK_IMAGE_USAGE_SAMPLED_BIT = 0x00000004,
VK_IMAGE_USAGE_STORAGE_BIT = 0x00000008, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT = 0x00000010, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT = 0x00000020, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT = 0x00000040, VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT = 0x00000080,
} VkImageUsageFlagBits;


让我们仔细查看这些按位字段,以了解它们的含义:

枚举类型 Enum type | 描述

—|---

VK_IMAGE_USAGE_TRANSFER_SRC_BIT | 使用此字段,图像会被传送命令的(复制命令)源使用。

VK_IMAGE_USAGE_TRANSFER_DST_BIT | 使用此字段,图像会被传送命令的(复制命令)目标使用。 VK_IMAGE_USAGE_SAMPLED_BIT | 此图像类型通过图像视图类型在着色阶段用作采样器,其中关联的描述符集槽(VkDescriptorSet)类型可以是 VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE 或 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER。
着色器中的采样图像用于地址计算,控制过滤行为和其他的属性。

VK_IMAGE_USAGE_STORAGE_BIT | 使用此图像类型在图像内存上进行加载,存储和原子操作。 图像视图与类型为 VK_DESCRIPTOR_TYPE_STORAGE_IMAGE 的描述符类型槽相关联。

VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | 从这种类型的图像资源创建的图像视图适用于与帧缓冲区对象(VkFrameBuffer)关联的颜色附件或解析附件。

VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | 从这种类型的图像资源创建的图像视图适用于与帧缓冲区对象(VkFrameBuffer)关联的深度、模板附件或解析附件。

VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | 这个标志表示的图像类型是惰性分配的。 为此这种内存类型必须指定为 VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT。 请注意,如果指定了此标志,则不得使用 VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT 和 VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT。

VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT | 从这种类型的图像资源创建的图像视图适用于着色器阶段和帧缓冲区中的输入附件。 图像视图必须与 VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT 类型的描述符集槽(VkDescriptorSet)关联。

注意

分配带有 VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 位标志的内存不会按照请求的大小预先分配物理存储空间,但可以按照单调递增的方式进行分配,其中内存会按照应用程序的需求逐渐增加。

VkImageCreateInfo 枚举中的 flag 字段提示底层应用程序如何使用 VkImageCreateFlagBits 枚举来管理各种图像资源,如内存,格式和属性。 以下是每种类型的语法和描述:

typedef enum VkImageCreateFlagBits {
VK_IMAGE_CREATE_SPARSE_BINDING_BIT = 0x00000001,
VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT = 0x00000002,
VK_IMAGE_CREATE_SPARSE_ALIASED_BIT = 0x00000004,
VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT = 0x00000008,
VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT = 0x00000010,
VK_IMAGE_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkImageCreateFlagBits;
typedef VkFlags VkImageCreateFlags;


让我们来了解一下标志的定义:

标志 | 描述

—|---

VK_IMAGE_CREATE_SPARSE_BINDING_BIT | 在这里,图像使用稀疏内存绑定进行完全存储。

VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT | 在这里,可以使用稀疏内存绑定来部分存储图像。 为了使用此字段,图像必须具有 VK_IMAGE_CREATE_SPARSE_BINDING_BIT 标志。

VK_IMAGE_CREATE_SPARSE_ALIASED_BIT | 在这种类型的标志中,图像被存储到稀疏内存中,并且它也可以在相同的存储区域中保存同一图像的多个部分。 必须使用 VK_IMAGE_CREATE_SPARSE_BINDING_BIT 标志创建图像。

VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | 这允许图像视图格式与图像格式不同。

VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT | 该标志表示可以从图像(VkImage)对象创建 VK_IMAGE_VIEW_TYPE_CUBE 或 VK_IMAGE_VIEW_TYPE_CUBE_ARRAY 类型的 VkImageView。

Memory allocation and binding 内存分配以及绑定

创建的图像对象没有任何设备内存的支持。 在这一步中,我们将分配物理设备内存并将其与创建的 texture->image 绑定。 有关内存分配和绑定过程的更多信息,请参阅第 6 章“分配图像资源以及使用 WSI 创建交换链”中的“内存分配以及绑定图像资源”部分:

// Get the buffer memory requirements
VkMemoryRequirements memoryRequirements; vkGetImageMemoryRequirements(deviceObj->device, texture->image,
&memoryRequirements);

// Create memory allocation metadata information VkMemoryAllocateInfo& memAlloc = texture->memoryAlloc; memAlloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memAlloc.pNext = NULL;
memAlloc.allocationSize = memoryRequirements.size; memAlloc.memoryTypeIndex = 0;

// Determine the type of memory required

// with the help of memory properties
deviceObj->memoryTypeFromProperties (memoryRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
&texture->memoryAlloc.memoryTypeIndex);

// Allocate the memory for buffer objects
vkAllocateMemory(deviceObj->device,
&texture->memoryAlloc, NULL, &(texture->mem));

// Bind the image device memory
vkBindImageMemory(deviceObj->device, texture->image,
texture->mem, 0);

Populating the allocated device memory

填充分配的设备内存

使用映射(vkMapMemory)和解映射(vkUnmapMemory)函数将图像数据 push 到 GPU 上。 首先,使用 vkGetImageSubresourceLayout()API 查询资源的布局信息。 布局信息提供了用于以逐行方式存储图像数据的 rowPitch 信息:

VkImageSubresource subresource  = {};
subresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; subresource.mipLevel = 0;
subresource.arrayLayer = 0;

VkSubresourceLayout layout; uint8_t *data;

vkGetImageSubresourceLayout(deviceObj->device,
texture->image, &subresource, &layout);

// Map the GPU memory on to local host
error = vkMapMemory(deviceObj->device, texture->mem, 0, texture
->memoryAlloc.allocationSize, 0, (void**)&data); assert(!error);

// Load image texture data in the mapped buffer
uint8_t* dataTemp = (uint8_t*)image2D.data();
for (int y = 0; y < image2D[0].dimensions().y; y++)
{
size_t imageSize = image2D[0].dimensions().y * 4; memcpy(data, dataTemp, imageSize);
dataTemp += imageSize;

// Advance row- by- row pitch information
data += layout.rowPitch;
}

// UnMap the host memory to push changes into the device memory
vkUnmapMemory(deviceObj->device, texture->mem);

Creating the command buffer object

创建命令缓冲区对象

图像资源对象是使用 VulkanRenderer 类中定义的命令缓冲区对象 cmdTexture 创建的:

// Command buffer allocation and recording begins
CommandBufferMgr::allocCommandBuffer(&deviceObj->device,
cmdPool, &cmdTexture); CommandBufferMgr::beginCommandBuffer(cmdTexture);

Setting the image layout 设置图像布局

使用 setImageLayout()函数设置图像布局(VkImageLayout)。 有关此函数的更多信息,请参阅第 6 章“分配图像资源以及使用 WSI 构建交换链”中的“使用内存屏障设置图像布局”部分:

VkImageSubresourceRange subresourceRange = {};
subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
subresourceRange.baseMipLevel = 0;
subresourceRange.levelCount = texture->mipMapLevels;
subresourceRange.layerCount = 1;
texture->imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
setImageLayout(texture->image, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_PREINITIALIZED, texture->imageLayout,
VK_ACCESS_HOST_WRITE_BIT, subresourceRange, cmdTexture);

Submitting the command buffer 提交命令缓冲区

结束命令缓冲区的记录并将命令缓冲区提交到图形队列。 另外,创建一个确保命令缓冲区执行完成的栏栅 fence。 如果栏栅是有信号的,则表示使用指定的图像布局成功创建了图像对象。 栏栅用于同步主机和设备的操作。

在下面的实现中,我们在主机上创建一个 fence 对象(fenceCI),并使用队列提交命令(vkQueueSubmit)将其提交给队列。 该栏栅将确保在执行任何进一步的命令之前保证先执行前半部分的内存操作。 该保证是由栏栅对象提供的,当成功创建了图像布局时,由设备用信号通知栏栅对象,即栏栅对象处于有信号状态。 主机必须等待执行其他任何操作,直到栏栅是有信号的。

注意

有关栏栅的更多信息,请参阅第 9 章“绘图对象”中的“了解 Vulkan 中的同步原语”部分。

以下代码片段显示了两件事情,首先,命令缓冲区的记录已完成并准备好提交到图形队列中。 其次,我们创建了 fence 对象,并在主机端等待,让 GPU 完成对提交的包含纹理处理请求的命令缓冲区的操作:

// Stop command buffer recording
CommandBufferMgr::endCommandBuffer(cmdTexture);

// Create a fence to make sure that the

// copies have finished before continuing
VkFence fence; VkFenceCreateInfo fenceCI= {};
fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceCI.flags = 0;
vkCreateFence(deviceObj->device, &fenceCI, nullptr, &fence); VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pNext = NULL; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &cmdTexture;

CommandBufferMgr::submitCommandBuffer(deviceObj->queue,
&cmdTexture, &submitInfo, fence);

// Wait for maximum 10 seconds, if fence is not signaled
vkWaitForFences(deviceObj->device, 1, &fence,
VK_TRUE, 10000000000);
vkDestroyFence(deviceObj->device, fence, nullptr);

Creating an image sampler 创建图像采样器

采样器是一个对象,它包含一组通过各种控制参数控制格式化图像数据外观的算法。 这些参数控制图像转换,缩小和放大滤波,mipmapping 和包装,并产生最终图像纹理的样本数组。

在 Vulkan 中,图像采样器是使用 vkCreateSampler()API 创建的。 该 API 接受四个参数,如以下 API 语法所示。 第一个参数 device 是负责创建采样器对象的逻辑设备。 第二个参数是 VkSamplerCreateInfo 类型的图像属性控制器结构;我们接下来会讨论这个结构。第三个参数(pAllocator)控制主机内存的分配;此 API 会创建采样器对象,并将其返回到最后一个名为 pSampler 的参数中:

VKAPI_ATTR VkResult VKAPI_CALL vkCreateSampler(
VkDevice device,
const VkSamplerCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSampler* pSampler);


使用 VkSamplerCreateInfo 结构将这些相关的图像属性控制器参数传递到上述 API 中;以下是此结构的语法:

typedef struct VkSamplerCreateInfo { VkStructureType  sType;
const void* pNext; VkSamplerCreateFlags flags; VkFilter magFilter;
VkFilter minFilter; VkSamplerMipmapMode mipmapMode; VkSamplerAddressMode addressModeU; VkSamplerAddressMode addressModeV; VkSamplerAddressMode addressModeW; float mipLodBias;
VkBool32 anisotropyEnable;
float maxAnisotropy;
VkBool32 compareEnable;
VkCompareOp compareOp;
float minLod;
float maxLod;
VkBorderColor borderColor;
VkBool32 unnormalizedCoordinates;
} VkSamplerCreateInfo;


让我们来看看下表中的每一个字段:

字段 | 描述

—|---

sType | 这是这个控制结构的类型信息。 必须将其指定为 VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO。 pNext | 这可能是一个指向扩展特定结构的有效指针,也可能是 NULL。

flags | 这个用于将来的保留字段目前未被使用。 magFilter | 该字段对应于放大过滤器。 它是 VkFilter 类型的枚举,可以将 VK_FILTER_NEAREST 和 VK_FILTER_LINEAR 值作为输入。 我们会讨论这些过滤选项,并在本节后面看到它们的作用。 minFilter | 该字段对应于缩小过滤器。

mipmapMode | 该字段用于指定 mipmapping 过滤模式并接受类型为 VkSamplerMipmapMode 的枚举值,如下所示:
typedef enum VkSamplerMipmapMode { VK_SAMPLER_MIPMAP_MODE_NEAREST = 0, VK_SAMPLER_MIPMAP_MODE_LINEAR = 1, } VkSamplerMipmapMode;

addressModeU | 当图像纹理元素坐标超出 [0 … 1] 范围时,此字段控制沿 U 坐标的图像环绕。

addressModeV | 当图像纹理元素坐标超出 [0 … 1] 范围时,此字段控制沿 V 坐标的图像环绕。

addressModeW | 当图像纹理元素坐标超出 [0 … 1] 范围时,此字段控制沿 W 坐标的图像环绕。

mipLodBias | 这是一个浮动的偏差值,它会被添加到 mipmap level of detail (LOD)计算中。

anisotropyEnable | 该布尔型字段指示是否启用各向异性过滤 anisotropic filtering(VK_TRUE)或禁用(VK_FALSE)。

maxAnisotropy | 这是用于钳位目的的最大各向异性过滤值 anisotropic filtering value。

compareEnable | 此布尔型字段指示在过滤查找时是否与参考值进行比较。

compareOp | 在执行所需的过滤之前,可以使用此字段中指定的比较函数比较取出的纹理元素数据。

minLod | 这表示要用于计算 LOD 的最小钳位值。

maxLod | 这表示要用于计算 LOD 的最大钳位值。

borderColor | 这是用于将指定的边框颜色替换现有纹理元素的预定义颜色。 边框颜色使用 VkBorderColor 指定,如下所示:
typedef enum VkBorderColor { VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK = 0, VK_BORDER_COLOR_INT_TRANSPARENT_BLACK = 1, VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK = 2, VK_BORDER_COLOR_INT_OPAQUE_BLACK = 3, VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE = 4, VK_BORDER_COLOR_INT_OPAQUE_WHITE = 5, } VkBorderColor; unnormalizedCoordinates | 此布尔字段指示是否使用非标准化的纹理元素坐标(VK_TRUE)或归一化的纹理元素坐标(VK_FALSE)进行纹理元素查找。

Filtering 过滤器

当纹理缩放时,纹理过滤控制着纹理质量的外观。 在正确的深度处,一个纹理元素可能正好与屏幕上的一个像素相对应。 但是,将更小的纹理映射到更大的几何图形上可能会导致纹理出现拉伸现象。 这就是所谓的放大。 换句话说,图像尺寸小于需要将其映射到其上的几何图形的尺寸。 另一方面,当几何形状小于图像尺寸时,许多纹素会共享若干个像素的位置,导致图像收缩。 这就是所谓的缩小。

放大和缩小图像的效果可以使用过滤模式进行控制;Vulkan 使用 VkFilter 枚举。 此枚举有以下两个字段: - VK_FILTER_NEAREST:样本使用与指定的纹理坐标最接近的纹理元素。 - VK_FILTER_LINEAR:这个字段的意思是,使用最接近计算纹理坐标的周围的四个像素的加权平均值。

<img src=“https://pic4.zhimg.com/v2-d8cf533d547390bdff17dfbf44e82223_b.jpg” data-caption="" data-size=“normal” data-rawwidth=“670” data-rawheight=“378” class=“origin_image zh-lightbox-thumb” width=“670” data-original=“https://pic4.zhimg.com/v2-d8cf533d547390bdff17dfbf44e82223_r.jpg”/>第 11 章. Drawing Textures 绘制纹理


过滤模式在 VKSamplerCreateInfo 的 magFilter 和 minFilter 中指定。

Wrapping modes 包装模式

当纹理映射的范围大于 1.0 时,Vulkan 通过沿着 U,V 和 W 纹理坐标的 VkSamplerAddressMode 支持样本寻址模式;Vulkan 采样允许使用以下类型的包装寻址模式:

  1. VK_SAMPLER_ADDRESS_MODE_REPEAT:这会产生重复模式。
  2. VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT:这会产生相邻纹素镜像的重复模式。
  3. VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE:这将会重复边界纹素直到达到边缘;有关更多信息,请参阅以下屏幕截图。
  4. VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER:超出边界的纹理元素会被钳位。

在下面的屏幕截图中,我们使用了大于 [0 … 1] 的纹理坐标范围,并演示了包装模式的用法:


<img src=“https://pic3.zhimg.com/v2-d657b8b6cc848700485f5aa491123882_b.jpg” data-caption="" data-size=“normal” data-rawwidth=“1362” data-rawheight=“1217” class=“origin_image zh-lightbox-thumb” width=“1362” data-original=“https://pic3.zhimg.com/v2-d657b8b6cc848700485f5aa491123882_r.jpg”/>第 11 章. Drawing Textures 绘制纹理

使用 addressModeU,addressModeV 和 addressModeW 字段在 VkSamplerCreateInfo 中指定寻址模式。

以下是 Vulkan API 中采样器的代码实现:

// Specify a particular kind of texture using samplers
VkSamplerCreateInfo samplerCI = {};
samplerCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerCI.pNext = NULL; samplerCI.magFilter = VK_FILTER_LINEAR; samplerCI.minFilter = VK_FILTER_LINEAR;
samplerCI.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; samplerCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerCI.mipLodBias = 0.0f;

if (deviceObj->deviceFeatures.samplerAnisotropy == VK_TRUE)
{

}
else
{

}

samplerCI.anisotropyEnable = VK_TRUE; samplerCI.maxAnisotropy = 8;

samplerCI.anisotropyEnable = VK_FALSE; samplerCI.maxAnisotropy = 1;

samplerCI.compareOp = VK_COMPARE_OP_NEVER;
samplerCI.minLod = 0.0f;
samplerCI.maxLod = 0.0f;
samplerCI.borderColor = VK_BORDER_COLOR_FLOAT
_OPAQUE_WHITE;
samplerCI.unnormalizedCoordinates = VK_FALSE;

// Create the sampler
error = vkCreateSampler(deviceObj->device, &samplerCI,
NULL, &texture->sampler);
assert(!error);

// Specify the sampler in the texture’s descsImgInfo
texture->descsImgInfo.sampler = texture->sampler;


如果使用各向同性过滤(isotropic filtering),那么设备必须启用采样器无向性 samplerAnisotropy 特性。 如果物理设备支持,则可以在创建逻辑设备(VkDevice)时启用此功能。

vkGetPhysicalDeviceFeatures()API 查询物理设备的特性;这个 API 接受两个参数。 第一个参数(physicalDevice)指示物理设备句柄。 第二个参数(pFeatures)是 VkPhysicalDeviceFeatures 控制结构,其中包含了预定义的特性列表,会再次检查以测试它是否受支持这些特性:

void vkGetPhysicalDeviceFeatures( VkPhysicalDevice    physicalDevice, VkPhysicalDeviceFeatures* pFeatures);


一旦调用 vkGetPhysicalDeviceFeatures()API,就会为每个特性存储一个布尔值,表明是否支持相应的特性。 布尔 VK_TRUE 表示该特性受支持;布尔 VK_FALSE 表示该特性不受物理设备支持。

如果物理设备支持 VkPhysicalDeviceFeatures :: samplerAnisotropy,则就会在创建逻辑设备时启用该功能。 这可以通过创建一个新的 VkPhysicalDeviceFeatures 对象并将 samplerAnisotropy 设置为 VK_TRUE(如果支持 samplerAnisotropy 的话)来完成,如下面的代码所示:

VkResult VulkanDevice::createDevice(std::vector<const char *>& layers, std::vector<const char *>& extensions)
{

// Many lines skipped please refer to source code. VkPhysicalDeviceFeatures getEnabledFeatures; vkGetPhysicalDeviceFeatures(*gpu, &getEnabledFeatures);

VkPhysicalDeviceFeatures setEnabledFeatures = { VK_FALSE }; setEnabledFeatures.samplerAnisotropy =
getEnabledFeatures.samplerAnisotropy;

VkDeviceCreateInfo deviceInfo = {};
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE
_CREATE_INFO;
deviceInfo.pNext = NULL; deviceInfo.queueCreateInfoCount = 1; deviceInfo.pQueueCreateInfos = &queueInfo; deviceInfo.enabledLayerCount = 0; deviceInfo.ppEnabledLayerNames = NULL; deviceInfo.enabledExtensionCount = (uint32_t) extensions.size(); deviceInfo.ppEnabledExtensionNames = extensions.size() ?
extensions.data() : NULL; deviceInfo.pEnabledFeatures = &setEnabledFeatures;

result = vkCreateDevice(*gpu, &deviceInfo, NULL, &device); assert(result == VK_SUCCESS);

return result;
}

Creating the image view 创建图像视图

创建图像视图并将其存储在本地 TextureData 对象的 texture 中。 标志 flag 字段必须是 VK_IMAGE_VIEW_TYPE_2D:

// Specify the attribute used in create the image view
VkImageViewCreateInfo viewCI = {};
viewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewCI.pNext = NULL;
viewCI.viewType = VK_IMAGE_VIEW_TYPE_2D; viewCI.format = format;
viewCI.components.r = VK_COMPONENT_SWIZZLE_R; viewCI.components.g = VK_COMPONENT_SWIZZLE_G; viewCI.components.b = VK_COMPONENT_SWIZZLE_B; viewCI.components.a = VK_COMPONENT_SWIZZLE_A; viewCI.subresourceRange = subresourceRange; viewCI.flags = 0;
viewCI.image = texture->image;

// Create the image view
error = vkCreateImageView(deviceObj->device, &viewCI,
NULL, &texture->view);
assert(!error);


标志 flag 字段表示将要创建的图像视图(VkImageViewType)的类型。 这些类型如下所示,它们都是自描述的:

typedef enum {
VK_IMAGE_VIEW_TYPE_1D = 0,
VK_IMAGE_VIEW_TYPE_2D = 1,
VK_IMAGE_VIEW_TYPE_3D = 2,
VK_IMAGE_VIEW_TYPE_CUBE = 3,
VK_IMAGE_VIEW_TYPE_1D_ARRAY = 4,
VK_IMAGE_VIEW_TYPE_2D_ARRAY = 5,
VK_IMAGE_VIEW_TYPE_CUBE_ARRAY = 6,
} VkImageViewType;

Implementing the image resource with optimal tiling

使用优化平铺实现图像资源

优化平铺是通过暂存缓冲区(staging buffer)实现的。 首先,创建一个缓冲区资源对象并使用原始的图像数据内容进行存储。 接下来,使用 buffer-to-image copy 命令将缓冲区资源数据内容复制到新创建的图像对象。 buffer-to-image copy 命令(vkCmdCopyBufferToImage)会将缓冲区内存的内容复制到图像内存。

在本节中,我们会使用优化平铺实现图像资源。 为了创建采用优化平铺形式的图像资源,我们可以使用用户定义的函数 VulkanRenderer :: createTextureOptimal()。 该函数以 createTextureLinear()函数相同的方式获取参数:

void VulkanRenderer::createTextureOptimal(const char* filename, TextureData
*texture, VkImageUsageFlags imageUsageFlags, VkFormat format);


让我们一步一步了解并实现这些函数。

Loading the image file 加载图像文件

加载图像文件并检索其尺寸和 mipmap-level 的信息:

// Load the image
gli::texture2D image2D(gli::load(filename)); assert(!image2D.empty());

// Get the image dimensions
texture->textureWidth = uint32_t(image2D[0].dimensions().x); texture->textureHeight = uint32_t(image2D[0].dimensions().y);

// Get number of mip- map levels
texture->mipMapLevels = uint32_t(image2D.levels());

Buffer object memory allocation and binding

缓冲区对象的内存分配以及绑定

创建的图像对象没有任何设备内存的支持。 在这一步中,我们会分配物理的设备内存并将其与创建的 texture->image 绑定。 有关内存分配和绑定过程的更多信息,请参阅第 6 章“分配图像资源以及使用 WSI 构建 Swapchain”中的“内存分配以及绑定图像资源”部分:

// Create a staging buffer resource states using.

// Indicate it be the source of the transfer command.

// .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT
VkBufferCreateInfo bufferCreateInfo = {};
bufferCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferCreateInfo.size = image2D.size();
bufferCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

// Get the buffer memory requirements for the staging buffer
VkMemoryRequirements memRqrmnt; VkDeviceMemory devMemory;
vkGetBufferMemoryRequirements(deviceObj->device, buffer,
&memRqrmnt);

VkMemoryAllocateInfo memAllocInfo = {};
memAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memAllocInfo.pNext = NULL; memAllocInfo.allocationSize = 0;
memAllocInfo.memoryTypeIndex = 0; memAllocInfo.allocationSize = memRqrmnt.size;

// Determine the type of memory required for

// the host- visible buffer
deviceObj->memoryTypeFromProperties(memRqrmnt.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
&memAllocInfo.memoryTypeIndex);

// Allocate the memory for host- visible buffer objects -
error = vkAllocateMemory(deviceObj->device, &memAllocInfo,
nullptr, &devMemory);
assert(!error);

// Bind the host- visible buffer with allocated device memory - error=vkBindBufferMemory(deviceObj->device,buffer,devMemory,0); assert(!error);

Populating the allocated device memory

填充分配的设备内存

使用 vkMapMemory()并将加载的图像的原始内容填充到缓冲区对象的设备内存中。 一旦映射完成,就使用 vkUnmapMemory()完成从主机上传数据到设备内存的过程:

// Populate the raw image data into the device memory
uint8_t *data;
error = vkMapMemory(deviceObj->device, devMemory, 0,
memRqrmnt.size, 0, (void **)&data);
assert(!error);

memcpy(data, image2D.data(), image2D.size()); vkUnmapMemory(deviceObj->device, devMemory);

Creating the image object 创建图像对象

必须使用平铺(.tiling)选项作为优化平铺(VK_IMAGE_TILING_OPTIMAL)创建图像的创建信息对象(VkImageCreateInfo)。 另外,必须使用 VK_IMAGE_USAGE_TRANSFER_DST_BIT 设置图像的 usage 标志,使其成为复制命令的目标,以便将数据内容从 buffer 对象传送到 texture->image:

// Create image info with optimal tiling

// support (.tiling = VK_IMAGE_TILING_OPTIMAL)
VkImageCreateInfo imageCreateInfo = {};
imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; imageCreateInfo.pNext = NULL;
imageCreateInfo.imageType = VK_IMAGE_TYPE_2D; imageCreateInfo.format = format; imageCreateInfo.mipLevels = texture->mipMapLevels; imageCreateInfo.arrayLayers = 1; imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; imageCreateInfo.extent = { texture->textureWidth,
texture->textureHeight, 1 };
imageCreateInfo.usage = imageUsageFlags;

// Set image object with VK_IMAGE_USAGE_TRANSFER_DST_BIT if

// not set already. This allows to copy the source VkBuffer

// object (with VK_IMAGE_USAGE_TRANSFER_DST_BIT) contents

// into this image object memory(destination).
if (!(imageCreateInfo.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT)){ imageCreateInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
}

error = vkCreateImage(deviceObj->device, &imageCreateInfo,
nullptr, &texture->image);
assert(!error);

Image object memory allocation and binding

图像对象内存的分配以及绑定

分配物理的内存支持并将其与创建的 texture->image 绑定。 有关内存分配和绑定过程的更多信息,请参阅第 6 章“分配图像资源以及使用 WSI 创建 Swapchain”中的“内存分配以及绑定图像资源”部分:

// Get the image memory requirements
vkGetImageMemoryRequirements(deviceObj->device, texture->image,
&memRqrmnt);

// Set the allocation size equal to the buffer allocation
memAllocInfo.allocationSize = memRqrmnt.size;

// Determine the type of memory required with the help of memory properties
deviceObj->memoryTypeFromProperties(memRqrmnt.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
&memAllocInfo.memoryTypeIndex);

// Allocate the physical memory on the GPU
error = vkAllocateMemory(deviceObj->device, &memAllocInfo,
nullptr, &texture->mem);
assert(!error);

// Bound the physical memory with the created image object
error = vkBindImageMemory(deviceObj->device, texture->image,
texture->mem, 0);
assert(!error);

Creating a command buffer object

创建命令缓冲区对象

图像资源对象是使用 VulkanRenderer 类中定义的命令缓冲区对象 cmdTexture 创建的。 分配命令缓冲区以便设置图像布局,以及开始记录命令缓冲区:

// Command buffer allocation and recording begins
CommandBufferMgr::allocCommandBuffer(&deviceObj->device,
cmdPool, &cmdTexture); CommandBufferMgr::beginCommandBuffer(cmdTexture);

Setting the image layout 设置图像布局

由于数据内容将从暂存缓冲区对象(源)复制到图像对象(目标),因此将图像布局(VkImageLayout)设置为 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL。 有关 setImageLayout()函数的更多信息,请参阅第 6 章“分配图像资源以及使用 WSI 构建交换链”中的“使用内存屏障设置图像布局”部分:

VkImageSubresourceRange subresourceRange = {}; subresourceRange.aspectMask  = VK_IMAGE_ASPECT_COLOR_BIT; subresourceRange.baseMipLevel  = 0; subresourceRange.levelCount    = texture->mipMapLevels; subresourceRange.layerCount    = 1;

// Set the image layout to be

// VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL

// since it is destination for copying buffer

// into image using vkCmdCopyBufferToImage -
setImageLayout(texture->image, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_LAYOUT_UNDEFINED,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
(VkAccessFlagBits)0, subresourceRange, cmdTexture);

Buffer to image copy 从缓冲区到图像的拷贝

为图像对象及其子资源 mipmap 创建缓冲区图像复制区域。 使用 copy 命令将缓冲区对象的(buffer)设备内存内容传送到图像对象的(texture->image)内存内容:

// List contain buffer image copy for each mipLevel
std::vector<VkBufferImageCopy> bufferImgCopyList;

uint32_t bufferOffset = 0;
// Iterater through each mip level and set buffer image copy
for (uint32_t i = 0; i < texture->mipMapLevels; i++)
{
VkBufferImageCopy bufImgCopyItem = {}; bufImgCopyItem.imageSubresource.aspectMask =
VK_IMAGE_ASPECT_COLOR_BIT;
bufImgCopyItem.imageSubresource.mipLevel = i; bufImgCopyItem.imageSubresource.layerCount = 1;
bufImgCopyItem.imageSubresource.baseArrayLayer = 0; bufImgCopyItem.imageExtent.width =
uint32_t(image2D[i].dimensions().x); bufImgCopyItem.imageExtent.height =
uint32_t(image2D[i].dimensions().y); bufImgCopyItem.imageExtent.depth = 1; bufImgCopyItem.bufferOffset = bufferOffset;

bufferImgCopyList.push_back(bufImgCopyItem);

// adjust buffer offset
bufferOffset += uint32_t(image2D[i].size());
}

// Copy the staging buffer memory data containing the

// staged raw data(with mip levels) into the image object
vkCmdCopyBufferToImage(cmdTexture, buffer, texture->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
uint32_t(bufferImgCopyList.size()), bufferImgCopyList.data());


有关复制命令的更多信息,请参阅下一节“了解复制命令”。

Setting the optimal image layout 设置优化的图像布局

设置图像布局,指示新布局为兼容的优化平铺形式。 底层实现会使用此标志并选择合适的技术以一种优化的方式排列图像的内容:

// Advised to change the image layout to shader read

// after staged buffer copied into image memory
texture->imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; setImageLayout(texture->image, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
texture->imageLayout, subresourceRange, cmdTexture);


注意

图像的布局可以通过以下方式进行控制:

a)通过指定初始布局 initial layout 来创建图像资源 image resource

b)使用内存障碍 memory barriers 显式指定

c)或者在渲染通道 Render Pass 中使用它的时候

Submitting the command buffer 提交命令缓冲区

完成命令缓冲区的记录并将其提交给图形队列:

// Submit command buffer containing copy

// and image layout commands
CommandBufferMgr::endCommandBuffer(cmdTexture);

// Create a fence object to ensure that the command

// buffer is executed, coping our staged raw data

// from the buffers to image memory with

// respective image layout and attributes into consideration
VkFence fence;
VkFenceCreateInfo fenceCI = {};
fenceCI.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceCI.flags = 0;

error = vkCreateFence(deviceObj->device, &fenceCI, nullptr,
&fence);
assert(!error);

VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pNext = NULL; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &cmdTexture;

CommandBufferMgr::submitCommandBuffer(deviceObj->queue,
&cmdTexture, &submitInfo, fence);

error = vkWaitForFences(deviceObj->device, 1, &fence, VK_TRUE,
10000000000);
assert(!error);

vkDestroyFence(deviceObj->device, fence, nullptr);

// destroy the allocated resoureces vkFreeMemory(deviceObj->device, devMemory, nullptr); vkDestroyBuffer(deviceObj->device, buffer, nullptr);


添加栏栅作为同步原语,以确保在可以使用图像之前成功准备好了图像布局。 一旦栏栅获得信号(有信号状态),就释放栏栅对象。 如果栏栅未能获得信号(In case the fence fails to signal),则等待命令 vkWaitForFences()最多等待 10 秒以确保系统不会暂停或进入无限等待状态。

注意

有关栏栅的更多信息,请参阅第 9 章“绘图对象”中的“了解 Vulkan 中的同步原语”部分。

Creating an image sampler 创建图像采样器

使用线性过滤(linear filtering)创建一个图像采样器用于缩小(minFilter)和放大(magFilter),并启用各向异性滤波(anisotropy filtering):

// Create sampler
VkSamplerCreateInfo samplerCI = {};
samplerCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerCI.pNext = NULL; samplerCI.magFilter = VK_FILTER_LINEAR; samplerCI.minFilter = VK_FILTER_LINEAR;
samplerCI.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; samplerCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerCI.mipLodBias = 0.0f;

if (deviceObj->deviceFeatures.samplerAnisotropy == VK_TRUE)
{

}
else
{

}

samplerCI.anisotropyEnable = VK_TRUE; samplerCI.maxAnisotropy = 8;

samplerCI.anisotropyEnable = VK_FALSE; samplerCI.maxAnisotropy = 1;

samplerCI.compareOp = VK_COMPARE_OP_NEVER;
samplerCI.minLod = 0.0f;
samplerCI.maxLod = (float)texture->mipMapLevels; samplerCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; samplerCI.unnormalizedCoordinates = VK_FALSE;

error = vkCreateSampler(deviceObj->device, &samplerCI, nullptr,
&texture->sampler);
assert(!error);

// Specify the sampler in the texture’s descsImgInfo
texture->descsImgInfo.sampler = texture->sampler;

Creating the image view 创建图像视图

创建图像视图并将其存储在本地 TextureData 对象的 texture 中:

// Create image view to allow shader to
// access the texture information
VkImageViewCreateInfo viewCI = {};
viewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewCI.pNext = NULL;
viewCI.image = VK_NULL_HANDLE;
viewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewCI.format = format;
viewCI.components.r = VK_COMPONENT_SWIZZLE_R;
viewCI.components.g = VK_COMPONENT_SWIZZLE_G;
viewCI.components.b = VK_COMPONENT_SWIZZLE_B;
viewCI.components.a = VK_COMPONENT_SWIZZLE_A;
viewCI.subresourceRange = subresourceRange; viewCI.subresourceRange.levelCount = texture->mipMapLevels;
// Optimal tiling supports mip map levels very well set it.
viewCI.image = texture->image;

error = vkCreateImageView(deviceObj->device, &viewCI, NULL,
&texture->view);
assert(!error);

// Fill descriptor image info that can be
// used for setting up descriptor sets
texture->descsImgInfo.imageView = texture->view;

Copying data content between images and buffers

在图像和缓冲区之间复制数据内容

复制命令是特殊的传送命令,将数据内容从一个存储区域传送到另一个存储区域。 这些区域可能位于缓冲区对象,图像对象以及缓冲区到图像 buffer- to-image 之间,反之亦然。

根据应用的需要,您可能需要在各种情况下在缓冲区和图像之间复制数据。 有四种类型的复制命令可用于完成这项工作:

  • vkCmdCopyBuffer:将数据内容从源缓冲区复制到目标缓冲区对象的设备内存。
  • vkCmdCopyImage:源图像对象的特定部分被复制到目标图像的区域。
  • vkCmdCopyBufferToImage:缓冲区对象的数据内容被复制到图像对象。
  • vkCmdCopyImageToBuffer:图像对象的数据内容被复制到缓冲区对象。

在优化的平铺实现中,我们使用了 vkCmdCopyBufferToImage。 以下是语法:

void vkCmdCopyBufferToImage(
VkCommandBuffer commandBuffer,
VkBuffer srcBuffer,
VkImage dstImage,
VkImageLayout dstImageLayout,
uint32_t regionCount, const VkBufferImageCopy* pRegions);


该 API 接受六个参数,在下表中进行了解释:

参数 | 描述

—|---

commandBuffer | vkCmdCopyImageToBuffer 命令将会被记录在此命令缓冲区对象中。

srcBuffer | 这指的是源缓冲区(VkBuffer)对象,数据内容将会被从其中进行复制。

dstImage | 这是指目标图像(VkImage)对象,会有部分数据内容会被复制到其中。

dstImageLayout | 这是 dstImage 对象的图像布局对象(VkImageLayout)。

regionCount | 这是执行数据内容传输的复制区域的总数。 pRegions | 该参数是一个指向 VkBufferCopy 数组的指针,VkBufferCopy 持有要进行数据传输区域的规范信息。

在下一节中,我们会使用包含图像布局和图像视图的图像对象来更新描述符集,并在 3D 立方体上渲染这个图像对象。

Updating the descriptor set 更新描述符集

一旦通过线性或优化平铺布局方式准备好了纹理,就只需要使用创建的图像资源对象更新描述符集。 这在已实现的 VulkanDrawable :: createDescriptorSet()函数中完成。 输入参数 useTexture 必须为 true 才能支持纹理。 以下着重显示了此函数为了支持纹理需要做的一些调整。

当 useTexture 参数为 true 时,第二个 VkWriteDescriptorSet 元素(索引为 1)就会使用纹理信息进行填充。 这里最重要的有两件事情:

  • 设置纹理对象:texture object,必须使用纹理对象的(TextureData)descsImgInfo(类型为 VkDescriptorImageInfo)来设置 VkWriteDescriptorSet 的 pImageInfo 字段。
  • 布局绑定:Layout binding,这必须等于片段着色器中指定的索引号。 例如,在本示例中,采样器在布局绑定 1 位置处接收,因此 writes[1].dstBinding= 1。:
// Creates the descriptor sets using descriptor pool.

// This function depend on the createDescriptorPool()

// and createUniformBuffer().
void VulkanDrawable::createDescriptorSet(bool useTexture)
{
VulkanPipeline* pipelineObj = rendererObj->getPipelineObject(); VkResult result;

// Create the descriptor allocation structure and specify
// the descriptor pool and descriptor layout VkDescriptorSetAllocateInfo dsAllocInfo[1];
dsAllocInfo[0].sType = VK_STRUCTURE_TYPE_DESCRIPTOR _SET_ALLOCATE_INFO;
dsAllocInfo[0].pNext = NULL; dsAllocInfo[0].descriptorPool = descriptorPool; dsAllocInfo[0].descriptorSetCount = 1; dsAllocInfo[0].pSetLayouts = descLayout.data();

// Allocate the number of descriptor set needs to be produced descriptorSet.resize(1);

// Allocate descriptor sets
result = vkAllocateDescriptorSets(deviceObj->device,
dsAllocInfo, descriptorSet.data()); assert(result == VK_SUCCESS);

// Allocate two write descriptors for - 1. MVP and 2. Texture VkWriteDescriptorSet writes[2];
memset(&writes, 0, sizeof(writes));

// Specify the uniform buffer related
// information into first write descriptor writes[0] = {};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].pNext = NULL; writes[0].dstSet = descriptorSet[0]; writes[0].descriptorCount = 1;
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[0].pBufferInfo = &UniformData.bufferInfo; writes[0].dstArrayElement = 0;
writes[0].dstBinding = 0; // DESCRIPTOR_SET_BINDING_INDEX

// If texture is used then update the second

// write descriptor structure
if (useTexture)
{
writes[1] = {};

writes[1].sType = VK_STRUCTURE_TYPE_WRITE-

DESCRIPTOR_SET;
writes[1].dstSet = descriptorSet[0]; writes[1].dstBinding = 1;
writes[1].descriptorCount = 1; writes[1].descriptorType = VK_DESCRIPTOR_TYPE

COMBINED_IMAGE_SAMPLER;

writes[1].pImageInfo = & textures->descsImgInfo;

/
}

// Update the uniform buffer into the allocated descriptor set vkUpdateDescriptorSets(deviceObj->device,
useTexture ? 2 : 1, writes, 0, NULL);
}


有关此函数以及描述符集的创建的更多信息,请参阅在第 10 章“描述符和 push 常量”中的“创建描述符集”小节。

以下是渲染纹理的输出:


<img src=“https://pic1.zhimg.com/v2-128dcb7abca5050f2e027ed23d06ae80_b.jpg” data-caption="" data-size=“normal” data-rawwidth=“747” data-rawheight=“792” class=“origin_image zh-lightbox-thumb” width=“747” data-original=“https://pic1.zhimg.com/v2-128dcb7abca5050f2e027ed23d06ae80_r.jpg”/>第 11 章. Drawing Textures 绘制纹理

总结

本章实现了图像资源并在 3D 几何体对象上渲染了纹理。 它简要回顾了图像资源的基础知识,并介绍了图像对象,图像布局和图像视图的基本概念。

我们使用线性和最佳平铺方案实现了纹理的渲染。 前者的实现简单,纯粹基于图像类型的资源。 后一种方案是使用暂存(staging)实现的,其中使用了缓冲区和图像资源;在这种方案中,图像首先存储在缓冲区对象中并使用复制命令传输到图像对象。

上一篇:(四)c#Winform自定义控件-选择按钮组


下一篇:C# WinForm自定义通用分页控件