Vulkan之Image, ImageView and Sample

vulkan中的资源从大的分类来说就两种,一种是Buffer,一种是Image。Buffer可以用来存放顶点,材质等等信息,而Image一般则是用来做attachment或者纹理贴图。

纹理贴图是需要从外界获取的,实际上他就是一张图片,因此我们需要首先将其加载到vulkan的缓存中

1) 外部图像加载

可以直接使用stb_image库来加载图像,只需要下载并将stb_image.h文件配置到工程里,就可以直接使用这个库了。
stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
其中texWidth,texHeight,texChannels分别表示加载的图像的宽,高以及通道。STBI_rgb_alpha参数表示强制生成alpha通道,因为我们在vulkan用的大多数图像都是4通道的。最终我们加载的图像信息存放在了pixels里面,这是一块内存,并不直接是图像。

2)创建stagingBuffer

更vertexBuffer的原理一样,这里也是需要用到stagingBuffer,原因也是一样的,因为Image实际上会被shader频繁采样,所以不能设置为CPU直接访问的格式,因此需要staging缓冲一下。
首先创建stagingBuffer,并分配stagingBufferMemory。然后做GPU的内存映射到CPU可访问的地址,然后执行memcpy,最后free掉pixels内存占用。

1
2
3
4
5
6
7
8
9
10
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);

3)创建纹理图像

跟创建Buffer很像,也是要先创建Image对象,然后allocate存储空间

1
2
3
4
5
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(
VkDevice device,
const VkImageCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkImage* pImage);

a)创建所需的结构体VkImageCreateInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct VkImageCreateInfo {
VkStructureType sType; // VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO
const void* pNext; // 指向扩展结构体
VkImageCreateFlags flags; // 图像属性的标志位
VkImageType imageType; // 图像类型,1D,2D,3D
VkFormat format; // 图像格式, 比如VK_FORMAT_R8G8B8A8_SRGB
VkExtent3D extent; // 存放图像的width,height,depth信息
uint32_t mipLevels; // mipMap级别
uint32_t arrayLayers; // 图像的层数
VkSampleCountFlagBits samples; // 采样的次数,如果不需要多重采样,就设置为VK_SAMPLE_COUNT_1_BIT
VkImageTiling tiling; // 图像的使用方式
VkImageUsageFlags usage; // 图像的用途
VkSharingMode sharingMode; // 是否可以多个Queue共享
uint32_t queueFamilyIndexCount; // 当VkSharingMode为VK_SHARING_MODE_CONCURRENT需要设置pQueueFamilyIndices,表示那些队列会用到数据,否则不需要设置
const uint32_t* pQueueFamilyIndices;
VkImageLayout initialLayout; // 初始的布局
} VkImageCreateInfo;

这个结构体也是很重要的,这里我们详细分析一下:

tiling的格式:

1
2
3
4
5
6
typedef enum VkImageTiling {
VK_IMAGE_TILING_OPTIMAL = 0,
VK_IMAGE_TILING_LINEAR = 1,
VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT = 1000158000,
VK_IMAGE_TILING_MAX_ENUM = 0x7FFFFFFF
} VkImageTiling;

VK_IMAGE_TILING_LINEAR就表示图像是采用平铺的形式在内存中存储,也就是图像从左到右,从上到下依次的线性存放。这种存储方式对于图像的访问效率是不高的。
VK_IMAGE_TILING_OPTIMAL是对上面的存储方式进行优化,不同的设备不同,主要目的就是提升device对图像的读取效率。

usage表示图像的用途,有如下的一些定义

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef enum VkImageUsageFlagBits {
VK_IMAGE_USAGE_TRANSFER_SRC_BIT = 0x00000001, // 用作传输的源
VK_IMAGE_USAGE_TRANSFER_DST_BIT = 0x00000002, // 用作传输的目的
VK_IMAGE_USAGE_SAMPLED_BIT = 0x00000004, // 其生成ImageView用作给shader来采样
VK_IMAGE_USAGE_STORAGE_BIT = 0x00000008, // 其生成ImageView适合作为
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT = 0x00000010, // 其生成ImageView用作FrameBuffer中的color attachment或者resolve attachement
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT = 0x00000020, // 其生成ImageView的用于FrameBuffer中的depth/stencil
VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT = 0x00000040,
VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT = 0x00000080, // 其生成的ImageView与VkDescriptorSet中的VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT对应,用于shader的input或者framebuffer中的input attachment
VK_IMAGE_USAGE_SHADING_RATE_IMAGE_BIT_NV = 0x00000100, //
VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT = 0x00000200, //
VK_IMAGE_USAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkImageUsageFlagBits;

VkImageLayout图像布局:

1
2
3
4
5
6
7
8
在typedef enum VkImageLayout定义,格式有点多,挑几个常用的:
VK_IMAGE_LAYOUT_UNDEFINED = 0, // 适合
VK_IMAGE_LAYOUT_GENERAL = 1, // 通用格式,device可以访问
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL = 2, // 适合用作color attachment
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL = 3, // 适用于depth/stencil attachement
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL = 6, // 使用与传输操作
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL = 7,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR = 1000001002, // 使用与向屏幕显示

b)然后我们调用下面的函数获取Image所需的存储空间大小

1
2
3
4
VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements(
VkDevice device,
VkImage image,
VkMemoryRequirements* pMemoryRequirements);

c)然后分配图像的存储空间pMemory

1
2
3
4
5
VKAPI_ATTR VkResult VKAPI_CALL vkAllocateMemory(
VkDevice device,
const VkMemoryAllocateInfo* pAllocateInfo,
const VkAllocationCallbacks* pAllocator,
VkDeviceMemory* pMemory);
1
2
3
4
5
6
typedef struct VkMemoryAllocateInfo {
VkStructureType sType; // VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO
const void* pNext;
VkDeviceSize allocationSize; // 空间大小
uint32_t memoryTypeIndex; // memoryType的Index,这个需要从physicalDevice获取
} VkMemoryAllocateInfo;

我们首先调用下面的函数从physicalDevice获取我们支持的Device Memory Proporty,然后pMemoryProperties数组中的每一个值跟我们需要的属性进行与运算,返回符合条件的pMemoryProperties数组的内容的索引

1
2
3
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMemoryProperties(
VkPhysicalDevice physicalDevice,
VkPhysicalDeviceMemoryProperties* pMemoryProperties);

类型主要有下面的定义:

1
2
3
4
5
6
7
8
9
10
11
12
typedef enum VkMemoryPropertyFlagBits {
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT = 0x00000001, // device可以最高效的访问数据
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT = 0x00000002, // 这种格式可以用vkMapMemory将device内存映射为host可以访问的地址上
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT = 0x00000004, // 主机对内存的写入是可见的(反之亦然),而不需要刷新内存缓存
VK_MEMORY_PROPERTY_HOST_CACHED_BIT = 0x00000008, // 表示此类型的内存中的数据由主机缓存
VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT = 0x00000010,
VK_MEMORY_PROPERTY_PROTECTED_BIT = 0x00000020,
VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD = 0x00000040,
VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD = 0x00000080,
VK_MEMORY_PROPERTY_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkMemoryPropertyFlagBits;
typedef VkFlags VkMemoryPropertyFlags;

d)最后将image和imageMemory绑定

1
2
3
4
5
VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory(
VkDevice device,
VkImage image,
VkDeviceMemory memory,
VkDeviceSize memoryOffset); // 偏移,一般为0。

4) 将stagingBuffer中的内容写入到创建的image中

由于在设备内存中操作,因此需要使用传输命令的方式来进行copy

1
2
3
4
5
6
7
VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage(
VkCommandBuffer commandBuffer,
VkBuffer srcBuffer, // stagingBuffer
VkImage dstImage, // 上面创建的Image对象
VkImageLayout dstImageLayout, // 目的图像的布局
uint32_t regionCount,
const VkBufferImageCopy* pRegions); // 定义需要copy的区域信息

因为是command,所以这个流程还是要有command pool分配内存,vkQueueSubmit提交命令的操作的。

5) ImageView

在vulkan中图像是存储资源,没法被frameBuffer或者shader什么的使用,能够被使用的是ImageView,因此我们需要通过Image来创建ImageView

1
2
3
4
5
VKAPI_ATTR VkResult VKAPI_CALL vkCreateImageView(
VkDevice device,
const VkImageViewCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkImageView* pView);

1
2
3
4
5
6
7
8
9
10
typedef struct VkImageViewCreateInfo {
VkStructureType sType; // 类型信息VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO
const void* pNext; // 扩展
VkImageViewCreateFlags flags; //
VkImage image; // 图像资源
VkImageViewType viewType; //指定图像被看作是一维纹理、二维纹理、三维纹理还是立方体贴图
VkFormat format; // 图像的格式
VkComponentMapping components; //图像颜色通道的映射
VkImageSubresourceRange subresourceRange;
} VkImageViewCreateInfo;

subresourceRange成员变量用于指定图像的用途和图像的哪一部分可以被访问

1
2
3
4
5
6
7
typedef struct VkImageSubresourceRange {
VkImageAspectFlags aspectMask; // 需要用到的图像资源的的层的位表示
uint32_t baseMipLevel; // 从mipMap的那个层开始
uint32_t levelCount; // 包含多少个mipMap层
uint32_t baseArrayLayer; // 起始层
uint32_t layerCount; //层数
} VkImageSubresourceRange;

6)Sampler

对于纹理图像,我们一般不直接用ImageView,而是用一种被称作采样器的东西来访问纹理数据,采样器可以自动地对纹理数据进行过滤和变换处理。
实际上纹理采样的水很深,要出好的效果需要做各种的优化,甚至有些效果还需要权衡,因此不用ImageView自己读取纹理而是直接使用Sampler是最明智的方式。

创建采样器的函数如下:

1
2
3
4
5
VKAPI_ATTR VkResult VKAPI_CALL vkCreateSampler(
VkDevice device,
const VkSamplerCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSampler* pSampler);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct VkSamplerCreateInfo {
VkStructureType sType; // VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO
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;

这个结构实际上就是设置我们的纹理采样到底是怎样做。

magFilter 和minFilter用于指定纹理需要放大和缩小时使用的插值方法。纹理放大会出现采样过密的问题,纹理缩小会出现采样过疏的问题。
纹理放大实际上就是纹理图像小,需要生成的图像大,这样就会导致临近多个采样点采出来的值是一样的,这回导致马赛克的效果
纹理缩小实际上就是纹理图像大,需要生成的图像小,这样就会导致幻影等效果,因为相邻的像素点采样的值差距过大。

1
2
3
4
5
6
7
typedef enum VkFilter {
VK_FILTER_NEAREST = 0,
VK_FILTER_LINEAR = 1,
VK_FILTER_CUBIC_IMG = 1000015000,
VK_FILTER_CUBIC_EXT = VK_FILTER_CUBIC_IMG,
VK_FILTER_MAX_ENUM = 0x7FFFFFFF
} VkFilter;

addressModeU/addressModeV/addressModeW 用于图像的三个方向,当采样超出范围后,该如何处理

1
2
3
4
5
6
7
8
9
typedef enum VkSamplerAddressMode {
VK_SAMPLER_ADDRESS_MODE_REPEAT = 0, // 采样超出图像范围时重复纹理
VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT = 1, // 采样超出图像范围时重复镜像后的纹理
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE = 2, // 采样出图像范围时使用距离最近的边界纹素
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER = 3, // 采样超出图像范围时使用镜像后距离最近的边界纹素
VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE = 4, // 采样超出图像返回时返回设置的边界颜色
VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE_KHR = VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE,
VK_SAMPLER_ADDRESS_MODE_MAX_ENUM = 0x7FFFFFFF
} VkSamplerAddressMode;

mipmapMode、mipLodBias、minLod和maxLod 用于设置mipmap,这几个参数的意义我们在mipMap的章节会来分析

anisotropyEnable和maxAnisotropy 是关于各向异性的,这个在各项异性的章节来介绍他们的含义

compareEnable和compareOp 用于将样本和一个设定的值进行比较,然后将比较结果用于之后的过滤操作。通常我们在进行阴影贴图时会使用它

borderColor 用于指定使用VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER寻址模式时采样超出图像范围时返回的边界颜色。边界颜色并非可以设置为任意颜色。它可以被设置为浮点或整型格式的黑色、白色或透明色

unnormalizedCoordinates 量用于指定采样使用的坐标系统,一般设置为VK_FALSE

显示 Gitment 评论