Draw Saturn's rings in the system view, lit, at the radii their texture draws
Audit #47. The body page's rings already lie in Saturn's equator (76fb386),
but the system view drew Saturn as a bare sphere, and the page sized the rings
to 1.4-2.6 planet radii regardless of what saturn_ring.png draws where.
The strip runs straight out from its left edge to its right. Read off its
alpha, the C ring's inner edge (74 490 km) is at px 91 of 1 280, the B ring's
inner and outer edges (92 000 and 117 580 km) at 404.5 and 860, the A ring's
outer edge (136 775 km) at 1 204 and the F ring (140 180 km) at 1 267.5: one
scale of 55.9 km a pixel fits all five within 1.8 px, so the strip spans
69 400 to 141 000 km. Only the Cassini Division's outer edge misses, drawn
30 px (1 700 km) too far in. Sized to the brief's 74 500 and 140 220 km (the C
ring's inner edge and the F ring) instead, the B ring's inner edge would sit
3 300 km out.
saturnRing (texture-catalog.ts) now builds the rings for both views: flat in
the XZ plane of a sphere built round +Y, sized against the planet as drawn,
MeshStandardMaterial lit from both faces, the strip's own alpha as opacity
(the page used the texture as its own alphaMap too, multiplying its alpha by
its green channel, at 0.85 opacity). In the system view the ring is a child of
Saturn's marker, so the IAU pole turns it into the equator and the pixel floor
scales it with the planet; a ray through it picks Saturn (memberForObject
accepts a marker's child). On the page it now reaches 2.42 radii, not 2.6.
Jupiter's, Uranus's and Neptune's rings are left out: dark, narrow or dusty,
too faint to see at any scale drawn here.
Measured in the running app, the ring's opening to Earth against Horizons'
sub-Earth latitude on Saturn (planetodetic, taken back to planetocentric with
f = 0.09796): 26.963 against 26.966 degrees on 2017-10-16, 0.075 against
0.042 on 2025-03-23, the plane crossing, and -7.764 against -7.813 on
2026-09-24. The Sun stood 26.64, 0.70 and -7.50 degrees above the ring plane
on those dates. At the closest the system view allows, 0.05 AU, Saturn is
about 5 px in radius and its rings reach about 12 px; on the plane-crossing
date they vanish edge-on.
Tests: the three openings against frozen Horizons values and a pick through
the B ring (system-orbits-renderer.spec.ts); the rings' extent, their lying
in the sphere's equator, and the strip sampled outwards so its B ring starts
at 92 000 km and its A ring ends at 136 775 (texture-catalog.spec.ts). Unit
suite 792 -> 799 tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -513,6 +513,7 @@ describe('solar-system bodies against Horizons', () => {
|
||||
earth: {poleRaDeg: [0, -0.641, 0], poleDecDeg: [90, -0.557, 0], primeMeridianDeg: [190.147, 360.9856235, 0]},
|
||||
mars: {poleRaDeg: [317.269202, -0.10927547, 0], poleDecDeg: [54.432516, -0.05827105, 0], primeMeridianDeg: [176.049863, 350.891982443297, 0], terms: [{angleDeg: [79.398797, 0.5042615, 0], ra: 0.419057, dec: 0, pm: 0}, {angleDeg: [166.325722, 0.5042615, 0], ra: 0, dec: 1.591274, pm: 0}, {angleDeg: [95.391654, 0.5042615, 0], ra: 0, dec: 0, pm: 0.584542}]},
|
||||
jupiter: {poleRaDeg: [268.056595, -0.006499, 0], poleDecDeg: [64.495303, 0.002413, 0], primeMeridianDeg: [284.95, 870.536, 0]},
|
||||
saturn: {poleRaDeg: [40.589, -0.036, 0], poleDecDeg: [83.537, -0.004, 0], primeMeridianDeg: [38.9, 810.7939024, 0]},
|
||||
uranus: {poleRaDeg: [257.311, 0, 0], poleDecDeg: [-15.175, 0, 0], primeMeridianDeg: [203.81, -501.1600928, 0]},
|
||||
pluto: {poleRaDeg: [132.993, 0, 0], poleDecDeg: [-6.163, 0, 0], primeMeridianDeg: [302.695, 56.3625225, 0]},
|
||||
moon: {poleRaDeg: [269.9949, 0.0031, 0], poleDecDeg: [66.5392, 0.013, 0], primeMeridianDeg: [38.3213, 13.17635815, -1.4e-12], terms: [{angleDeg: [125.045, -1935.5364525], ra: -3.8787, dec: 1.5419, pm: 3.561}, {angleDeg: [250.089, -3871.072905], ra: -0.1204, dec: 0.0239, pm: 0.1208}, {angleDeg: [260.008, 475263.3328725], ra: 0.07, dec: -0.0278, pm: -0.0642}, {angleDeg: [176.625, 487269.629985], ra: -0.0172, dec: 0.0068, pm: 0.0158}, {angleDeg: [357.529, 35999.0509575], ra: 0, dec: 0, pm: 0.0252}]},
|
||||
@@ -651,6 +652,48 @@ describe('solar-system bodies against Horizons', () => {
|
||||
expect(Math.abs(facing('earth', new THREE.Vector3()).eastDeg)).toBeLessThan(4);
|
||||
});
|
||||
|
||||
// Horizons' sub-Earth latitude on Saturn (observer quantity 14, from Earth's centre), which is
|
||||
// planetodetic: taken back to planetocentric through the flattening, it is the angle the rings are
|
||||
// opened to Earth by. Measured: 26.963, 0.075 and -7.764 degrees drawn, against 26.966, 0.042 and
|
||||
// -7.813.
|
||||
const SATURN_FLATTENING = 0.09796;
|
||||
const RING_OPENING: Array<[date: string, jd: number, planetodeticDeg: number]> = [
|
||||
['16 October 2017, near their widest', 2458042.5, 32.017423],
|
||||
['23 March 2025, as Earth crossed their plane', 2460757.5, 0.051359],
|
||||
['24 September 2026, the south face turned to Earth', 2461307.5, -9.571756]
|
||||
];
|
||||
|
||||
const saturnRingMesh = (): THREE.Mesh => renderer.members.find((member) => member.id === 'saturn')!.marker.children[0] as THREE.Mesh;
|
||||
|
||||
/** The ring's face normal in the scene, read off its own geometry rather than its transform. */
|
||||
function ringNormal(ring: THREE.Mesh): THREE.Vector3 {
|
||||
ring.updateWorldMatrix(true, false);
|
||||
return new THREE.Vector3().fromBufferAttribute(ring.geometry.attributes['normal'], 0).transformDirection(ring.matrixWorld);
|
||||
}
|
||||
|
||||
for (const [date, jd, planetodeticDeg] of RING_OPENING) {
|
||||
it(`opens Saturn's rings to Earth as far as Horizons has them on ${date}`, () => {
|
||||
renderer.update(jd);
|
||||
const normal = ringNormal(saturnRingMesh());
|
||||
const toEarth = worldPosition('earth').sub(worldPosition('saturn')).normalize();
|
||||
const openingDeg = (Math.asin(normal.dot(toEarth)) * 180) / Math.PI;
|
||||
const expectedDeg = (Math.atan((1 - SATURN_FLATTENING) ** 2 * Math.tan((planetodeticDeg * Math.PI) / 180)) * 180) / Math.PI;
|
||||
expect(Math.abs(openingDeg - expectedDeg)).toBeLessThan(0.1);
|
||||
});
|
||||
}
|
||||
|
||||
it('picks Saturn through its rings', () => {
|
||||
renderer.update(JUNE_1_2025_NOON_UTC);
|
||||
const ring = saturnRingMesh();
|
||||
const normal = ringNormal(ring);
|
||||
const inRingPlane = new THREE.Vector3().fromBufferAttribute(ring.geometry.attributes['position'], 0).transformDirection(ring.matrixWorld);
|
||||
// Straight down onto the B ring, 100 000 km out: nowhere near the planet itself.
|
||||
const onRing = worldPosition('saturn').addScaledVector(inRingPlane, 100000 / 149597870.7);
|
||||
const [hit] = new THREE.Raycaster(onRing.clone().addScaledVector(normal, 0.01), normal.clone().negate()).intersectObjects(renderer.pickableObjects);
|
||||
expect(hit.object).toBe(ring);
|
||||
expect(renderer.memberForObject(hit.object)?.id).toBe('saturn');
|
||||
});
|
||||
|
||||
// Horizons' observer quantities 14 and 15 at 2025-06-01 12:00 UTC, from Earth's centre (from the
|
||||
// Sun's, for Earth): the sub-observer and sub-solar longitude and latitude, east-positive for
|
||||
// Earth and the Moon and west-positive for Mars and Jupiter, as each is printed. Horizons gives
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as THREE from 'three/webgpu';
|
||||
import { appearanceForBody, appearanceForExoplanet } from '../../shared/astro/body-appearance';
|
||||
import { PlanetAppearance } from '../../shared/astro/planet-appearance';
|
||||
import { planetTexture } from '../../shared/rendering/procedural-planet-texture';
|
||||
import { bodyTexturePath, loadCachedTexture } from '../../shared/rendering/texture-catalog';
|
||||
import { bodyTexturePath, loadCachedTexture, saturnRing } from '../../shared/rendering/texture-catalog';
|
||||
import { isPropagatableOrbit, keplerRates, meanElementsAt, orbitEllipsePoints, positionAtEpoch, resolveGravitationalParameter, resolveOrbitalElements } from '../../shared/astro/kepler';
|
||||
import { CartesianCoordinates, OBLIQUITY_J2000_DEG } from '../../shared/astro/coordinates';
|
||||
import { BodyRecord, MeanElementRates, OrbitalElements, RotationalElements } from '../../shared/models/body.model';
|
||||
@@ -381,6 +381,14 @@ export class SystemOrbitsRenderer {
|
||||
// A body reaches here only when it has no parentBodyId, so `kind` is 'planet' or 'dwarf'.
|
||||
const kind: SystemMemberKind = body.kind;
|
||||
const tracked = this.addTopLevelBody(body.id, kind, body.orbit, body.rates, body.radiusKm, ECLIPTIC_FRAME, appearanceForBody(body, bodies, hostLuminositySolar), { periodHours: body.rotationPeriodHours, elements: body.rotationalElements });
|
||||
if (body.id === 'saturn') {
|
||||
// A child of the sphere, so it lies in the equator the IAU pole turns the sphere into and
|
||||
// is scaled with it where the marker is held to its pixel floor. Jupiter's, Uranus's and
|
||||
// Neptune's rings are left out: dark, narrow or dusty, they are too faint to see here.
|
||||
const ring = saturnRing(body.radiusKm, bodyMarkerRadiusAu(body.radiusKm));
|
||||
tracked.marker.add(ring);
|
||||
this.trackDisposable(ring.geometry, ring.material as THREE.Material);
|
||||
}
|
||||
members.push({ id: body.id, kind, marker: tracked.marker });
|
||||
}
|
||||
|
||||
@@ -503,9 +511,12 @@ export class SystemOrbitsRenderer {
|
||||
this.tethers?.setTargets(this.tetherPoints);
|
||||
}
|
||||
|
||||
/** Looks up which system member a marker object belongs to (e.g. from a raycast hit). */
|
||||
/**
|
||||
* Looks up which system member a marker object belongs to (e.g. from a raycast hit), or a part
|
||||
* of one: a ray through Saturn's rings picks Saturn.
|
||||
*/
|
||||
memberForObject(object: THREE.Object3D): SystemMember | undefined {
|
||||
return this.members.find((member) => member.marker === object);
|
||||
return this.members.find((member) => member.marker === object || member.marker === object.parent);
|
||||
}
|
||||
|
||||
/** All marker objects, for raycasting. */
|
||||
|
||||
Reference in New Issue
Block a user