3
Lorenz Attractor (3D)
drag to orbit the camera
idle
132 lines · three
view source
// Three.js Lorenz attractor — the full 3D butterfly.
//
// Contract: this sim runs in the main-thread iframe with three.js loaded
// as window.THREE. init() builds the scene; tick() integrates the ODE
// and draws. The runtime auto-calls renderer.render(scene, camera) each
// tick if you don't.
const SIGMA = 10;
const RHO = 28;
const BETA = 8 / 3;
const TRAIL_MAX = 4000;
const SUBSTEPS = 6;
const DT_SIM = 0.005;
let state; // [x, y, z]
let positions; // Float32Array of trail points
let head; // ring-buffer write index
let count; // points written so far
let lineGeom, lineMat, line;
let head3D, headMat;
let hueOffset = 0;
let elapsed = 0;
let userYaw = 0, userPitch = 0;
let isDragging = false;
let lastMouseX = 0, lastMouseY = 0;
function derivs(x, y, z) {
return [
SIGMA * (y - x),
x * (RHO - z) - y,
x * y - BETA * z,
];
}
function rk4Step(s, h) {
const [x, y, z] = s;
const k1 = derivs(x, y, z);
const k2 = derivs(x + 0.5 * h * k1[0], y + 0.5 * h * k1[1], z + 0.5 * h * k1[2]);
const k3 = derivs(x + 0.5 * h * k2[0], y + 0.5 * h * k2[1], z + 0.5 * h * k2[2]);
const k4 = derivs(x + h * k3[0], y + h * k3[1], z + h * k3[2]);
return [
x + (h / 6) * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]),
y + (h / 6) * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]),
z + (h / 6) * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2]),
];
}
function init({ scene, camera, renderer, width, height }) {
renderer.setClearColor(0x05060a, 1);
camera.position.set(0, 0, 90);
camera.lookAt(0, 0, 0);
state = [0.1, 0.0, 0.0];
// burn-in so we start on the attractor, not the spiral-out
for (let i = 0; i < 1500; i++) state = rk4Step(state, DT_SIM);
positions = new Float32Array(TRAIL_MAX * 3);
head = 0;
count = 0;
lineGeom = new THREE.BufferGeometry();
lineGeom.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3).setUsage(THREE.DynamicDrawUsage),
);
lineGeom.setDrawRange(0, 0);
lineMat = new THREE.LineBasicMaterial({
color: 0xffffff,
vertexColors: true,
transparent: true,
opacity: 0.9,
});
// Per-vertex hue for the trail.
const colors = new Float32Array(TRAIL_MAX * 3);
lineGeom.setAttribute(
"color",
new THREE.BufferAttribute(colors, 3).setUsage(THREE.DynamicDrawUsage),
);
line = new THREE.Line(lineGeom, lineMat);
// Lorenz lives roughly in z ∈ [0, 50]; recenter on the canonical bowtie.
line.position.set(0, -25, 0);
scene.add(line);
// Glowing head dot.
const headGeo = new THREE.SphereGeometry(0.6, 12, 10);
headMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
head3D = new THREE.Mesh(headGeo, headMat);
scene.add(head3D);
return { scene, camera };
}
function hslToRgb(h, s, l) {
// h in [0,1)
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 tick({ dt, time, scene, camera, renderer, width, height, input }) {
elapsed += dt;
// Mouse-drag camera orbit (and auto-rotate when idle).
const clicks = input.consumeClicks(); // drain so the buffer doesn't grow
void clicks;
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;
}
} else {
isDragging = false;
// Slow auto-rotate when no one is touching.
userYaw += dt * 0.18;
}
const r = 90;
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);
// Advance ODE by `SUBSTEPS` substeps, scaled to wall-clock dt.
const steps = Math.max(1, Math.min(12, Math.round(SUBSTEPS * (dt / (1 / 60)))));
hueOffset = (hueOffset + dt * 12) % 360;
const colors = lineGeom.attributes.color.array;
for (let i = 0; i < steps; i++) {
state = rk4Step(state, DT_SIM);
const idx = head * 3;
positions[idx] = state[0];
positions[idx + 1] = state[1];
positions[idx + 2] = state[2];
// Color this point on its hue.
const t = count / TRAIL_MAX;
const h = (hueOffset / 360 + t * 0.78) % 1;
const [r0, g0, b0] = hslToRgb(h, 0.95, 0.6);
colors[idx] = r0;
colors[idx + 1] = g0;
colors[idx + 2] = b0;
head = (head + 1) % TRAIL_MAX;
if (count < TRAIL_MAX) count++;
}
lineGeom.attributes.position.needsUpdate = true;
lineGeom.attributes.color.needsUpdate = true;
lineGeom.setDrawRange(0, count);
// Reorder so the ring buffer's logical start is the visual start of the
// line. Cheap: just keep redrawing one fixed window and accept that the
// wrap point shows a single discontinuity.
head3D.position.set(state[0], state[1] - 25, state[2]);
head3D.material.color.setHSL(hueOffset / 360, 1.0, 0.8);
renderer.render(scene, camera);
}
Comments (0)
Log in to comment.