mcpField
P
particle-system

particle-system

github.com/iart-ai创意与媒体451.4k 次安装18 周前更新
代码仓库

关于 particle-system

particle-system 是用于构建粒子效果的技能,通过逐粒子运动规则实现动画。内容涵盖 2D canvas 和 GPU 渲染,并指导生成可复现的独立 HTML 文件。

主要功能

  1. 01用力学规则推进粒子运动
  2. 02创建爆发式和连续发射器
  3. 03制作流场与 curl-noise 效果
  4. 04用空间网格连接邻近粒子
  5. 05用 Three.js Points 渲染大量粒子
  6. 06生成确定性的独立 HTML

适用场景

  1. 01制作彩纸、雪或烟雾效果
  2. 02构建星点连线背景
  3. 03制作流场动画视觉
  4. 04生成可复现的粒子效果预览

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/iart-ai/webgl-animation-skills --skill particle-system

安装后在 Claude Code 中输入/particle-system确认已加载。

附带文件README.mdreferences/particle-recipes.md

详细介绍

粒子系统

通过简单的逐粒子规则驱动大量小元素,产生涌现式的自然运动。数百个粒子使用 2D 画布,数千个粒子使用 GPU Points。

适用场景

  • 粒子/星座背景和氛围动效。
  • 庆祝效果:彩纸、火花。天气效果:雪、雨。体积效果:烟雾。
  • 流场/旋度噪声漩涡和数据驱动的点云。
  • 互连点网络(连接彼此邻近粒子的线条)。

核心循环:逐粒子积分

每个粒子都保存状态,并在每一帧推进:将力累加到加速度上,对速度和位置进行积分,更新粒子年龄,并在粒子死亡时重新生成。按 dt 缩放,以确保与帧率无关。

class Particle {
  constructor() { this.reset(); }
  reset() {
    this.x = Math.random() * W; this.y = Math.random() * H;
    this.vx = 0; this.vy = 0;
    this.life = 1; this.size = 1 + Math.random() * 2;
  }
  step(dt, forces) {
    let ax = 0, ay = 0;
    for (const f of forces) { const [fx, fy] = f(this); ax += fx; ay += fy; }
    this.vx += ax * dt; this.vy += ay * dt;
    this.vx *= 0.99; this.vy *= 0.99;            // drag
    this.x += this.vx * dt; this.y += this.vy * dt;
    this.life -= dt * 0.2;
    if (this.life <= 0) this.reset();
  }
}

优先使用半隐式欧拉法(如上所示,先更新速度,再使用新速度更新位置)——它对粒子所用的弹簧力/阻力具有较好的稳定性。使用固定或限制后的 dt(Math.min(dt, 1/30)),避免标签页卡顿时模拟失控。

力

力是一个返回加速度 [fx, fy] 的函数。将多个力组合成列表。

const gravity = () => [0, 400];                  // constant downward
const drag = (p) => [-p.vx * 0.5, -p.vy * 0.5];  // proportional resistance
function attract(tx, ty, strength) {             // pull toward a point (e.g. mouse)
  return (p) => {
    const dx = tx - p.x, dy = ty - p.y;
    const d2 = dx*dx + dy*dy + 100;              // +100 softens the singularity
    const f = strength / d2;
    return [dx * f, dy * f];
  };
}

排斥力就是强度为负值的 attract。朝向初始位置的弹簧力可以产生“回归原位”的效果。

流场/旋度噪声(自然漩涡)

对噪声场进行采样,为每个粒子计算速度方向。将噪声值用作角度:

// `noise2D` from a library (e.g. simplex-noise's createNoise2D), range -1..1
function flowField(noise2D, scale = 0.002, speed = 60) {
  return (p) => {
    const angle = noise2D(p.x * scale, p.y * scale) * Math.PI * 2;
    return [Math.cos(angle) * speed - p.vx, Math.sin(angle) * speed - p.vy];
  };
}

真正的旋度噪声是无散的(没有源/汇,因此呈现类似流体的效果)。通过有限差分计算势函数的旋度:

function curl(noise2D, x, y, eps = 1e-2) {
  const n1 = noise2D(x, y + eps), n2 = noise2D(x, y - eps);
  const n3 = noise2D(x + eps, y), n4 = noise2D(x - eps, y);
  return [ (n1 - n2) / (2*eps), -(n3 - n4) / (2*eps) ];  // (dN/dy, -dN/dx)
}

在噪声输入中加入时间(noise2D(x*scale, y*scale + t)),让场随时间演变。

发射:爆发式与连续式

  • 爆发式(彩纸、火花):从某个点一次性生成 N 个粒子,让它们在一个锥形范围内随机选取角度/速度,然后由重力和阻力接管。不要重新生成——粒子死亡后将其移除。
  • 连续式(雪、烟雾):以稳定速率生成粒子;在粒子死亡时于顶部/源点重新生成。

在一个范围内随机取值,让分布更自然:angle = base + (Math.random()-0.5)*spread; speed = min + Math.random()*(max-min)。

function burst(x, y, n = 120) {
  const out = [];
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2;
    const s = 200 + Math.random() * 400;
    out.push({ x, y, vx: Math.cos(a)*s, vy: Math.sin(a)*s - 200,  // upward bias
               life: 1, size: 4 + Math.random()*4,
               color: `hsl(${Math.random()*360},90%,60%)`,
               rot: Math.random()*6.28, vr: (Math.random()-0.5)*10 });
  }
  return out;
}

彩纸效果之所以像彩纸,是因为旋转 + 扁平矩形 + 重力 + 空气阻力,而不是圆点。雪花效果则来自缓慢下落 + 轻柔的水平正弦摆动 + 大小各异的深度变化。

避免 O(n²) 的互连点网络

直接检查所有粒子对的复杂度为 O(n²),粒子超过约 300 个时就会变得很慢。使用均匀空间网格:按网格单元对粒子分箱,只比较相邻的 8 个单元。

function connect(ctx, parts, radius) {
  const cell = radius, cols = Math.ceil(W / cell);
  const grid = new Map();
  const key = (cx, cy) => cx + cy * cols;
  for (const p of parts) {
    const cx = (p.x / cell) | 0, cy = (p.y / cell) | 0;
    (grid.get(key(cx, cy)) ?? grid.set(key(cx, cy), []).get(key(cx, cy))).push(p);
  }
  for (const p of parts) {
    const cx = (p.x / cell) | 0, cy = (p.y / cell) | 0;
    for (let oy = -1; oy <= 1; oy++) for (let ox = -1; ox <= 1; ox++) {
      const bucket = grid.get(key(cx+ox, cy+oy)); if (!bucket) continue;
      for (const q of bucket) {
        if (q === p) continue;
        const dx = p.x - q.x, dy = p.y - q.y, d = Math.hypot(dx, dy);
        if (d < radius) {
          ctx.globalAlpha = 1 - d / radius;     // fade line with distance
          ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.stroke();
        }
      }
    }
  }
  ctx.globalAlpha = 1;
}

对于均匀分布的粒子,这种方法的复杂度为 O(n)。每对粒子都会被找到两次;如有需要,只检查前向邻居即可将工作量减半。

GPU 粒子:Three.js Points + 着色器

对于数千个或更多粒子,将所有位置推送到 BufferGeometry,并以 Points 进行渲染。在顶点着色器中添加动画,实现真正的 GPU 级扩展能力。

const N = 50000;
const pos = new Float32Array(N * 3);
for (let i = 0; i < N * 3; i++) pos[i] = (Math.random() - 0.5) * 20;
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
const mat = new THREE.ShaderMaterial({
  uniforms: { u_time: { value: 0 }, u_size: { value: 6 } },
  transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
  vertexShader: `
    uniform float u_time, u_size;
    void main(){
      vec3 p = position;
      p.y += sin(u_time + position.x) * 0.5;          // animate on GPU
      vec4 mv = modelViewMatrix * vec4(p, 1.0);
      gl_PointSize = u_size * (10.0 / -mv.z);          // perspective size
      gl_Position = projectionMatrix * mv;
    }`,
  fragmentShader: `
    void main(){
      float d = length(gl_PointCoord - 0.5);
      if (d > 0.5) discard;                            // round, soft points
      gl_FragColor = vec4(1.0, 0.8, 0.4, smoothstep(0.5, 0.0, d));
    }`,
});
scene.add(new THREE.Points(geo, mat));
// loop: mat.uniforms.u_time.value = clock.getElapsedTime();

AdditiveBlending + depthWrite: false 可以呈现发光粒子的效果。通过对 gl_PointCoord 的距离使用 discard,可将方形点变为圆形。对于粒子数据(生命值、种子),添加自定义属性并在着色器中读取。

交付与验证(独立 HTML)

打包的辅助脚本(scripts/):scripts/seek-shot.sh anim.html 0 1.5 3 会冻结 ?t=N 测试框架并截取各个时刻的屏幕截图;scripts/contact-sheet.sh sheet.png frame-*.png 会将它们拼成一张图,方便快速查看。参见 scripts/README.md。

对于自包含的粒子效果(星座背景、纸屑爆发、流场、GPU 点),交付物应是一个可直接在浏览器中打开的 HTML 文件——将 Canvas 2D 内联,或通过 importmap 从 CDN 加载 Three.js 来渲染 GPU Points;使用一个渲染循环,无需构建步骤。单文件是合适的方案;一个文件就能完成的事,不要用打包工具。

输出要求:

  • 一个 .html:对于 Canvas,将模拟和 2D 绘制循环放在同一个内联 <script> 中;对于 GPU 点,使用 importmap 将 three 固定到 CDN,并内联 Points 设置。
  • 使用单一累积 time 驱动模拟(累加经过钳制的 dt,或 GPU 使用 clock.getElapsedTime() / u_time)。不要在各个粒子中分散调用 Date.now()。
  • 为 RNG 设置种子——用带种子的 PRNG(例如 mulberry32)替换直接调用的 Math.random(),以便粒子的生成位置、角度和爆发效果都能逐帧复现。

跳转/冻结测试框架——推进至固定时间,为截图渲染一帧。 ?t=N 会重置随机数种子,以固定时间步确定性地将模拟推进至 N 秒,渲染一次后停止循环。

<script>
  let rng = mulberry32(1234);                 // fixed seed → reproducible
  const particles = spawn(() => rng());
  function render() { /* draw particles to canvas / renderer.render(...) */ }
  const t = new URLSearchParams(location.search).get("t");
  if (t !== null) {
    const step = 1 / 60, end = parseFloat(t);
    for (let s = 0; s < end; s += step) update(step);  // fixed-step to t
    render();                                  // one frozen frame
    window.__ready = true;
  } else {
    let prev = performance.now();
    (function loop(now){ update(Math.min((now-prev)/1000, 1/30)); prev = now;
      render(); requestAnimationFrame(loop); })(prev);
  }
</script>

验证循环——渲染 → 冻结 → 截图 → 检查:在三个时刻打开页面——开始、中间、稳定(?t=0、?t=<mid>、?t=<end>;对于爆发效果,t≈0 生成 / t≈0.5 扩散 / t≈1.5 稳定)——分别截取屏幕截图,并检查保真度(是否符合需求)和异常:画布空白 = 解析/初始化错误(检查控制台);粒子跑出画面(缺少钳制/环绕逻辑);位置为 NaN(所有粒子都会消失);所有粒子挤在原点(RNG 未正确接入)。GPU 点需要 GPU 上下文;Playwright/Chromium 在无头模式下会提供一个(swiftshader)。

npx playwright screenshot --wait-for-timeout=600 "file://$PWD/particles.html?t=1.0" frame-mid.png

完成前:

  1. Canvas 能渲染粒子——画面不为空白,没有控制台/WebGL 错误,也没有 CDN 404。
  2. ?t=N 能冻结可复现的帧(带种子的 RNG + 固定时间步 → 相同的 N → 相同的像素)。
  3. 已对开始/中间/稳定时刻截取屏幕截图——符合需求,没有跑出画面、位置为 NaN 或挤在原点的粒子。
  4. 嵌入 SPA 时已清理且不会泄漏(取消 rAF 循环;对于 GPU,释放几何体/材质/渲染器)。
  5. 遵循 prefers-reduced-motion 设置——减少粒子数量,或在运动仅用于装饰时改用静态场。

快速参考

效果配方
纸屑爆发 + 重力 + 阻力 + 旋转矩形
雪持续从顶部生成 + 缓慢下落 + 正弦摆动
烟雾持续生成 + 向上移动 + 增大尺寸 + 淡出透明度
火花短寿命爆发 + 叠加 + 快速淡出
流场噪声角度 → 速度,随时间演变
Curl 噪声噪声势函数的 Curl(无散度)
星座空间网格,在半径范围内连线,按距离淡出
1000+ 个Three.js Points + ShaderMaterial,在顶点着色器中制作动画

参考文件

  • references/particle-recipes.md — 完整的 Canvas 纸屑、雪和烟雾系统;鼠标吸引/排斥;完整的 simplex 流场与 Curl 噪声场,呈现流动效果;从头到尾实现的空间网格连接点背景;以及带有逐粒子生命期/种子属性、叠加辉光并在着色器中重新生成粒子的 GPU Points 系统。

评论

支持 Markdown