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 离屏 RenderGraphRenderGraph既可以绑定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::Image、vsg::ImageView以及vsg::Framebuffer// 创建离屏 Framebuffer含 color 和 depth 附件 vsg::ref_ptrvsg::Framebuffer createOffscreenFramebuffer(vsg::ref_ptrvsg::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_UNORMcolor 附件格式8 位 RGBA适合后处理采样。VK_FORMAT_D32_SFLOATdepth 附件格式32 位浮点深度。VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BITcolor 图像必须同时具备「颜色附件」和「可采样」两种用途否则后续全屏 Pass 无法读取。VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BITdepth 图像仅用作深度/模板附件无需采样。extent与窗口或期望的离屏分辨率一致确保附件尺寸匹配。vsg::createRenderPass创建与窗口兼容的 RenderPass确保 color/depth 附件格式正确。使用此函数后offscreenFB即可作为vsg::RenderGraph::create(offscreenFB)的参数将场景渲染到离屏纹理。离屏Framebuffer的创建涉及vsg::Framebuffervsg::Imagevsg::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 错确认DescriptorImage的binding与着色器一致离屏Image可作为采样器读取顺序错乱采到空RenderGraph顺序/依赖未声明先scenePass后presentPass必要时显式依赖性能差全屏 Pass 太重 / 多次拷贝用单个全屏 Pass 串多个效果避免多余中间纹理模糊有缝采样偏移/边界处理处理纹理边界clampToEdge或做多趟模糊20.8 小结后处理 离屏RenderGraph渲染场景到纹理 全屏RenderGraph采样处理 → 屏幕全屏四边形 后处理ShaderSet采样离屏Image多RenderGraph同进一个CommandGraph顺序即依赖可串多趟效果。20.9 延伸阅读与下一章预告第 12 章《RenderGraph 与 CommandGraph》多通道的理论基础第 10 章《描述符与资源绑定》离屏纹理作为DescriptorImage第 26 章《调试技巧》用 RenderDoc 看离屏 Pass 的中间结果。