About this technique →
flow-field-3d/sketch.js
// 3D Flow Field
//
// A smooth 3D vector field built from sin/cos of offset coordinates.
// Thousands of particles advect through the field inside a [-1,1]^3 bounding
// box, wrapping at the boundaries. Their collective drift reveals the invisible
// field structure — orbit the camera to explore from any angle.

import * as THREE from 'three';

export const WARMUP = { framesBeforeReady: 480 };

export const PARAMS = {
  particles:  { value: 3000, min: 500, max: 12000, step: 100, label: "Particles", folder: "Structure", rebuildOnChange: true },
  noiseScale: { value: 0.4, min: 0.05, max: 2, step: 0.01, label: "Field Frequency", folder: "Behavior" },
  speed:      { value: 0.6, min: 0.05, max: 3, step: 0.05, label: "Speed", folder: "Behavior" },
  pointSize:  { value: 0.04, min: 0.005, max: 0.2, step: 0.005, label: "Point Size", folder: "Appearance" },
  hue:        { value: 220, min: 0, max: 360, step: 1, label: "Base Hue", folder: "Appearance" },
};

export const SHARE = {
  bookmarked: ["particles", "noiseScale", "speed", "pointSize", "hue"],
};

// Smooth deterministic 3D vector field via sin/cos combinations with offsets.
// Produces curl-like flow without needing a noise library.
function vectorField(x, y, z, scale) {
  const s = scale;
  const fx = Math.sin(y * s * 1.7) * Math.cos(z * s * 1.3) - Math.sin(x * s);
  const fy = Math.sin(z * s * 1.9) * Math.cos(x * s * 1.5) - Math.sin(y * s);
  const fz = Math.sin(x * s * 2.1) * Math.cos(y * s * 1.1) - Math.sin(z * s);
  return [fx, fy, fz];
}

// Wrap a value into [-bound, bound]
function wrap(v, bound) {
  const range = bound * 2;
  return ((v + bound) % range + range) % range - bound;
}

export function sceneSetup(THREE, scene, camera, renderer, params, seed) {
  scene.background = new THREE.Color(0x07080a);
  camera.position.set(0, 0, 3.5);
  camera.lookAt(0, 0, 0);

  const n = params.particles | 0;

  // Positions: current particle locations
  const positions = new Float32Array(n * 3);
  // Colors: per-particle HSL-derived color
  const colors = new Float32Array(n * 3);

  const hueBase = params.hue / 360;

  for (let i = 0; i < n; i++) {
    // Random start in [-1, 1]^3
    positions[i * 3]     = Math.random() * 2 - 1;
    positions[i * 3 + 1] = Math.random() * 2 - 1;
    positions[i * 3 + 2] = Math.random() * 2 - 1;

    // Vary hue slightly per particle for depth
    const hueOffset = (Math.random() - 0.5) * 0.15;
    const col = new THREE.Color().setHSL(
      ((hueBase + hueOffset) % 1 + 1) % 1,
      0.8,
      0.55
    );
    colors[i * 3]     = col.r;
    colors[i * 3 + 1] = col.g;
    colors[i * 3 + 2] = col.b;
  }

  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geo.setAttribute('color',    new THREE.BufferAttribute(colors, 3));

  const mat = new THREE.PointsMaterial({
    size: params.pointSize,
    sizeAttenuation: true,
    vertexColors: true,
    transparent: true,
    opacity: 0.85,
  });

  const points = new THREE.Points(geo, mat);
  scene.add(points);

  return { positions, colors, geo, mat, points, n };
}

export function sceneAnimate(THREE, scene, camera, state, params, time, delta) {
  const { positions, colors, geo, mat, n } = state;

  // Update material properties live
  mat.size = params.pointSize;

  const speed = params.speed * 0.01; // scale down to world units
  const scale = params.noiseScale;
  const hueBase = params.hue / 360;
  const BOUND = 1.0;

  for (let i = 0; i < n; i++) {
    const base = i * 3;
    let x = positions[base];
    let y = positions[base + 1];
    let z = positions[base + 2];

    const [fx, fy, fz] = vectorField(x, y, z, scale);

    // Normalize to get consistent speed regardless of field magnitude
    const mag = Math.sqrt(fx * fx + fy * fy + fz * fz) || 1;

    x += (fx / mag) * speed;
    y += (fy / mag) * speed;
    z += (fz / mag) * speed;

    // Wrap into bounding box
    positions[base]     = wrap(x, BOUND);
    positions[base + 1] = wrap(y, BOUND);
    positions[base + 2] = wrap(z, BOUND);

    // Recolor periodically based on current hue param + position for variety
    const hueOffset = (positions[base] + positions[base + 1]) * 0.05;
    const col = new THREE.Color().setHSL(
      ((hueBase + hueOffset) % 1 + 1) % 1,
      0.8,
      0.5 + positions[base + 2] * 0.15
    );
    colors[base]     = col.r;
    colors[base + 1] = col.g;
    colors[base + 2] = col.b;
  }

  geo.attributes.position.needsUpdate = true;
  geo.attributes.color.needsUpdate = true;
}