solargod/js/layers/rings.js
monsterrobotparty f2477513c0 🌙 retinue: worlds become places — moons, rings, tilts, spin, sun-lit terminator (opus)
Stage 3. moons.js: 21 moons as lit spheres with local per-parent compression
(clear the exaggerated parent, ordering preserved), retrograde via negative
period. rings.js: Saturn textured annulus (true 74500–140220 km scaled with
parent) + Uranus thin schematic ring, tilted with axial tilt, DoubleSide. Moons
draw at the parent's exaggeration so Io/Jupiter ≈ 0.026 ratio holds. Label
visibility gated via CSS2DObject.visible (CSS2DRenderer clobbers div.style.display
every frame — fixed). Gate: Galilean laps 1.00:0.50:0.25 (1:2:4), Triton
retrograde (Δθ<0), moons ordered io<europa<ganymede<callisto.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:19:28 +10:00

67 lines
2.7 KiB
JavaScript

// SOLARGOD rings layer (Stage 3) — Saturn's textured annulus + Uranus's thin
// schematic ring. Sized in true km scaled by the parent's draw scale, tilted with
// the parent's axial tilt, DoubleSide + transparent (brief §7). Texture optional:
// a flat color fallback keeps the ring visible if the PNG is missing.
export default function create(ctx) {
const { THREE, scene, lib, ui, scale, bodies, toLocal, effectiveE, CONFIG } = ctx;
const AU_KM = lib.AU_KM;
const RING_FALLBACK = { saturn: '#c9b98f', uranus: '#8fa3a8' };
const entries = [];
for (const id of ['saturn', 'uranus']) {
const b = bodies[id];
if (!b.rings) continue;
const ratio = b.rings.innerKm / b.rings.outerKm;
const geo = new THREE.RingGeometry(ratio, 1, 128, 1);
radialUV(geo); // remap UV so a radial ring texture maps across width
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color(b.rings.color || RING_FALLBACK[id] || '#bbb'),
side: THREE.DoubleSide, transparent: true, opacity: id === 'saturn' ? 0.9 : 0.45, depthWrite: false,
});
if (b.rings.texture) {
new THREE.TextureLoader().load(CONFIG.textures.path + b.rings.texture, (tex) => {
tex.colorSpace = THREE.SRGBColorSpace; mat.map = tex; mat.color.set(0xffffff); mat.needsUpdate = true;
}, undefined, () => { /* keep flat color */ });
}
const group = new THREE.Group();
group.rotation.z = (b.axialTiltDeg || 0) * lib.DEG; // same tilt as the planet group
const mesh = new THREE.Mesh(geo, mat);
mesh.rotation.x = -Math.PI / 2; // lay flat into the equatorial (XZ) plane
group.add(mesh);
scene.add(group);
entries.push({ id, b, group, mesh });
}
let show = true;
ui.addLayer('rings', 'Rings', true, (on) => { show = on; for (const e of entries) e.group.visible = on; });
ui.setStatus('rings', 'Saturn · Uranus', 'ok');
return {
id: 'rings',
onClockTick() {
const K = scale.getK();
for (const e of entries) {
const abs = ctx.bodyWorld[e.id];
if (!abs) continue;
toLocal(abs, e.group.position);
const E = effectiveE(e.id);
e.mesh.scale.setScalar((e.b.rings.outerKm / AU_KM) * K * E); // outer km → view units
}
},
};
}
// Remap RingGeometry UVs so u runs 0→1 across the radius (for a 1-D ring texture).
function radialUV(geo) {
const pos = geo.attributes.position, uv = geo.attributes.uv;
const inner = Math.hypot(pos.getX(0), pos.getY(0));
let outer = inner;
for (let i = 0; i < pos.count; i++) outer = Math.max(outer, Math.hypot(pos.getX(i), pos.getY(i)));
for (let i = 0; i < pos.count; i++) {
const r = Math.hypot(pos.getX(i), pos.getY(i));
uv.setXY(i, (r - inner) / (outer - inner || 1), 1);
}
uv.needsUpdate = true;
}