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
10VkBuffer 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
5VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(
VkDevice device,
const VkImageCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkImage* pImage);
a)创建所需的结构体VkImageCreateInfo
1 | typedef struct VkImageCreateInfo { |
这个结构体也是很重要的,这里我们详细分析一下:
tiling的格式:1
2
3
4
5
6typedef 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
13typedef 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 | VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements( |
c)然后分配图像的存储空间pMemory
1 | VKAPI_ATTR VkResult VKAPI_CALL vkAllocateMemory( |
1 | typedef struct VkMemoryAllocateInfo { |
我们首先调用下面的函数从physicalDevice获取我们支持的Device Memory Proporty,然后pMemoryProperties数组中的每一个值跟我们需要的属性进行与运算,返回符合条件的pMemoryProperties数组的内容的索引1
2
3VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMemoryProperties(
VkPhysicalDevice physicalDevice,
VkPhysicalDeviceMemoryProperties* pMemoryProperties);
类型主要有下面的定义:1
2
3
4
5
6
7
8
9
10
11
12typedef 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 | VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory( |
4) 将stagingBuffer中的内容写入到创建的image中
由于在设备内存中操作,因此需要使用传输命令的方式来进行copy1
2
3
4
5
6
7VKAPI_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来创建ImageView1
2
3
4
5VKAPI_ATTR VkResult VKAPI_CALL vkCreateImageView(
VkDevice device,
const VkImageViewCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkImageView* pView);
1 | typedef struct VkImageViewCreateInfo { |
subresourceRange成员变量用于指定图像的用途和图像的哪一部分可以被访问1
2
3
4
5
6
7typedef 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
5VKAPI_ATTR VkResult VKAPI_CALL vkCreateSampler(
VkDevice device,
const VkSamplerCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSampler* pSampler);
1 | typedef struct VkSamplerCreateInfo { |
这个结构实际上就是设置我们的纹理采样到底是怎样做。
magFilter 和minFilter用于指定纹理需要放大和缩小时使用的插值方法。纹理放大会出现采样过密的问题,纹理缩小会出现采样过疏的问题。
纹理放大实际上就是纹理图像小,需要生成的图像大,这样就会导致临近多个采样点采出来的值是一样的,这回导致马赛克的效果
纹理缩小实际上就是纹理图像大,需要生成的图像小,这样就会导致幻影等效果,因为相邻的像素点采样的值差距过大。1
2
3
4
5
6
7typedef 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
9typedef 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