14
Inner Solar System (3D)
tap to launch a comet ยท drag to orbit the camera
idle
357 lines ยท three
view source
// Inner solar system in three.js โ Sun + Mercury, Venus, Earth (+ Moon), Mars,
// plus distant Jupiter, a Mars/Jupiter asteroid belt, and clickable comets.
//
// Distances and sizes are not literal AU/km โ they're squashed into something
// you can actually see on a 600px canvas. But the *relative* ordering and the
// orbital periods follow Kepler's third law: T = sqrt(a^3) in our units, so
// the relative speeds match reality. Mercury whips around fast, Jupiter crawls.
// Semi-major axes (scene units). Real ratios are roughly 0.39 : 0.72 : 1 : 1.52
// : 5.2 in AU; we scale by ~8 for visibility (Jupiter is squashed harder).
const PLANETS = [
{ name: "mercury", a: 3.1, size: 0.35, color: 0xa6a29c, axial: 1.2 },
{ name: "venus", a: 5.8, size: 0.75, color: 0xe6c98a, axial: 0.4 },
{ name: "earth", a: 8.0, size: 0.85, color: 0x4a90d6, axial: 1.5 },
{ name: "mars", a: 12.2, size: 0.55, color: 0xc1502c, axial: 1.4 },
{ name: "jupiter", a: 24.0, size: 1.6, color: 0xd4a574, axial: 2.1 },
];
// Master orbital speed. We multiply by 1/T per Kepler, where T = sqrt(a^3).
// 0.45 gives Earth a ~14 second year โ fast enough to feel alive, slow
// enough that you can track individual planets.
const ORBIT_SPEED = 0.45;
const COMET_CAP = 5;
const BELT_COUNT = 220;
let sun;
let pointLight;
let starfield;
let planets = []; // { mesh, pivot, a, T, axial, phase, name }
let moon, moonPivot;
let belt; // THREE.Points for asteroid belt
let beltData; // parallel array of { a, e, phase, omega, inc } per particle
let elapsed = 0;
let userYaw = 0.4, userPitch = 0.5;
let isDragging = false;
let dragMoved = false;
let lastMouseX = 0, lastMouseY = 0;
let comets = []; // { head, trail, trailPositions, drawCount, writeIdx, a, e, omega, inc, M, n, color, life }
function makeStarfield(count) {
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const u = Math.random() * 2 - 1;
const theta = Math.random() * Math.PI * 2;
const r = 180 + Math.random() * 40;
const s = Math.sqrt(1 - u * u);
positions[i * 3] = r * s * Math.cos(theta);
positions[i * 3 + 1] = r * u;
positions[i * 3 + 2] = r * s * Math.sin(theta);
const tint = 0.7 + Math.random() * 0.3;
const warm = Math.random();
colors[i * 3] = tint * (warm > 0.7 ? 1.0 : 0.85);
colors[i * 3 + 1] = tint * 0.9;
colors[i * 3 + 2] = tint * (warm > 0.7 ? 0.8 : 1.0);
}
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: 0.6,
vertexColors: true,
sizeAttenuation: true,
transparent: true,
opacity: 0.9,
});
return new THREE.Points(geo, mat);
}
function makeOrbitLine(radius, segments = 128) {
const positions = new Float32Array((segments + 1) * 3);
for (let i = 0; i <= segments; i++) {
const t = (i / segments) * Math.PI * 2;
positions[i * 3] = Math.cos(t) * radius;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = Math.sin(t) * radius;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
const mat = new THREE.LineBasicMaterial({
color: 0x33405a,
transparent: true,
opacity: 0.35,
});
return new THREE.Line(geo, mat);
}
function makeBelt() {
// 200+ small Points scattered between Mars (12.2) and Jupiter (24.0),
// each with its own eccentricity, phase, inclination. We treat each
// particle as if on its own Keplerian orbit; we keep parameters in a
// parallel array and recompute positions every frame.
const positions = new Float32Array(BELT_COUNT * 3);
const colors = new Float32Array(BELT_COUNT * 3);
beltData = new Array(BELT_COUNT);
for (let i = 0; i < BELT_COUNT; i++) {
const a = 14 + Math.random() * 7.5; // semi-major in [14, 21.5]
const e = Math.random() * 0.18; // small eccentricity
const phase = Math.random() * Math.PI * 2;
const omega = Math.random() * Math.PI * 2; // argument of periapsis
const inc = (Math.random() - 0.5) * 0.18; // ยฑ~10ยฐ tilt
beltData[i] = { a, e, phase, omega, inc, n: 1 / Math.sqrt(a * a * a) };
const shade = 0.55 + Math.random() * 0.35;
colors[i * 3] = shade * 0.85;
colors[i * 3 + 1] = shade * 0.78;
colors[i * 3 + 2] = shade * 0.65;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3).setUsage(THREE.DynamicDrawUsage),
);
geo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
const mat = new THREE.PointsMaterial({
size: 0.18,
vertexColors: true,
sizeAttenuation: true,
transparent: true,
opacity: 0.9,
});
return new THREE.Points(geo, mat);
}
// Solve Kepler's equation M = E - e*sin(E) by Newton iteration. Good to
// ~1e-6 in 5 iterations for e < 0.9.
function solveKepler(M, e) {
let E = M;
for (let i = 0; i < 6; i++) {
const f = E - e * Math.sin(E) - M;
const fp = 1 - e * Math.cos(E);
E -= f / fp;
}
return E;
}
function spawnComet(scene) {
// Highly eccentric orbit with random inclination โ perihelion grazes
// the Sun (q ~ 1.5โ4 scene units), aphelion well beyond Jupiter so the
// comet visibly elongates near the sun.
const q = 1.5 + Math.random() * 2.5;
const e = 0.75 + Math.random() * 0.2; // [0.75, 0.95]
const a = q / (1 - e);
const omega = Math.random() * Math.PI * 2;
const inc = (Math.random() - 0.5) * Math.PI * 0.7; // up to ~63ยฐ tilt
const node = Math.random() * Math.PI * 2; // longitude of ascending node
// Start the comet *near perihelion* (mean anomaly small) so the user
// sees something dramatic right away.
const M0 = (Math.random() < 0.5 ? -1 : 1) * (0.05 + Math.random() * 0.4);
const n = ORBIT_SPEED / Math.sqrt(a * a * a); // mean motion
const TRAIL_LEN = 80;
const trailPositions = new Float32Array(TRAIL_LEN * 3);
const trailColors = new Float32Array(TRAIL_LEN * 3);
const geo = new THREE.BufferGeometry();
geo.setAttribute(
"position",
new THREE.BufferAttribute(trailPositions, 3).setUsage(THREE.DynamicDrawUsage),
);
geo.setAttribute(
"color",
new THREE.BufferAttribute(trailColors, 3).setUsage(THREE.DynamicDrawUsage),
);
geo.setDrawRange(0, 0);
const mat = new THREE.LineBasicMaterial({
vertexColors: true,
transparent: true,
opacity: 0.9,
});
const trail = new THREE.Line(geo, mat);
scene.add(trail);
const headGeo = new THREE.SphereGeometry(0.18, 12, 10);
const headMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const head = new THREE.Mesh(headGeo, headMat);
scene.add(head);
// Color hue from a small palette โ icy whites and cyans.
const hue = 0.5 + Math.random() * 0.15;
const c = {
head,
trail,
trailPositions,
trailColors,
trailGeo: geo,
drawCount: 0,
writeIdx: 0,
trailLen: TRAIL_LEN,
a, e, omega, inc, node, M: M0, n,
hue,
age: 0,
fadingOut: false,
fadeT: 0,
};
comets.push(c);
// Cap: oldest fades out gracefully.
if (comets.length > COMET_CAP) {
const oldest = comets[0];
oldest.fadingOut = true;
oldest.fadeT = 0;
}
}
function updateComet(c, dt) {
c.age += dt;
c.M += c.n * dt;
const E = solveKepler(c.M, c.e);
// Position in orbital plane (perifocal frame).
const cosE = Math.cos(E), sinE = Math.sin(E);
const xp = c.a * (cosE - c.e);
const yp = c.a * Math.sqrt(1 - c.e * c.e) * sinE;
// Rotate by argument of perihelion (omega) in orbital plane, then tilt
// by inclination around X, then rotate around Y by longitude of node.
const co = Math.cos(c.omega), so = Math.sin(c.omega);
let x = xp * co - yp * so;
let y = xp * so + yp * co;
let z = 0;
// inclination about X
const ci = Math.cos(c.inc), si = Math.sin(c.inc);
const y2 = y * ci - z * si;
const z2 = y * si + z * ci;
y = y2; z = z2;
// node about Y
const cn = Math.cos(c.node), sn = Math.sin(c.node);
const x3 = x * cn + z * sn;
const z3 = -x * sn + z * cn;
x = x3; z = z3;
// Push to ring buffer (trail).
const i = c.writeIdx * 3;
c.trailPositions[i] = x;
c.trailPositions[i + 1] = y;
c.trailPositions[i + 2] = z;
// Color: brighter near head, faded near tail. We re-color the whole
// ring each frame so the gradient stays anchored at the head.
c.writeIdx = (c.writeIdx + 1) % c.trailLen;
if (c.drawCount < c.trailLen) c.drawCount++;
// Position the head sphere.
c.head.position.set(x, y, z);
// Distance from sun controls glow intensity โ comets brighten near
// perihelion (real comets do this from outgassing).
const r = Math.sqrt(x * x + y * y + z * z);
const closeness = Math.max(0, Math.min(1, 1 - (r - 1.5) / 30));
const headHsl = c.fadingOut
? Math.max(0, 1 - c.fadeT / 1.5)
: 1.0;
c.head.material.color.setHSL(c.hue, 0.4, 0.6 + 0.35 * closeness);
c.head.scale.setScalar((0.7 + 1.4 * closeness) * headHsl);
// Repaint trail colors as a gradient from head -> tail with fade.
const arr = c.trailColors;
const fadeMul = c.fadingOut ? Math.max(0, 1 - c.fadeT / 1.5) : 1;
for (let k = 0; k < c.drawCount; k++) {
// Distance back from the head in ring-buffer terms.
const stepsBack = (c.writeIdx - 1 - k + c.trailLen) % c.trailLen;
const t = k / c.drawCount; // 0 = head-side, 1 = tail-end
const bright = (1 - t) * (0.5 + 0.5 * closeness) * fadeMul;
const idx = stepsBack * 3;
// Cool blue-white near head, dimmer cyan toward tail.
arr[idx] = bright * (0.7 + 0.3 * (1 - t));
arr[idx + 1] = bright * (0.85 + 0.15 * (1 - t));
arr[idx + 2] = bright * 1.0;
}
c.trailGeo.attributes.position.needsUpdate = true;
c.trailGeo.attributes.color.needsUpdate = true;
c.trailGeo.setDrawRange(0, c.drawCount);
}
function disposeComet(c, scene) {
scene.remove(c.head);
scene.remove(c.trail);
c.head.geometry.dispose();
c.head.material.dispose();
c.trail.geometry.dispose();
c.trail.material.dispose();
}
function init({ scene, camera, renderer, width, height }) {
renderer.setClearColor(0x02030a, 1);
camera.position.set(0, 18, 30);
camera.lookAt(0, 0, 0);
// Sun โ emissive sphere so it glows even without the point light hitting it.
const sunGeo = new THREE.SphereGeometry(2.0, 32, 24);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xffd560 });
sun = new THREE.Mesh(sunGeo, sunMat);
scene.add(sun);
pointLight = new THREE.PointLight(0xfff0c4, 2.4, 0, 2);
pointLight.position.set(0, 0, 0);
scene.add(pointLight);
scene.add(new THREE.AmbientLight(0x222838, 0.5));
starfield = makeStarfield(1400);
scene.add(starfield);
for (const p of PLANETS) {
const geo = new THREE.SphereGeometry(p.size, 32, 24);
const mat = new THREE.MeshStandardMaterial({
color: p.color,
roughness: 0.85,
metalness: 0.05,
});
const mesh = new THREE.Mesh(geo, mat);
const pivot = new THREE.Object3D();
mesh.position.set(p.a, 0, 0);
pivot.add(mesh);
scene.add(pivot);
const orbitLine = makeOrbitLine(p.a);
// Outer orbits are dimmer so the inner system stays the focus.
if (p.name === "jupiter") {
orbitLine.material.opacity = 0.18;
}
scene.add(orbitLine);
const T = Math.sqrt(p.a * p.a * p.a);
planets.push({
mesh,
pivot,
a: p.a,
T,
axial: p.axial,
phase: Math.random() * Math.PI * 2,
name: p.name,
});
pivot.rotation.y = planets[planets.length - 1].phase;
}
// Moon โ child of Earth's mesh so it inherits Earth's orbital position.
const earth = planets.find((p) => p.name === "earth");
const moonGeo = new THREE.SphereGeometry(0.22, 20, 16);
const moonMat = new THREE.MeshStandardMaterial({
color: 0xd4d2cc,
roughness: 0.95,
metalness: 0.0,
});
moon = new THREE.Mesh(moonGeo, moonMat);
moon.position.set(1.6, 0, 0);
moonPivot = new THREE.Object3D();
moonPivot.add(moon);
earth.mesh.add(moonPivot);
// Asteroid belt.
belt = makeBelt();
scene.add(belt);
return { scene, camera };
}
function tick({ dt, time, scene, camera, renderer, width, height, input }) {
elapsed += dt;
// Drain clicks โ each click spawns a comet (unless it was the end of a
// camera drag).
const clicks = input.consumeClicks();
if (clicks > 0 && !dragMoved) {
for (let i = 0; i < clicks; i++) spawnComet(scene);
}
// Camera control โ drag to orbit, auto-rotate when idle.
if (input.mouseDown) {
if (!isDragging) {
isDragging = true;
dragMoved = false;
lastMouseX = input.mouseX;
lastMouseY = input.mouseY;
} else {
const dx = input.mouseX - lastMouseX;
const dy = input.mouseY - lastMouseY;
if (Math.abs(dx) + Math.abs(dy) > 2) dragMoved = true;
userYaw -= dx * 0.005;
userPitch -= dy * 0.005;
userPitch = Math.max(-1.3, Math.min(1.3, userPitch));
lastMouseX = input.mouseX;
lastMouseY = input.mouseY;
}
} else {
isDragging = false;
// Reset dragMoved on next mousedown only; keep it true through the
// current frame so the click-up after a drag doesn't spawn a comet.
userYaw += dt * 0.07;
}
// Pull camera back a touch to fit Jupiter's orbit.
const r = 48;
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);
// Sun spins slowly and pulses a hair.
sun.rotation.y += dt * 0.1;
const pulse = 1.0 + 0.02 * Math.sin(elapsed * 1.3);
sun.scale.setScalar(pulse);
// Planets: advance orbital angle by ORBIT_SPEED / T (Kepler), spin on axis.
for (const p of planets) {
p.pivot.rotation.y += (dt * ORBIT_SPEED) / p.T;
p.mesh.rotation.y += dt * p.axial;
}
// Moon โ exaggerated to be visible.
if (moonPivot) {
const earthT = planets.find((p) => p.name === "earth").T;
moonPivot.rotation.y += (dt * ORBIT_SPEED * 4) / earthT * 6;
}
// Asteroid belt โ Keplerian elliptical motion per particle.
const beltPos = belt.geometry.attributes.position.array;
for (let i = 0; i < BELT_COUNT; i++) {
const d = beltData[i];
d.phase += d.n * ORBIT_SPEED * dt;
const M = d.phase;
const E = solveKepler(M, d.e);
const xp = d.a * (Math.cos(E) - d.e);
const yp = d.a * Math.sqrt(1 - d.e * d.e) * Math.sin(E);
const co = Math.cos(d.omega), so = Math.sin(d.omega);
let x = xp * co - yp * so;
let z = xp * so + yp * co;
// tilt by inclination about X
const y = z * Math.sin(d.inc);
z = z * Math.cos(d.inc);
beltPos[i * 3] = x;
beltPos[i * 3 + 1] = y;
beltPos[i * 3 + 2] = z;
}
belt.geometry.attributes.position.needsUpdate = true;
// Comets โ update, fade, retire.
for (let i = comets.length - 1; i >= 0; i--) {
const c = comets[i];
updateComet(c, dt);
if (c.fadingOut) {
c.fadeT += dt;
if (c.fadeT >= 1.5) {
disposeComet(c, scene);
comets.splice(i, 1);
}
}
}
// Starfield drift.
starfield.rotation.y += dt * 0.005;
renderer.render(scene, camera);
}
Comments (0)
Log in to comment.