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_initialformat_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的长宽都是上一等级的一半,如下图所示:

_images/ch5-2-1.png

Mipmap的存在意义是:以有限的采样开销获得正确的采样结果。
举例而言:将一张16x16大小的图像渲染到4x4大小,滤波方式为线性插值VK_FILTER_LINEAR,那么渲染出的一个像素就对应原图中4x4的区域,若要获得正确的采样结果,每渲染一个像素得采样原图的16个像素,如此一来开销就很大。
不过实际情况是,驱动当然会抑制夸张的采样开销,被采样图像被缩小很多时,很可能只会在原图中选取有限的像素来做线性插值,这就会导致显著的摩尔纹:

_images/ch5-2-2.png

事先生成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_BITimb_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时,会出现如下奇异现象:

_images/ch5-2-3.png

上图渲染了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 使用贴图了!