Ch5-2 2D贴图及生成Mipmap
请搭配Ch7-7 使用贴图食用本节。
贴图基类
先前已在Ch7-6 拷贝图像到屏幕中创建了texture类,这个类将会是之后一系列贴图类的基类。
之后每个贴图类,都会需要一个imageView对象和imageMemory对象,先定义相应的成员变量、Getter和创建函数:
class texture { protected: imageView imageView; imageMemory imageMemory; //-------------------- texture() = default; //该函数用于方便地创建imageMemory void CreateImageMemory(VkImageType imageType, VkFormat format, VkExtent3D extent, uint32_t mipLevelCount, uint32_t arrayLayerCount, VkImageCreateFlags flags = 0) { VkImageCreateInfo imageCreateInfo = { .flags = flags, .imageType = imageType, .format = format, .extent = extent, .mipLevels = mipLevelCount, .arrayLayers = arrayLayerCount, .samples = VK_SAMPLE_COUNT_1_BIT, .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT }; imageMemory.Create(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); } //该函数用于方便地创建imageView void CreateImageView(VkImageViewType viewType, VkFormat format, uint32_t mipLevelCount, uint32_t arrayLayerCount, VkImageViewCreateFlags flags = 0) { imageView.Create(imageMemory.Image(), viewType, format, { VK_IMAGE_ASPECT_COLOR_BIT, 0, mipLevelCount, 0, arrayLayerCount }, flags); } //Static Function static std::unique_ptr<uint8_t[]> LoadFile_Internal(const auto* address, size_t fileSize, VkExtent2D& extent, formatInfo requiredFormatInfo) { /*...*/ } public: //Getter VkImageView ImageView() const { return imageView; } VkImage Image() const { return imageMemory.Image(); } const VkImageView* AddressOfImageView() const { return imageView.Address(); } const VkImage* AddressOfImage() const { return imageMemory.AddressOfImage(); } //Const Function //该函数返回写入描述符时需要的信息 VkDescriptorImageInfo DescriptorImageInfo(VkSampler sampler) const { return { sampler, imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; } //Static Function [[nodiscard]] static std::unique_ptr<uint8_t[]> LoadFile(const char* filepath, VkExtent2D& extent, formatInfo requiredFormatInfo) { return LoadFile_Internal(filepath, 0, extent, requiredFormatInfo); } [[nodiscard]] static std::unique_ptr<uint8_t[]> LoadFile(const uint8_t* fileBinaries, size_t fileSize, VkExtent2D& extent, formatInfo requiredFormatInfo) { return LoadFile_Internal(fileBinaries, fileSize, extent, requiredFormatInfo); } static uint32_t CalculateMipLevelCount(VkExtent2D extent) { /*涉及生成mipmap,待填充*/ } static void CopyBlitAndGenerateMipmap2d(VkBuffer buffer_copyFrom, VkImage image_copyTo, VkImage image_blitTo, VkExtent2D imageExtent, uint32_t mipLevelCount = 1, uint32_t layerCount = 1, VkFilter minFilter = VK_FILTER_LINEAR) { /*涉及生成mipmap,待填充*/ } static void BlitAndGenerateMipmap2d(VkImage image_preinitialized, VkImage image_final, VkExtent2D imageExtent, uint32_t mipLevelCount = 1, uint32_t layerCount = 1, VkFilter minFilter = VK_FILTER_LINEAR) { /*涉及生成mipmap,待填充*/ } };
-
既然是被用来采样的图像,图像用途当然得包含VK_IMAGE_USAGE_SAMPLED_BIT,而包含VK_IMAGE_USAGE_TRANSFER_SRC_BIT是因为之后生成mipmap时要将其作为源图像。
-
贴图的采样点个数当然是VK_SAMPLE_COUNT_1_BIT,多重采样图像不能被用作各种传输命令的源和目标,而且指定事先准备好的贴图为多重采样也没有意义(注:GLSL中的texture2DMS等类型,用于需要在着色器中读取多重采样的图像附件的情况,与一般的贴图无关)。
-
DescriptorImageInfo(...)返回写入描述符时所需的信息,因为图像被用于在着色器中采样,内存布局当然是VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL。
2D贴图
从texture派生出texture2d,新增的成员变量只有记录图像大小的extent:
class texture2d :public texture { protected: VkExtent2D extent = {}; //-------------------- void Create_Internal(VkFormat format_initial, VkFormat format_final, bool generateMipmap) { /*待填充*/ } public: texture2d() = default; texture2d(const char* filepath, VkFormat format_initial, VkFormat format_final, bool generateMipmap = true) { Create(filepath, format_initial, format_final, generateMipmap); } texture2d(const uint8_t* pImageData, VkExtent2D extent, VkFormat format_initial, VkFormat format_final, bool generateMipmap = true) { Create(pImageData, extent, format_initial, format_final, generateMipmap); } //Getter VkExtent2D Extent() const { return extent; } uint32_t Width() const { return extent.width; } uint32_t Height() const { return extent.height; } //Non-const Function //直接从硬盘读取文件 void Create(const char* filepath, VkFormat format_initial, VkFormat format_final, bool generateMipmap = true) { VkExtent2D extent; formatInfo formatInfo = FormatInfo(format_initial);//根据指定的format_initial取得格式信息 std::unique_ptr<uint8_t[]> pImageData = LoadFile(filepath, extent, formatInfo); if (pImageData) Create(pImageData.get(), extent, format_initial, format_final, generateMipmap); } //从内存读取文件数据 void Create(const uint8_t* pImageData, VkExtent2D extent, VkFormat format_initial, VkFormat format_final, bool generateMipmap = true) { this->extent = extent; size_t imageDataSize = size_t(FormatInfo(format_initial).sizePerPixel) * extent.width * extent.height; stagingBuffer::BufferData_MainThread(pImageData, imageDataSize);//拷贝数据到暂存缓冲区 Create_Internal(format_initial, format_final, generateMipmap); } };
注意到参数format_initial和format_final,读取到的图像数据的格式,可能与我们最终期望的图像格式不同,比如stb_image读取hdr文件得到的是32位浮点数,而通常16位的精度便已足够,出于节省设备内存的目的,往往会使用blit命令进行图像格式转换。
填充Create_Internal(...),首先根据generateMipmap的值,确定是否调用CalculateMipLevelCount(...)计算mipmap的等级总数:
uint32_t mipLevelCount = generateMipmap ? CalculateMipLevelCount(extent) : 1;
CalculateMipLevelCount(...)的函数体留到下一小节填充,先接着往下写,创建imageMemory和imageView:
uint32_t mipLevelCount = generateMipmap ? CalculateMipLevelCount(extent) : 1; //创建图像并分配内存 CreateImageMemory(VK_IMAGE_TYPE_2D, format_final, { extent.width, extent.height, 1 }, mipLevelCount, 1); //创建图像视图 CreateImageView(VK_IMAGE_VIEW_TYPE_2D, format_final, mipLevelCount, 1);
根据是否需要格式转换来进行分支:
if (format_initial == format_final) /*待填充*/ else /*待填充*/
接下来需要使用先前声明的两个函数:
-
CopyBlitAndGenerateMipmap2d(VkBuffer buffer_copyFrom, VkImage image_copyTo, VkImage image_blitTo, /*...*/)
将数据从buffer_copyFrom拷贝到image_copyTo,然后在从image_copyTo将数据blit到image_blitTo,然后生成mipmap。 -
BlitAndGenerateMipmap2d(VkImage image_preinitialized, VkImage image_final, /*...*/)
从image_preinitialized将数据blit到image_final,其中image_preinitialized指的是暂存缓冲区的混叠图像,然后生成mipmap。
两个函数中还会处理如下逻辑:若blit的源图像和目标图像一致,则不会进行blit。
希望这不会让你觉得过度封装。CopyBlitAndGenerateMipmap2d(...)还会在之后创建2D贴图数组和立方体贴图时派上用场,如果不做这种封装,代码会写得重复而麻烦。函数体留到之后填充,先完成这块的代码逻辑。
你可以回忆下Ch7-6,然后就会明白为何会有上述两个函数,以及该如何区分使用它们:重点在于能否为暂存缓冲区创建混叠图像。
if (format_initial == format_final) //若不需要格式转换,直接从暂存缓冲区拷贝到图像,不发生blit CopyBlitAndGenerateMipmap2d(stagingBuffer::Buffer_MainThread(), imageMemory.Image(), imageMemory.Image(), extent, mipLevelCount, 1); else if (VkImage image_conversion = stagingBuffer::AliasedImage2d_MainThread(format_initial, extent)) //若需要格式转换,但是能为暂存缓冲区创建混叠图像,则直接blit BlitAndGenerateMipmap2d(image_conversion, imageMemory.Image(), extent, mipLevelCount, 1); else { //否则,创建新的暂存图像用于中转 VkImageCreateInfo imageCreateInfo = { .imageType = VK_IMAGE_TYPE_2D, .format = format_initial, .extent = { extent.width, extent.height, 1 }, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT }; vulkan::imageMemory imageMemory_conversion(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); //从暂存缓冲区拷贝到图像,然后再blit CopyBlitAndGenerateMipmap2d(stagingBuffer::Buffer_MainThread(), imageMemory_conversion.Image(), imageMemory.Image(), extent, mipLevelCount, 1); }
相信上述逻辑不需要多做说明,整个Create_Internal(...)如下:
void Create_Internal(VkFormat format_initial, VkFormat format_final, bool generateMipmap) { uint32_t mipLevelCount = generateMipmap ? CalculateMipLevelCount(extent) : 1; //创建图像并分配内存 CreateImageMemory(VK_IMAGE_TYPE_2D, format_final, { extent.width, extent.height, 1 }, mipLevelCount, 1); //创建图像视图 CreateImageView(VK_IMAGE_VIEW_TYPE_2D, format_final, mipLevelCount, 1); //Blit数据到图像,并生成mipmap if (format_initial == format_final) CopyBlitAndGenerateMipmap2d(stagingBuffer::Buffer_MainThread(), imageMemory.Image(), imageMemory.Image(), extent, mipLevelCount, 1); else if (VkImage image_conversion = stagingBuffer::AliasedImage2d_MainThread(format_initial, extent)) BlitAndGenerateMipmap2d(image_conversion, imageMemory.Image(), extent, mipLevelCount, 1); else { VkImageCreateInfo imageCreateInfo = { .imageType = VK_IMAGE_TYPE_2D, .format = format_initial, .extent = { extent.width, extent.height, 1 }, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT }; vulkan::imageMemory imageMemory_conversion(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); CopyBlitAndGenerateMipmap2d(stagingBuffer::Buffer_MainThread(), imageMemory_conversion.Image(), imageMemory.Image(), extent, mipLevelCount, 1); } }
BlitAndGenerateMipmap2d
填充BlitAndGenerateMipmap2d(...),首先来判定是否有必要blit或生成mipmap,如皆无必要则函数直接执行完毕:
static void BlitAndGenerateMipmap2d(VkImage image_preinitialized, VkImage image_final, VkExtent2D imageExtent, uint32_t mipLevelCount = 1, uint32_t layerCount = 1, VkFilter minFilter = VK_FILTER_LINEAR) { //生成mipmap的条件是mip等级大于1 bool generateMipmap = mipLevelCount > 1; //发生成blit的条件是源图像和目标图像不同 bool blitMipLevel0 = image_preinitialized != image_final; if (generateMipmap || blitMipLevel0) { //满足条件的话录制命令 auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer(); commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); /*待填充*/ commandBuffer.End(); //执行命令 graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer); } }
-
当然存在只生成mipmap的情况(比如为程序运行期间渲染的图像生成mipmap),尽管在本套教程中不会涉及此类使用场景。
不必多说,在blit前将源图像image_preinitialized的内存布局从VK_IMAGE_LAYOUT_PREINITIALIZED转换到VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
if (generateMipmap || blitMipLevel0) { auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer(); commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); //如有必要,从image_preinitialized将原图blit到image_final if (blitMipLevel0) { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_PREINITIALIZED, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, image_preinitialized, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layerCount } }; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); /*待填充*/ } //如有必要,生成mipmap /*待填充*/ commandBuffer.End(); graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer); }
填写VkImageBlit并调用CmdBlitImage(...):
if (generateMipmap || blitMipLevel0) { auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer(); commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); //如有必要,从image_preinitialized将原图blit到image_final if (blitMipLevel0) { VkImageMemoryBarrier imageMemoryBarrier = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_PREINITIALIZED, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, image_preinitialized, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layerCount } }; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); VkImageBlit region = { { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } }, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } } }; if (generateMipmap) imageOperation::CmdBlitImage(commandBuffer, image_preinitialized, image_final, region, { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL }, minFilter); else imageOperation::CmdBlitImage(commandBuffer, image_preinitialized, image_final, region, { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, minFilter); } //如有必要,生成mipmap /*待填充*/ commandBuffer.End(); graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer); }
这里根据之后是否生成mipmap进行了分支:
如果之后生成mipmap,那么将内存布局转到VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL(剧透:生成mipmap也是通过blit命令达成的);
否则转到VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,以备后续在片段着色器(对应VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT)中被采样(属于VK_ACCESS_SHADER_READ_BIT)。
不过上述写法略显重复冗余,略做优化:
static constexpr imageOperation::imageMemoryBarrierParameterPack imbs[2] = { { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL } }; if (generateMipmap || blitMipLevel0) { auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer(); commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); //如有必要,从image_preinitialized将原图blit到image_final if (blitMipLevel0) { VkImageMemoryBarrier imageMemoryBarrier = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_PREINITIALIZED, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, image_preinitialized, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layerCount } }; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); VkImageBlit region = { { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } }, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } } }; imageOperation::CmdBlitImage(commandBuffer, image_preinitialized, image_final, region, { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, imbs[generateMipmap], minFilter); //因为没有放缩,滤波方式无所谓 } //如有必要,生成mipmap /*待填充*/ commandBuffer.End(); graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer); }
CopyBlitAndGenerateMipmap2d
CopyBlitAndGenerateMipmap2d(...)的逻辑与BlitAndGenerateMipmap2d(...)类似,只不过copy这一步一定发生。
填写VkBufferImageCopy结构体,调用imageOperation::CmdCopyBufferToImage(...)将图像从缓冲区buffer_copyFrom拷贝到图像image_copyTo:
static void CopyBlitAndGenerateMipmap2d(VkBuffer buffer_copyFrom, VkImage image_copyTo, VkImage image_blitTo, VkExtent2D imageExtent, uint32_t mipLevelCount = 1, uint32_t layerCount = 1, VkFilter minFilter = VK_FILTER_LINEAR) { static constexpr imageOperation::imageMemoryBarrierParameterPack imbs[2] = { { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL } }; //生成mipmap的条件是mip等级大于1 bool generateMipmap = mipLevelCount > 1; //发生成blit的条件是源图像和目标图像不同 bool blitMipLevel0 = image_copyTo != image_final; //录制命令 auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer(); commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); VkBufferImageCopy region = { .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, .imageExtent = { imageExtent.width, imageExtent.height, 1 } }; imageOperation::CmdCopyBufferToImage(commandBuffer, buffer_copyFrom, image_copyTo, region, { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, imbs[generateMipmap || blitMipLevel0]); /*待填充*/ commandBuffer.End(); //执行命令 graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer); }
接着从image_copyTo将数据blit到image_blitTo,写法跟BlitAndGenerateMipmap2d(...)中一样:
VkBufferImageCopy region = { .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, .imageExtent = { imageExtent.width, imageExtent.height, 1 } }; imageOperation::CmdCopyBufferToImage(commandBuffer, buffer_copyFrom, image_copyTo, region, { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, imbs[generateMipmap || blitMipLevel0]); //如有必要,进行blit if (blitMipLevel0) { VkImageBlit region = { { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } }, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } } }; imageOperation::CmdBlitImage(commandBuffer, image_preinitialized, image_final, region, { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, imbs[generateMipmap], minFilter); } //如有必要,生成mipmap /*待填充*/
生成Mipmap
Mipmap是一组原图像的的微缩版本,每个等级mipmap的长宽都是上一等级的一半,如下图所示:
Mipmap的存在意义是:以有限的采样开销获得正确的采样结果。
举例而言:将一张16x16大小的图像渲染到4x4大小,滤波方式为线性插值VK_FILTER_LINEAR,那么渲染出的一个像素就对应原图中4x4的区域,若要获得正确的采样结果,每渲染一个像素得采样原图的16个像素,如此一来开销就很大。
不过实际情况是,驱动当然会抑制夸张的采样开销,被采样图像被缩小很多时,很可能只会在原图中选取有限的像素来做线性插值,这就会导致显著的摩尔纹:
事先生成mipmap,再根据渲染时的放缩程度选择相应的mip等级进行采样,就能以有限的开销获得令人满意的采样结果。
计算Mip等级数
创建图像时填写的VkImageCreateInfo::mipLevels指的是微缩图的总数,计入缩放比为1的原尺寸图像,因此最小为1。
计算各个mip等级的图像的大小:
原图的mip等级为0,从mip等级1开始的每一个mip等级的图像,尺寸都是上一等级的一半。若上一等级的长或宽是奇数,则取不足近似值;若上一等级的长或宽已经是1了,则取1(官方标准中的说明见此),即:
extent[i + 1].width = max(extent[i].width / 2, 1);
等价于下式:
extent[i].width = max(extent[0].width >> i, 1);
计算mip等级总数:
一套完整的mipmap是从原图到1x1大小的一系列图像,这意味着mip等级总数的计算步骤为:取长宽中较大者,取2的对数,再对结果取不足近似值并加1。
在texture中定义以下函数以计算mip等级总数:
static uint32_t CalculateMipLevelCount(VkExtent2D extent) { return uint32_t(std::floor(std::log2(std::max(extent.width, extent.height)))) + 1; }
CmdGenerateMipmap2d
先前在Ch7-6 拷贝图像到屏幕定义了imageOperation,现在其中加入静态成员函数CmdGenerateMipmap2d(...):
static void CmdGenerateMipmap2d(VkCommandBuffer commandBuffer, VkImage image, VkExtent2D imageExtent, uint32_t mipLevelCount, uint32_t layerCount, imageMemoryBarrierParameterPack imb_to, VkFilter minFilter = VK_FILTER_LINEAR) { /*待填充*/ }
定义一个lambda表达式来计算mipmap的大小,因为一会儿要填写VkImageBlit结构体,这里直接返回一个VkOffset3D:
auto MipmapExtent = [](VkExtent2D imageExtent, uint32_t mipLevel) { VkOffset3D extent = { std::max(int32_t(imageExtent.width >> mipLevel), 1), std::max(int32_t(imageExtent.height >> mipLevel), 1), 1 }; return extent; };
这里std::max(...)的作用是把0变成1,既是如此就可以将其优化掉:
auto MipmapExtent = [](VkExtent2D imageExtent, uint32_t mipLevel) { VkOffset3D extent = { int32_t(imageExtent.width >> mipLevel), int32_t(imageExtent.height >> mipLevel), 1 }; extent.x += !extent.x; extent.y += !extent.y; return extent; };
既然mipmap是原图的微缩版本,那么当然是用blit命令来生成的啦,先在循环中填写VkImageBlit结构体:
for (uint32_t i = 1; i < mipLevelCount; i++) { VkImageBlit region = { { VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 0, layerCount },//srcSubresource { {}, MipmapExtent(imageExtent, i - 1) }, //srcOffsets { VK_IMAGE_ASPECT_COLOR_BIT, i, 0, layerCount }, //dstSubresource { {}, MipmapExtent(imageExtent, i) } //dstOffsets }; CmdBlitImage(/*待填充*/); }
接着来考虑CmdBlitImage(...)的参数:
一套mipmap属于同一图像底下的资源,因此源图像和目标图像相同。
每个mip级别在被blit入数据前没有数据,内存布局填写VK_IMAGE_LAYOUT_UNDEFINED即可,blit后转到VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,以备blit到下一mip级别。
每个blit命令既要等待先前的blit结束(这样才能读取上一mip级别),又要在下一次blit前完成(除了最后一个mip级别),所以前后管线屏障中填写的阶段都是VK_PIPELINE_STAGE_TRANSFER_BIT。
for (uint32_t i = 1; i < mipLevelCount; i++) { VkImageBlit region = { { VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 0, layerCount },//srcSubresource { {}, MipmapExtent(imageExtent, i - 1) }, //srcOffsets { VK_IMAGE_ASPECT_COLOR_BIT, i, 0, layerCount }, //dstSubresource { {}, MipmapExtent(imageExtent, i) } //dstOffsets }; CmdBlitImage(commandBuffer, image, image, region, { VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL }, minFilter); }
这么一来,所有mip级别的内存布局都转到了VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,包括没必要转过去的1x1大小的mip级别。
接着用一个内存屏障将它们一次性全部转到参数imb_to所指定的内存布局:
if (imb_to.isNeeded) { VkImageMemoryBarrier imageMemoryBarrier = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, 0, imb_to.access, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, imb_to.layout, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, image, { VK_IMAGE_ASPECT_COLOR_BIT, 0, mipLevelCount, 0, layerCount } }; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, imb_to.stage, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); }
1x1大小的那张在blit完成后还没有被使用过,然后按照VK_ACCESS_TRANSFER_READ_BIT→imb_to.layout的顺序,经历两次内存布局转换(内存布局转换具有隐式同步,不会被重排)。
整个函数如下:
static void CmdGenerateMipmap2d(VkCommandBuffer commandBuffer, VkImage image, VkExtent2D imageExtent, uint32_t mipLevelCount, uint32_t layerCount, imageMemoryBarrierParameterPack imb_to, VkFilter minFilter = VK_FILTER_LINEAR) { auto MipmapExtent = [](VkExtent2D imageExtent, uint32_t mipLevel) { VkOffset3D extent = { int32_t(imageExtent.width >> mipLevel), int32_t(imageExtent.height >> mipLevel), 1 }; extent.x += !extent.x; extent.y += !extent.y; return extent; }; for (uint32_t i = 1; i < mipLevelCount; i++) { VkImageBlit region = { { VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 0, layerCount },//srcSubresource { {}, MipmapExtent(imageExtent, i - 1) }, //srcOffsets { VK_IMAGE_ASPECT_COLOR_BIT, i, 0, layerCount }, //dstSubresource { {}, MipmapExtent(imageExtent, i) } //dstOffsets }; CmdBlitImage(commandBuffer, image, image, region, { VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL }, minFilter); } if (imb_to.isNeeded) { VkImageMemoryBarrier imageMemoryBarrier = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, 0, imb_to.access, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, imb_to.layout, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, image, { VK_IMAGE_ASPECT_COLOR_BIT, 0, mipLevelCount, 0, layerCount } }; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, imb_to.stage, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); } }
。。。你以为事情这么一来就结束了吗?天真!
这个函数被设计为也适用于给贴图数组生成mipmap,然而,在一些GPU上(比如我的Nvidia 1060 Max-Q),为NPOT(Non-power-of-two,长宽非2的幂)图像的数组生成mipmap时,会出现如下奇异现象:
上图渲染了LastResortHE-Regular字体中的所有字形,左图是期望的情况,右图中则出现了诡异的重复。
我没有在英伟达的文档里查到具体原因,也不清楚其他GPU是否可能出问题。
为防止这种现象的发生,出于保险起见,可以对先前代码中的循环部分进行修改。因完全基于先前所学内容,解说略:
static void CmdGenerateMipmap2d(VkCommandBuffer commandBuffer, VkImage image, VkExtent2D imageExtent, uint32_t mipLevelCount, uint32_t layerCount, imageMemoryBarrierParameterPack imb_to, VkFilter minFilter = VK_FILTER_LINEAR) { auto MipmapExtent = [](VkExtent2D imageExtent, uint32_t mipLevel) { VkOffset3D extent = { int32_t(imageExtent.width >> mipLevel), int32_t(imageExtent.height >> mipLevel), 1 }; extent.x += !extent.x; extent.y += !extent.y; return extent; }; if (layerCount > 1) { std::unique_ptr<VkImageBlit[]> regions = std::make_unique<VkImageBlit[]>(layerCount); for (uint32_t i = 1; i < mipLevelCount; i++) { VkOffset3D mipmapExtent_src = MipmapExtent(imageExtent, i - 1); VkOffset3D mipmapExtent_dst = MipmapExtent(imageExtent, i); for (uint32_t j = 1; j < layerCount; j++) regions[j] = { { VK_IMAGE_ASPECT_COLOR_BIT, i - 1, j, 1 }, { {}, mipmapExtent_src }, { VK_IMAGE_ASPECT_COLOR_BIT, i, j, 1 }, { {}, mipmapExtent_dst } }; VkImageMemoryBarrier imageMemoryBarrier = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, image, { VK_IMAGE_ASPECT_COLOR_BIT, i, 1, 0, layerCount } }; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); vkCmdBlitImage(commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layerCount, regions.get(), minFilter); imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); } } else for (uint32_t i = 1; i < mipLevelCount; i++) { VkImageBlit region = { { VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 0, 1 }, { {}, MipmapExtent(imageExtent, i - 1) }, { VK_IMAGE_ASPECT_COLOR_BIT, i, 0, 1 }, { {}, MipmapExtent(imageExtent, i) } }; CmdBlitImage(commandBuffer, image, image, region, { VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL }, minFilter); } if (imb_to.isNeeded) { VkImageMemoryBarrier imageMemoryBarrier = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, 0, imb_to.access, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, imb_to.layout, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, image, { VK_IMAGE_ASPECT_COLOR_BIT, 0, mipLevelCount, 0, layerCount } }; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, imb_to.stage, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); } }
现在来完成先前的CopyBlitAndGenerateMipmap2d(...)和BlitAndGenerateMipmap2d(...)两个函数:
static void BlitAndGenerateMipmap2d(VkImage image_preinitialized, VkImage image_final, VkExtent2D imageExtent, uint32_t mipLevelCount = 1, uint32_t layerCount = 1, VkFilter minFilter = VK_FILTER_LINEAR) { static constexpr imageOperation::imageMemoryBarrierParameterPack imbs[2] = { { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL } }; bool generateMipmap = mipLevelCount > 1; bool blitMipLevel0 = image_preinitialized != image_final; if (generateMipmap || blitMipLevel0) { auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer(); commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); if (blitMipLevel0) { VkImageMemoryBarrier imageMemoryBarrier = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, 0, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_PREINITIALIZED, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, image_preinitialized, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layerCount } }; vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageMemoryBarrier); VkImageBlit region = { { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } }, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } } }; imageOperation::CmdBlitImage(commandBuffer, image_preinitialized, image_final, region, { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, imbs[generateMipmap], minFilter); } if (generateMipmap) imageOperation::CmdGenerateMipmap2d(commandBuffer, image_final, imageExtent, mipLevelCount, layerCount, { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, minFilter); commandBuffer.End(); graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer); } } static void CopyBlitAndGenerateMipmap2d(VkBuffer buffer_copyFrom, VkImage image_copyTo, VkImage image_blitTo, VkExtent2D imageExtent, uint32_t mipLevelCount = 1, uint32_t layerCount = 1, VkFilter minFilter = VK_FILTER_LINEAR) { static constexpr imageOperation::imageMemoryBarrierParameterPack imbs[2] = { { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL } }; bool generateMipmap = mipLevelCount > 1; bool blitMipLevel0 = image_copyTo != image_final; auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer(); commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); VkBufferImageCopy region = { .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, .imageExtent = { imageExtent.width, imageExtent.height, 1 } }; imageOperation::CmdCopyBufferToImage(commandBuffer, buffer_copyFrom, image_copyTo, region, { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, imbs[generateMipmap || blitMipLevel0]); if (blitMipLevel0) { VkImageBlit region = { { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } }, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, layerCount }, { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } } }; imageOperation::CmdBlitImage(commandBuffer, image_preinitialized, image_final, region, { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, imbs[generateMipmap], minFilter); } if (generateMipmap) imageOperation::CmdGenerateMipmap2d(commandBuffer, image_final, imageExtent, mipLevelCount, layerCount, { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }, minFilter); commandBuffer.End(); graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer); }
全部代码请比照VKBase+.h。
接着就是回到Ch7-7 使用贴图了!