一、图形管线之外:GPU 就是一台并行计算机
之前的 DX11 和 GPU 架构笔记里,GPU 都在”渲染”的上下文里。但 GPU 的核心能力——几千个核心同时跑——不只能用来画三角形。
Compute Shader 是一类特殊的 Shader,完全脱离图形管线。它不处理顶点、不输出像素、不走光栅化。它只是:
输入:一块 GPU Buffer(数据)处理:每个线程处理 Buffer 的一部分输出:写回另一块 GPU Buffer(结果)适用于任何可以被拆成”每个元素独立处理”的计算。
二、线程模型:Group × Thread
1 三维线程组织
[numthreads(8, 8, 1)] // 每个线程组有 8×8×1 = 64 个线程void CSMain( uint3 groupID : SV_GroupID, // 我是第几个线程组 uint3 groupThreadID : SV_GroupThreadID, // 我是组内第几个线程 uint3 dispatchThreadID : SV_DispatchThreadID // 我是全局第几个线程) { // 处理 dispatchThreadID 对应的工作}Dispatch 时可以指定 X/Y/Z 三个维度的线程组数量:
// C++ 侧:启动 100×50×1 个线程组,每组 8×8×1 线程// 总线程数:100×8 × 50×8 = 800×400 = 320,000 个线程deviceContext->Dispatch(100, 50, 1);2 为什么是三维
因为很多 GPU 计算的输入本身就是 2D(纹理)或 3D(体素、Volume Texture)。线程的索引天然对应数据的坐标:
[numthreads(16, 16, 1)]void Downsample4x4(uint3 tid : SV_DispatchThreadID) { // tid.xy = 像素坐标,完美对应纹理坐标 float4 sum = 0; for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) sum += InputTexture[tid.xy * 4 + int2(i, j)]; OutputTexture[tid.xy] = sum / 16.0;}3 线程组的硬件实现
一个线程组(Thread Group)在 GPU 的单个 SM 上执行。一个 SM 可以同时运行多个线程组。
组内线程可以共享一块高速内存(Group Shared Memory):
groupshared float sharedData[256]; // 组内所有线程可见,极快
[numthreads(256, 1, 1)]void ParallelReduction(uint tid : SV_DispatchThreadID, uint groupTid : SV_GroupThreadID) { sharedData[groupTid] = Input[tid]; GroupMemoryBarrierWithGroupSync(); // 等待所有线程写完
// 在组内做并行归约(如求最大值/总和) for (uint s = 128; s > 0; s >>= 1) { if (groupTid < s) sharedData[groupTid] = max(sharedData[groupTid], sharedData[groupTid + s]); GroupMemoryBarrierWithGroupSync(); }}三、经典用例 1:GPU 粒子
1 CPU 粒子的瓶颈
传统粒子系统:CPU 更新每个粒子的位置、速度、生命值 → CPU 把新位置写回 Vertex Buffer → GPU 读取渲染。当粒子数超过几万时,CPU 的更新 + GPU 的上传变成了瓶颈。
2 GPU 粒子:Compute Shader 全包
1. Compute Shader 更新所有粒子的状态(位置、速度、生命) → 粒子数据从未离开 GPU 显存 → 更新速度:数百万粒子/帧(vs CPU 的几万/帧)
2. 不需要任何 Vertex Buffer 上传 → Compute Shader 直接把粒子数据写入一个 Structured Buffer
3. 渲染时 → Vertex Shader 从 Structured Buffer 读取粒子位置 → 把每个粒子扩展为一个 Billboard(广告牌四边形) → 无需 CPU 参与// 粒子更新 Compute Shaderstruct Particle { float3 Pos; float3 Vel; float Life; };
RWStructuredBuffer<Particle> Particles;
[numthreads(256, 1, 1)]void UpdateParticles(uint tid : SV_DispatchThreadID, float dt : register(b0)) { Particle p = Particles[tid]; p.Vel.y -= 9.8 * dt; // 重力 p.Pos += p.Vel * dt; // 运动 p.Life -= dt; Particles[tid] = p;}UE 的 Niagara 系统底层就是基于 Compute Shader 的。 粒子模拟在 GPU 上完成,零 CPU 开销。
四、经典用例 2:GPU Frustum Culling
之前在场景管理篇讲过视锥体剔除——CPU 遍历所有物体,检测包围盒是否在视锥体内。如果场景有 10000 个物体,CPU 要做 10000 次包围盒 vs 视锥体测试。
GPU 可以用 Compute Shader 并行做:
// 输入:所有物体的包围盒(Structured Buffer)// 输出:可见物体的索引列表(Indirect Draw Buffer)
struct Bounds { float3 Center; float3 Extent; };
[numthreads(256, 1, 1)]void GPUCulling(uint tid : SV_DispatchThreadID) { Bounds b = AllBounds[tid]; if (!IsInFrustum(b, ViewProjMatrix)) return; // 被剔除,不写入
uint index; InterlockedAdd(VisibleCount[0], 1, index); // 原子操作:可见计数+1 VisibleIndices[index] = tid; // 写入可见物体列表}结果是 GPU 在几个微秒内完成了 10000 个物体的剔除(vs CPU 可能几十微秒)。更关键的是——剔除结果直接存在 GPU Buffer 里,不需要回传 CPU。 下一轮的 DrawInstancedIndirect 直接用这个 Buffer 作为参数。
这就是 GPU-Driven Rendering 的基础。Nanite 的剔除、VSM 的 Page 分配,底层都是 Compute Shader 在跑。
五、经典用例 3:后处理加速
很多后处理效果天然适合 Compute Shader:
| 效果 | 为什么适合 Compute |
|---|---|
| Downsample / Blur | 每个像素独立计算——正好 256 线程一组 |
| Histogram(直方图) | 用 Group Shared Memory 在组内做并行归约 |
| FFT Ocean(FFT 海洋) | FFT 的蝴蝶操作可以跨线程并行 |
| Voxel GI | 3D 体素遍历天然用 3D Dispatch |
六、Compute Shader 的限制
- 不能输出到 Render Target 直接显示——Compute Shader 写的是 UAV(Unordered Access View),不是 Render Target。需要另外的 Pass(Copy 或全屏 Quad)把结果搬到屏幕。
- 读写冲突——多个线程可能同时写同一个位置。需要用原子操作(
InterlockedAdd等)或设计无冲突的写入模式。 - 不能调用图形管线的固定功能——没有光栅化、没有深度测试、没有混合。你想用 Compute 实现这些效果需要手动写。
- 调试困难——Compute Shader 的中间结果在 GPU 显存里,无法 printf。需要把中间结果回读到 CPU 或通过 RenderDoc 查看 Buffer 内容。
七、Compute Shader 与图形管线的混合
现代引擎最常用的模式是用 Compute Shader 准备间接绘制参数,然后图形管线消费这些参数:
1. Compute: Frustum Culling → 生成 VisibleIndices Buffer + IndirectArgs Buffer2. Graphics: DrawInstancedIndirect(IndirectArgs) → 只画可见物体3. Compute: 后处理 → Bloom Blur, Tone Mapping4. Graphics: 全屏 Quad → 显示最终结果UE 的 RenderGraph(RDG)用 ERDGPassFlags::Compute 和 ERDGPassFlags::Raster 区分 Compute Pass 和 Graphics Pass,在同一个 RDG 图中编排两者的顺序。
八、总结
| 概念 | 一句话 |
|---|---|
| Compute Shader | 脱离图形管线的 GPU 通用计算——输入 Buffer,输出 Buffer |
| 线程组 | 3D 组织(Group × Thread),组内共享内存 |
| GPU 粒子 | 粒子数据永不离开显存,数百万粒子零 CPU 开销 |
| GPU Culling | 并行剔除 10000 物体 → Indirect Draw → 零 CPU 回读 |
| 原子操作 | 多个线程写同一地址的协调机制——慢但要会用 |
| 混合管线 | Compute 准备参数 → Graphics 消费 → Compute 后处理 |
Compute Shader 是连接”GPU 画三角形”和”GPU 做通用计算”的桥梁。理解了它,你就不再只是 GPU 的使用者——你是 GPU 的程序员。