1602 字
8 分钟
Compute Shader——GPU 不只是用来画画的

一、图形管线之外: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 Shader
struct 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 GI3D 体素遍历天然用 3D Dispatch

六、Compute Shader 的限制#

  1. 不能输出到 Render Target 直接显示——Compute Shader 写的是 UAV(Unordered Access View),不是 Render Target。需要另外的 Pass(Copy 或全屏 Quad)把结果搬到屏幕。
  2. 读写冲突——多个线程可能同时写同一个位置。需要用原子操作(InterlockedAdd 等)或设计无冲突的写入模式。
  3. 不能调用图形管线的固定功能——没有光栅化、没有深度测试、没有混合。你想用 Compute 实现这些效果需要手动写。
  4. 调试困难——Compute Shader 的中间结果在 GPU 显存里,无法 printf。需要把中间结果回读到 CPU 或通过 RenderDoc 查看 Buffer 内容。

七、Compute Shader 与图形管线的混合#

现代引擎最常用的模式是用 Compute Shader 准备间接绘制参数,然后图形管线消费这些参数:

1. Compute: Frustum Culling → 生成 VisibleIndices Buffer + IndirectArgs Buffer
2. Graphics: DrawInstancedIndirect(IndirectArgs) → 只画可见物体
3. Compute: 后处理 → Bloom Blur, Tone Mapping
4. Graphics: 全屏 Quad → 显示最终结果

UE 的 RenderGraph(RDG)用 ERDGPassFlags::ComputeERDGPassFlags::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 的程序员。

Compute Shader——GPU 不只是用来画画的
https://www.m4doka.xyz/posts/cg/cg-8-compute-shader/
作者
m4doka
发布于
2026-03-08
许可协议
CC BY-NC-SA 4.0