第 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::Image、vsg::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 错 | 确认 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 的中间结果。
&spm=1001.2101.3001.5002&articleId=163894475&d=1&t=3&u=a6690c4956ab4f92a2498a21383744ba)
1047

被折叠的 条评论
为什么被折叠?



