每天一个高级前端知识 - Day 7
今日主题:WebGPU 实战 - 浏览器中的海量粒子系统,从十万到百万粒子
核心概念:GPU Compute 彻底释放显卡算力
WebGPU是下一代图形API,比WebGL更接近现代图形硬件,特别适合海量并行计算。
性能对比(100万粒子):
WebGL 2.0: 15-25 fps (受限于CPU-GPU通信)
WebGPU: 60+ fps (GPU Compute直通)
CPU: 0.5 fps (根本跑不动)
🔬 WebGPU 核心架构
┌─────────────────────────────────────┐
│ JavaScript (控制层) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ WebGPU 设备 (Device) │
│ ├── 队列 (Queue) ── 命令提交 │
│ ├── 缓冲区 (Buffer) ── 数据存储 │
│ ├── 绑定组 (BindGroup) ── 资源绑定 │
│ └── 计算管线 (ComputePipeline) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ GPU 计算单元 (数千个核心并行) │
│ WGSL 着色器执行计算 │
└─────────────────────────────────────┘
🚀 海量粒子系统完整实现
// particle.wgsl - 计算着色器
struct Particle {
position: vec2f,
velocity: vec2f,
color: vec3f,
life: f32,
maxLife: f32,
};
struct SimParams {
deltaTime: f32,
width: f32,
height: f32,
attractorX: f32,
attractorY: f32,
mouseInfluence: f32,
};
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
@group(0) @binding(1) var<uniform> params: SimParams;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) id: vec3u) {
let index = id.x;
if (index >= arrayLength(&particles)) { return; }
var p = particles[index];
// 生命周期管理
if (p.life <= 0.0) {
// 重生粒子 - 随机位置和速度
p.position = vec2f(
random(index) * params.width,
random(index + 1000) * params.height
);
p.velocity = vec2f(
(random(index + 2000) - 0.5) * 400.0,
(random(index + 3000) - 0.5) * 400.0
);
p.life = p.maxLife;
p.color = vec3f(random(index + 4000), random(index + 5000), random(index + 6000));
} else {
// 物理更新
// 引力场 - 吸引子效果
let attractor = vec2f(params.attractorX, params.attractorY);
let dir = attractor - p.position;
let dist = length(dir);
let force = dir / max(dist, 10.0) * 200.0;
// 鼠标影响力场
let mouseForce = dir / max(dist, 1.0) * params.mouseInfluence;
p.velocity = p.velocity + (force + mouseForce) * params.deltaTime;
// 空气阻力
p.velocity = p.velocity * 0.99;
// 边界反弹
p.position = p.position + p.velocity * params.deltaTime;
let margin = 10.0;
if (p.position.x < margin) { p.position.x = margin; p.velocity.x = abs(p.velocity.x); }
if (p.position.x > params.width - margin) { p.position.x = params.width - margin; p.velocity.x = -abs(p.velocity.x); }
if (p.position.y < margin) { p.position.y = margin; p.velocity.y = abs(p.velocity.y); }
if (p.position.y > params.height - margin) { p.position.y = params.height - margin; p.velocity.y = -abs(p.velocity.y); }
p.life -= params.deltaTime;
// 根据生命值改变亮度
let lifeRatio = p.life / p.maxLife;
p.color = p.color * (0.3 + lifeRatio * 0.7);
}
particles[index] = p;
}
// 简单的伪随机函数
fn random(seed: u32) -> f32 {
let val = (seed * 1103515245u + 12345u) & 0x7fffffffu;
return f32(val) / f32(0x7fffffff);
}
// 前端控制代码
class WebGPUParticleSystem {
constructor(canvas) {
this.canvas = canvas;
this.particleCount = 500000; // 50万粒子
this.device = null;
this.particleBuffer = null;
this.uniformBuffer = null;
this.computePipeline = null;
this.bindGroup = null;
this.attractor = { x: canvas.width / 2, y: canvas.height / 2 };
this.mouseInfluence = 0;
}
async init() {
// 1. 获取适配器和设备
const adapter = await navigator.gpu.requestAdapter();
this.device = await adapter.requestDevice();
// 2. 配置上下文
const context = this.canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: this.device,
format: format,
alphaMode: 'premultiplied',
});
// 3. 创建粒子数据缓冲区 (存储+计算)
const particleDataSize = this.particleCount * 5 * 4; // 5个float32
this.particleBuffer = this.device.createBuffer({
size: particleDataSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
// 初始化粒子数据
const initialData = this.generateInitialParticleData();
this.device.queue.writeBuffer(this.particleBuffer, 0, initialData);
// 4. 创建统一缓冲区 (参数)
this.uniformBuffer = this.device.createBuffer({
size: 7 * 4, // 7个float32
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
// 5. 编译着色器
const shaderModule = this.device.createShaderModule({
code: PARTICLE_SHADER_CODE, // 上面的WGSL代码
});
// 6. 创建计算管线
this.computePipeline = this.device.createComputePipeline({
layout: 'auto',
compute: {
module: shaderModule,
entryPoint: 'main',
},
});
// 7. 创建绑定组
this.bindGroup = this.device.createBindGroup({
layout: this.computePipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: this.particleBuffer } },
{ binding: 1, resource: { buffer: this.uniformBuffer } },
],
});
// 8. 创建渲染管线 (显示粒子)
this.createRenderPipeline(format);
// 9. 开始动画
this.lastTime = performance.now();
this.animate();
}
generateInitialParticleData() {
// 生成随机粒子数据
const floatsPerParticle = 5; // x, y, vx, vy, life, maxLife, r, g, b
const data = new Float32Array(this.particleCount * 9);
for (let i = 0; i < this.particleCount; i++) {
const stride = i * 9;
// 随机位置
data[stride] = Math.random() * this.canvas.width; // x
data[stride + 1] = Math.random() * this.canvas.height; // y
// 随机速度
data[stride + 2] = (Math.random() - 0.5) * 400; // vx
data[stride + 3] = (Math.random() - 0.5) * 400; // vy
// 随机颜色
data[stride + 4] = Math.random(); // r
data[stride + 5] = Math.random(); // g
data[stride + 6] = Math.random(); // b
// 生命值
data[stride + 7] = Math.random() * 5; // life
data[stride + 8] = data[stride + 7] + Math.random(); // maxLife
}
return data.buffer;
}
updateSimulation() {
const now = performance.now();
let deltaTime = Math.min(0.033, (now - this.lastTime) / 1000);
this.lastTime = now;
// 更新uniform参数
const uniformData = new Float32Array([
deltaTime,
this.canvas.width,
this.canvas.height,
this.attractor.x,
this.attractor.y,
this.mouseInfluence,
0, // 对齐填充
]);
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
// 执行计算着色器
const commandEncoder = this.device.createCommandEncoder();
const computePass = commandEncoder.beginComputePass();
computePass.setPipeline(this.computePipeline);
computePass.setBindGroup(0, this.bindGroup);
computePass.dispatchWorkgroups(Math.ceil(this.particleCount / 256));
computePass.end();
// 渲染粒子
const renderPass = commandEncoder.beginRenderPass(this.renderPassDescriptor);
renderPass.setPipeline(this.renderPipeline);
renderPass.setVertexBuffer(0, this.particleBuffer);
renderPass.draw(this.particleCount);
renderPass.end();
this.device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(() => this.updateSimulation());
}
createRenderPipeline(format) {
const vertexShaderCode = `
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) color: vec3f,
}
struct Particle {
position: vec2f,
velocity: vec2f,
color: vec3f,
life: f32,
maxLife: f32,
}
@group(0) @binding(0) var<storage, read> particles: array<Particle>;
@vertex
fn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
let particle = particles[vertexIndex / 6];
let corners = array(
vec2f(-1.0, -1.0), vec2f( 1.0, -1.0),
vec2f(-1.0, 1.0), vec2f( 1.0, -1.0),
vec2f( 1.0, 1.0), vec2f(-1.0, 1.0)
);
let size = 4.0;
let corner = corners[vertexIndex % 6];
let finalPos = particle.position + corner * size;
let ndc = vec2f(
(finalPos.x / 800.0) * 2.0 - 1.0,
-(finalPos.y / 600.0) * 2.0 + 1.0
);
var output: VertexOutput;
output.position = vec4f(ndc, 0.0, 1.0);
output.color = particle.color;
return output;
}
@fragment
fn main(@location(0) color: vec3f) -> @location(0) vec4f {
return vec4f(color, 1.0);
}
`;
const vertexModule = this.device.createShaderModule({ code: vertexShaderCode });
this.renderPipeline = this.device.createRenderPipeline({
layout: 'auto',
vertex: { module: vertexModule, entryPoint: 'main' },
fragment: {
module: vertexModule,
entryPoint: 'main',
targets: [{ format: format }],
},
primitive: { topology: 'triangle-list' },
});
}
setAttractor(x, y) {
this.attractor = { x, y };
}
setMouseInfluence(x, y, strength = 200) {
this.attractor = { x, y };
this.mouseInfluence = strength;
}
}
// 使用
const canvas = document.getElementById('particle-canvas');
const system = new WebGPUParticleSystem(canvas);
await system.init();
// 交互效果
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
system.setMouseInfluence(x, y, 300);
});
🎯 今日挑战
实现一个WebGPU加速的流体模拟:
要求:
- 使用GPU Compute计算Navier-Stokes方程
- 实时交互(鼠标拖拽产生扰动)
- 粒子数量 > 20万时保持60fps
- 实现涡度和不可压缩约束
核心算法提示
// 流体模拟核心 - 雅可比迭代求解压力
fn solvePressure(pressure: array<f32>, divergence: array<f32>, iterations: u32) {
for (var iter = 0u; iter < iterations; iter++) {
for (var i = 1u; i < width - 1u; i++) {
for (var j = 1u; j < height - 1u; j++) {
let idx = i + j * width;
let p = (pressure[idx - 1] + pressure[idx + 1] +
pressure[idx - width] + pressure[idx + width] -
divergence[idx]) * 0.25;
pressure[idx] = p;
}
}
}
}
📊 WebGPU vs WebGL 对比
| 特性 | WebGL | WebGPU |
|---|---|---|
| 最大粒子数 | ~50k | ~5M+ |
| GPU Compute | ❌ 需hack | ✅ 原生支持 |
| 性能 | 基准 | 快5-10倍 |
| 调试 | 困难 | Chrome DevTools支持 |
| 着色器语言 | GLSL | WGSL (更安全) |
浏览器支持:Chrome 113+, Edge 113+, Safari 18+ (2025)
明日预告:前端AI - 在浏览器中运行大语言模型,使用WebGPU + Transformers.js实现本地AI助手
💡 关键认知:WebGPU的真正威力不在于画三角形,而在于通用GPU计算!