// Celestial layer (Wave 3.2) — the sky around the world: the 12 zodiac signs as // a labeled ecliptic ring, plus the Sun, Moon and naked-eye planets, all placed // in their TRUE directions and locked to the stars (inertial frame), not spun // with the globe. A "distant visual" you see when zoomed out. // // Everything is computed (no external data): Sun/Moon from Cesium's Simon1994 // model, planets from Schlyter's low-precision series, the zodiac from the // ecliptic geometry. Directions are correct; distances are COMPRESSED to one // display radius so they ring the Earth instead of vanishing at true scale. export default function create(ctx) { const { viewer, Cesium, lib, ui } = ctx; const scene = viewer.scene; const ds = new Cesium.CustomDataSource('celestial'); viewer.dataSources.add(ds); const R = 8.0e7; // display radius (~80,000 km) — a ring beyond GEO const OBLIQ = Cesium.Math.toRadians(23.4393); // ecliptic obliquity const cosE = Math.cos(OBLIQ), sinE = Math.sin(OBLIQ); // Shared per-frame inertial→fixed rotation (computed once per tick, reused by // every object's position callback). let frameIcrf = Cesium.Matrix3.IDENTITY.clone(); scene.postUpdate.addEventListener((s, time) => { const m = Cesium.Transforms.computeIcrfToFixedMatrix(time); if (Cesium.defined(m)) frameIcrf = m; }); // Inertial unit direction → fixed-frame position at radius `rad`. const scratch = new Cesium.Cartesian3(); function fixedPos(dirInertial, rad) { Cesium.Matrix3.multiplyByVector(frameIcrf, dirInertial, scratch); return Cesium.Cartesian3.multiplyByScalar(scratch, rad, new Cesium.Cartesian3()); } // A CallbackProperty position from a function(time) -> inertial unit vector. function skyPosition(dirFn, rad = R) { return new Cesium.CallbackProperty((time) => fixedPos(dirFn(time), rad), false); } // Ecliptic (lon λ, lat β=0) → inertial equatorial unit vector. function eclipticDir(lonRad, latRad = 0) { const cl = Math.cos(lonRad), sl = Math.sin(lonRad), cb = Math.cos(latRad), sb = Math.sin(latRad); const xe = cb * cl; const ye = cb * sl * cosE - sb * sinE; const ze = cb * sl * sinE + sb * cosE; return new Cesium.Cartesian3(xe, ye, ze); } // Days since 2000 Jan 0.0 UTC (Schlyter's epoch). // d = JD − 2451543.5 (Schlyter epoch); unix-epoch JD is 2440587.5 → offset 10956.0. const dayNumber = (time) => Cesium.JulianDate.toDate(time).getTime() / 86400000 - 10956.0; // ---- planets (Schlyter low-precision) ---- const rev = (x) => x - Math.floor(x / 360) * 360; const D2R = Math.PI / 180; // Orbital elements as functions of day d (degrees / AU). const ELEM = { Mercury: (d) => ({ N: 48.3313 + 3.24587e-5 * d, i: 7.0047 + 5.00e-8 * d, w: 29.1241 + 1.01444e-5 * d, a: 0.387098, e: 0.205635 + 5.59e-10 * d, M: 168.6562 + 4.0923344368 * d }), Venus: (d) => ({ N: 76.6799 + 2.46590e-5 * d, i: 3.3946 + 2.75e-8 * d, w: 54.8910 + 1.38374e-5 * d, a: 0.723330, e: 0.006773 - 1.302e-9 * d, M: 48.0052 + 1.6021302244 * d }), Mars: (d) => ({ N: 49.5574 + 2.11081e-5 * d, i: 1.8497 - 1.78e-8 * d, w: 286.5016 + 2.92961e-5 * d, a: 1.523688, e: 0.093405 + 2.516e-9 * d, M: 18.6021 + 0.5240207766 * d }), Jupiter: (d) => ({ N: 100.4542 + 2.76854e-5 * d, i: 1.3030 - 1.557e-7 * d, w: 273.8777 + 1.64505e-5 * d, a: 5.20256, e: 0.048498 + 4.469e-9 * d, M: 19.8950 + 0.0830853001 * d }), Saturn: (d) => ({ N: 113.6634 + 2.38980e-5 * d, i: 2.4886 - 1.081e-7 * d, w: 339.3939 + 2.97661e-5 * d, a: 9.55475, e: 0.055546 - 9.499e-9 * d, M: 316.9670 + 0.0334442282 * d }), }; function sunRect(d) { // Sun's geocentric ecliptic rectangular (AU) const w = 282.9404 + 4.70935e-5 * d, e = 0.016709 - 1.151e-9 * d, M = rev(356.0470 + 0.9856002585 * d); const E = M + (180 / Math.PI) * e * Math.sin(M * D2R) * (1 + e * Math.cos(M * D2R)); const xv = Math.cos(E * D2R) - e, yv = Math.sqrt(1 - e * e) * Math.sin(E * D2R); const v = Math.atan2(yv, xv), r = Math.hypot(xv, yv), lon = v + w * D2R; return { x: r * Math.cos(lon), y: r * Math.sin(lon) }; } function planetDir(name, time) { const d = dayNumber(time); const el = ELEM[name](d); const N = el.N * D2R, i = el.i * D2R, w = el.w * D2R, e = el.e, M = rev(el.M) * D2R; let E = M + e * Math.sin(M) * (1 + e * Math.cos(M)); for (let k = 0; k < 5; k++) E = E - (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E)); const xv = el.a * (Math.cos(E) - e), yv = el.a * Math.sqrt(1 - e * e) * Math.sin(E); const v = Math.atan2(yv, xv), r = Math.hypot(xv, yv), u = v + w; // Heliocentric ecliptic rectangular. const xh = r * (Math.cos(N) * Math.cos(u) - Math.sin(N) * Math.sin(u) * Math.cos(i)); const yh = r * (Math.sin(N) * Math.cos(u) + Math.cos(N) * Math.sin(u) * Math.cos(i)); const zh = r * Math.sin(u) * Math.sin(i); // Geocentric = heliocentric + (Earth→Sun) = helio + sun-geocentric. const s = sunRect(d); const xg = xh + s.x, yg = yh + s.y, zg = zh; const lon = Math.atan2(yg, xg), lat = Math.atan2(zg, Math.hypot(xg, yg)); return eclipticDir(lon, lat); } const sunDir = (time) => { const p = Cesium.Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame(time); return Cesium.Cartesian3.normalize(p, new Cesium.Cartesian3()); }; const moonDir = (time) => { const p = Cesium.Simon1994PlanetaryPositions.computeMoonPositionInEarthInertialFrame(time); return Cesium.Cartesian3.normalize(p, new Cesium.Cartesian3()); }; // ---- build entities ---- const bodyLabel = (extra) => ({ font: '13px "Segoe UI", system-ui, sans-serif', fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.fromCssColorString('#05080b'), outlineWidth: 3, style: Cesium.LabelStyle.FILL_AND_OUTLINE, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(0, -12), ...extra, }); const BODIES = [ { name: '☉ Sun', dir: sunDir, color: '#ffcf40', size: 22, rad: R }, { name: '☾ Moon', dir: moonDir, color: '#d8d8e0', size: 16, rad: R * 0.9 }, { name: '☿ Mercury', dir: (t) => planetDir('Mercury', t), color: '#b0a080', size: 7 }, { name: '♀ Venus', dir: (t) => planetDir('Venus', t), color: '#f5e8c0', size: 10 }, { name: '♂ Mars', dir: (t) => planetDir('Mars', t), color: '#ff6b4d', size: 8 }, { name: '♃ Jupiter', dir: (t) => planetDir('Jupiter', t), color: '#e8c090', size: 12 }, { name: '♄ Saturn', dir: (t) => planetDir('Saturn', t), color: '#e8dcb0', size: 10 }, ]; for (const b of BODIES) { ds.entities.add({ name: b.name, position: skyPosition(b.dir, b.rad || R), point: { pixelSize: b.size, color: lib.cz(b.color), outlineColor: Cesium.Color.BLACK, outlineWidth: 1 }, label: bodyLabel({ text: b.name, fillColor: lib.cz(b.color) }), }); } // Zodiac: 12 sign labels at the centre of each 30° sector + the ecliptic ring. const SIGNS = ['♈ Aries', '♉ Taurus', '♊ Gemini', '♋ Cancer', '♌ Leo', '♍ Virgo', '♎ Libra', '♏ Scorpio', '♐ Sagittarius', '♑ Capricorn', '♒ Aquarius', '♓ Pisces']; for (let k = 0; k < 12; k++) { const lon = Cesium.Math.toRadians(k * 30 + 15); ds.entities.add({ name: SIGNS[k], position: fixedPosStatic(eclipticDir(lon), R * 1.06), // static ok: sky barely moves in ±6h label: bodyLabel({ text: SIGNS[k], font: '12px "Segoe UI", system-ui, sans-serif', fillColor: lib.cz('#8fb7ff'), pixelOffset: new Cesium.Cartesian2(0, 0) }), }); } // Ecliptic ring + 30° boundary ticks (rebuilt each frame in the fixed frame). ds.entities.add({ name: 'Ecliptic', polyline: { width: 1.5, material: new Cesium.PolylineGlowMaterialProperty({ glowPower: 0.25, color: lib.cz('#8fb7ff', 0.5) }), positions: new Cesium.CallbackProperty(() => { const pts = []; for (let a = 0; a <= 360; a += 4) pts.push(fixedPos(eclipticDir(Cesium.Math.toRadians(a)), R * 1.02)); return pts; }, false), }, }); // A single static-ish snapshot position (zodiac labels don't need per-frame update // at this zoom; recompute once so they sit in the current fixed frame). function fixedPosStatic(dirInertial, rad) { return skyPosition(() => dirInertial, rad); // still callback so it tracks Earth rotation } ui.addLayer('celestial', 'Sky: zodiac · planets', false, (on) => { ds.show = on; }, 'Space'); ui.setStatus('celestial', 'Sun · Moon · 5 planets · zodiac', 'ok'); ds.show = false; return { id: 'celestial' }; }