Turn every body in the system view by its IAU pole and prime meridian, so the lit face is the real one

Until now each body's axis was its orbit normal, tipped by the obliquity about the orbit's node,
an azimuth the data never gave. Its phase started at an arbitrary point at the elements' epoch.
The rate and the sense were real; the face towards the Sun was not. Now each of the 33 bodies with
IAU elements is set, every tick, from its pole and its W at the clock's date. Eris, Haumea and
Makemake keep the old fallback: their published period, about their orbit normal. None of them
has an obliquity, so the tilt code that only served bodies now turned by the IAU is gone.
Exoplanets have no rotation published and stay still, as before.

The texture convention is settled once, in src/app/shared/rendering/body-orientation.ts (MAP_TO_BODY):
- SphereGeometry runs u eastward about +Y from a seam on -X, so u = 0.5 faces +X.
- Every photograph in the catalogue is centred on longitude 0 with east to the right. Checked on
  the maps: Greenwich; Olympus Mons 134 degrees left of centre; Mare Crisium right and Mare
  Orientale left; Kuiper just left.
- A map labelled in west longitude is still drawn east-right, so where longitude 0 sits is the
  only question, and for all of them it is the centre.
- So a quarter turn about X puts the map on the IAU body frame: pole +Z, prime meridian +X.

The scene is already ICRF equatorial (the ecliptic is turned into it by the J2000 obliquity), so
the pole goes in as it is. The equator frame is built through laplacePlaneToEquatorial, the same
conversion the moons' Laplace planes use; moonFrame now calls it too. The clock is UTC and the
elements TDB, so TT - UTC (69.184 s) is added: Earth turns 0.29 degrees in that time, Jupiter 0.70
and Phobos 0.90.

Measured on the live app (port 4311), clock pinned to 2025-06-01 12:00 UTC:
- The Sun stands over 0.433 W, 22.125 N on Earth's drawn sphere. The equation of time puts it at
  0.53 W.
- Each body was drawn one light-time earlier and compared with Horizons' observer quantities 14
  and 15:
  - Earth (from the Sun): longitude 0.095 off.
  - Mars: sub-Earth 0.001, sub-solar 0.004.
  - Jupiter: sub-Earth 0.005, sub-solar 0.002.
  - The Moon: sub-solar 0.004; sub-Earth 0.699, which is the error of its mean orbit.
- Horizons' latitudes are planetodetic. Raw, they differ by the flattening: Earth 0.14, Mars
  0.23-0.27, Jupiter 0.33, the Moon (a sphere) 0.000.

The unit tests put the same comparison through real raycasts on the drawn spheres' texture
coordinates, with the latitudes put on each body's flattened figure. Every residual is within
0.09 degrees, but for the Moon's sub-Earth point (0.70 and 0.09).

The retrograde tests of #33 are rewritten for the IAU's convention: a planet's named pole is
the one on the north side, so W runs backwards for Venus and Uranus, while Pluto follows the
right-hand rule. The spin read off the drawn sphere, against the drawn orbit's normal, is 177.36
for Venus, 97.77 for Uranus and 119.61 for Pluto, all past 90, and 23.44 for Earth. Each is
within 0.5 of Horizons.

Six mutants, each failing its named test: the map upside down; UTC taken for TDB (Jupiter's
test); W turned the wrong way (the retrograde test, and again Earth's noon test); moons, or
planets, not turned by the IAU; and the fallback ignoring a negative period.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 22:48:05 +02:00
co-authored by Claude Opus 5.5
parent 1c86584642
commit cdf474bcd5
4 changed files with 211 additions and 64 deletions
+8
View File
@@ -19,6 +19,14 @@ export const DEFAULT_EPOCH_JD = 2451545.0;
*/
export const GM_SUN_AU3_PER_DAY2 = 0.01720209895 * 0.01720209895;
/**
* TT - UTC, in days: 32.184 s plus the 37 leap seconds UTC has taken since 1972, the last at the
* end of 2016. TDB, which ephemerides run on, stays within 2 ms of TT. Held constant, as Horizons
* holds it for dates past the last announced leap second; before 2017 it was smaller, about 29 s
* in 1950.
*/
export const TT_MINUS_UTC_DAYS = 69.184 / 86400;
/** Converts a JS `Date` into a Julian date (days), for driving the Kepler propagator "now". */
export function dateToJulianDate(date: Date = new Date()): number {
return date.getTime() / 86400000 + 2440587.5;
@@ -0,0 +1,60 @@
import * as THREE from 'three/webgpu';
import { TT_MINUS_UTC_DAYS } from '../astro/constants';
import { laplacePlaneToEquatorial } from '../astro/coordinates';
import { orientationAt } from '../astro/rotational-elements';
import { RotationalElements } from '../models/body.model';
const DEG_TO_RAD = Math.PI / 180;
const Z_AXIS = new THREE.Vector3(0, 0, 1);
/**
* How a surface map sits on a sphere, settled once for every map the app wraps.
*
* `THREE.SphereGeometry` is built round +Y and runs its u coordinate eastward, anticlockwise seen
* from +Y, from a seam on -X: u = 0.5 faces +X and u = 0.75 faces -Z. Every photograph in
* `texture-catalog.ts` is an equirectangular map centred on longitude 0 with east to the right —
* Greenwich is in the middle of Earth's; on Mars's, Olympus Mons (226.2 E, which is -133.8) sits a
* little over a third of the width left of centre; on the Moon's, Mare Crisium (59 E) is right of
* centre and Mare Orientale (95 W) left of it; on Mercury's, the rayed crater Kuiper (31.5 W, 11 S)
* is just left of centre and below the equator. A map labelled in west longitude, as most planets'
* are, is still drawn with east to the right, as any map of a sphere seen from outside is; only its
* numbers run the other way. So longitude 0 is +X and 90 E is -Z, and a quarter turn about X
* carries that onto the IAU's body-fixed frame: pole +Z, prime meridian +X, 90 E +Y.
*
* The derived surfaces have no meridian of their own, and take the same convention.
*/
export const MAP_TO_BODY = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2);
const scratchMatrix = new THREE.Matrix4();
const scratchAxes = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
const scratchTurn = new THREE.Quaternion();
/**
* Sets `target` to the rotation carrying a frame whose +Z is `pole` and whose +X is where its
* equator rises through the ICRF equator into the ICRF: the frame the IAU counts W in, and the
* one JPL refers a moon's Laplace plane to — so both go through {@link laplacePlaneToEquatorial}.
*/
export function poleFrame(pole: { raDeg: number; decDeg: number }, target = new THREE.Quaternion()): THREE.Quaternion {
const [x, y, z] = scratchAxes.map((axis, index) => {
const turned = laplacePlaneToEquatorial({ x: index === 0 ? 1 : 0, y: index === 1 ? 1 : 0, z: index === 2 ? 1 : 0 }, pole);
return axis.set(turned.x, turned.y, turned.z);
});
return target.setFromRotationMatrix(scratchMatrix.makeBasis(x, y, z));
}
/**
* Sets `target` to the rotation carrying a sphere, wrapped in its map as `SphereGeometry` wraps it,
* into the ICRF at the map's own clock: the map onto the body's frame, turned by W about the pole,
* and on to where the pole points.
*
* The clock is UTC and the IAU's elements run on TDB, 69.184 s ahead; in that time Earth turns
* 0.29 degrees, Jupiter 0.70 and Phobos 0.90, so the difference is added here.
*/
export function bodyOrientation(elements: RotationalElements, jdUtc: number, target = new THREE.Quaternion()): THREE.Quaternion {
const { poleRaDeg, poleDecDeg, primeMeridianDeg } = orientationAt(elements, jdUtc + TT_MINUS_UTC_DAYS);
return poleFrame({ raDeg: poleRaDeg, decDeg: poleDecDeg }, target)
.multiply(scratchTurn.setFromAxisAngle(Z_AXIS, primeMeridianDeg * DEG_TO_RAD))
.multiply(MAP_TO_BODY);
}