跳到主要内容
3D Playground
GPU 与 WebGPU 计算第 5 / 11 节

GPU 粒子:计算 + 实例化

进阶WebGPU
此示例需要 WebGPU(Chrome / Edge 113+、Safari 26+)。

数据从不离开 GPU

每个粒子是 16 字节的结构体:位置和速度各一个 vec2f。所有粒子放在一个存储缓冲里。每一帧:

  1. 计算 pass:每个线程负责一个粒子,读出、积分、写回;
  2. 渲染 pass:顶点着色器按 instance_index 从同一个缓冲读取位置,画一个小方块。

CPU 只负责每帧写几十字节的 uniform(鼠标位置、时间步长等),十几万粒子的状态从不往返。

一个线程一个粒子

wgsl
@compute @workgroup_size(256)
fn simulate(@builtin(workgroup_id) wg : vec3u,
            @builtin(local_invocation_index) lid : u32) {
  let i = wg.x * 256u + lid;      // 等价于 global_invocation_id.x
  if (i >= S.count) { return; }
  var p = particles[i];
  p.vel = (p.vel + acc * S.dt) * pow(S.damping, S.dt * 60.0);
  p.pos += p.vel * S.dt;
  particles[i] = p;
}

wg.x * 256u + lid 就是全局线程编号,和 @builtin(global_invocation_id) 的 .x 一模一样。因为每个线程只读写自己的元素,原地更新是安全的;如果要读邻居(比如粒子间碰撞),就需要两个缓冲轮流读写。

同一个缓冲,两种视图

计算管线把缓冲声明为 read_write,渲染管线声明为 read。同一块内存创建两个绑定组即可,WebGPU 会在两个 pass 之间自动插入必要的同步。

实例化绘制

pass.draw(6, count):每个实例 6 个顶点(两个三角形),共 count 个实例。顶点着色器用 vertex_index 选方块的角,用 instance_index 选粒子。一次绘制调用就画出所有粒子。

加法混合

片元输出很暗的颜色,混合模式设为 src + dst。粒子密集处颜色叠加发光,而且与绘制顺序无关,不需要排序。颜色由速度决定:慢的偏靛蓝,快的偏琥珀到白色。

节奏控制

时间步长 dt 限制在 1/30 秒以内,防止卡顿后粒子“飞出去”。在途帧数用 onSubmittedWorkDone 限制在 2 帧,避免 GPU 跟不上时命令无限堆积、延迟越来越大。

核心 API

var<storage, read_write>@builtin(instance_index)pass.dispatchWorkgroupspass.draw(6, count)device.createComputePipelineblend: one + onedevice.queue.writeBufferdevice.queue.onSubmittedWorkDone

动手试试

  1. 1把 count 调到 1M,看帧率能否保持;再把 point size 调大,观察填充率的影响。
  2. 2把 swirl 设为负数,粒子会反向旋转;按住鼠标试试更强的吸引。
  3. 3在计算着色器里改用 @builtin(global_invocation_id) gid,用 gid.x 作为粒子下标。
运行中