16

Mandelbulb Point Cloud

drag to orbit

The Mandelbulb is the 3D analog of the Mandelbrot set, popularized by Daniel White and Paul Nylander in 2009. It iterates where the power is taken in spherical coordinates: given with , , , define

The canonical degree is . A point belongs to the set if iterating from keeps bounded — we use bailout and 12 iterations. This sim Monte-Carlo samples a few hundred candidates per frame, accumulating up to 50 000 interior points into an additive-blended point cloud. Color encodes the final radius at the last iteration. Drag to orbit; the view auto-rotates when idle.

idle
141 lines Ā· three
view source
// Three.js Mandelbulb — Monte Carlo point cloud sampler.
//
// Each frame we rejection-sample random points c in [-1.5, 1.5]^3 and
// iterate v_{n+1} = v_n^8 + c (with spherical exponentiation). Points
// that never escape |v| > 2 within 12 iterations get added to a growing
// ring-buffer of THREE.Points. Color = normalized radius at last iter.

const BOUND = 1.5;
const POWER = 8;
const MAX_ITER = 12;
const BAILOUT = 2.0;
const SAMPLES_PER_FRAME = 320;
const MAX_POINTS = 50000;

let positions;       // Float32Array, ring buffer
let colors;          // Float32Array, ring buffer
let pointsGeom, pointsMat, pointsObj;
let head = 0;        // next write index
let filled = 0;      // how many slots written so far (caps at MAX_POINTS)

let userYaw = 0.6;
let userPitch = -0.2;
let isDragging = false;
let lastMouseX = 0, lastMouseY = 0;
let idleTime = 0;

// One step of the spherical-coordinate exponentiation: v -> v^p + c.
// Returns [x, y, z, r].
function bulbStep(x, y, z, cx, cy, cz) {
  const r = Math.sqrt(x * x + y * y + z * z);
  if (r === 0) {
    return [cx, cy, cz, 0];
  }
  // theta = acos(z/r), phi = atan2(y, x).
  const theta = Math.acos(z / r) * POWER;
  const phi = Math.atan2(y, x) * POWER;
  const rp = Math.pow(r, POWER);
  const sinT = Math.sin(theta);
  const nx = rp * sinT * Math.cos(phi) + cx;
  const ny = rp * sinT * Math.sin(phi) + cy;
  const nz = rp * Math.cos(theta) + cz;
  return [nx, ny, nz, r];
}

// Returns { inside: bool, lastR: number } — sample one candidate c.
function testPoint(cx, cy, cz) {
  let x = cx, y = cy, z = cz;
  let lastR = 0;
  for (let i = 0; i < MAX_ITER; i++) {
    const [nx, ny, nz, r] = bulbStep(x, y, z, cx, cy, cz);
    x = nx; y = ny; z = nz;
    lastR = Math.sqrt(x * x + y * y + z * z);
    if (lastR > BAILOUT) {
      return { inside: false, lastR };
    }
  }
  return { inside: true, lastR };
}

function hslToRgb(h, s, l) {
  const a = s * Math.min(l, 1 - l);
  const f = (n, k = (n + h * 12) % 12) =>
    l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)));
  return [f(0), f(8), f(4)];
}

function init({ scene, camera, renderer, width, height }) {
  renderer.setClearColor(0x000000, 1);
  camera.position.set(0, 0, 4.2);
  camera.lookAt(0, 0, 0);

  positions = new Float32Array(MAX_POINTS * 3);
  colors = new Float32Array(MAX_POINTS * 3);

  pointsGeom = new THREE.BufferGeometry();
  pointsGeom.setAttribute(
    "position",
    new THREE.BufferAttribute(positions, 3).setUsage(THREE.DynamicDrawUsage),
  );
  pointsGeom.setAttribute(
    "color",
    new THREE.BufferAttribute(colors, 3).setUsage(THREE.DynamicDrawUsage),
  );
  pointsGeom.setDrawRange(0, 0);

  pointsMat = new THREE.PointsMaterial({
    size: 0.015,
    vertexColors: true,
    transparent: true,
    opacity: 0.7,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
    sizeAttenuation: true,
  });

  pointsObj = new THREE.Points(pointsGeom, pointsMat);
  scene.add(pointsObj);

  return { scene, camera };
}

function tick({ dt, scene, camera, renderer, width, height, input }) {
  // Drain click queue; we don't act on individual clicks here.
  void input.consumeClicks();

  if (input.mouseDown) {
    idleTime = 0;
    if (!isDragging) {
      isDragging = true;
      lastMouseX = input.mouseX;
      lastMouseY = input.mouseY;
    } else {
      const dx = input.mouseX - lastMouseX;
      const dy = input.mouseY - lastMouseY;
      userYaw -= dx * 0.006;
      userPitch -= dy * 0.006;
      userPitch = Math.max(-1.3, Math.min(1.3, userPitch));
      lastMouseX = input.mouseX;
      lastMouseY = input.mouseY;
    }
  } else {
    isDragging = false;
    idleTime += dt;
    // Auto-rotate after a brief idle.
    if (idleTime > 0.3) {
      userYaw += dt * 0.22;
    }
  }

  const r = 4.2;
  const cy = Math.cos(userYaw), sy = Math.sin(userYaw);
  const cp = Math.cos(userPitch), sp = Math.sin(userPitch);
  camera.position.set(r * cp * sy, r * sp, r * cp * cy);
  camera.lookAt(0, 0, 0);
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height, false);

  // Sample new candidates this frame.
  let writeStart = head;
  let wroteAny = false;
  for (let s = 0; s < SAMPLES_PER_FRAME; s++) {
    const cx = (Math.random() * 2 - 1) * BOUND;
    const cyc = (Math.random() * 2 - 1) * BOUND;
    const cz = (Math.random() * 2 - 1) * BOUND;
    // Cheap rejection: outside the obvious bounding sphere of the bulb
    // can't be inside, so skip.
    if (cx * cx + cyc * cyc + cz * cz > 1.6 * 1.6) continue;
    const res = testPoint(cx, cyc, cz);
    if (!res.inside) continue;

    const idx = head * 3;
    positions[idx]     = cx;
    positions[idx + 1] = cyc;
    positions[idx + 2] = cz;

    // Color: hue from normalized last radius, with a little angular tint.
    const nr = Math.min(1, res.lastR / BAILOUT);
    const hue = (0.62 - 0.55 * nr + 1) % 1; // blue->magenta->red sweep
    const [rr, gg, bb] = hslToRgb(hue, 0.85, 0.55);
    colors[idx]     = rr;
    colors[idx + 1] = gg;
    colors[idx + 2] = bb;

    head = (head + 1) % MAX_POINTS;
    if (filled < MAX_POINTS) filled++;
    wroteAny = true;
  }

  if (wroteAny) {
    // Cheapest correct path: just mark the whole buffer dirty. The ring
    // buffer may wrap, so a single contiguous updateRange isn't always
    // valid; flag everything.
    pointsGeom.attributes.position.needsUpdate = true;
    pointsGeom.attributes.color.needsUpdate = true;
    pointsGeom.setDrawRange(0, filled);
  }
  void writeStart;

  renderer.render(scene, camera);
}

Comments (0)

Log in to comment.