플로우 필드 파티클

Flow-field particles

노이즈로 만든 흐름장을 따라 수천 개의 입자가 움직이며 자연스러운 소용돌이를 그리는 기법.

다른 이름: 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);
});

플로우 필드(flow field)는 공간의 각 지점마다 "여기 있으면 이 방향으로 움직여라"는 벡터를 정의한 것입니다. 이 데모는 Simplex 노이즈 값 하나를 각도로 해석해서(angle = noise(x, z) * 2π) 방향 벡터를 만듭니다 — 노이즈가 부드럽게 이어지므로 인접한 두 위치의 방향도 크게 다르지 않아, 입자들이 서로 끊기지 않고 흐르는 것처럼 보입니다.

매 프레임 각 파티클 위치에서 그 지점의 노이즈 각도를 다시 계산하고 그 방향으로 한 걸음 전진시킵니다. three/addons/math/SimplexNoise.js를 CPU에서 매 프레임 수천 번 호출하는 방식이라, 파티클 수가 수만 개를 넘어가면 GPU 셰이더에서 노이즈를 계산하는 쪽(gpgpu 항목 참고)으로 옮기는 게 유리합니다.

경계를 벗어난 입자는 반대편으로 감싸지 않고 무작위 위치에 다시 뿌려서, 흐름이 끊기지 않고 계속 새 입자가 유입되는 것처럼 보이게 했습니다. 색은 진행 각도에 따라 색상환을 돌려 흐름의 "결"을 시각적으로 드러냅니다.

언제 쓰나

데이터 흐름, 바람·물살 같은 자연 현상, 배경 앰비언스 모션을 자연스럽게 표현하고 싶을 때.