Flow-field particles

플로우 필드 파티클

Thousands of particles drifting along a noise-driven flow field to trace organic, swirling paths.

Also known as: Curl noise flowVector field particlesFlow field
···
js
import * as THREE from 'three';
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
const COUNT = 2600;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05060c);
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100);
camera.position.set(0, 3.2, 5.2);
camera.lookAt(0, 0, 0);

const simplex = new SimplexNoise();
const BOUND = 4.2;
const SCALE = 0.35;
const pos = new Float32Array(COUNT * 3);
const col = new Float32Array(COUNT * 3);
const speed = new Float32Array(COUNT);
const c = new THREE.Color();
for (let i = 0; i < COUNT; i++) {
  pos[i * 3] = (Math.random() * 2 - 1) * BOUND;
  pos[i * 3 + 1] = 0;
  pos[i * 3 + 2] = (Math.random() * 2 - 1) * BOUND;
  speed[i] = 0.6 + Math.random() * 0.8;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
const mat = new THREE.PointsMaterial({ size: 0.045, vertexColors: true, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false });
const points = new THREE.Points(geo, mat);
scene.add(points);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();

renderer.setAnimationLoop((t) => {
  const time = t * 0.00035;
  const posAttr = geo.attributes.position;
  const colAttr = geo.attributes.color;
  for (let i = 0; i < COUNT; i++) {
    const ix = i * 3;
    const x = posAttr.array[ix], z = posAttr.array[ix + 2];
    const angle = simplex.noise(x * SCALE, z * SCALE + time) * Math.PI * 4;
    let nx = x + Math.cos(angle) * speed[i] * 0.02;
    let nz = z + Math.sin(angle) * speed[i] * 0.02;
    if (nx > BOUND || nx < -BOUND || nz > BOUND || nz < -BOUND) {
      nx = (Math.random() * 2 - 1) * BOUND;
      nz = (Math.random() * 2 - 1) * BOUND;
    }
    posAttr.array[ix] = nx;
    posAttr.array[ix + 2] = nz;
    c.setHSL(0.55 + ((angle / (Math.PI * 4)) % 1 + 1) % 1 * 0.4, 0.85, 0.6);
    colAttr.array[ix] = c.r; colAttr.array[ix + 1] = c.g; colAttr.array[ix + 2] = c.b;
  }
  posAttr.needsUpdate = true;
  colAttr.needsUpdate = true;
  points.rotation.y = time * 0.15;
  renderer.render(scene, camera);
});

A flow field assigns every point in space a vector — "if you’re here, move this way." This demo turns a single Simplex noise value into an angle (angle = noise(x, z) * 2π) to build that direction — since noise changes smoothly, neighboring points get similar directions, so particles read as one continuous flow rather than independent dots.

Each frame it re-samples the noise angle at every particle’s current position and steps it forward along that direction. three/addons/math/SimplexNoise.js runs on the CPU thousands of times per frame; once particle counts climb into the tens of thousands, moving the noise computation into a shader (see gpgpu) pays off.

Particles that drift past the bounds respawn at a random spot instead of wrapping to the opposite edge, so the flow keeps feeding fresh particles rather than visibly teleporting. Color is mapped from the flow angle, so hue itself reveals the field’s "grain."

When to use

Visualizing data flow, natural phenomena like wind or currents, or organic ambient background motion.