Ch8-4 预乘Alpha

你是否曾看到过这样的图像,3D场景中,薄片状物体的边缘显得很不自然:

mush_bad.png
  • 图片出处,能流畅阅读英文的话看链接里这篇文章即可理解预乘alpha的意义。

采样带透明度的图像时,若没有使用预乘alpha的图像,线型滤波的结果便会呈现出上面这般模样。

先前在Ch1-4 创建交换链中已经简单说明过了什么是预乘透明度,此概念即为预乘alpha。
这里再次简要说明下什么是预乘alpha:

  • 直接alpha(straight alpha):RGB通道代表原始的红蓝绿色值,与A通道(A即代表alpha)相互独立。

  • 预乘alpha(premultiplied alpha):RGB通道的数值为原始的红蓝绿色值与A通道值的乘积(这意味着当你需要修改A通道的数值时,RGB通道的数值应被一并放缩)。

考虑到Ch1-4是入门篇章,将“premultiplied alpha”译作“预乘透明度”比较易懂,网络上通常也是这么翻译的。
这个翻译并不能算准确,有两个原因:A通道值未必是用作不透明度。

混色与线性插值

要理解预乘alpha的作用,得先从混色讲起。

通常会将A通道的值用作不透明度:若一个片段的不透明度为a(a为一个比值),在混色时,应有a的颜色来自该片段本身,(1 - a)的颜色来自其背景。
如果你不采样带透明像素的贴图(或采样这类贴图但不发生放缩),仅仅是片段着色器可能输出半透明的颜色,且输出的色值采用直接alpha,那么正确处理透明度的混色方式如下:

VkPipelineColorBlendAttachmentState{
    .blendEnable = VK_TRUE,
    .srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA,
    .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
    .colorBlendOp = VK_BLEND_OP_ADD,
    .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE,
    .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
    .alphaBlendOp = VK_BLEND_OP_ADD,
    .colorWriteMask = 0b1111
};

将混色结果标记为result,则(加斜字体对应混色因子枚举项):
result.rgb = src.rgb * src.a + dst.rgb * (1 - src.a)
result.a  = src.a  * 1   + dst.a  * (1 - src.a)
注意到dst.rgb没有乘dst.a,这是因为按这种方法混色的话,混色结果是预乘alpha的,而dst是已经在颜色附件中的色值,如果先前使用的是同样的混色方式,那么dst.rgb当然已经预乘了A通道值。

然而,这种混色方式在放缩带透明度的贴图(尤其是放大),且应用线性滤波时会造成不自然的颜色过渡:

_images/ch8-4-1.png
  • 上图中,线型滤波的结果图像,如果从128到255是线性地过渡的,中线上的灰度理应是191或192。
    但比对底下用做参考的灰度为192的色块,显然192出现在了结果图像的中线右方,而中线上的数值是159。

这是为什么呢?试着计算中线上src的数值:vec4(0, 0, 0, 0.5) * 0.5 + vec4(1, 1, 1, 1) * 0.5,结果是{ 0.5, 0.5, 0.5, 0.75 }
结果无所谓啦,关键在于将RGBA各通道线性插值后,再将RGB通道乘以A通道,这是在将两个线性变化的量相乘。
线性变化即按一次函数变化,两个一次函数的乘积是二次函数,并非线性函数。因而上图渲染结果的渐变部分,靠近128的一侧的变化较为平缓,而靠近255一侧则较为剧烈。

解决办法就是在采样贴图前,将贴图的RGB通道变为预乘alpha,即将“RGB通道乘以A通道”这一步放到线性插值前
则混色方式得相应地变为:

VkPipelineColorBlendAttachmentState{
    .blendEnable = VK_TRUE,
    .srcColorBlendFactor = VK_BLEND_FACTOR_ONE, //改动
    .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
    .colorBlendOp = VK_BLEND_OP_ADD,
    .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE,
    .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
    .alphaBlendOp = VK_BLEND_OP_ADD,
    .colorWriteMask = 0b1111
};

比较直接alpha与预乘alpha图像的渲染结果:

_images/ch8-4-2.png
  • 使用预乘alpha的RGB通道值后,虽然线型滤波的结果在数值上确实是线性变化的,但显示出的渐变效果在视觉上可能仍显得不够柔和,将色值映射到sRGB色彩空间可以获得更好的效果,见Ch8-5 sRGB色彩空间与开启HDR

对于颜色的线性插值,除去应使用预乘alpha的贴图外,还有以下注意事项:

  • 若有色值被插值后输入到片段着色器,先前的着色器中应当输出预乘alpha的色值。

  • 注意色彩空间:插值之前,应确保色值是线性色彩空间中的色值(相关概念会在下一节中进行讲解)。

创建预乘alpha的贴图

虽然渲染中有使用预乘alpha贴图的必要性,但通常你接触到的图像文件会是直接alpha的。

你从网上下到的PNG通常都会是直接alpha的PNG标准中规定其图像数值应是直接alpha的形式:
The colour values in a pixel are not premultiplied by the alpha value assigned to the pixel.

虽然图像查看器/图像处理软件在渲染图像时可能会做预乘alpha的处理,但图像处理软件保存的图片通常会是直接alpha的
这是因为预乘alpha的图像不适合被用于编辑:考虑到图像的存储精度,让RGB通道乘以A通道值,会造成不可逆的精度损失。
例如:每通道8位的图像,直接alpha的R值为1/255,A值只要在[0, 127/255]之间,预乘后的R值便会舍入为0,将0除以A值无法还原到1/255。

综上,既是如此,有必要实现从直接alpha贴图创建预乘alpha贴图的功能。

值得注意的一点是,即便一张图中只有完全透明的像素(A通道值为0)及完全不透明的像素(A通道值为1),可能也需要让RGB通道预乘A通道值。
如果这些完全透明像素的RGB通道都是0,那么显然是否预乘没有任何差别。
然而,用stb_image读取PNG这类压缩格式得到的图像数据,完全透明像素的RGB通道可能不为0,应当将它们预乘为0。
(解决这个问题的方法之一当然是换一个图像读取库,不过我没有费事去尝试别的库,没什么可推荐的)

下图中以三种不同的方式渲染了一个...呃,手上拿着一颗胶囊的胶囊小人(画得很烂,请不要在意),它的头是黄色,透明的下半身透出底色,在此基础上加了半透明的高光和阴影:

_images/ch8-4-3.png
  • 原图被渲染到20倍大小,可以看到,非预乘透明度的情况下,在不透明/半透明与全透明的边界上,呈现出了诡异的灰边——有些地方在高亮度背景上显暗,有些地方低亮度背景上发白,这正是全透明像素的RGB通道非0导致的结果。

概念和注意事项到此为止便讲完了,后续是解说具体的代码。
如果你觉得自己至今为止学得还不错,可以基于所学自行实现相应功能,然后再来对答案。

怎么实现?
没必要每次都在运行期创建预乘alpha的贴图,可以预渲染完然后存盘。如果你只是单纯地想实现这一目的的话,最省事的方法当然是直接用C++代码在CPU侧处理stb_image读到的数据,因为不必考虑运行期的效率,都不需要用上Vulkan。
不过这样的话这一节的内容也就没必要往下写了。
考虑用Vulkan做这件事情,需要采样贴图吗?
既然我特地将(大部分教程中会早早地就讲到的)混色拖到这一节才讲,自然是有些原因啦:
将已经存有直接alpha色值的图像用作颜色附件,然后利用混色,将dst.rgb * dst.a的数值写入该附件即可。

“创建预乘alpha的贴图”是一套较为固定的流程,可以用一个类封装与之相关的对象和函数。
EasyVulkan.hpp,easyVulkan命名空间中,加入以下内容:

using callback_copyData_t = void(*)(const void* pData, VkDeviceSize dataSize);
class fCreateTexture2d_multiplyAlpha {
protected:
    VkFormat format_final = VK_FORMAT_UNDEFINED;
    bool generateMipmap = false;
    callback_copyData_t callback_copyData = nullptr;
    renderPass renderPass;
    pipelineLayout pipelineLayout;
    pipeline pipeline;
    //该函数会被operator()(...)调用
    void CmdTransferDataToImage(VkCommandBuffer commandBuffer, const uint8_t* pImageData, VkExtent2D extent, VkFormat format_initial, imageMemory& imageMemory_conversion, VkImage image) const {
        /*待填充*/
    }
    //Static
    static constexpr const char* filepath_vert = "shader/RenderToImage2d_NoUV.vert.spv";
    static constexpr const char* filepath_frag = "shader/RenderNothing.frag.spv";
    //Static Function
    static const VkPipelineShaderStageCreateInfo Ssci_Vert() {
        /*待填充*/
    }
    static const VkPipelineShaderStageCreateInfo Ssci_Vert() {
        /*待填充*/
    }
    static const VkPipelineLayout PipelineLayout() {
        /*待填充*/
    }
public:
    fCreateTexture2d_multiplyAlpha() = default;
    fCreateTexture2d_multiplyAlpha(VkFormat format_final, bool generateMipmap, callback_copyData_t callback_copyMipLevel0) {
        Instantiate(format_final, generateMipmap, callback_copyMipLevel0);
    }
    fCreateTexture2d_multiplyAlpha(fCreateTexture2d_multiplyAlpha&&) = default;
    //Const Function
    texture2d operator()(const char* filepath, VkFormat format_initial) const {
        VkExtent2D extent;
        formatInfo formatInfo = FormatInfo(format_initial);
        std::unique_ptr<uint8_t[]> pImageData = texture::LoadFile(filepath, extent, formatInfo);
        if (pImageData)
            return (*this)(pImageData.get(), extent, format_initial);
        return texture2d{};
    }
    texture2d operator()(const uint8_t* pImageData, VkExtent2D extent, VkFormat format_initial) const {
        /*待填充*/
    }
    //Non-const Function
    void Instantiate(VkFormat format_final, bool generateMipmap, callback_copyData_t callback_copyMipLevel0) {
        this->format_final = format_final;
        this->generateMipmap = generateMipmap;
        callback_copyData = callback_copyData_t;
        /*待后续填充*/
    }
}

顾名思义,fCreateTexture2d_multiplyAlpha这个类用来创建预乘alpha的2D贴图,fCreateTexture2d_multiplyAlpha::operator()说明该类的实例可以像函数一样被使用(这种类叫做“仿函数”)。
这个类不处理2D贴图数组,要处理2D贴图数组得把事情复杂化:要么为每个图层创建帧缓冲,要么使用多层帧缓冲和//TODO 几何着色器,要么用动态渲染省去帧缓冲。
传入Instantiate(...)的参数callback_copyMipLevel0被保存到成员变量callback_copyData,它是一个回调函数的指针,方便你获取预乘后的图像数据。
其余各个成员函数的实参名,你应该在Ch5-2 2D贴图及生成Mipmap中都见过,就不多做解释了。

创建渲染通道

Instantiate(...)中创建渲染通道和管线。

所要创建的的渲染通道只需要一个子通道和一个颜色附件,这没有什么疑义。
Instantiate(...)提供的参数中,有指示是否生成mipmap的generateMipmap、用于获取图像数据的回调函数指针callback_copyMipLevel0,这两个参数对渲染通道结束后干什么造成影响,相应地也就会影响附件描述中的finalLayout和子通道依赖。

通过混色来预乘alpha这件事结束后,如果到采样贴图为止不需要拿该图像干别的什么事,附件描述中的finalLayout当然是VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
要在CPU一侧获取图像数据,得把数据拷贝到暂存缓冲区,而生成mipmap也得用blit命令,则附件描述中的finalLayout该是VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL

VkAttachmentDescription attachmentDescription = {
    .format = format_final,
    .samples = VK_SAMPLE_COUNT_1_BIT,
    .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
    .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
    .initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
    .finalLayout = generateMipmap || callback_copyData ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
};
  • 与之前写2D贴图的封装时一样,之后会调用imageOperation::CmdCopyBufferToImage(...)和imageOperation::CmdBlitImage(...)来搬运图像数据,我打算让这俩函数里的图像内存屏障把内存布局给顺带转到VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL

渲染通道开始前的同步由前述图像内存屏障确保。
渲染通道结束后没啥事的话不需要在命令缓冲区里做同步。若要在同一命令缓冲区中copy或blit预乘后的图像数据,则需要以下子通道依赖:

VkSubpassDependency subpassDependency = {
    .srcSubpass = 0,
    .dstSubpass = VK_SUBPASS_EXTERNAL,
    .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
    .dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT,
    .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
    .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT
};

毫无难度地填写掉渲染通道创建信息中的其他内容,并对代码逻辑略作整理,目前Instantiate(...)长这样:

void Instantiate(VkFormat format_final, bool generateMipmap, callback_copyData_t callback_copyMipLevel0) {
    this->format_final = format_final;
    this->generateMipmap = generateMipmap;
    callback_copyData = callback_copyData_t;

    //Create renderpass
    VkAttachmentDescription attachmentDescription = {
        .format = format_final,
        .samples = VK_SAMPLE_COUNT_1_BIT,
        .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
        .initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
        .finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
    };
    VkAttachmentReference attachmentReference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
    VkSubpassDescription subpassDescription = {
        .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
        .colorAttachmentCount = 1,
        .pColorAttachments = &attachmentReference
    };
    VkSubpassDependency subpassDependency = {
        .srcSubpass = 0,
        .dstSubpass = VK_SUBPASS_EXTERNAL,
        .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT,
        .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT
    };
    VkRenderPassCreateInfo renderPassCreateInfo = {
        .attachmentCount = 1,
        .pAttachments = &attachmentDescription,
        .subpassCount = 1,
        .pSubpasses = &subpassDescription
    };
    if (generateMipmap || callback_copyData)
        attachmentDescription.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
        renderPassCreateInfo.dependencyCount = 1,
        renderPassCreateInfo.pDependencies = &subpassDependency;
    renderPass.Create(renderPassCreateInfo);

    /*待后续填充*/
}

书写着色器并创建管线

RenderToImage2d_NoUV.vert.shader

因为不会采样贴图,只要将整个NDC坐标范围输出到gl_Position即可,不需要输出贴图坐标:

#version 460
#pragma shader_stage(vertex)

vec2 positions[4] = {
    {-1,-1},
    {-1, 1},
    { 1,-1},
    { 1, 1}
};

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0, 1);
}

RenderNothing.frag.shader

#version 460
#pragma shader_stage(fragment)

layout(location = 0) out vec4 o_Color;

void main() {
    o_Color = vec4(0);
}
  • 输出什么数值都无所谓(反正之后混色的时候也用不着),这里就输出四个通道皆为0的数值了(所以着色器名称叫RenderNothing)。
    但是得有输出,否则驱动很可能不会执行包括混色在内的后续图形管线功能。

创建管线

首先填充用来读取着色器的函数Ssci_Vert()和Ssci_Frag():

static const VkPipelineShaderStageCreateInfo Ssci_Vert() {
    static shaderModule shader;
    if (!shader) {
        shader.Create(filepath_vert);
        ExecuteOnce(shader.StageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT)); //防止再次执行此if语句块时,重复添加回调函数
        graphicsBase::Base().AddCallback_DestroyDevice([] { shader.~shaderModule(); });
    }
    return shader.StageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT);
}
static const VkPipelineShaderStageCreateInfo Ssci_Frag() {
    static shaderModule shader;
    if (!shader) {
        shader.Create(filepath_frag);
        ExecuteOnce(shader.StageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT)); //防止再次执行此if语句块时,重复添加回调函数
        graphicsBase::Base().AddCallback_DestroyDevice([] { shader.~shaderModule(); });
    }
    return shader.StageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
}
  • 这里考虑了需要重建逻辑设备的情况:如果不会重建逻辑设备,则if (!shader)之后的语句块只会执行一次,否则会在销毁旧逻辑设备时调用这里定义的回调函数,将shader覆写为VK_NULL_HANDLE,然后再次执行shader.Create(filepath_frag)

创建管线布局,因为不需要push constant和任何描述符,也不依赖于任何其他参数,在静态成员函数PipelineLayout()中创建一个没任何东西的管线布局:

static const VkPipelineLayout PipelineLayout() {
    static pipelineLayout pipelineLayout;
    if (!pipelineLayout) {
        VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = {};
        pipelineLayout.Create(pipelineLayoutCreateInfo);
        ExecuteOnce(pipelineLayout);
        graphicsBase::Base().AddCallback_DestroyDevice([] { pipelineLayout.~pipelineLayout(); });
    }
    return pipelineLayout;
}

继续填充Instantiate(...):

void Instantiate(VkFormat format_final, bool generateMipmap, callback_copyData_t callback_copyMipLevel0) {
    /*...前面略*/

    //Create pipeline
    graphicsPipelineCreateInfoPack pipelineCiPack;
    pipelineCiPack.createInfo.layout = PipelineLayout();
    pipelineCiPack.createInfo.renderPass = renderPass;
    pipelineCiPack.shaderStages.push_back(Ssci_Vert());
    pipelineCiPack.shaderStages.push_back(Ssci_Frag());
    pipelineCiPack.inputAssemblyStateCi.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
    pipelineCiPack.multisampleStateCi.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
    /*待后续填充*/
}

Instantiate(...)的参数中没有图像大小,这个管线将会适用于任意大小的图像附件。
为满足这一要求,使用动态的视口大小:

pipelineCiPack.dynamicStates.push_back(VK_DYNAMIC_STATE_VIEWPORT);

不需要剪裁,则剪裁范围得涵盖整个视口,既然要比视口大,视口大小又不确定,那自然是填写帧缓冲所能达到的最大长宽:

pipelineCiPack.scissors.emplace_back(VkOffset2D{}, VkExtent2D{
    graphicsBase::Base().PhysicalDeviceProperties().limits.maxFramebufferWidth,
    graphicsBase::Base().PhysicalDeviceProperties().limits.maxFramebufferHeight });

混色方式当然是这样啦:

pipelineCiPack.colorBlendAttachmentStates.emplace_back(
    .blendEnable = VK_TRUE,
    .srcColorBlendFactor = VK_BLEND_FACTOR_ZERO,
    .dstColorBlendFactor = VK_BLEND_FACTOR_DST_ALPHA, //RGB乘以A
    .colorBlendOp = VK_BLEND_OP_ADD,
    .srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO,
    .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE,       //A不变
    .alphaBlendOp = VK_BLEND_OP_ADD,
    .colorWriteMask = 0b1111
);

于是整个Instantiate(...)如下:

void Instantiate(VkFormat format_final, bool generateMipmap, callback_copyData_t callback_copyMipLevel0) {
    this->format_final = format_final;
    this->generateMipmap = generateMipmap;
    callback_copyData = callback_copyData_t;

    //Create renderpass
    VkAttachmentDescription attachmentDescription = {
        .format = format_final,
        .samples = VK_SAMPLE_COUNT_1_BIT,
        .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
        .initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
        .finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
    };
    VkAttachmentReference attachmentReference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
    VkSubpassDescription subpassDescription = {
        .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
        .colorAttachmentCount = 1,
        .pColorAttachments = &attachmentReference
    };
    VkSubpassDependency subpassDependency = {
        .srcSubpass = 0,
        .dstSubpass = VK_SUBPASS_EXTERNAL,
        .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT,
        .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT
    };
    VkRenderPassCreateInfo renderPassCreateInfo = {
        .attachmentCount = 1,
        .pAttachments = &attachmentDescription,
        .subpassCount = 1,
        .pSubpasses = &subpassDescription
    };
    if (generateMipmap || callback_copyData)
        attachmentDescription.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
        renderPassCreateInfo.dependencyCount = 1,
        renderPassCreateInfo.pDependencies = &subpassDependency;
    renderPass.Create(renderPassCreateInfo);

    //Create pipeline
    graphicsPipelineCreateInfoPack pipelineCiPack;
    pipelineCiPack.createInfo.layout = PipelineLayout();
    pipelineCiPack.createInfo.renderPass = renderPass;
    pipelineCiPack.shaderStages.push_back(Ssci_Vert());
    pipelineCiPack.shaderStages.push_back(Ssci_Frag());
    pipelineCiPack.inputAssemblyStateCi.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
    pipelineCiPack.scissors.emplace_back(VkOffset2D{}, VkExtent2D{
        graphicsBase::Base().PhysicalDeviceProperties().limits.maxFramebufferWidth,
        graphicsBase::Base().PhysicalDeviceProperties().limits.maxFramebufferHeight });
    pipelineCiPack.multisampleStateCi.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
    pipelineCiPack.colorBlendAttachmentStates.emplace_back(
        VK_TRUE,
        VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_DST_ALPHA, VK_BLEND_OP_ADD,
        VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD,
        0b1111
    );
    pipelineCiPack.dynamicStates.push_back(VK_DYNAMIC_STATE_VIEWPORT);
    pipelineCiPack.UpdateAllArrays();
    pipeline.Create(pipelineCiPack);
}

CmdTransferDataToImage

这个函数用来将数据搬到VkImage对象,跟之前texture2d::Create_Internal(...)的逻辑类似,但不生成mipmap:

void CmdTransferDataToImage(VkCommandBuffer commandBuffer, const uint8_t* pImageData, VkExtent2D extent, VkFormat format_initial, imageMemory& imageMemory_conversion, VkImage image) const {
    VkDeviceSize imageDataSize = VkDeviceSize(FormatInfo(format_initial).sizePerPixel) * extent.width * extent.height;
    stagingBuffer::BufferData_MainThread(pImageData, imageDataSize);
    if (format_initial == format_final) {
        //若不需要格式转换,直接从暂存缓冲区拷贝到图像,不发生blit
        VkBufferImageCopy region = {
            .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
            .imageExtent = { extent.width, extent.height, 1 }
        };
        imageOperation::CmdCopyBufferToImage(commandBuffer, stagingBuffer::Buffer_MainThread(), image, region,
            { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
            { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL });
    }
    else
        if (VkImage image_preinitialized = stagingBuffer::AliasedImage2d_MainThread(format_initial, extent) {
            //若需要格式转换,但是能为暂存缓冲区创建混叠图像,则直接blit
            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, 1 }
            };
            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, 1 },
                { {}, { int32_t(extent.width), int32_t(extent.height), 1 } },
                { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
                { {}, { int32_t(extent.width), int32_t(extent.height), 1 } }
            };
            imageOperation::CmdBlitImage(commandBuffer, image_preinitialized, image, region,
                { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
                { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL });
        }
        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
            };
            imageMemory_conversion.Create(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

            VkBufferImageCopy region_copy = {
                .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
                .imageExtent = { extent.width, extent.height, 1 }
            };
            imageOperation::CmdCopyBufferToImage(commandBuffer, stagingBuffer::Buffer_MainThread(), imageMemory_conversion.Image(), region_copy,
                { 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 });

            VkImageBlit region_blit = {
                { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
                { {}, { int32_t(extent.width), int32_t(extent.height), 1 } },
                { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
                { {}, { int32_t(extent.width), int32_t(extent.height), 1 } }
            };
            imageOperation::CmdBlitImage(commandBuffer, imageMemory_conversion.Image(), image, region_blit,
                { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
                { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL });
        }
}
  • 本函数不负责执行录制的命令,在提交并执行完命令前须确保用于转换格式的暂存图像的生存期,因此imageMemory_conversion由函数外部传入。

你都读到这一节了,上述代码的逻辑应该完全不必解释。
不过这里的代码看着有些重复,其实要干的事也就视情况进行copy、blit、使用内存屏障,将代码简化如下:

void CmdTransferDataToImage(VkCommandBuffer commandBuffer, const uint8_t* pImageData, VkExtent2D extent, VkFormat format_initial, imageMemory& imageMemory_conversion, VkImage image) const {
    static constexpr imageOperation::imageMemoryBarrierParameterPack imbs[2] = {
        { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL },
        { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL }
    }
    VkDeviceSize imageDataSize = VkDeviceSize(FormatInfo(format_initial).sizePerPixel) * extent.width * extent.height;
    stagingBuffer::BufferData_MainThread(pImageData, imageDataSize);

    VkImage image_copyTo = VK_NULL_HANDLE;
    VkImage image_conversion = VK_NULL_HANDLE;
    VkImage image_blitTo = VK_NULL_HANDLE;
    if (format_initial == format_final)
        //若不需要格式转换,只发生拷贝,覆写image_copyTo
        image_copyTo = image;
    else {
        //若需要格式转换,但是能为暂存缓冲区创建混叠图像,覆写image_conversion为该混叠图像,image_copyTo保留为VK_NULL_HANDLE
        if (!(image_conversion = stagingBuffer::AliasedImage2d_MainThread(format_initial, extent)) {
            //否则,创建新的暂存图像用于中转
            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
            };
            imageMemory_conversion.Create(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
            //需要先将数据拷贝到上述暂存图像,再将其用作blit来源,覆写image_copyTo和image_conversion为该暂存图像的handle
            image_copyTo = image_conversion = imageMemory_conversion.Image();
        }
        //若需要格式转换,必发生blit,覆写image_blitTo
        image_blitTo = image;
    }

    if (image_copyTo) { //根据image_copyTo是否为VK_NULL_HANDLE,判定是否需要拷贝
        VkBufferImageCopy region = {
            .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
            .imageExtent = { extent.width, extent.height, 1 }
        };
        imageOperation::CmdCopyBufferToImage(commandBuffer, stagingBuffer::Buffer_MainThread(), image_copyTo, region,
            { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED }, imbs[bool(image_blitTo)]); //内存屏障参数由之后是否blit确定
    }

    if (image_blitTo) { //根据image_blitTo是否为VK_NULL_HANDLE,判定是否需要blit
        if (!image_copyTo) { //如果此时image_copyTo为VK_NULL_HANDLE,说明先前没发生拷贝,image_conversion是暂存缓冲区的混叠图像,需转换其内存布局
            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_conversion,
                { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
            };
            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, 1 },
            { {}, { int32_t(extent.width), int32_t(extent.height), 1 } },
            { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
            { {}, { int32_t(extent.width), int32_t(extent.height), 1 } }
        };
        imageOperation::CmdBlitImage(commandBuffer, image_conversion, image_blitTo, region,
            { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
            { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL });
    }
}

operator()

填充operator()(const uint8_t* pImageData, VkExtent2D extent, VkFormat format_initial),实现创建预乘alpha贴图的整个流程。
首先当然是创建一个texture2d对象:

texture2d operator()(const uint8_t* pImageData, VkExtent2D extent, VkFormat format_initial) const {
    texture2d texture;

    /*待后续填充*/

    return texture;
}
  • 该函数将创建的texture2d对象返回出去,在不开启NRVO(具名返回值优化)时可能会调用移动构造器。
    注:如今很多编译器(比如VS2022 17.4之后的版本)即便在Debug Build下也会默认开启NRVO。

在我先前的代码中,将texture2d的各个成员的访问级别设置为了protected,无法被外部直接修改。
(如果事到如今你还疑惑为什么要有访问级别这种东西来自找麻烦:防止使用你代码的其他人犯蠢)
但是一会儿要写入这几个成员,这里使用一个C++编程技巧来获得访问:

struct texture2d_local :texture2d {
//public:
    using texture::imageView;
    using texture::imageMemory;
    using texture2d::extent;
}*pTexture;
pTexture = static_cast<texture2d_local*>(&texture);
  • 在派生类中用using 基类名::成员名;重设成员的访问级别为前一个访问修饰符指定的级别,若要重设到类或结构体的默认访问级别,可省略访问修饰符。
    然后将基类的指针转到派生类的指针,通过派生类的指针来访问需访问的成员(或转引用,通过派生类的引用访问)。

  • Q:为什么不直接创建一个texture2d_local类型的变量并返回出去?
    A:返回一个texture2d_local类型的变量与函数的返回类型texture2d不匹配,是不会发生NRVO的(派生类转到基类必然要调用复制/移动构造器,哪怕底层数据不变)。

将图像的大小写入到texture.extent

pTexture->extent = extent;

创建texture.imageMemory

uint32_t mipLevelCount = generateMipmap ? texture::CalculateMipLevelCount(extent) : 1;
VkImageCreateInfo imageCreateInfo = {
    .imageType = VK_IMAGE_TYPE_2D,
    .format = format_final,
    .extent = { extent.width, extent.height, 1 },,
    .mipLevels = mipLevelCount,
    .arrayLayers = 1,
    .samples = VK_SAMPLE_COUNT_1_BIT,
    .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
};
pTexture->imageMemory.Create(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

//这个handle之后要用5次,先存到变量以简化之后的书写
VkImage image = pTexture->imageMemory.Image();

创建只有一个mip等级的texture.imageView(帧缓冲所用的图像视图必须只有一个mip级别),然后创建帧缓冲:

pTexture->imageView.Create(image, VK_IMAGE_VIEW_TYPE_2D, format_final, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
VkFramebufferCreateInfo framebufferCreateInfo = {
    .renderPass = renderPass,
    .attachmentCount = 1,
    .pAttachments = pTexture->imageView.Address(),
    .width = extent.width,
    .height = extent.height,
    .layers = 1
};
framebuffer framebuffer(framebufferCreateInfo);

开始录制命令缓冲区,首先调用CmdTransferDataToImage(...),:

auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer();
commandBuffer.Begin();

imageMemory imageMemory_conversion;
CmdTransferDataToImage(commandBuffer, pImageData, extent, format_initial, imageMemory_conversion, image);

/*待后续填充*/

commandBuffer.End();
graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer);

在渲染通道中绑定管线,用vkCmdSetViewport(...)动态地指定视口,然后绘制:

void VKAPI_CALL vkCmdSetViewport(...) 的参数说明

VkCommandBuffer commandBuffer

命令缓冲区的handle

uint32_t firstViewport

首个视口的索引(单视口时为0)

uint32_t viewportCount

要指定的视口的个数(单视口时为1)

const VkViewport* pViewports

指向VkViewport的数组,用于指定各个视口

auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer();
commandBuffer.Begin();

imageMemory imageMemory_conversion;
CmdTransferDataToImage(commandBuffer, pImageData, extent, format_initial, imageMemory_conversion, image);

renderPass.CmdBegin(commandBuffer, framebuffer, { {}, extent });
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
VkViewport viewport = { 0, 0, float(extent.width), float(extent.height), 0.f, 1.f };
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
vkCmdDraw(commandBuffer, 4, 1, 0, 0);
renderPass.CmdEnd(commandBuffer);

/*待后续填充*/

commandBuffer.End();
graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer);

若callback_copyData不为nullptr,将数据拷贝到暂存缓冲区,留待之后使用。

if (callback_copyData) {
    VkBufferImageCopy region = {
        .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
        .imageExtent = { extent.width, extent.height, 1 }
    };
    vkCmdCopyImageToBuffer(commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingBuffer::Buffer_MainThread(), 1, &region);
}

需要注意的是,先前在CmdTransferDataToImage(...)中将初始的图像数据拷到了暂存缓冲区,然后现在又将混色结果拷到了暂存缓冲区。
由于format_initial和format_final不同,考虑两者每个通道的比特数不同的情况,这两拨数据的大小可能不同,必须在“将初始的图像拷到暂存缓冲区”前确保暂存缓冲区的容量能满足这两个操作,不能等到“录制将混色结果拷到暂存缓冲区的命令”时再扩容缓冲区(扩容极可能要在不同地址上重新分配内存,则先前的数据会丢失)。

如果你没看懂上面在讲啥:
若函数的调用顺序如此:数据拷入暂存缓冲区 → 录制渲染命令 → 扩容暂存缓冲区 → 录制把混色结果拷到暂存缓冲区的命令 → 提交命令
则动作的执行顺序如此:数据拷入暂存缓冲区 → 扩容暂存缓冲区(不保留先前数据) → 提交命令 → 渲染 → 把混色结果拷到暂存缓冲区
因此得在调用CmdTransferDataToImage(...)前就先扩容:

//扩容暂存缓冲区
uint32_t pixelCount = extent.width * extent.height;
VkDeviceSize imageDataSize_initial = VkDeviceSize(FormatInfo(format_initial).sizePerPixel) * pixelCount;
VkDeviceSize imageDataSize_final = VkDeviceSize(FormatInfo(format_final).sizePerPixel) * pixelCount;
if (callback_copyData)
    stagingBuffer::Expand_MainThread(std::max(imageDataSize_initial, imageDataSize_final));

auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer();
commandBuffer.Begin();

imageMemory imageMemory_conversion;
CmdTransferDataToImage(commandBuffer, pImageData, extent, format_initial, imageMemory_conversion, image);
renderPass.CmdBegin(commandBuffer, framebuffer, { {}, extent });
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
VkViewport viewport = { 0, 0, float(extent.width), float(extent.height), 0.f, 1.f };
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
vkCmdDraw(commandBuffer, 4, 1, 0, 0);
renderPass.CmdEnd(commandBuffer);

//拷贝混色结果到暂存缓冲区
if (callback_copyData) {
    VkBufferImageCopy region = {
        .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
        .imageExtent = { extent.width, extent.height, 1 }
    };
    vkCmdCopyImageToBuffer(commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingBuffer::Buffer_MainThread(), 1, &region);
}

/*待后续填充*/

commandBuffer.End();
graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer);

如有必要就生成mipmap,若不生成mipmap但先前有将混色结果拷贝到暂存缓冲区,将内存布局转到VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL

//扩容暂存缓冲区
uint32_t pixelCount = extent.width * extent.height;
VkDeviceSize imageDataSize_initial = VkDeviceSize(FormatInfo(format_initial).sizePerPixel) * pixelCount;
VkDeviceSize imageDataSize_final = VkDeviceSize(FormatInfo(format_final).sizePerPixel) * pixelCount;
if (callback_copyData)
    stagingBuffer::Expand_MainThread(std::max(imageDataSize_initial, imageDataSize_final));

auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer();
commandBuffer.Begin();

imageMemory imageMemory_conversion;
CmdTransferDataToImage(commandBuffer, pImageData, extent, format_initial, imageMemory_conversion, image);
renderPass.CmdBegin(commandBuffer, framebuffer, { {}, extent });
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
VkViewport viewport = { 0, 0, float(extent.width), float(extent.height), 0.f, 1.f };
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
vkCmdDraw(commandBuffer, 4, 1, 0, 0);
renderPass.CmdEnd(commandBuffer);

//拷贝混色结果到暂存缓冲区
if (callback_copyData) {
    VkBufferImageCopy region = {
        .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
        .imageExtent = { extent.width, extent.height, 1 }
    };
    vkCmdCopyImageToBuffer(commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingBuffer::Buffer_MainThread(), 1, &region);
}
//生成mipmap,转换内存布局
if (mipLevelCount > 1 || callback_copyData)
    imageOperation::CmdGenerateMipmap2d(commandBuffer, image, extent, mipLevelCount, 1,
        { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL });

commandBuffer.End();
graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer);

命令执行完后还剩两件事:
1.执行回调函数
2.若生成了mipmap,析构原本只有1个mip级别的图像视图,创建有多个mip级别的图像视图

//执行回调函数
if (callback_copyData)
    callback_copyData(stagingBuffer::UnmapMemory_MainThread(imageDataSize_final), imageDataSize_final),
    stagingBuffer::UnmapMemory_MainThread();
//创建image view
if (mipLevelCount > 1)
    pTexture->imageView.~imageView(),
    pTexture->imageView.Create(image, VK_IMAGE_VIEW_TYPE_2D, format_final, { VK_IMAGE_ASPECT_COLOR_BIT, 0, mipLevelCount, 0, 1 });

对代码进行整理,最后整个函数如下:

texture2d operator()(const uint8_t* pImageData, VkExtent2D extent, VkFormat format_initial) const {
    texture2d texture;
    imageMemory imageMemory_conversion;
    //扩容暂存缓冲区
    uint32_t pixelCount = extent.width * extent.height;
    VkDeviceSize imageDataSize_initial = VkDeviceSize(FormatInfo(format_initial).sizePerPixel) * pixelCount;
    VkDeviceSize imageDataSize_final = VkDeviceSize(FormatInfo(format_final).sizePerPixel) * pixelCount;
    if (callback_copyData)
        stagingBuffer::Expand_MainThread(std::max(imageDataSize_initial, imageDataSize_final));
    //获取对受保护成员的访问
    struct texture2d_local :texture2d {
        using texture::imageView;
        using texture::imageMemory;
        using texture2d::extent;
    }*pTexture;
    pTexture = static_cast<texture2d_local*>(&texture);
    pTexture->extent = extent;
    //创建imameMemory
    uint32_t mipLevelCount = generateMipmap ? texture::CalculateMipLevelCount(extent) : 1;
    VkImageCreateInfo imageCreateInfo = {
        .imageType = VK_IMAGE_TYPE_2D,
        .format = format_final,
        .extent = { extent.width, extent.height, 1 },,
        .mipLevels = mipLevelCount,
        .arrayLayers = 1,
        .samples = VK_SAMPLE_COUNT_1_BIT,
        .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
    };
    pTexture->imageMemory.Create(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
    VkImage image = pTexture->imageMemory.Image();
    //创建帧缓冲
    pTexture->imageView.Create(image, VK_IMAGE_VIEW_TYPE_2D, format_final, { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
    VkFramebufferCreateInfo framebufferCreateInfo = {
        .renderPass = renderPass,
        .attachmentCount = 1,
        .pAttachments = pTexture->imageView.Address(),
        .width = extent.width,
        .height = extent.height,
        .layers = 1
    };
    framebuffer framebuffer(framebufferCreateInfo);

    //录制命令
    auto& commandBuffer = graphicsBase::Plus().CommandBuffer_Transfer();
    commandBuffer.Begin();
    //把图像数据搬到图像附件
    CmdTransferDataToImage(commandBuffer, pImageData, extent, format_initial, imageMemory_conversion, image);
    //渲染,使用图像附件中已有的色值做混色
    renderPass.CmdBegin(commandBuffer, framebuffer, { {}, extent });
    vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
    VkViewport viewport = { 0, 0, float(extent.width), float(extent.height), 0.f, 1.f };
    vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
    vkCmdDraw(commandBuffer, 4, 1, 0, 0);
    renderPass.CmdEnd(commandBuffer);
    //如有必要,拷贝混色结果到暂存缓冲区
    if (callback_copyData) {
        VkBufferImageCopy region = {
            .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
            .imageExtent = { extent.width, extent.height, 1 }
        };
        vkCmdCopyImageToBuffer(commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingBuffer::Buffer_MainThread(), 1, &region);
    }
    //如有必要,生成mipmap,转换内存布局
    if (mipLevelCount > 1 || callback_copyData)
        imageOperation::CmdGenerateMipmap2d(commandBuffer, image, extent, mipLevelCount, 1,
            { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL });
    //结束录制,执行命令
    commandBuffer.End();
    graphicsBase::Plus().ExecuteCommandBuffer_Graphics(commandBuffer);

    //执行回调函数
    if (callback_copyData)
        callback_copyData(stagingBuffer::UnmapMemory_MainThread(imageDataSize_final), imageDataSize_final),
        stagingBuffer::UnmapMemory_MainThread();
    //创建image view
    if (mipLevelCount > 1)
        pTexture->imageView.~imageView(),
        pTexture->imageView.Create(image, VK_IMAGE_VIEW_TYPE_2D, format_final, { VK_IMAGE_ASPECT_COLOR_BIT, 0, mipLevelCount, 0, 1 });
    return texture;
}

至此为止的示例代码请参见:EasyVulkan.hpp

使用这个类的方法很简单,像这样:

static VkExtent2D imageExtent;
auto pImageData = texture::LoadFile("texture/testImage.png", imageExtent, FormatInfo(VK_FORMAT_R8G8B8A8_UNORM));
easyVulkan::fCreateTexture2d_multiplyAlpha function(VK_FORMAT_R8G8B8A8_UNORM, true,
    [](const void* pData, VkDeviceSize) {
        stbi_write_tga("texture/testImage_pa.tga", imageExtent.width, imageExtent.height, 4, pData);
    }
);
texture2d texture_premultipliedAlpha = function(pImageData.get(), imageExtent, VK_FORMAT_R8G8B8A8_UNORM);
  • 使用stb_image_write.h中的函数stbi_write_tga(...)将图像数据保存为TGA格式,TGA格式使用RLE算法进行压缩,这是一种非常简单的压缩算法,还原后透明像素的数据不变。

之后就是使用预乘alpha的贴图进行渲染来看看效果了,你拿Ch7-6里的代码改改就行。
我在Ch8-4.hpp中提供了比对使用直接alpha/预乘alpha贴图渲染效果的简单示例。