VulkanSceneGraph学习教程(二十)

第 20 章 后处理效果

摘要:本章系统介绍了在 Vulkan Scene Graph (VSG) 中实现后处理效果的核心流程。后处理本质上是多通道渲染:首先通过离屏 RenderGraph 将场景渲染到纹理,然后通过全屏 RenderGraph 采样该纹理,利用全屏四边形和自定义片段着色器(如模糊、HDR、色调映射)进行处理,最终输出到屏幕。文章详细阐述了离屏 Framebuffer 的创建、全屏处理 Pass 的构建、以及通过 CommandGraph 串联多个 RenderGraph 以实现正确的执行顺序与资源依赖。同时,提供了常见问题的排查思路与解决方案,为读者掌握 VSG 后处理技术提供了清晰的实践路径。

本章定位:模糊、HDR、色调映射等效果都依赖「先把场景渲染到离屏纹理,再用全屏四边形采样处理」。这正是第 12 章多 RenderGraph 串联的用武之地。


20.1 本章目标

  • 理解后处理的「多通道」本质(离屏 → 全屏处理 → 呈现);
  • 用两个 RenderGraph 串联实现后处理;
  • 知道全屏四边形 + 采样离屏纹理的标准套路。

20.2 前置准备

  • 第 12 章(RenderGraph/CommandGraph)已读;
  • 第 10 章(描述符/纹理)已读。

20.3 多通道管线长什么样

场景图
  │  RenderGraph A(离屏 Framebuffer)
  ▼  渲染到 colorImage(离屏纹理)
全屏四边形(采样 colorImage)
  │  RenderGraph B(窗口)
  ▼  后处理 shader → 屏幕

关键点:RenderGraph A 渲染到离屏 Framebuffer(不是窗口),RenderGraph B 渲染到窗口,并用一个全屏四边形采样 A 的输出纹理,在片段着色器里做模糊/HDR。


20.4 离屏 RenderGraph

RenderGraph 既可以绑定 window(渲染到屏幕),也可以绑定 framebuffer(渲染到离屏图像)。创建离屏 Framebuffer(含 color + depth 图像)后:

auto offscreenFB = createOffscreenFramebuffer(window, extent);  // 见下方说明
auto scenePass = vsg::RenderGraph::create(offscreenFB);          // 渲染到离屏
scenePass->setClearValues(VkClearColorValue{{0.2f, 0.2f, 0.4f, 1.0f}});

auto view = vsg::View::create(camera, scenegraph);
scenePass->addChild(view);

下面是一个完整的 createOffscreenFramebuffer 工具函数实现,包含创建 color 和 depth 的 vsg::Imagevsg::ImageView 以及 vsg::Framebuffer

// 创建离屏 Framebuffer(含 color 和 depth 附件)
vsg::ref_ptr<vsg::Framebuffer> createOffscreenFramebuffer(vsg::ref_ptr<vsg::Window> window, VkExtent2D extent)
{
    auto device = window->getOrCreateDevice();
    auto physicalDevice = device->getPhysicalDevice();

    // 1. 创建 color 图像(作为渲染目标,同时可采样)
    auto colorImage = vsg::Image::create();
    colorImage->imageType = VK_IMAGE_TYPE_2D;
    colorImage->format = VK_FORMAT_R8G8B8A8_UNORM;          // 常用 8 位 RGBA
    colorImage->extent = {extent.width, extent.height, 1};
    colorImage->mipLevels = 1;
    colorImage->arrayLayers = 1;
    colorImage->samples = VK_SAMPLE_COUNT_1_BIT;
    colorImage->tiling = VK_IMAGE_TILING_OPTIMAL;
    colorImage->usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; // 关键:既能作附件,又能被采样
    colorImage->initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    colorImage->sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    auto colorImageMemory = vsg::DeviceMemory::create(device, colorImage->getMemoryRequirements(physicalDevice));
    colorImage->bind(colorImageMemory, 0);

    // 2. 创建 color 图像视图
    auto colorImageView = vsg::ImageView::create(colorImage, VK_IMAGE_VIEW_TYPE_2D);
    colorImageView->viewType = VK_IMAGE_VIEW_TYPE_2D;
    colorImageView->format = colorImage->format;
    colorImageView->subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

    // 3. 创建 depth 图像(深度测试用)
    auto depthImage = vsg::Image::create();
    depthImage->imageType = VK_IMAGE_TYPE_2D;
    depthImage->format = VK_FORMAT_D32_SFLOAT;              // 常用 32 位深度
    depthImage->extent = {extent.width, extent.height, 1};
    depthImage->mipLevels = 1;
    depthImage->arrayLayers = 1;
    depthImage->samples = VK_SAMPLE_COUNT_1_BIT;
    depthImage->tiling = VK_IMAGE_TILING_OPTIMAL;
    depthImage->usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
    depthImage->initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    depthImage->sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    auto depthImageMemory = vsg::DeviceMemory::create(device, depthImage->getMemoryRequirements(physicalDevice));
    depthImage->bind(depthImageMemory, 0);

    // 4. 创建 depth 图像视图
    auto depthImageView = vsg::ImageView::create(depthImage, VK_IMAGE_VIEW_TYPE_2D);
    depthImageView->viewType = VK_IMAGE_VIEW_TYPE_2D;
    depthImageView->format = depthImage->format;
    depthImageView->subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};

    // 5. 创建 RenderPass(与窗口兼容的简单单次渲染)
    auto renderPass = vsg::createRenderPass(device, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_D32_SFLOAT);

    // 6. 组装 Framebuffer
    vsg::ImageViews attachments = {colorImageView, depthImageView};
    auto framebuffer = vsg::Framebuffer::create(renderPass, attachments, extent.width, extent.height, 1);

    return framebuffer;
}

关键参数说明:

  • VK_FORMAT_R8G8B8A8_UNORM:color 附件格式,8 位 RGBA,适合后处理采样。
  • VK_FORMAT_D32_SFLOAT:depth 附件格式,32 位浮点深度。
  • VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT:color 图像必须同时具备「颜色附件」和「可采样」两种用途,否则后续全屏 Pass 无法读取。
  • VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT:depth 图像仅用作深度/模板附件,无需采样。
  • extent:与窗口或期望的离屏分辨率一致,确保附件尺寸匹配。
  • vsg::createRenderPass:创建与窗口兼容的 RenderPass,确保 color/depth 附件格式正确。

使用此函数后,offscreenFB 即可作为 vsg::RenderGraph::create(offscreenFB) 的参数,将场景渲染到离屏纹理。

离屏 Framebuffer 的创建涉及 vsg::Framebuffer + vsg::Image + vsg::ImageView(含 color/depth 附件)。其具体构造可封装成工具函数;核心是把渲染结果「留」在一张可读的 Image 上。


20.5 全屏处理 Pass

第二个 RenderGraph 渲染到窗口,内部只画一个全屏四边形,片段着色器采样离屏 colorImage

// 离屏结果作为纹理(DescriptorImage)
auto input = vsg::DescriptorImage::create(vsg::Sampler::create(), offscreenColorImage, 0);

// 全屏四边形几何 + 后处理管线(采样 input,做模糊/HDR)
auto postStateGroup = vsg::StateGroup::create();
postStateGroup->add(vsg::BindGraphicsPipeline::create(postPipeline));
postStateGroup->add(vsg::BindDescriptorSet::create(
    VK_PIPELINE_BIND_POINT_GRAPHICS, postPipelineLayout, 0, vsg::Descriptors{input}));
postStateGroup->addChild(fullscreenQuad);   // 两个三角形覆盖 NDC 全屏

auto presentPass = vsg::RenderGraph::create(window);   // 渲染到窗口
presentPass->addChild(vsg::View::create(camera2D, postStateGroup));

片段着色器(模糊示例):

#version 450
layout(binding = 0) uniform sampler2D scene;
layout(location = 0) in vec2 vUV;
layout(location = 0) out vec4 outColor;
void main() {
    vec3 sum = vec3(0.0);
    for (int i = -2; i <= 2; ++i)
        sum += texture(scene, vUV + vec2(float(i)*0.002, 0.0)).rgb;
    outColor = vec4(sum / 5.0, 1.0);   // 简单横向模糊
}

20.6 串联与依赖

把两个 RenderGraph 放进同一个 CommandGraph顺序即依赖:A 必须先在离屏画完,B 才能采样本:

auto commandGraph = vsg::CommandGraph::create(window);
commandGraph->addChild(scenePass);    // 先:离屏场景
commandGraph->addChild(presentPass);  // 后:全屏后处理 → 屏幕

viewer->assignRecordAndSubmitTaskAndPresentation({commandGraph});

VSG 在录制时会按 RenderGraph 的添加顺序生成命令缓冲,并保证屏障/依赖正确(如需显式屏障,可在 RenderGraph 间声明依赖)。多 Pass(如先降采样 → 模糊 → 合成)就是继续往 CommandGraph 里追加 RenderGraph


20.7 常见问题

现象原因解决
后处理全黑离屏纹理没接好 / 采样 binding 错确认 DescriptorImagebinding 与着色器一致,离屏 Image 可作为采样器读取
顺序错乱(采到空)RenderGraph 顺序/依赖未声明scenePasspresentPass;必要时显式依赖
性能差全屏 Pass 太重 / 多次拷贝用单个全屏 Pass 串多个效果,避免多余中间纹理
模糊有缝采样偏移/边界处理处理纹理边界(clampToEdge),或做多趟模糊

20.8 小结

  • 后处理 = 离屏 RenderGraph(渲染场景到纹理)+ 全屏 RenderGraph(采样处理 → 屏幕);
  • 全屏四边形 + 后处理 ShaderSet 采样离屏 Image
  • RenderGraph 同进一个 CommandGraph,顺序即依赖,可串多趟效果。

20.9 延伸阅读与下一章预告

  • 第 12 章《RenderGraph 与 CommandGraph》:多通道的理论基础;
  • 第 10 章《描述符与资源绑定》:离屏纹理作为 DescriptorImage
  • 第 26 章《调试技巧》:用 RenderDoc 看离屏 Pass 的中间结果。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是慎独

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值