Ch8-1 离屏渲染

本节的main.cpp对应示例代码中的:Ch8-1.hpp

我们先前是始终是将内容直接渲染到屏幕,即交换链图像的。
离屏渲染(offscreen rendering)指将图片渲染到交换链图像之外的图像上,意义在于:
1.可以之后通过采样(需要后处理或旋转图像等情况)或blit(只是显示画中画的情况)进行一定的处理之后再呈现到交换链图像
2.可以将渲染结果用作程序运行过程中长期使用的贴图
3.程序可能没有交换链(比如只用来生成图像并保存到文件的控制台程序)
4.需要绘制的内容被保留到后续(当前帧交换链图像的内容当然也可以保留到下一帧,但是因为交换链图像有复数张,处理起来比较麻烦)

离屏渲染的流程

在你已经写过了Ch7代码的基础上,进行离屏渲染并将结果采样到屏幕的步骤如下:
1.创建图像附件
2.创建离屏渲染的渲染通道和帧缓冲
3.为离屏渲染书写着色器并创建管线(可以接着沿用画三角形的,不过干脆换个别的吧!)
4.为屏幕渲染书写着色器并创建管线(可以接着沿用...但换个别的!)
5.分配描述符集(描述符布局在上一步中创建),将图像附件的view写入描述符
6.渲染到离屏帧缓冲
7.采样贴图,渲染到交换链图像

我打算让上述的最后两步“离屏渲染”和“渲染到屏幕”发生在一个命令缓冲区中,中间不使用信号量或栅栏等同步对象。
“离屏渲染”和“渲染到屏幕”的两个渲染通道可以变成同一个渲染通道的两个子通道,不过这一节姑且就不这么做了。

图像附件

先前我们都是使用交换链图像的image view作为图像附件。从头创建图像附件,必然要先创建图像,然后再为其创建view。
VKBase+.h中,vulkan命名空间中定义类attachment

class attachment {
protected:
    imageView imageView;
    imageMemory imageMemory;
    //--------------------
    attachment() = default;
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 };
    }
};

然后派生出两个类:颜色附件colorAttachment、深度模板附件depthStencilAttachment(这一节还用不着深度模板附件,算是为之后做准备)。
输入附件不需要封装,输入附件是被后续子通道读取的图像附件,在创建颜色/深度模板附件时注明VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT即可。至于解析附件只不过是采样点数为1的颜色/深度模板附件罢了。

class colorAttachment :public attachment {
public:
    colorAttachment() = default;
    colorAttachment(VkFormat format, VkExtent2D extent, uint32_t layerCount = 1,
        VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT, VkImageUsageFlags otherUsages = 0) {
        Create(format, extent, layerCount, sampleCount, otherUsages);
    }
    //Non-const Function
    void Create(VkFormat format, VkExtent2D extent, uint32_t layerCount = 1,
        VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT, VkImageUsageFlags otherUsages = 0) {
        /*待填充*/
    }
    //Static Function
    //该函数用于检查某一格式的图像可否被用作颜色附件
    static bool FormatAvailability(VkFormat format, bool supportBlending = true) {
        /*待填充*/
    }
};
class depthStencilAttachment :public attachment {
public:
    depthStencilAttachment() = default;
    depthStencilAttachment(VkFormat format, VkExtent2D extent, uint32_t layerCount = 1,
        VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT, VkImageUsageFlags otherUsages = 0, bool stencilOnly = false) {
        Create(format, extent, layerCount, sampleCount, otherUsages, stencilOnly);
    }
    //Non-const Function
    void Create(VkFormat format, VkExtent2D extent, uint32_t layerCount = 1,
        VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT, VkImageUsageFlags otherUsages = 0, bool stencilOnly = false) {
        /*待填充*/
    }
    //Static Function
    //该函数用于检查某一格式的图像可否被用作深度模板附件
    static bool FormatAvailability(VkFormat format) {
        /*待填充*/
    }
};

于是先来填充两个FormatAvailability(...)。VkFormatProperties的成员optimalTilingFeatures和linearTilingFeatures分别代表图像数据的排列方式(VkImageCreateInfo::tiling)为最优排列和线性排列时,某种格式的图像具有的特性。
出于以下理由,不关心线性排列时的格式特性:
1.独显对线性数据排列的渲染目标支持较差,可能完全不支持。
2.总是倾向于使用最优数据排列以获得最佳性能。

先前在Ch5-0 VKBase+.h中获取了所有格式的VkFormatProperties,现在用先前准备好的函数FormatProperties(...)获取格式属性,让其optimalTilingFeatures成员与相应的VkImageUsageFlagBits枚举项做位与,即可知某种格式的图像能否用作颜色/深度模板附件:

static colorAttachment::FormatAvailability(VkFormat format, bool supportBlending = true) {
    return FormatProperties(format).optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT << uint32_t(supportBlending);
}
bool depthStencilAttachment::FormatAvailability(VkFormat format) {
    return FormatProperties(format).optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
}
  • VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT左移1位得到VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT,表示支持作为图像附件且支持混色。

然后填充colorAttachment::Create(...):

void colorAttachment::Create(VkFormat format, VkExtent2D extent, uint32_t layerCount = 1,
    VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT, VkImageUsageFlags otherUsages = 0) {
    VkImageCreateInfo imageCreateInfo = {
        .imageType = VK_IMAGE_TYPE_2D,
        .format = format,
        .extent = { extent.width, extent.height, 1 },
        .mipLevels = 1,
        .arrayLayers = layerCount,
        .samples = sampleCount,
        .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | otherUsages
    };
    imageMemory.Create(
        imageCreateInfo,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | bool(otherUsages & VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT) * VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT);
    imageView.Create(
        imageMemory.Image(),
        layerCount > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D,
        format,
        { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layerCount });
}
  • 内存属性当然得有VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT以获得最佳性能,除此之外根据惰性分配的规定,如果图像用途包含VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT,内存属性必须具有VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT

  • 图像视图的类型根据图层数进行了分支,但如果你之后打算把渲染好的图像作为单层2D贴图数组(而非普通2D贴图)采样,在图层数为1时将类型指定为VK_IMAGE_VIEW_TYPE_2D_ARRAY也没什么问题(前提是设备得支持多层帧缓冲)。

相比之下,depthStencilAttachment::Create(...)多出一段根据图像格式确定VkImageSubresourceRange::aspectMask的逻辑:

void depthStencilAttachment::Create(VkFormat format, VkExtent2D extent, uint32_t layerCount = 1,
    VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT, VkImageUsageFlags otherUsages = 0, bool stencilOnly = false) {
    VkImageCreateInfo imageCreateInfo = {
        .imageType = VK_IMAGE_TYPE_2D,
        .format = format,
        .extent = { extent.width, extent.height, 1 },
        .mipLevels = 1,
        .arrayLayers = layerCount,
        .samples = sampleCount,
        .usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | otherUsages
    };
    imageMemory.Create(
        imageCreateInfo,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | bool(otherUsages & VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT) * VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT);
    //确定aspcet mask-------------------------
    VkImageAspectFlags aspectMask = (!stencilOnly) * VK_IMAGE_ASPECT_DEPTH_BIT;
    if (format > VK_FORMAT_S8_UINT)
        aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
    else if (format == VK_FORMAT_S8_UINT)
        aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
    //----------------------------------------
    imageView.Create(
        imageMemory.Image(),
        layerCount > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D,
        format,
        { aspectMask, 0, 1, 0, layerCount });
}
  • 这里if-else分支的逻辑是:在仅有的几种深度模板格式当中,只有VK_FORMAT_S8_UINT是只存储模板值的,而大于VK_FORMAT_S8_UINT的深度模板格式同时具有深度和模板值。

  • 格式中同时包含深度和模板分量,之后只使用深度测试/模板测试之一的话,aspectMask中可同时包含两者,但只指定其中之一也是没问题的。
    但是用作输入附件或贴图时,必须只指定VK_IMAGE_ASPECT_DEPTH_BITVK_IMAGE_ASPECT_STENCIL_BIT

stencilOnly参数的解释:
你只打算使用深度测试的话,使用只有深度的格式即可,Vulkan标准规定实现必须支持VK_FORMAT_D16_UNORM格式的深度模板附件,然后VK_FORMAT_X8_D24_UNORM_PACK32(X8是填充字节)和VK_FORMAT_D32_SFLOAT之中至少有一个受支持。
但是,只打算使用模板测试时,实现不一定支持VK_FORMAT_S8_UINT格式的深度模板附件,可能不得不使用带深度值的格式,这里stencilOnly意在让格式中带深度分量时使aspectMask中不标明深度(这样一来,要将图像用于采样时,就不必再另外创建image view)。

CreateRpwf_Canvas

于是,向EasyVulkan.hpp,easyVulkan命名空间中,定义renderPassWithFramebuffer结构体,及用来创建图像附件、渲染通道、帧缓冲的函数CreateRpwf_Canvas(...):

namespace easyVulkan {
    struct renderPassWithFramebuffer {
        renderPass renderPass;
        framebuffer framebuffer;
    };
    const auto& CreateRpwf_Canvas(VkExtent2D canvasSize) {
        static renderPassWithFramebuffer rpwf;

        /*待后续填充*/

        return rpwf;
    }
}

所谓canvas,顾名思义,我打算将这次创建的图像附件用作画布,实现一个超简陋的画画功能,画布大小从CreateRpwf_Canvas(...)的参数传入。

在easyVulkan命名空间中定义一个colorAttachment对象,然后在CreateRpwf_Canvas(...)中创建图像附件,格式就跟交换链一样吧:

namespace easyVulkan {
    struct renderPassWithFramebuffer {
        renderPass renderPass;
        framebuffer framebuffer;
    };

    colorAttachment ca_canvas;

    const auto& CreateRpwf_Canvas(VkExtent2D canvasSize) {
        static renderPassWithFramebuffer rpwf;

        ca_canvas.Create(graphicsBase::Base().SwapchainCreateInfo().imageFormat, canvasSize, 1, VK_SAMPLE_COUNT_1_BIT,
            VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT);

        /*待后续填充*/

        return rpwf;
    }
}
  • 将离屏渲染的图像附件用作画布的话,渲染通道开始时当然不会清屏,那么图像用途中应包含VK_IMAGE_USAGE_TRANSFER_DST_BIT,以使用vkCmdClearColorImage(...)清屏。

来创建渲染通道,先填写附件描述。
因为是画布,每次读取时不清屏,且要保留先前的内容,所以loadOp为VK_ATTACHMENT_LOAD_OP_LOAD,storeOp为VK_ATTACHMENT_STORE_OP_STORE
这次的应用场景中,图像附件是“渲染→被采样→渲染→被采样”如此循环,所以前后内存布局都用VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL即可。

const auto& CreateRpwf_Canvas(VkExtent2D canvasSize) {
    static renderPassWithFramebuffer rpwf;

    ca_canvas.Create(graphicsBase::Base().SwapchainCreateInfo().imageFormat, canvasSize, 1, VK_SAMPLE_COUNT_1_BIT,
        VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT);

    VkAttachmentDescription attachmentDescription = {
        .format = graphicsBase::Base().SwapchainCreateInfo().imageFormat,
        .samples = VK_SAMPLE_COUNT_1_BIT,
        .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
        .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
        .stencilStoreOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
        .initialLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        .finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
    };
    /*待后续填充*/

    return rpwf;
}

毫无难度地填写掉子通道描述,并把渲染通道创建信息先填了:

VkAttachmentReference attachmentReference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
VkSubpassDescription subpassDescription = {
    .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
    .colorAttachmentCount = 1,
    .pColorAttachments = &attachmentReference
};
VkRenderPassCreateInfo renderPassCreateInfo = {
    .attachmentCount = 1,
    .pAttachments = &attachmentDescription,
    .subpassCount = 1,
    .pSubpasses = &subpassDescription,
    .dependencyCount = 2,
    .pDependencies = subpassDependencies,
};

子通道依赖是子通道开始和结束时各一个,先看代码:

VkSubpassDependency subpassDependencies[2] = {
    {
        .srcSubpass = VK_SUBPASS_EXTERNAL,
        .dstSubpass = 0,
        .srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
        .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .srcAccessMask = 0,
        .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT },
    {
        .srcSubpass = 0,
        .dstSubpass = VK_SUBPASS_EXTERNAL,
        .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
        .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
        .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT }
};

先来解说结束时的情况,结束时的子通道依赖可以说是每个参数都有恰如其分的作用。
通道结束时的dstSubpass为VK_SUBPASS_EXTERNAL,这里实质上指代之后“渲染到屏幕”。
srcStageMask和srcAccessMask应该不需要解释,采样属于VK_ACCESS_SHADER_READ_BIT,发生在片段着色器阶段所以dstStageMask是VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT

渲染通道开始时的情况则与结束时相反即可。
srcAccessMask为0,指定VK_ACCESS_SHADER_READ_BIT这种读操作无意义,而前一帧中对该图像附件的写入结果在当前帧的可见性,由在前一帧中等待semaphore_renderingIsOver来确保。
指定srcStageMask为VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT是多余的,因为有栅栏来同步,填写VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT即可(你问我为什么每次都不这么写?为了特地反复强调这件事!)。

VkSubpassDependency subpassDependencies[2] = {
    {
        .srcSubpass = VK_SUBPASS_EXTERNAL,
        .dstSubpass = 0,
        .srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
        .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .srcAccessMask = 0,
        .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT },
    {
        .srcSubpass = 0,
        .dstSubpass = VK_SUBPASS_EXTERNAL,
        .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
        .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
        .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT }
};

最后非常简单无脑地创建帧缓冲,整个CreateRpwf_Canvas(...)如下:

const auto& CreateRpwf_Canvas(VkExtent2D canvasSize = windowSize) {
    static renderPassWithFramebuffer rpwf;

    ca_canvas.Create(graphicsBase::Base().SwapchainCreateInfo().imageFormat, canvasSize, 1, VK_SAMPLE_COUNT_1_BIT,
        VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT);

    VkAttachmentDescription attachmentDescription = {
        .format = graphicsBase::Base().SwapchainCreateInfo().imageFormat,
        .samples = VK_SAMPLE_COUNT_1_BIT,
        .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
        .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
        .stencilStoreOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
        .initialLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        .finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
    };
    VkSubpassDependency subpassDependencies[2] = {
        {
            .srcSubpass = VK_SUBPASS_EXTERNAL,
            .dstSubpass = 0,
            .srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
            .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
            .srcAccessMask = 0,
            .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
            .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT },
        {
            .srcSubpass = 0,
            .dstSubpass = VK_SUBPASS_EXTERNAL,
            .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
            .dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
            .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
            .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
            .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT }
    };
    VkAttachmentReference attachmentReference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
    VkSubpassDescription subpassDescription = {
        .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
        .colorAttachmentCount = 1,
        .pColorAttachments = &attachmentReference
    };
    VkRenderPassCreateInfo renderPassCreateInfo = {
        .attachmentCount = 1,
        .pAttachments = &attachmentDescription,
        .subpassCount = 1,
        .pSubpasses = &subpassDescription,
        .dependencyCount = 2,
        .pDependencies = subpassDependencies,
    };

    VkFramebufferCreateInfo framebufferCreateInfo = {
        .renderPass = rpwf.renderPass,
        .attachmentCount = 1,
        .pAttachments = ca_canvas.AddressOfImageView(),
        .width = canvasSize.width,
        .height = canvasSize.height,
        .layers = 1
    };
    rpwf.framebuffer.Create(framebufferCreateInfo);

    return rpwf;
}

来画线吧!

我打算实现一个非常简单的绘画功能:追纵鼠标指针的位置,将鼠标指针在当前帧和上一帧的位置用红线连起来。
然后在“渲染到屏幕时”使用两两间隔相同的三个uv坐标,采用三次,使其显示为红绿蓝三色的线,并让颜色叠加在一起,效果如下:

_images/ch8-1-1.png

(我知道不怎么有趣,不过我不想写一些数学上解释起来很麻烦的)

为简化叙事,这节会全部使用push constant来向着色器提供数据,免去顶点/uniform等缓冲区。

画线的着色器

用来画线的顶点着色器Line.vert.shader

#version 460
#pragma shader_stage(vertex)

layout(push_constant) uniform pushConstants {
    vec2 viewportSize;
    vec2 offsets[2];
};

void main() {
    gl_Position = vec4(2 * offsets[gl_VertexIndex] / viewportSize - 1, 0, 1);
}
    这里viewportSize是视口大小,将是等于画布大小。
    offsets是两个点相对于视口左上角的位置。

    视口对应整个NDC坐标[-1, 1]的区间,对于屏幕上任意点的像素单位坐标pos,到其在视口中NDC坐标范围[-1, 1]的转换为(pos - viewportSize的一半) / viewportSize的一半, 即2 * pos / viewportSize - 1

    片段着色器Line.frag.shader(真是省事啊!):

    #version 460
    #pragma shader_stage(fragment)
    
    layout(location = 0) out vec4 o_Color;
    
    void main() {
        o_Color = vec4(1, 0, 0, 1);
    }
    

    之后创建管线时的VkPushConstantRange

    VkPushConstantRange pushConstantRange = { VK_SHADER_STAGE_VERTEX_BIT, 0, 24 };
    

采样画布的着色器

CanvasToScreen.vert.shader

#version 460
#pragma shader_stage(vertex)

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

layout(location = 0) out vec2 o_TexCoord;

layout(push_constant) uniform pushConstants {
    vec2 viewportSize;
    vec2 canvasSize;
};

void main() {
    o_TexCoord = positions[gl_VertexIndex];
    gl_Position = vec4(2 * positions[gl_VertexIndex] * canvasSize / viewportSize - 1, 0, 1);
}

positions是着色器里的变量,我没有用const修饰它,所以它是可修改的,修改只到影响本次(处理当前顶点的)着色器调用中的数值。
这里viewportSize是视口大小,之后代码里“渲染到屏幕”的视口一如既往是交换链图像大小。
canvasSize是画布大小,数值跟前面Line.vert.shader中的viewportSize一致。

这个着色器的效果是贴着屏幕左上角渲染整张画布,因此四个顶点以像素为单位的位置是positions[gl_VertexIndex] * canvasSize,而贴图坐标非0即1。

CanvasToScreen.frag.shader

#version 460
#pragma shader_stage(fragment)

layout(location = 0) in vec2 i_TexCoord;
layout(location = 0) out vec4 o_Color;
layout(binding = 0) uniform sampler2D u_Texture;

layout(push_constant) uniform pushConstants {
    layout(offset = 8)
    vec2 canvasSize;
};

void main() {
    o_Color = texture(u_Texture, i_TexCoord);
    o_Color.g = texture(u_Texture, i_TexCoord + 8 / canvasSize).r;
    o_Color.b = texture(u_Texture, i_TexCoord - 8 / canvasSize).r;
}
  • 因为整张贴图大小是canvasSizei_TexCoord + 8 / canvasSize即从i_TexCoord纵向横向都偏离8个像素的采样位置。

  • 这是除了对语法进行说明的Ch4-1 着色器模组外,本教程中第一次出现layout(offset = 距起始位置的字节数)这个语法。
    这个着色器不需要位于push constant开头8个字节的数据,如果你想把viewportSize声明上去,那也没什么关系(注:下面的VkPushConstantRange要相应变更)。

之后创建管线时的VkPushConstantRange

VkPushConstantRange pushConstantRanges[2] = {
    { VK_SHADER_STAGE_VERTEX_BIT, 0, 16 },
    { VK_SHADER_STAGE_FRAGMENT_BIT, 8, 8 }
};

创建管线

跟管线相关的对象一共5个:

#include "GlfwGeneral.hpp"
#include "EasyVulkan.hpp"
using namespace vulkan;

//离屏,画线
pipelineLayout pipelineLayout_line;
pipeline pipeline_line;
//屏幕,采样贴图
descriptorSetLayout descriptorSetLayout_texture;
pipelineLayout pipelineLayout_screen;
pipeline pipeline_screen;

const auto& RenderPassAndFramebuffers_Screen() {
    static const auto& rpwf = easyVulkan::CreateRpwf_Screen();
    return rpwf;
}
const auto& RenderPassAndFramebuffer_Offscreen(VkExtent2D canvasSize) {
    static const auto& rpwf = easyVulkan::CreateRpwf_Canvas(canvasSize);
    return rpwf;
}
void CreateLayout() {
    /*待填充*/
}
void CreatePipeline(VkExtent2D canvasSize) {
    /*待填充*/
}

先创建“离屏”的管线布局,只有push constant范围:

void CreateLayout() {
    VkPushConstantRange pushConstantRange_offscreen = { VK_SHADER_STAGE_VERTEX_BIT, 0, 24 };
    VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = {
        .pushConstantRangeCount = 1,
        .pPushConstantRanges = &pushConstantRange_offscreen,
    };
    pipelineLayout_line.Create(pipelineLayoutCreateInfo);
    /*待填充*/
}

创建descriptorSetLayout_texture,同Ch7-7完全一样:

VkDescriptorSetLayoutBinding descriptorSetLayoutBinding_texture = {
    .binding = 0,
    .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
    .descriptorCount = 1,
    .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT
};
VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo_texture = {
    .bindingCount = 1,
    .pBindings = &descriptorSetLayoutBinding_texturePosition
};
descriptorSetLayout_texture.Create(descriptorSetLayoutCreateInfo_texture);

剩下也没什么好说的,完事:

void CreateLayout() {
    //离屏
    VkPushConstantRange pushConstantRange_offscreen = { VK_SHADER_STAGE_VERTEX_BIT, 0, 24 };
    VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = {
        .pushConstantRangeCount = 1,
        .pPushConstantRanges = &pushConstantRange_offscreen,
    };
    pipelineLayout_line.Create(pipelineLayoutCreateInfo);
    //屏幕
    VkDescriptorSetLayoutBinding descriptorSetLayoutBinding_texture = { 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT };
    VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo_texture = {
        .bindingCount = 1,
        .pBindings = &descriptorSetLayoutBinding_texturePosition
    };
    descriptorSetLayout_texture.Create(descriptorSetLayoutCreateInfo_texture);
    VkPushConstantRange pushConstantRanges_screen[] = {
        { VK_SHADER_STAGE_VERTEX_BIT, 0, 16 },
        { VK_SHADER_STAGE_FRAGMENT_BIT, 8, 8 }
    };
    pipelineLayoutCreateInfo.pushConstantRangeCount = 2;
    pipelineLayoutCreateInfo.pPushConstantRanges = pushConstantRanges_screen;
    pipelineLayoutCreateInfo.setLayoutCount = 1;
    pipelineLayoutCreateInfo.pSetLayouts = descriptorSetLayout_texture.Address();
    pipelineLayout_screen.Create(pipelineLayoutCreateInfo);
}

创建画线的管线,代码不需要放进lambda,因为窗口大小改变时,画布大小不必跟着变。
相比先前画三角形时的不同在于绘制模式和指定了线宽:

void CreateLayout() {
    static shaderModule vert_offscreen("shader/Line.vert.spv");
    static shaderModule frag_offscreen("shader/Line.frag.spv");
    VkPipelineShaderStageCreateInfo shaderStageCreateInfos_line[2] = {
        vert_offscreen.StageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT),
        frag_offscreen.StageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT)
    };
    graphicsPipelineCreateInfoPack pipelineCiPack;
    pipelineCiPack.createInfo.layout = pipelineLayout_line;
    pipelineCiPack.createInfo.renderPass = RenderPassAndFramebuffer_Offscreen(canvasSize).renderPass;
    pipelineCiPack.inputAssemblyStateCi.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST;//绘制线
    pipelineCiPack.viewports.emplace_back(0.f, 0.f, float(canvasSize.width), float(canvasSize.height), 0.f, 1.f);
    pipelineCiPack.scissors.emplace_back(VkOffset2D{}, canvasSize);
    pipelineCiPack.rasterizationStateCi.lineWidth = 1;                             //线宽
    pipelineCiPack.multisampleStateCi.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
    pipelineCiPack.colorBlendAttachmentStates.push_back({ .colorWriteMask = 0b1111 });
    pipelineCiPack.UpdateAllArrays();
    pipelineCiPack.createInfo.stageCount = 2;
    pipelineCiPack.createInfo.pStages = shaderStageCreateInfos_line;
    pipeline_line.Create(pipelineCiPack);
    /*待填充*/
}

渲染到屏幕的管线,跟Ch2中创建画三角形管线的代码之差别只有绘制模式(不算文件路径和变量名的不同的话):

void CreateLayout() {
    //离屏
    static shaderModule vert_offscreen("shader/Line.vert.spv");
    static shaderModule frag_offscreen("shader/Line.frag.spv");
    VkPipelineShaderStageCreateInfo shaderStageCreateInfos_line[2] = {
        vert_offscreen.StageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT),
        frag_offscreen.StageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT)
    };
    graphicsPipelineCreateInfoPack pipelineCiPack;
    pipelineCiPack.createInfo.layout = pipelineLayout_line;
    pipelineCiPack.createInfo.renderPass = RenderPassAndFramebuffers_Screen(canvasSize).renderPass;
    pipelineCiPack.inputAssemblyStateCi.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST;//绘制线
    pipelineCiPack.viewports.emplace_back(0.f, 0.f, float(canvasSize.width), float(canvasSize.height), 0.f, 1.f);
    pipelineCiPack.scissors.emplace_back(VkOffset2D{}, canvasSize);
    pipelineCiPack.rasterizationStateCi.lineWidth = 1;                             //线宽
    pipelineCiPack.multisampleStateCi.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
    pipelineCiPack.colorBlendAttachmentStates.push_back({ .colorWriteMask = 0b1111 });
    pipelineCiPack.UpdateAllArrays();
    pipelineCiPack.createInfo.stageCount = 2;
    pipelineCiPack.createInfo.pStages = shaderStageCreateInfos_line;
    pipeline_line.Create(pipelineCiPack);
    //屏幕
    static shaderModule vert_screen("shader/CanvasToScreen.vert.spv");
    static shaderModule frag_screen("shader/CanvasToScreen.frag.spv");
    static VkPipelineShaderStageCreateInfo shaderStageCreateInfos_screen[2] = {
        vert_screen.StageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT),
        frag_screen.StageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT)
    };
    auto Create = [] {
        graphicsPipelineCreateInfoPack pipelineCiPack;
        pipelineCiPack.createInfo.layout = pipelineLayout_screen;
        pipelineCiPack.createInfo.renderPass = RenderPassAndFramebuffers_Screen().renderPass;
        pipelineCiPack.inputAssemblyStateCi.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;//绘制模式跟Ch7-7一样
        pipelineCiPack.viewports.emplace_back(0.f, 0.f, float(windowSize.width), float(windowSize.height), 0.f, 1.f);
        pipelineCiPack.scissors.emplace_back(VkOffset2D{}, windowSize);
        pipelineCiPack.multisampleStateCi.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
        pipelineCiPack.colorBlendAttachmentStates.push_back({ .colorWriteMask = 0b1111 });
        pipelineCiPack.UpdateAllArrays();
        pipelineCiPack.createInfo.stageCount = 2;
        pipelineCiPack.createInfo.pStages = shaderStageCreateInfos_screen;
        pipeline_screen.Create(pipelineCiPack);
    };
    auto Destroy = [] {
        pipeline_screen.~pipeline();
    };
    graphicsBase::Base().AddCallback_CreateSwapchain(Create);
    graphicsBase::Base().AddCallback_DestroySwapchain(Destroy);
    Create();
}

清空画布

在书写主函数前还有一步,需要写一个清空画布的函数。
不光是为了实现这个功能本身,刚才创建渲染通道时,图像附件的初始图像布局为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,可是刚创建好的图像附件的布局为VK_IMAGE_LAYOUT_UNDEFINED,内容也可能是一堆内存垃圾。

EasyVulkan.hpp,easyVulkan命名空间中,定义函数CmdClearCanvas(...),用于清屏并转换图像布局:

void CmdClearCanvas(VkCommandBuffer commandBuffer, VkClearColorValue clearColor) {
    /*待填充*/
}

要干的事情一共三件:清屏前的内存屏障、调用vkCmdClearColorImage(...)清屏、清屏后的内存屏障。

vkCmdClearColorImage(...)属于数据转移命令,清屏前的内存屏障要将图像内存布局从VK_IMAGE_LAYOUT_UNDEFINED转到VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL待写:

void CmdClearCanvas(VkCommandBuffer commandBuffer, VkClearColorValue clearColor) {
    VkImageSubresourceRange imageSubresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
    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,
        ca_canvas.Image(),
        imageSubresourceRange
    };
    vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
        0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);

    /*待填充*/
}
  • 在渲染循环中第二次执行本函数时,图像的内存布局已经是VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL了,但因为是清屏(不关心原有内容),清屏前的内存屏障中oldLayout永远都是VK_IMAGE_LAYOUT_UNDEFINED即可。

书写清屏后的内存屏障。
先前创建渲染通道时,指定了渲染通道开始时的图像内存布局为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,清屏后内存布局就转到这个,不要转到VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
因为之后有子通道依赖,dstStageMask就算填写成晚于VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT的阶段也没关系,内存布局转换具有隐式同步保证,这个屏障引发的转换一定先于渲染通道开始时的依赖:

void CmdClearCanvas(VkCommandBuffer commandBuffer, VkClearColorValue clearColor) {
    VkImageSubresourceRange imageSubresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
    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,
        ca_canvas.Image(),
        imageSubresourceRange
    };
    vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
        0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);

    /*调用命令清屏,待填充*/

    imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    imageMemoryBarrier.dstAccessMask = 0;
    imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0,
        0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
}

正题:

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

VkCommandBuffer commandBuffer

命令缓冲区的handle

VkImage image

图像的handle

VkImageLayout imageLayout

图像的内存布局

const VkClearColorValue* pColor

指向用于清屏的填充色

uint32_t rangeCount

清空范围的个数

const VkImageSubresourceRange* pRanges

指向VkImageSubresourceRange的数组,用于指定清空范围

清空整张图,毫无难度地写完:

void CmdClearCanvas(VkCommandBuffer commandBuffer, VkClearColorValue clearColor) {
    VkImageSubresourceRange imageSubresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
    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,
        ca_canvas.Image(),
        imageSubresourceRange
    };
    vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
        0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);

    vkCmdClearColorImage(commandBuffer, ca_canvas.Image(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1, &imageSubresourceRange);

    imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    imageMemoryBarrier.dstAccessMask = 0;
    imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0,
        0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
}

录制渲染命令

主函数框架如下,都是你见过的东西。
在里面定义画布大小canvas,将图像附件的view写入描述符:

int main() {
    if (!InitializeWindow({1280,720}))
        return -1;

    //画布大小;
    VkExtent2D canvasSize = windowSize;

    const auto& [renderPass_screen, framebuffers_screen] = RenderPassAndFramebuffers_Screen();
    const auto& [renderPass_offscreen, framebuffer_offscreen] = RenderPassAndFramebuffer_Offscreen();
    CreateLayout();
    CreatePipeline();

    fence fence;
    semaphore semaphore_imageIsAvailable;
    semaphore semaphore_renderingIsOver;

    commandBuffer commandBuffer;
    commandPool commandPool(graphicsBase::Base().QueueFamilyIndex_Graphics(), VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);
    commandPool.AllocateBuffers(commandBuffer);

    VkSamplerCreateInfo samplerCreateInfo = texture::SamplerCreateInfo();
    sampler sampler(samplerCreateInfo);
    VkDescriptorPoolSize descriptorPoolSizes[] = {
        { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 }
    };
    descriptorPool descriptorPool descriptorPool(1, descriptorPoolSizes);
    descriptorSet descriptorSet_texture;
    descriptorPool.AllocateSets(descriptorSet_texture, descriptorSetLayout_texture);
    //写入描述符;
    descriptorSet_texture.Write(easyVulkan::ca_canvas.DescriptorImageInfo(sampler), VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);

    /*待填充*/

    while (!glfwWindowShouldClose(pWindow)) {
        while (glfwGetWindowAttrib(pWindow, GLFW_ICONIFIED))
            glfwWaitEvents();

        graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);
        auto i = graphicsBase::Base().CurrentImageIndex();
        commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

        /*待填充*/

        commandBuffer.End();
        graphicsBase::Base().SubmitCommandBuffer_Graphics(commandBuffer, semaphore_imageIsAvailable, semaphore_renderingIsOver, fence);
        graphicsBase::Base().PresentImage(semaphore_renderingIsOver);

        glfwPollEvents();

        /*操作逻辑,待填充*/

        TitleFps();

        fence.WaitAndReset();
    }
    TerminateWindow();
    return 0;
}
  • 因为每一帧中使用的是相同的离屏帧缓冲,不能应用即时帧(即每一帧应使用同一套同步对象)。

这里操作逻辑被我放到了glfwPollEvents()之后,嘛在循环里的话其实放哪儿都差不多,不过这里是有点讲究的。
我们首先将画布清空为全透明,因为不会每次都清屏,所以用一个初值为true的布尔值来指示是否清空:

int main() {
    /*...前略*/

    /*待填充*/

    bool clearCanvas = true;

    while (!glfwWindowShouldClose(pWindow)) {
        while (glfwGetWindowAttrib(pWindow, GLFW_ICONIFIED))
            glfwWaitEvents();

        graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);
        auto i = graphicsBase::Base().CurrentImageIndex();
        commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

        if (clearCanvas)
            easyVulkan::CmdClearCanvas(commandBuffer, VkClearColorValue{}),
            clearCanvas = false;

        /*待填充*/

        commandBuffer.End();
        graphicsBase::Base().SubmitCommandBuffer_Graphics(commandBuffer, semaphore_imageIsAvailable, semaphore_renderingIsOver, fence);
        graphicsBase::Base().PresentImage(semaphore_renderingIsOver);

        glfwPollEvents();

        /*操作逻辑,待填充*/

        TitleFps();

        fence.WaitAndReset();
    }
    TerminateWindow();
    return 0;
}

操作逻辑就简单点,按下左键就清空吧。
glfwGetMouseButton(...)获取鼠标按键状态,参数不言自明,到GLFW3.4为止只返回GLFW_PRESS(按下,值为1)或GLFW_RELEASE(没按下,值为0)两种:

int main() {
    /*...前略*/

    /*待填充*/

    bool clearCanvas = true;

    while (!glfwWindowShouldClose(pWindow)) {
        while (glfwGetWindowAttrib(pWindow, GLFW_ICONIFIED))
            glfwWaitEvents();

        graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);
        auto i = graphicsBase::Base().CurrentImageIndex();
        commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

        if (clearCanvas)
            easyVulkan::CmdClearCanvas(commandBuffer, VkClearColorValue{}),
            clearCanvas = false;

        /*待填充*/

        commandBuffer.End();
        graphicsBase::Base().SubmitCommandBuffer_Graphics(commandBuffer, semaphore_imageIsAvailable, semaphore_renderingIsOver, fence);
        graphicsBase::Base().PresentImage(semaphore_renderingIsOver);

        glfwPollEvents();

        /*待后续填充*/
        clearCanvas = glfwGetMouseButton(pWindow, GLFW_MOUSE_BUTTON_LEFT)

        TitleFps();

        fence.WaitAndReset();
    }
    TerminateWindow();
    return 0;
}
  • 所以操作逻辑放这个位置的原因也一目了然:防止clearCanvas在首次清空画布前变成false

接着在渲染循环前准备“离屏渲染”用到的push constant数据。
glfwGetCursorPos(...)获取鼠标指针位置,参数不言自明,得到的是相对于屏幕左上角的坐标,将其结果用作传入着色器的两个点坐标的初始值:

double mouseX, mouseY;
glfwGetCursorPos(pWindow, &mouseX, &mouseY);
struct {
    glm::vec2 viewportSize;
    glm::vec2 offsets[2];
} pushConstants_offscreen = {
    { canvasSize.width, canvasSize.height },
    { { mouseX, mouseY }, { mouseX, mouseY } }
};

之后也要不断获取鼠标指针位置,考虑到我们只有两个点,用布尔值作为索引,交替赋值就行了:

int main() {
    /*...前略*/

    double mouseX, mouseY;
    glfwGetCursorPos(pWindow, &mouseX, &mouseY);
    struct {
        glm::vec2 viewportSize;
        glm::vec2 offsets[2];
    } pushConstants_offscreen = {
        { canvasSize.width, canvasSize.height },
        { { mouseX, mouseY }, { mouseX, mouseY } }
    };

    bool clearCanvas = true;
    /*新增*/bool index = 0;

    while (!glfwWindowShouldClose(pWindow)) {
        /*...前略*/

        glfwPollEvents();

        /*新增*/glfwGetCursorPos(pWindow, &mouseX, &mouseY);
        /*新增*/pushConstants_offscreen.offsets[index = !index] = { mouseX, mouseY };
        clearCanvas = glfwGetMouseButton(pWindow, GLFW_MOUSE_BUTTON_LEFT)
    }
    TerminateWindow();
    return 0;
}

操作逻辑到此写完,该写离屏部分的渲染代码了,一如既往:开始渲染通道、绑定管线、更新常量、绘制、结束渲染通道

int main() {
    /*...前略*/

    double mouseX, mouseY;
    glfwGetCursorPos(pWindow, &mouseX, &mouseY);
    struct {
        glm::vec2 viewportSize;
        glm::vec2 offsets[2];
    } pushConstants_offscreen = {
        { canvasSize.width, canvasSize.height },
        { { mouseX, mouseY }, { mouseX, mouseY } }
    };

    bool clearCanvas = true;
    bool index = 0;

    while (!glfwWindowShouldClose(pWindow)) {
        while (glfwGetWindowAttrib(pWindow, GLFW_ICONIFIED))
            glfwWaitEvents();

        graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);
        auto i = graphicsBase::Base().CurrentImageIndex();
        commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

        if (clearCanvas)
            easyVulkan::CmdClearCanvas(commandBuffer, VkClearColorValue{}),
            clearCanvas = false;

        //离屏
        renderPass_offscreen.CmdBegin(commandBuffer, framebuffer_offscreen, { {}, canvasSize });
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_line);
        vkCmdPushConstants(commandBuffer, pipelineLayout_line, VK_SHADER_STAGE_VERTEX_BIT, 0, 24, &pushConstants_offscreen);
        vkCmdDraw(commandBuffer, 2, 1, 0, 0);
        renderPass_offscreen.CmdEnd(commandBuffer);

        /*采样到屏幕,待填充*/

        commandBuffer.End();
        graphicsBase::Base().SubmitCommandBuffer_Graphics(commandBuffer, semaphore_imageIsAvailable, semaphore_renderingIsOver, fence);
        graphicsBase::Base().PresentImage(semaphore_renderingIsOver);

        glfwPollEvents();
        glfwGetCursorPos(pWindow, &mouseX, &mouseY);
        pushConstants_offscreen.offsets[index = !index] = { mouseX, mouseY };
        clearCanvas = glfwGetMouseButton(pWindow, GLFW_MOUSE_BUTTON_LEFT)
        TitleFps();

        fence.WaitAndReset();
    }
    TerminateWindow();
    return 0;
}

采样到屏幕:开始渲染通道、绑定管线、绑定描述符、更新常量、绘制、结束渲染通道

renderPass_screen.CmdBegin(commandBuffer, framebuffers_screen[i], { {}, windowSize }, VkClearValue{ .color = { 1.f, 1.f, 1.f, 1.f } });
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_screen);

//VkExtent2D底层是两个uint32_t,得转为float
glm::vec2 windowSize = { ::windowSize.width, ::windowSize.height };
vkCmdPushConstants(commandBuffer, pipelineLayout_screen, VK_SHADER_STAGE_VERTEX_BIT, 0, 8, &windowSize);
//pushConstants_offscreen.viewportSize就是canvasSize转到vec2类型
vkCmdPushConstants(commandBuffer, pipelineLayout_screen, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 8, 8, &pushConstants_offscreen.viewportSize);

vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_screen, 0, 1, descriptorSet_texture.Address(), 0, nullptr);
vkCmdDraw(commandBuffer, 4, 1, 0, 0);
renderPass_screen.CmdEnd(commandBuffer);

这里需要注意的是,不同着色器阶段使用的push constant范围重叠时:
更新重叠的部分时要注明涉及的全部着色器阶段,而如果某个范围不会被某个阶段使用,更新该范围时不能注明无关的阶段(将只有某个阶段使用的范围,和重叠的范围分开更新即可)。

下面这种写法不符合规范(验证层会报错,虽然运行未必出错):

struct {
    glm::vec2 viewportSize;
    glm::vec2 canvasSize;
} pushConstants = {
    { windowSize.width, windowSize.height },
    { canvasSize.width, canvasSize.height }
};
vkCmdPushConstants(commandBuffer, pipelineLayout_screen, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, &pushConstants);

运行程序,效果已经在前面书写着色器处展示过了。

。。。你以为事情这么一来就结束了吗?(似曾相识的台词)

写这节时遭遇的硬件差异问题

书写这节的示例代码的过程中我遇到了一个预料之外的问题。
上面这些代码,一点不改(你可以直接拿示例代码Ch8-1.hpp跑跑看),如果你用Intel核显来跑(我的显卡型号在下图中),可能会有这种事:

_images/ch8-1-2.png
  • 这是个1280x720的窗口,周围白的也是窗口图像的一部分。

简而言之:在“渲染到屏幕”时,没有正确更新push constant中的canvasSize,在其位置上的数据似乎保留为了鼠标指针位置(Line.vert.shader中的offset[0])。至于为什么连蓝色和绿色的线都消失了?。。。我也不知道!

这个情况只在上述代码中canvasSize和交换链图像的大小windowSize一样时发生。
笔者的怀疑是:英特尔驱动可能有某种方式判断更新的数据是否跟先前的一致,然后决定要不要将更新记录在命令缓冲区,但代码实现有错误。
(我这个解释很可能不对,我以往用Intel核显跑代码时还遇到过一些push constant相关的类似问题,似乎无法以此来简单解释)

仅仅就当前这个情况,在完全符合规范的范畴内的改法,最省事的是这样子的:

struct {
    glm::vec2 viewportSize;
    glm::vec2 canvasSize;
} pushConstants = {
    { windowSize.width, windowSize.height },
    { canvasSize.width, canvasSize.height }
};
vkCmdPushConstants(commandBuffer, pipelineLayout_screen, VK_SHADER_STAGE_VERTEX_BIT, 0, 16, &pushConstants);
//对于我的两款显卡,有没有下面这行跑出来结果似乎都一样,但根据规范需要有
vkCmdPushConstants(commandBuffer, pipelineLayout_screen, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 8, 8, &pushConstants.canvasSize);
  • 你问为什么这么改就好了?因为实验结果告诉我能行!。。。
    这样本该理应是发生了重复更新的,而且这只是这个情况下的做法,我不知道管线阶段更多或者push constant重叠范围更复杂时会怎么样及该怎么办。

也能这么解决:因为push constant的常量是记录在命令缓冲区中的,只要把“离屏渲染”和“渲染到屏幕”分别录制在不同的命令缓冲区中,前者的push constant就不可能影响到后者。
也能这么解决:不管某个值在某个阶段用不用得到,在所有着色器阶段中声明全部的push constant范围,然后总是一次性更新整个范围(做法是很保险,就是太简单粗暴了)。
还能这么解决:抛弃Vulkan,去用OpenGL!。。。

如果你遇到了类似的情况并且搞清楚了怎么回事的话,可以到Github issue里告诉我。

类似的问题在18年有人向英特尔反应过:Vulkan Push Constants doesn't work correctly on Windows Intel GPU drivers
(显然没有引起官方的重视)