Vulkan 高级渲染技术:延迟渲染、光追与计算着色器

深入解析 Vulkan 高级渲染管线:从 Deferred Rendering 到实时光追,涵盖 Shadow Map、SSAO、Compute Shader、Mesh Shader 与后处理特效,构建现代图形引擎的核心技术栈。

现代图形引擎的渲染管线已经从简单的 Forward Rendering 进化为高度并行、模块化的复杂系统。Vulkan 作为新一代底层图形 API,通过显式的资源管理、多队列提交和丰富的扩展机制,为高级渲染技术的落地提供了强大支撑。本文将系统性地梳理延迟渲染、阴影技术、环境光遮蔽、计算着色器、实时光追、Mesh Shader 以及后处理特效的实现原理与 Vulkan 实践要点,帮助开发者建立完整的高级渲染知识框架。


延迟渲染(Deferred Rendering)

一句话总结:延迟渲染将几何信息与光照计算解耦,通过 G-Buffer 将复杂度从 O( lights × objects ) 降为 O( pixels + lights )。

G-Buffer 设计

G-Buffer 是延迟渲染的核心数据结构,它会在几何 Pass 中将每个像素的关键表面属性写入多张 Render Target。一个典型的 G-Buffer 配置包含:

附件格式存储内容
Position/R深度RGBA16_SFLOAT世界空间坐标或线性深度
NormalRGBA16_SFLOAT / RG16_UNORM (Octahedral)世界法线 + 可选材质标志
AlbedoRGBA8_UNORM漫反射颜色
MaterialRGBA8_UNORMMetallic / Roughness / AO / Emissive 通道
Depth-StencilD32_SFLOAT深度值,用于重建视图空间位置

使用 VK_FORMAT_R16G16B16A16_SFLOAT 可保证 HDR 值范围,避免低精度下的 banding。Position 附件在带宽受限时可省略,改为在光照 Pass 中通过逆投影矩阵从深度重构。

// 伪代码:G-Buffer Pass 创建逻辑
void createGBufferRenderPass() {
    VkAttachmentDescription attachments[5] = {};
    // Position, Normal, Albedo, Material, Depth
    attachments[0].format = VK_FORMAT_R16G16B16A16_SFLOAT;
    attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
    attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
    attachments[0].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    // ... 类似配置其他附件

    VkSubpassDescription subpass = {};
    subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
    subpass.colorAttachmentCount = 4;
    // subpass.pColorAttachments = ...
    // subpass.pDepthStencilAttachment = ...

    vkCreateRenderPass(device, &createInfo, nullptr, &gBufferRenderPass);
}

光照 Pass

G-Buffer 填充完成后,引擎进入光照阶段。经典做法是绑定 G-Buffer 纹理作为输入,使用全屏四边形或 compute shader 对每个像素执行 BRDF。若光源较多,需要引入 Tiled / Clustered Deferred 来将屏幕分块,每块只处理可见光源。

// 伪代码:Deferred Lighting Pass
cmd.bindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, deferredPipeline);
cmd.bindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS,
                       pipelineLayout, 0, 1, &lightingDescriptorSet, 0, nullptr);
// G-Buffer 纹理绑定在 set 0 的 binding 0-3
// 光源 SSBO 绑定在 set 0 的 binding 4
cmd.draw(3, 1, 0, 0); // 全屏三角形

优缺点对比

维度Forward RenderingDeferred Rendering
复杂度O(lights × objects)O(pixels + lights)
MSAA原生支持需额外处理(通常用 TAA 替代)
半透明天然支持必须单独 Forward Pass
材质多样性灵活G-Buffer 布局固定,需压缩
带宽压力高(多 texture write + read)
光源数量受限大量动态光源表现优异

级联阴影与 PCF 过滤(Shadow Map)

一句话总结:级联阴影将视锥分层投射以保证远近阴影精度,PCF 过滤则以采样周围 texel 来模拟软阴影边缘。

Cascaded Shadow Maps (CSM)

单一阴影贴图在大型场景中要么近处锯齿严重、要么远处分辨率浪费。CSM 将视锥沿 Z 轴划分为 N 个级联(通常 3-4 级),每级用独立的 Shadow Map 投射。

// 伪代码:计算级联分割距离
std::vector<float> splitDistances(int cascadeCount, float near, float far, float lambda) {
    std::vector<float> dists(cascadeCount + 1);
    dists[0] = near;
    for (int i = 1; i <= cascadeCount; i++) {
        float t = (float)i / cascadeCount;
        float logDist = near * pow(far / near, t);
        float linDist = near + (far - near) * t;
        dists[i] = lambda * logDist + (1.0f - lambda) * linDist;
    }
    return dists;
}

每个级联需要:VkImageView、对应的 VkFramebuffer、以及稳定投射矩阵(通过 texel-snapping 消除阴影抖动)。在 Lighting Pass 中通过像素深度判断其属于哪一级联,采样对应 Shadow Map。

Percentage-Closer Filtering (PCF)

// 伪代码:HLSL/GLSL 风格的 3x3 PCF
float sampleShadowPCF(sampler2D shadowMap, vec2 uv, float refDepth) {
    float shadow = 0.0;
    vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));
    for (int x = -1; x <= 1; x++) {
        for (int y = -1; y <= 1; y++) {
            vec2 offset = vec2(x, y) * texelSize;
            float sampled = texture(shadowMap, uv + offset).r;
            shadow += (refDepth > sampled) ? 1.0 : 0.0;
        }
    }
    return shadow / 9.0;
}

更高级的做法包括 PCSS(Percentage-Closer Soft Shadows)和 VSM(Variance Shadow Maps),前者根据遮挡物距离动态调整 penumbra 宽度,后者将问题转化为可过滤的分布估计。


屏幕空间环境光遮蔽(SSAO)

一句话总结:SSAO 在屏幕空间通过采样像素周围深度差异来估算环境光被局部几何遮挡的程度。

SSAO 由 Crytek 提出,核心假设是:一个点周围半球内的随机采样点若深度大于场景深度,则说明该方向被遮挡。

// 伪代码:SSAO 核心计算流程
// Prepass: 从深度图重建视图空间位置与法线
// Main: 对每个像素
vec3 viewPos = reconstructViewPos(uv, depth);
vec3 viewNormal = sampleNormalGBuffer(uv);

float occlusion = 0.0;
int sampleCount = 64;
for (int i = 0; i < sampleCount; i++) {
    vec3 sampleDir = tangentSpaceSamples[i]; // 预计算半球采样
    vec3 samplePos = viewPos + sampleDir * sampleRadius;
    vec4 screenPos = projMatrix * vec4(samplePos, 1.0);
    vec2 sampleUV = (screenPos.xy / screenPos.w) * 0.5 + 0.5;
    float sampleDepth = linearize(texture(depthTex, sampleUV).r);
    float rangeCheck = smoothstep(0.0, 1.0, sampleRadius / abs(viewPos.z - sampleDepth));
    occlusion += (sampleDepth < samplePos.z ? 1.0 : 0.0) * rangeCheck;
}
occlusion = 1.0 - (occlusion / sampleCount);

Vulkan 实现通常将 SSAO 拆分为两个 Compute Pass:

  1. AO Generation Pass:16x16 thread group,输出 raw AO 图
  2. Blur Pass:4x4 bilateral blur 消除噪声但保留边界

性能优化要点:使用 VK_FORMAT_R8_UNORM 存储 AO,配合半分辨率计算再 up-sample;存储随机向量用旋转纹理避免逐采样 sin/cos


计算着色器通用计算(Compute Shader)

一句话总结:Compute Shader 绕过图形固定的顶点/片元阶段,以线程组为单位执行任意并行计算,是图像处理与模拟任务的利器。

图像模糊

高斯模糊可分拆为水平与垂直两个一维 Pass,大幅降低采样次数。

// 伪代码:Compute Gaussian Blur (Horizontal)
layout(local_size_x = 256, local_size_y = 1) in;
layout(set = 0, binding = 0, rgba8) uniform readonly image2D srcImage;
layout(set = 0, binding = 1, rgba8) uniform writeonly image2D dstImage;

shared vec4 sharedData[256 + kernelRadius * 2]; // 组内共享内存

void main() {
    ivec2 gid = ivec2(gl_GlobalInvocationID.xy);
    // 加载 texel 到 shared memory
    // 同步 barrier()
    // 加权求和并写入 dstImage
    imageStore(dstImage, gid, blurred);
}

使用 shared 内存将重复读取的 texel 缓存在 LDS(Local Data Share)中,可减少 texture fetch 带宽。

粒子系统

GPU-driven 粒子系统利用 Compute Shader 更新位置、速度、生命周期,避免 CPU-GPU 数据回传。

// 伪代码:粒子更新 Compute Shader
struct Particle { vec4 pos; vec4 vel; float life; float maxLife; };
layout(std430, set = 0, binding = 0) buffer Particles { Particle p[]; } particles;

void main() {
    uint idx = gl_GlobalInvocationID.x;
    Particle part = particles.p[idx];
    part.life -= deltaTime;
    if (part.life > 0.0) {
        part.pos += part.vel * deltaTime;
        part.vel += vec4(gravity, 0.0) * deltaTime;
    } else {
        // 重生或标记死亡
        respawn(idx);
    }
    particles.p[idx] = part;
}

前缀和(Parallel Prefix Sum)

前缀和(Blelloch scan)是 Compute Shader 的经典算法,用于粒子剔除、遮挡查询等需要聚合数据的场景。Vulkan 代码需使用两个 Pass:Up-sweep 和 Down-sweep,配合 gl_WorkgroupSizebarrier() 保证同步。


实时光追管线(VK_KHR_ray_tracing_pipeline)

一句话总结:Vulkan 光追扩展将射线遍历与命中判定硬件化,通过 Acceleration Structure 与可编程 Shader 组合实现物理级正确的反射、阴影与 GI。

Acceleration Structure

光追需要构建两级加速结构(BLAS + TLAS):

  • Bottom-Level Acceleration Structure (BLAS):按几何物体(mesh)构建,包含顶点/索引/变换信息
  • Top-Level Acceleration Structure (TLAS):由多个 Instance 组成,每个 Instance 引用一个 BLAS 并附带变换矩阵
// 伪代码:AS 构建流程
VkAccelerationStructureGeometryKHR geometry = {};
geometry.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR;
geometry.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR;
geometry.geometry.triangles.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR;
geometry.geometry.triangles.vertexData.deviceAddress = vertexBufferAddr;
geometry.geometry.triangles.indexData.deviceAddress = indexBufferAddr;

// 获取构建尺寸
vkGetAccelerationStructureBuildSizesKHR(device, VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR,
                                        &buildInfo, &primitiveCount, &sizes);

// 创建 scratch buffer 与 AS buffer,再调用 vkCmdBuildAccelerationStructuresKHR

Shader 组合

光追管线由多个 Shader Stage 协同完成:

Shader 类型职责
Ray Generation发射射线(对应屏幕像素),调用 traceRayEXT()
Closest Hit射线命中最近三角,计算 BRDF 与颜色
Miss射线未命中任何几何,返回天空盒 / 环境光
Any Hit命中时立即触发,常用于透明剔除或开关阴影
Intersection自定义图元求交(如 AABB、球体、SDF)
// Ray Gen Shader 伪代码 (GLSL)
layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS;
layout(binding = 1, set = 0, rgba8) uniform writeonly image2D outputImage;

void main() {
    vec2 uv = vec2(gl_LaunchIDEXT.xy) / vec2(gl_LaunchSizeEXT.xy);
    vec3 origin = cameraPos;
    vec3 direction = normalize(mix(ray00, ray11, uv));

    traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xff, 0, 1, 0,
                origin, tMin, direction, tMax, 0);

    imageStore(outputImage, ivec2(gl_LaunchIDEXT.xy), payload.color);
}

光追管线创建需要使用 VkRayTracingPipelineCreateInfoKHR,并通过 vkGetRayTracingShaderGroupHandlesKHR 获取 Shader Group Handle,最终写入 Shader Binding Table (SBT)。


Mesh Shader 管线(VK_EXT_mesh_shader)

一句话总结:Mesh Shader 用计算式编程模型替代传统的 IA-VS-HS-DS-GS 固定链路,实现几何层面的 GPU-Driven 渲染与微网格剔除。

传统顶点管线 vs Mesh Shader

特性传统顶点管线Mesh Shader (VK_EXT_mesh_shader)
输入装配固定函数 vkCmdBindVertexBuffersTask Shader 派发 Mesh Shader 工作组
曲面细分显式 HS + DS 阶段无固定阶段,由 Mesh Shader 程序化生成
剔除粒度Draw Call 级别(CPU)网格簇(Meshlet)级别(GPU)
LOD 选择CPU 侧计算Task Shader 动态决定 LOD
开发心智图形化流水线思维计算着色器式自由编程
API 版本Vulkan 1.0+Vulkan 1.2+ with extension
// 伪代码:Task + Mesh Shader 派发
cmd.bindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, meshPipeline);
cmd.bindDescriptorSets(...);

// 每个 Task Workgroup 处理一组 Meshlet
cmd.drawMeshTasksEXT(meshletCount, 1, 1); // groupCountX/Y/Z

Task Shader 职责类似于间接绘制的派发器:读取剔除结果,决定输出多少 Mesh Shader 工作组。Mesh Shader 则直接以 gl_MeshVerticesEXT[]gl_PrimitiveTriangleIndicesEXT[] 形式输出三角形,跳过了顶点属性插值、图元装配等固定阶段。

// Mesh Shader 伪代码
#define MAX_VERTICES 64
#define MAX_PRIMITIVES 126

layout(local_size_x = 1) in;
layout(triangles, max_vertices = MAX_VERTICES, max_primitives = MAX_PRIMITIVES) out;

void main() {
    uint meshletID = gl_WorkGroupID.x;
    Meshlet m = meshlets[meshletID];

    // 将 meshlet 顶点输出到 gl_MeshVerticesEXT
    for (uint i = 0; i < m.vertexCount; i++) {
        uint vi = vertexIndices[m.vertexOffset + i];
        gl_MeshVerticesEXT[i].gl_Position = mvp * vertices[vi].pos;
    }

    // 输出三角形索引
    for (uint i = 0; i < m.primitiveCount; i++) {
        uvec3 tri = primitiveIndices[m.primitiveOffset + i];
        gl_PrimitiveTriangleIndicesEXT[i] = tri;
    }

    SetMeshOutputsEXT(m.vertexCount, m.primitiveCount);
}

后处理效果

一句话总结:后处理 Pass 通过全屏图像操作模拟镜头光学特性、修正动态范围并消除锯齿,是渲染管线中画龙点睛的一环。

Bloom

Bloom 提取图像高亮区域,经 down-sample、blur、up-sample 后叠加回原图。使用 Compute Shader 配合 ping-pong image 实现通常需要 4-6 级金字塔。

// 伪代码:Bloom 提取 Pass (compute)
layout(set = 0, binding = 0) uniform texture2D sourceColor;
layout(set = 0, binding = 1, rgba16f) uniform writeonly image2D bloomExtract;

void main() {
    ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
    vec3 color = texelFetch(sourceColor, coord, 0).rgb;
    float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));
    vec3 extracted = (luminance > threshold) ? color : vec3(0.0);
    imageStore(bloomExtract, coord, vec4(extracted, 1.0));
}

Tone Mapping

HDR 场景需将高动态范围压缩到 LDR 显示设备。Reinhard 操作简洁,ACES Filmic 曲线则在色彩保真上更优。

// ACES Filmic Tone Mapping 近似 (伪代码)
vec3 ACESFilm(vec3 x) {
    float a = 2.51f, b = 0.03f;
    float c = 2.43f, d = 0.59f;
    float e = 0.14f;
    return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}

时域抗锯齿(TAA)简述

TAA 将每帧采样位置在子像素级偏移(Halton/Jitter),将当前帧与历史帧通过速度向量(Motion Vector)在屏幕空间重投影并混合。关键挑战在于鬼影抑制(clamping/clipping 历史色值)与亚像素抖动模式选择。

// 伪代码:TAA Resolve (compute)
vec2 motion = sampleMotionVector(uv);
vec2 historyUV = uv - motion;
vec3 historyColor = sampleHistory(historyUV);
vec3 currentColor = sampleCurrent(uv);

// 用当前帧 3x3 范围的 AABB 裁剪历史颜色
vec3 minColor, maxColor;
computeNeighborhoodAABB(uv, minColor, maxColor);
historyColor = clamp(historyColor, minColor, maxColor);

vec3 output = mix(currentColor, historyColor, 0.9); // blend factor

技术对比总览

技术核心收益主要开销适用场景
Deferred Rendering大量动态光源性能优异高显存带宽、MSAA 困难FPS、开放世界室外
CSM大范围高质量阴影多 Pass 渲染、阴影接缝处理大型场景阳光阴影
SSAO廉价接触阴影增强噪声需 blur、仅屏幕空间通用场景氛围提升
Compute ShaderGPU 并行任意计算需手工同步 barrier模糊、粒子、剔除、后处理
Ray Tracing物理正确反射/阴影/GIBVH 构建、高射线成本影视级追求、镜面反射
Mesh ShaderGPU-Driven 剔除、LOD硬件/驱动支持仍在普及高密度三角场景、Nanite 风格
TAA + Bloom + Tone Mapping视觉保真度飞跃TAA 鬼影、延迟所有现代 PBR 渲染管线

常见问题(FAQ)

Q1:延迟渲染中 G-Buffer 占用带宽过高如何解决?

A1:可采取以下策略:压缩法线到 2 通道(Octahedral 编码)、使用角度法线或 RG16_UNORM;Material 通道合并到 Albedo 的 alpha;Position 直接省略,在光照阶段通过逆投影矩阵从深度图重建视图空间坐标。此外,Tiled/Clustered Deferred 还能进一步减少无效像素的光照计算。

Q2:CSM 级联之间阴影接缝怎么消除?

A2:接缝通常因相邻级联的分辨率/深度偏移(bias)不一致导致。常用做法是:在 shader 中采样相邻两级联并基于插值因子 cascadeBlend 平滑过渡;或者统一各级的深度 bias 并以 texel snapping 稳定阴影图。

Q3:SSAO 的黑白噪点严重怎么办?

A3:首先确保随机采样向量在切空间均匀分布;其次增加采样数(64~128)并引入 import sampling(若用 Horizon-Based Ambient Occlusion 替代 SSAO 效果更佳);最后务必做 bilateral blur,利用深度/法线差异作为权重以保留几何边缘。

Q4:实时光追性能不够,在哪里可以妥协?

A4:可降低射线反弹次数(1次 reflection + 1次 AO/shadow);对反射使用混合渲染(SSR 为主,光追补充边缘漏检);降低光追输出分辨率并使用重建上采样(如 FSR2/XeSS 的 Motion Vector 管线);对静态物体缓存 TLAS,仅动态物体每帧更新 BLAS。

Q5:Mesh Shader 是否能完全取代传统顶点管线?

A5:目前 Mesh Shader 仍属于扩展(VK_EXT_mesh_shader,部分平台为 VK_NV_mesh_shader),且需要 GPU 硬件支持(如 NVIDIA Ada、AMD RDNA3、Intel Arc+)。对于已发布的跨平台产品,建议将 Mesh Shader 作为可选路径,对传统管线做 fallback 兼容。


总结

Vulkan 高级渲染并非单一技术的堆砌,而是围绕 Deferred/Forward+ 框架,按需组合 Shadow Map、SSAO、Compute Shader、Ray Tracing 与后处理模块的系统工程。Mesh Shader 代表了未来 GPU-Driven 渲染的演进方向,而计算着色器已成为连接图形与通用计算的桥梁。开发者在设计管线时应始终关注带宽瓶颈(Bandwidth Bound)与同步开销(Barrier/Overlap),利用 Vulkan 显式控制的优势将 GPU 资源压榨到极致。掌握这七大核心技术,便具备了构建现代商业级渲染引擎的理论基础与实践能力。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「graphics」更多文章

  1. OpenGL VAO/VBO/EBO 与顶点管理:高效顶点数据传输
  2. OpenGL Core Profile 与管线:现代OpenGL编程指南
  3. Vulkan 初始化与渲染管线:从零画一个三角形