每天一个高级前端知识 - Day 7

3 阅读4分钟

每天一个高级前端知识 - 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加速的流体模拟

要求:

  1. 使用GPU Compute计算Navier-Stokes方程
  2. 实时交互(鼠标拖拽产生扰动)
  3. 粒子数量 > 20万时保持60fps
  4. 实现涡度和不可压缩约束
核心算法提示
// 流体模拟核心 - 雅可比迭代求解压力
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 对比

特性WebGLWebGPU
最大粒子数~50k~5M+
GPU Compute❌ 需hack✅ 原生支持
性能基准快5-10倍
调试困难Chrome DevTools支持
着色器语言GLSLWGSL (更安全)

浏览器支持:Chrome 113+, Edge 113+, Safari 18+ (2025)


明日预告:前端AI - 在浏览器中运行大语言模型,使用WebGPU + Transformers.js实现本地AI助手

💡 关键认知:WebGPU的真正威力不在于画三角形,而在于通用GPU计算