8
Sierpinski Tetrahedron (3D)
drag to orbit ยท click to reseed
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.