8

Sierpinski Tetrahedron (3D)

drag to orbit ยท click to reseed

The 3D generalization of the chaos game. Place four vertices at the corners of a regular tetrahedron, start from any point , and iterate

Plotting every produces the **Sierpinski tetrahedron**, the attractor of the iterated function system โ€” each map is a contraction with ratio , and they together satisfy the open-set condition. By Moran's formula the Hausdorff dimension solves , giving exactly โ€” an integer dimension for a fractal that lives in 3D and has zero volume. Each plotted point is colored by which vertex was chosen on the jump that produced it; click to reseed from a fresh random starting point.

idle
142 lines ยท three
view source
// Three.js Sierpinski tetrahedron โ€” 3D chaos game.
//
// Place four vertices at the corners of a regular tetrahedron. Start from
// some point in space and, every iteration, jump halfway toward a uniformly
// chosen vertex. Plot every landing position. The point cloud condenses
// onto the Sierpinski tetrahedron โ€” a self-similar fractal with Hausdorff
// dimension log(4)/log(2) = 2 exactly. Each plotted point is colored by
// which of the four vertices it most recently jumped toward.

const POINT_CAP = 30000;
const POINTS_PER_FRAME = 140;

let positions;     // Float32Array of plotted points (length POINT_CAP*3)
let colors;        // Float32Array (length POINT_CAP*3)
let head;          // ring-buffer write index
let count;         // points written so far (saturates at POINT_CAP)
let pointGeom, pointMat, pointsObj;

let cur;           // [x,y,z] โ€” current chaos-game point
let verts;         // [[x,y,z]x4] โ€” tetrahedron vertices
let vertColors;    // [[r,g,b]x4] โ€” one color per vertex
let markerMeshes;  // small spheres at the four vertex positions

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

function regularTetrahedron(scale) {
  // Standard regular-tetrahedron coordinates (vertices of a cube's
  // alternating corners), centered at origin.
  const s = scale;
  return [
    [ s,  s,  s],
    [ s, -s, -s],
    [-s,  s, -s],
    [-s, -s,  s],
  ];
}

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

  verts = regularTetrahedron(28);
  vertColors = [
    [1.00, 0.35, 0.45], // warm red
    [0.40, 0.85, 1.00], // cyan
    [1.00, 0.85, 0.30], // amber
    [0.65, 0.55, 1.00], // violet
  ];

  cur = [0, 0, 0];

  positions = new Float32Array(POINT_CAP * 3);
  colors    = new Float32Array(POINT_CAP * 3);
  head = 0;
  count = 0;

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

  pointMat = new THREE.PointsMaterial({
    size: 0.45,
    vertexColors: true,
    sizeAttenuation: true,
    transparent: true,
    opacity: 0.9,
    depthWrite: false,
  });
  pointsObj = new THREE.Points(pointGeom, pointMat);
  scene.add(pointsObj);

  // Visualize the four vertices as small bright spheres so the user
  // can see what the cloud is condensing toward.
  markerMeshes = [];
  for (let i = 0; i < 4; i++) {
    const g = new THREE.SphereGeometry(1.2, 16, 12);
    const m = new THREE.MeshBasicMaterial({
      color: new THREE.Color(vertColors[i][0], vertColors[i][1], vertColors[i][2]),
    });
    const mesh = new THREE.Mesh(g, m);
    mesh.position.set(verts[i][0], verts[i][1], verts[i][2]);
    scene.add(mesh);
    markerMeshes.push(mesh);
  }

  return { scene, camera };
}

function reseed() {
  cur = [
    (Math.random() - 0.5) * 8,
    (Math.random() - 0.5) * 8,
    (Math.random() - 0.5) * 8,
  ];
  head = 0;
  count = 0;
  pointGeom.setDrawRange(0, 0);
  // Zero the buffers so stale points don't linger when count < POINT_CAP.
  positions.fill(0);
  colors.fill(0);
  pointGeom.attributes.position.needsUpdate = true;
  pointGeom.attributes.color.needsUpdate = true;
}

function tick({ dt, scene, camera, renderer, width, height, input }) {
  // Click anywhere to restart from a fresh seed.
  const clicks = input.consumeClicks();
  if (clicks > 0) reseed();

  // Drag-to-orbit; auto-rotate when idle.
  if (input.mouseDown) {
    if (!isDragging) {
      isDragging = true;
      lastMouseX = input.mouseX;
      lastMouseY = input.mouseY;
    } else {
      const dx = input.mouseX - lastMouseX;
      const dy = input.mouseY - lastMouseY;
      userYaw   -= dx * 0.005;
      userPitch -= dy * 0.005;
      userPitch = Math.max(-1.2, Math.min(1.2, userPitch));
      lastMouseX = input.mouseX;
      lastMouseY = input.mouseY;
    }
    idleTime = 0;
  } else {
    isDragging = false;
    idleTime += dt;
    if (idleTime > 0.5) {
      userYaw += dt * 0.18;
    }
  }

  const r = 95;
  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);

  // Run POINTS_PER_FRAME chaos-game iterations.
  for (let i = 0; i < POINTS_PER_FRAME; i++) {
    const v = (Math.random() * 4) | 0;
    const target = verts[v];
    cur[0] = 0.5 * (cur[0] + target[0]);
    cur[1] = 0.5 * (cur[1] + target[1]);
    cur[2] = 0.5 * (cur[2] + target[2]);

    const idx = head * 3;
    positions[idx]     = cur[0];
    positions[idx + 1] = cur[1];
    positions[idx + 2] = cur[2];
    const col = vertColors[v];
    colors[idx]     = col[0];
    colors[idx + 1] = col[1];
    colors[idx + 2] = col[2];

    head = (head + 1) % POINT_CAP;
    if (count < POINT_CAP) count++;
  }
  pointGeom.attributes.position.needsUpdate = true;
  pointGeom.attributes.color.needsUpdate = true;
  pointGeom.setDrawRange(0, count);

  renderer.render(scene, camera);
}

Comments (0)

Log in to comment.