欢迎光临
我们一直在努力

WebGPU Compute Shader 实现并行粒子系统

WebGPU Compute Shader 实现并行粒子系统

封面信息图

在数据可视化大屏、气象风场模拟以及复杂流体渲染中,高密度粒子系统(100 万至 500 万个粒子)是检验浏览器图形性能的试金石。在经典的 WebGL 体系下,即使利用 Transform Feedback 或浮点纹理(GPGPU)进行物理状态更新,也往往受到架构限制:要么必须在 CPU 端维护物理状态并通过带宽瓶颈极大的 PCI-E 通道向 GPU 频繁上传缓冲区,要么受限于有限的顶点着色器灵活性与驱动层开销。

WebGPU 带来的革命性特性之一,便是真正意义上的**计算着色器(Compute Shader)**与统一的内存模型。通过 WGSL(WebGPU Shading Language),我们可以让数百万个粒子在 GPU 显存内部完成位置、速度、引力场与生命周期的并行演算,并直接将计算缓冲区绑定为渲染管线的顶点输入,实现“计算即渲染”的零拷贝数据流闭环。

WebGPU 显存内零拷贝粒子管线:
[Compute Pipeline (WGSL Compute Shader)]
| (并行计算 100万粒子 位置/速度/生命周期)
v
[GPU Storage Buffer (显存内部粒子数据)] <— 零 CPU 拷贝开销
|
v
[Render Pipeline (WGSL Vertex & Fragment Shader)]
| (直接作为顶点输入 / 实例化数据进行绘制)
v
[Frame Buffer (Canvas 60fps 画面输出)]

WGSL 粒子物理计算着色器

在 WebGPU 中,粒子状态通常以结构体形式存储在 Storage Buffer 中。计算着色器通过分派工作组(Workgroup)在多核流处理器上并行处理每个粒子。

// particle-compute.wgsl
struct Particle {
position : vec3<f32>,
life : f32,
velocity : vec3<f32>,
maxLife : f32,
};

struct SimParams {
deltaTime : f32,
gravity : vec3<f32>,
attractorPos : vec3<f32>,
attractorStrength : f32,
particleCount : u32,
};

@group(0) @binding(0) var<uniform> params : SimParams;
@group(0) @binding(1) var<storage, read_write> particles : array<Particle>;

// 伪随机生成器 (用于生命周期耗尽后的重置)
fn hash(value: u32) -> f32 {
var x = value * 1103515245u + 12345u;
return f32(x & 0x7fffffffu) / f32(0x7fffffffu);
}

@compute @workgroup_size(64, 1, 1)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
let index = global_id.x;
if (index >= params.particleCount) {
return;
}

var p = particles[index];
p.life -= params.deltaTime;

if (p.life <= 0.0) {
// 粒子消亡,重新在中心源头生成
let seed = index + u32(params.deltaTime * 1000.0);
let rx = (hash(seed) – 0.5) * 2.0;
let ry = (hash(seed + 1u) – 0.5) * 2.0;
let rz = (hash(seed + 2u) – 0.5) * 2.0;

p.position = vec3<f32>(0.0, 0.0, 0.0);
p.velocity = vec3<f32>(rx, ry + 2.0, rz) * 1.5;
p.life = 2.0 + hash(seed + 3u) * 3.0;
p.maxLife = p.life;
} else {
// 物理力场演算:重力 + 引力中心
let toAttractor = params.attractorPos – p.position;
let dist = max(length(toAttractor), 0.1);
let forceDir = normalize(toAttractor);
let attractForce = forceDir * (params.attractorStrength / (dist * dist));

let acceleration = params.gravity + attractForce;
p.velocity += acceleration * params.deltaTime;
p.position += p.velocity * params.deltaTime;
}

particles[index] = p;
}

渲染管线着色器与点云映射

渲染阶段直接读取同一块 Storage Buffer,利用粒子生命周期的衰减比例计算透明度与色彩渐变。

// particle-render.wgsl
struct VertexOutput {
@builtin(position) position : vec4<f32>,
@location(0) color : vec4<f32>,
};

struct RenderUniforms {
viewProjectionMatrix : mat4x4<f32>,
};

@group(0) @binding(0) var<uniform> uniforms : RenderUniforms;
@group(0) @binding(1) var<storage, read> particles : array<Particle>;

@vertex
fn vs_main(@builtin(vertex_index) v_idx : u32) -> VertexOutput {
let p = particles[v_idx];
let lifeRatio = clamp(p.life / p.maxLife, 0.0, 1.0);

var out : VertexOutput;
out.position = uniforms.viewProjectionMatrix * vec4<f32>(p.position, 1.0);

// 根据生命周期由青色过渡为橙黄并渐隐
let baseColor = mix(vec3<f32>(1.0, 0.3, 0.1), vec3<f32>(0.1, 0.8, 0.9), lifeRatio);
out.color = vec4<f32>(baseColor, lifeRatio);
return out;
}

@fragment
fn fs_main(@location(0) color : vec4<f32>) -> @location(0) vec4<f32> {
return color;
}

TypeScript 驱动层与指令编码(Command Encoding)

在 JavaScript/TypeScript 胶水层中,最关键的是构建正确的 GPUBufferUsage,使得粒子缓冲区既能作为计算着色器的 STORAGE 可读写缓冲,又能作为渲染着色器的只读 STORAGE 或顶点缓冲。

// particle-engine.ts
export class WebGPUParticleEngine {
private device!: GPUDevice;
private context!: GPUCanvasContext;
private computePipeline!: GPUComputePipeline;
private renderPipeline!: GPURenderPipeline;
private particleBuffer!: GPUBuffer;
private computeBindGroup!: GPUBindGroup;
private renderBindGroup!: GPUBindGroup;
private readonly particleCount = 1_000_000;

public async init(canvas: HTMLCanvasElement): Promise<void> {
if (!navigator.gpu) throw new Error('WebGPU not supported on this device');

const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
if (!adapter) throw new Error('No appropriate GPUAdapter found');
this.device = await adapter.requestDevice();

this.context = canvas.getContext('webgpu') as GPUCanvasContext;
const format = navigator.gpu.getPreferredCanvasFormat();
this.context.configure({ device: this.device, format, alphaMode: 'premultiplied' });

await this.setupPipelines(format);
}

private async setupPipelines(canvasFormat: GPUTextureFormat): Promise<void> {
// 粒子结构体对齐:position (12B) + life (4B) + velocity (12B) + maxLife (4B) = 32 Bytes
const particleByteSize = 32;
const totalBufferSize = this.particleCount * particleByteSize;

// 初始化粒子数据
const initialData = new Float32Array(this.particleCount * 8);
for (let i = 0; i < this.particleCount; i++) {
initialData[i * 8 + 0] = 0; // x
initialData[i * 8 + 1] = 0; // y
initialData[i * 8 + 2] = 0; // z
initialData[i * 8 + 3] = 0; // life (立即触发重置)
initialData[i * 8 + 4] = 0; // vx
initialData[i * 8 + 5] = 0; // vy
initialData[i * 8 + 6] = 0; // vz
initialData[i * 8 + 7] = 3.0; // maxLife
}

this.particleBuffer = this.device.createBuffer({
size: totalBufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Float32Array(this.particleBuffer.getMappedRange()).set(initialData);
this.particleBuffer.unmap();

// 编译 Compute Pipeline 与 Render Pipeline (此处省略 WGSL 模块创建细节)
// …
}

public frame(deltaTime: number): void {
const commandEncoder = this.device.createCommandEncoder();

// 1. 执行计算通道(Physics Compute Pass)
const computePass = commandEncoder.beginComputePass();
computePass.setPipeline(this.computePipeline);
computePass.setBindGroup(0, this.computeBindGroup);
// 工作组分派:向上取整计算需要的 Workgroups
const workgroupCount = Math.ceil(this.particleCount / 64);
computePass.dispatchWorkgroups(workgroupCount);
computePass.end();

// 2. 执行渲染通道(Render Pass)
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [{
view: this.context.getCurrentTexture().createView(),
clearValue: { r: 0.02, g: 0.03, b: 0.06, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
}],
});
renderPass.setPipeline(this.renderPipeline);
renderPass.setBindGroup(0, this.renderBindGroup);
renderPass.draw(this.particleCount, 1, 0, 0);
renderPass.end();

// 提交 GPU 队列执行
this.device.queue.submit([commandEncoder.finish()]);
}
}

利用 WebGPU Compute Shader 彻底摒弃了传统图形流水线中 CPU 与 GPU 之间冗长频繁的显存回传与拷贝,将粒子演化的每一个微积分运算完全释放给并行硬件。即使在普通笔记本集成显卡上,百万级粒子的涡旋与聚合依然能够稳定维持在 60 帧满帧运行。

赞(0)
未经允许不得转载:171主机测评 » WebGPU Compute Shader 实现并行粒子系统
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址