import * as THREE from "three"; import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; // ---------------------------------------------------------------- DOM const canvas = document.querySelector("#universe"); const hud = { sector: document.querySelector("#sector"), status: document.querySelector("#status"), authored: document.querySelector("#authored"), discovered: document.querySelector("#discovered"), card: document.querySelector("#hud"), phenomenon: document.querySelector("#phenomenon"), distance: document.querySelector("#distance"), name: document.querySelector("#body-name"), science: document.querySelector("#science"), transWrap: document.querySelector("#transmission-wrap"), civ: document.querySelector("#civ"), transmission: document.querySelector("#transmission"), log: document.querySelector("#log"), logList: document.querySelector("#log-list"), toast: document.querySelector("#event-toast"), }; const overlay = document.querySelector("#overlay"); const engage = document.querySelector("#engage"); const introEl = document.querySelector("#intro"); // ---------------------------------------------------------------- renderer const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.05; const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x01030a, 0.000045); const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 60000); const yaw = new THREE.Object3D(); const pitch = new THREE.Object3D(); yaw.add(pitch); pitch.add(camera); scene.add(yaw); yaw.position.set(0, 0, 1200); const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); const bloom = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.9, 0.55, 0.0); composer.addPass(bloom); scene.add(new THREE.AmbientLight(0x4a5e88, 0.5)); // ---------------------------------------------------------------- state const clock = new THREE.Clock(); const keys = new Set(); const velocity = new THREE.Vector3(); const move = new THREE.Vector3(); let flying = false; const bodies = []; // { data, group, anchor, mesh, glow, effects, payload, requested, revealed, animate } const cache = new Map(); let discoveredCount = 0; let nearest = null; let lastEvent = 0; // ---------------------------------------------------------------- textures function radialTexture(stops) { const c = document.createElement("canvas"); c.width = c.height = 128; const ctx = c.getContext("2d"); const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64); for (const [o, col] of stops) g.addColorStop(o, col); ctx.fillStyle = g; ctx.fillRect(0, 0, 128, 128); return new THREE.CanvasTexture(c); } const glowTex = radialTexture([ [0, "rgba(255,255,255,1)"], [0.25, "rgba(255,255,255,0.8)"], [0.5, "rgba(255,255,255,0.28)"], [1, "rgba(255,255,255,0)"], ]); const softTex = radialTexture([ [0, "rgba(255,255,255,0.7)"], [0.4, "rgba(255,255,255,0.25)"], [1, "rgba(255,255,255,0)"], ]); function glowSprite(color, scale) { const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, color: new THREE.Color(color), transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, opacity: 0.95, })); s.scale.setScalar(scale); return s; } // ---------------------------------------------------------------- shaders const NOISE = ` float hash(vec3 p){ return fract(sin(dot(p, vec3(127.1,311.7,74.7)))*43758.5453); } float noise(vec3 p){ vec3 i=floor(p), f=fract(p); f=f*f*(3.0-2.0*f); return mix(mix(mix(hash(i),hash(i+vec3(1,0,0)),f.x),mix(hash(i+vec3(0,1,0)),hash(i+vec3(1,1,0)),f.x),f.y), mix(mix(hash(i+vec3(0,0,1)),hash(i+vec3(1,0,1)),f.x),mix(hash(i+vec3(0,1,1)),hash(i+vec3(1,1,1)),f.x),f.y),f.z); } float fbm(vec3 p){ float v=0.0,a=0.5; for(int i=0;i<5;i++){ v+=a*noise(p); p*=2.03; a*=0.5; } return v; } `; function starMaterial(shader) { return new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, primary: { value: new THREE.Color(shader.primary) }, secondary: { value: new THREE.Color(shader.secondary) }, emissive: { value: shader.emissive }, noiseScale: { value: shader.noise_scale }, }, vertexShader: `varying vec3 vp; void main(){ vp=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`, fragmentShader: ` uniform float time; uniform vec3 primary; uniform vec3 secondary; uniform float emissive; uniform float noiseScale; varying vec3 vp; ${NOISE} void main(){ vec3 p = normalize(vp)*noiseScale; float n = fbm(p + vec3(0.0, time*0.15, time*0.05)); float gran = fbm(p*3.5 - time*0.2); float h = pow(n*0.7 + gran*0.4, 1.4); vec3 col = mix(primary, secondary, smoothstep(0.25,0.95,h)); col += secondary * pow(gran, 3.0) * 0.6; gl_FragColor = vec4(col*(0.7+emissive*0.7), 1.0); }`, }); } function planetMaterial(shader) { return new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, primary: { value: new THREE.Color(shader.primary) }, secondary: { value: new THREE.Color(shader.secondary) }, noiseScale: { value: shader.noise_scale }, lightDir: { value: new THREE.Vector3(0.6, 0.4, 0.7).normalize() }, }, vertexShader: `varying vec3 vp; varying vec3 vn; void main(){ vp=position; vn=normalize(normalMatrix*normal); gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`, fragmentShader: ` uniform float time; uniform vec3 primary; uniform vec3 secondary; uniform float noiseScale; uniform vec3 lightDir; varying vec3 vp; varying vec3 vn; ${NOISE} void main(){ vec3 p = normalize(vp); float bands = fbm(vec3(p.x*1.2, p.y*noiseScale*1.4, p.z*1.2) + vec3(time*0.04,0.0,0.0)); float swirl = fbm(p*4.0 + vec3(time*0.08, 0.0, 0.0)); float t = smoothstep(0.2, 0.85, bands*0.7 + swirl*0.35); vec3 col = mix(primary, secondary, t); float light = clamp(dot(vn, lightDir), 0.0, 1.0)*0.9 + 0.12; float rim = pow(1.0 - max(dot(vn, vec3(0.0,0.0,1.0)),0.0), 3.0); gl_FragColor = vec4(col*light + secondary*rim*0.6, 1.0); }`, }); } // ---------------------------------------------------------------- body builders function buildBody(data) { const group = new THREE.Group(); const p = data.position; group.position.set(p.x, p.y, p.z); scene.add(group); const shader = { primary: data.visual.primary_color, secondary: data.visual.secondary_color, emissive: data.visual.emissive, noise_scale: 2.4, atmosphere: 0.4, }; const r = data.visual.radius; const render = data.visual.render; const entry = { data, group, animate: [], glow: null, far: glowSprite(data.visual.primary_color, r * 7) }; group.add(entry.far); if (render === "star" || render === "pulsar") { const star = new THREE.Mesh(new THREE.IcosahedronGeometry(r, 5), starMaterial(shader)); group.add(star); entry.animate.push((t) => { star.material.uniforms.time.value = t; star.rotation.y = t * 0.04; }); group.add(glowSprite(data.visual.primary_color, r * 5.2)); if (data.visual.corona) { const corona = glowSprite(data.visual.secondary_color, r * 8.5); corona.material.opacity = 0.4; group.add(corona); entry.animate.push((t) => corona.scale.setScalar(r * (8.0 + Math.sin(t * 1.3) * 0.6))); } if (render === "pulsar" || data.visual.beam) addBeams(group, entry, r, data.visual.primary_color); } else if (render === "planet") { const planet = new THREE.Mesh(new THREE.SphereGeometry(r, 96, 64), planetMaterial(shader)); group.add(planet); entry.animate.push((t) => { planet.material.uniforms.time.value = t; planet.rotation.y = t * 0.06; }); const atmo = new THREE.Mesh(new THREE.SphereGeometry(r * 1.06, 48, 32), new THREE.MeshBasicMaterial({ color: new THREE.Color(data.visual.secondary_color), transparent: true, opacity: 0.18, blending: THREE.AdditiveBlending, side: THREE.BackSide, depthWrite: false, })); group.add(atmo); if (data.visual.rings) addRings(group, r, data.visual.particles, 1.4, 2.3, false); } else if (render === "blackhole") { const core = new THREE.Mesh(new THREE.SphereGeometry(r, 48, 32), new THREE.MeshBasicMaterial({ color: 0x000000 })); group.add(core); addRings(group, r, data.visual.primary_color, 1.5, 3.4, true, data.visual.secondary_color); const photon = glowSprite(data.visual.secondary_color, r * 4.0); photon.material.opacity = 0.5; group.add(photon); if (data.visual.jets) addBeams(group, entry, r, data.visual.secondary_color, true); entry.animate.push((t) => { group.children.forEach((c) => { if (c.userData.disk) c.rotation.z = t * 0.5; }); }); } else if (render === "nebula") { addNebula(group, r, data.visual.primary_color, data.visual.secondary_color, entry); } else if (render === "remnant") { addRemnant(group, r, data.visual.primary_color, data.visual.secondary_color, entry); } return entry; } function addBeams(group, entry, r, color, vertical = false) { const mat = new THREE.MeshBasicMaterial({ color: new THREE.Color(color), transparent: true, opacity: 0.32, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, }); const len = r * (vertical ? 30 : 22); const geo = new THREE.CylinderGeometry(r * 0.05, r * 0.5, len, 24, 1, true); const a = new THREE.Mesh(geo, mat); const b = new THREE.Mesh(geo, mat.clone()); a.position.y = len / 2; b.position.y = -len / 2; b.rotation.z = Math.PI; const pivot = new THREE.Group(); pivot.add(a, b); if (!vertical) pivot.rotation.z = Math.PI / 2.6; group.add(pivot); entry.animate.push((t) => { if (!vertical) pivot.rotation.y = t * 1.4; mat.opacity = 0.22 + Math.abs(Math.sin(t * 2.0)) * 0.28; }); } function addRings(group, r, color, inner, outer, hot, color2) { const geo = new THREE.RingGeometry(r * inner, r * outer, 128); const mat = new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, c1: { value: new THREE.Color(color) }, c2: { value: new THREE.Color(color2 || color) } }, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, vertexShader: `varying vec2 vu; void main(){ vu=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);} `, fragmentShader: `uniform float time; uniform vec3 c1; uniform vec3 c2; varying vec2 vu; void main(){ float a=atan(vu.y-0.5, vu.x-0.5); float r=length(vu-0.5); float band=0.5+0.5*sin(a*${hot ? "18.0" : "40.0"}+time*${hot ? "3.0" : "0.0"}); float glow=smoothstep(0.5,0.18,abs(r-0.34)); vec3 col=mix(c2,c1,band); gl_FragColor=vec4(col, glow*${hot ? "0.9" : "0.6"}); }`, }); const ring = new THREE.Mesh(geo, mat); ring.rotation.x = Math.PI / 2 + (hot ? 0.32 : 0.5); ring.userData.disk = hot; group.add(ring); group.userData.ringMat = mat; } function addNebula(group, r, c1, c2, entry) { const colors = [c1, c2, c1]; for (let i = 0; i < 18; i++) { const col = colors[i % 3]; const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: softTex, color: new THREE.Color(col), transparent: true, opacity: 0.16, blending: THREE.AdditiveBlending, depthWrite: false, })); s.position.set((Math.random() - 0.5) * r * 1.6, (Math.random() - 0.5) * r * 1.1, (Math.random() - 0.5) * r * 1.6); s.scale.setScalar(r * (0.7 + Math.random() * 0.9)); group.add(s); } group.add(glowSprite(c2, r * 0.8)); entry.animate.push((t) => { group.rotation.y = t * 0.012; }); } function addRemnant(group, r, c1, c2, entry) { const count = 2200; const pos = new Float32Array(count * 3); const col = new Float32Array(count * 3); const a = new THREE.Color(c1), b = new THREE.Color(c2); for (let i = 0; i < count; i++) { const rad = r * (0.85 + Math.random() * 0.2); const th = Math.random() * Math.PI * 2, ph = Math.acos(Math.random() * 2 - 1); pos[i * 3] = rad * Math.sin(ph) * Math.cos(th); pos[i * 3 + 1] = rad * Math.cos(ph); pos[i * 3 + 2] = rad * Math.sin(ph) * Math.sin(th); const c = Math.random() < 0.5 ? a : b; col[i * 3] = c.r; col[i * 3 + 1] = c.g; col[i * 3 + 2] = c.b; } const geo = new THREE.BufferGeometry(); geo.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3)); geo.setAttribute("color", new THREE.Float32BufferAttribute(col, 3)); const pts = new THREE.Points(geo, new THREE.PointsMaterial({ size: r * 0.03, vertexColors: true, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, depthWrite: false, map: softTex, })); group.add(pts); group.add(glowSprite(c1, r * 0.6)); entry.animate.push((t) => { const s = 1 + Math.sin(t * 0.25) * 0.04; group.scale.setScalar(s); group.rotation.y = t * 0.02; }); } // ---------------------------------------------------------------- starfield + nebulae backdrop function backgroundStars(positions, colors, size) { const geo = new THREE.BufferGeometry(); geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geo.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); const pts = new THREE.Points(geo, new THREE.PointsMaterial({ size, sizeAttenuation: false, vertexColors: true, transparent: true, opacity: 0.95, depthWrite: false, map: glowTex, alphaTest: 0.02, })); scene.add(pts); } async function loadHygStars() { const fallback = () => { const pos = [], col = []; for (let i = 0; i < 2600; i++) { const d = 30000, th = Math.random() * Math.PI * 2, ph = Math.acos(Math.random() * 2 - 1); pos.push(d * Math.sin(ph) * Math.cos(th), d * Math.cos(ph), d * Math.sin(ph) * Math.sin(th)); const c = new THREE.Color().setHSL(0.55 + Math.random() * 0.12, 0.4, 0.7 + Math.random() * 0.25); col.push(c.r, c.g, c.b); } backgroundStars(pos, col, 1.6); }; try { const res = await fetch("/data/hyg_sample.csv"); if (!res.ok) return fallback(); const rows = (await res.text()).trim().split(/\r?\n/).slice(1); const pos = [], col = []; for (const row of rows) { const [, , ra, dec, dist, mag, spect] = row.split(","); const R = 28000; const a = (Number(ra) / 24) * Math.PI * 2, d = THREE.MathUtils.degToRad(Number(dec)); pos.push(R * Math.cos(d) * Math.cos(a), R * Math.sin(d), R * Math.cos(d) * Math.sin(a)); const c = spectralColor(spect, Number(mag)); col.push(c.r, c.g, c.b); } if (pos.length) backgroundStars(pos, col, 2.2); fallback(); // add a faint deep field behind the real catalog } catch { fallback(); } } function spectralColor(spect = "G", mag = 1) { const palette = { O: 0x9bb0ff, B: 0xaabfff, A: 0xdce6ff, F: 0xfff4ea, G: 0xfff2cc, K: 0xffd2a1, M: 0xffb38a }; const c = new THREE.Color(palette[(spect.trim()[0] || "G").toUpperCase()] || 0xffffff); return c.multiplyScalar(THREE.MathUtils.clamp(1.5 - (mag + 1.5) * 0.12, 0.5, 1.5)); } function backdropNebulae() { const spots = [ [-9000, 2000, -12000, 5200, 0x3a2f6e], [11000, -3000, 9000, 6000, 0x123b4a], [-4000, -6000, 14000, 5000, 0x4a1d3a], [8000, 5000, -16000, 7000, 0x1c3a2e], ]; for (const [x, y, z, s, c] of spots) { const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: softTex, color: new THREE.Color(c), transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false, })); sp.position.set(x, y, z); sp.scale.setScalar(s); scene.add(sp); } } // ---------------------------------------------------------------- universe load async function loadUniverse() { const manifest = await fetch("/api/universe").then((r) => r.json()); introEl.textContent = manifest.intro; for (const data of manifest.bodies) bodies.push(buildBody(data)); hud.discovered.textContent = `relics 0 / ${bodies.length}`; hud.status.textContent = "STANDBY"; } // ---------------------------------------------------------------- sector fetch + reveal async function requestSector(entry) { if (entry.requested) return; entry.requested = true; if (cache.has(entry.data.id)) { entry.payload = cache.get(entry.data.id); return; } try { const r = await fetch(`/api/sector/${entry.data.id}`); const payload = await r.json(); entry.payload = payload; cache.set(entry.data.id, payload); } catch { entry.requested = false; // allow retry } } function showCard(entry) { const p = entry.payload; hud.card.classList.remove("hidden"); hud.sector.textContent = `SECTOR ${entry.data.id.toUpperCase()}`; hud.phenomenon.textContent = (p ? p.body.phenomenon : entry.data.name).toUpperCase(); hud.name.textContent = p ? p.body.name : entry.data.name; hud.authored.textContent = `author: ${p ? p.authored_by : "…"}`; if (p) { hud.science.textContent = p.fact_layer.explanation; } else { hud.science.textContent = "Authoring sector… approaching telemetry."; } hud.transWrap.classList.add("hidden"); } function revealTransmission(entry) { const p = entry.payload; if (!p || entry.revealed) return; entry.revealed = true; hud.civ.textContent = p.fiction_layer.civilization; hud.transmission.textContent = `“${p.fiction_layer.transmission}”`; hud.transWrap.classList.remove("hidden"); hud.status.textContent = "TRANSMISSION LOCK"; discoveredCount += 1; hud.discovered.textContent = `relics ${discoveredCount} / ${bodies.length}`; addLog(p); } function addLog(p) { hud.log.classList.remove("hidden"); const li = document.createElement("li"); li.innerHTML = `${p.body.name} — ${p.fiction_layer.transmission}`; hud.logList.prepend(li); while (hud.logList.children.length > 8) hud.logList.lastChild.remove(); } // ---------------------------------------------------------------- cosmic events function maybeEvent(t) { if (t - lastEvent < 22 || Math.random() > 0.004) return; lastEvent = t; Math.random() < 0.5 ? supernova() : comet(); } function supernova() { const far = bodies.filter((b) => b.group.position.distanceTo(yaw.position) > 1500); if (!far.length) return; const target = far[Math.floor(Math.random() * far.length)]; const flash = glowSprite(0xfff0d0, 40); flash.position.copy(target.group.position); scene.add(flash); toast("◎ SUPERNOVA DETECTED — a star is seeding the void with heavy elements"); let s = 0; const grow = () => { s += 0.04; flash.scale.setScalar(40 + s * 2600); flash.material.opacity = Math.max(0, 1 - s); if (s < 1) requestAnimationFrame(grow); else scene.remove(flash); }; grow(); } function comet() { const start = new THREE.Vector3().copy(yaw.position).add(new THREE.Vector3((Math.random() - 0.5) * 3000, 800, -2500)); const head = glowSprite(0xbfe9ff, 60); head.position.copy(start); scene.add(head); const vel = new THREE.Vector3((Math.random() - 0.5) * 60, -8, 120); toast("☄ COMET passing — primordial ice from the outer dark"); let life = 0; const fly = () => { life += 1; head.position.add(vel); head.material.opacity = Math.max(0, 1 - life / 120); if (life < 120) requestAnimationFrame(fly); else scene.remove(head); }; fly(); } let toastTimer = 0; function toast(msg) { hud.toast.textContent = msg; hud.toast.classList.remove("hidden"); clearTimeout(toastTimer); toastTimer = setTimeout(() => hud.toast.classList.add("hidden"), 6000); } // ---------------------------------------------------------------- controls engage.addEventListener("click", () => { overlay.style.opacity = "0"; setTimeout(() => overlay.classList.add("hidden"), 600); canvas.requestPointerLock?.(); flying = true; hud.status.textContent = "FLIGHT ONLINE"; }); document.addEventListener("pointerlockchange", () => { if (document.pointerLockElement !== canvas && flying) hud.status.textContent = "CURSOR FREE · CLICK TO RESUME"; }); canvas.addEventListener("click", () => { if (flying) canvas.requestPointerLock?.(); }); document.addEventListener("mousemove", (e) => { if (document.pointerLockElement !== canvas) return; yaw.rotation.y -= e.movementX * 0.0021; pitch.rotation.x = THREE.MathUtils.clamp(pitch.rotation.x - e.movementY * 0.0021, -1.45, 1.45); }); document.addEventListener("keydown", (e) => keys.add(e.code)); document.addEventListener("keyup", (e) => keys.delete(e.code)); window.addEventListener("resize", () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); }); function updateFlight(dt) { move.set(0, 0, 0); if (keys.has("KeyW") || keys.has("ArrowUp")) move.z -= 1; if (keys.has("KeyS") || keys.has("ArrowDown")) move.z += 1; if (keys.has("KeyA") || keys.has("ArrowLeft")) move.x -= 1; if (keys.has("KeyD") || keys.has("ArrowRight")) move.x += 1; if (keys.has("Space")) move.y += 1; if (keys.has("ShiftLeft") && keys.has("KeyW")) move.z -= 2.2; // boost if (keys.has("ControlLeft") || keys.has("KeyC")) move.y -= 1; const boost = keys.has("ShiftLeft") ? 2.4 : 1; const speed = 420 * boost; const q = new THREE.Quaternion(); camera.getWorldQuaternion(q); move.applyQuaternion(q); velocity.lerp(move.multiplyScalar(speed), 0.06); yaw.position.addScaledVector(velocity, dt); } // ---------------------------------------------------------------- main loop function animate() { requestAnimationFrame(animate); const dt = Math.min(clock.getDelta(), 0.05); const t = clock.elapsedTime; if (flying) updateFlight(dt); // nearest body + prefetch + reveal let best = null, bestD = Infinity; for (const entry of bodies) { const d = entry.group.position.distanceTo(yaw.position); for (const fn of entry.animate) fn(t); if (entry.group.userData.ringMat) entry.group.userData.ringMat.uniforms.time.value = t; entry.far.material.opacity = THREE.MathUtils.clamp((d - 600) / 4000, 0.0, 0.95); const trigger = entry.data.visual.radius * 26 + 900; if (d < trigger) requestSector(entry); if (d < bestD) { bestD = d; best = entry; } } if (best) { const approach = best.data.visual.radius * 14 + 600; if (bestD < approach) { if (nearest !== best) { nearest = best; best.revealed = false; showCard(best); } if (!best.payload && best.requested) requestSector(best); if (best.payload && hud.science.textContent.startsWith("Authoring")) showCard(best); hud.distance.textContent = `${Math.round(bestD)} u`; hud.status.textContent = bestD < best.data.visual.radius * 5 + 200 ? "ARRIVAL" : "APPROACH"; if (bestD < best.data.visual.radius * 6 + 260) revealTransmission(best); } else if (nearest && bestD > approach * 1.4) { nearest = null; hud.card.classList.add("hidden"); if (flying) hud.status.textContent = "CRUISING"; } } if (flying) maybeEvent(t); composer.render(); } // ---------------------------------------------------------------- boot loadHygStars(); backdropNebulae(); loadUniverse(); animate();