Derive a surface for every body that was never photographed
Fifteen bodies here have a real photograph. Every exoplanet does not, and never will on current instruments — none has ever been imaged — and nor do several of the solar system's own moons. Those all shared one crude stand-in: a few noisy bands tinted by category, cached per colour, so every exoplanet in the app was literally the same picture. They now get a surface reasoned from what has actually been measured. The chain is standard at every link. A host star's luminosity comes from its catalogued apparent magnitude and its parallax distance — that pair is exactly an absolute magnitude — plus a bolometric correction for its spectral class. The correction is not optional: an M dwarf radiates most of its light in the infrared, so its visual magnitude understates it more than tenfold, and M dwarfs are what most nearby planet hosts are. Luminosity and the semi-major axis then give an equilibrium temperature, mass and radius give a bulk density, and size, temperature and density together give a class of world. Checked against the solar system the temperatures land on Earth 255 K, Jupiter 112 K, Neptune 46 K, all within a kelvin or two of published values, and 51 Pegasi b comes out at 1227 K against a published 1200. Each class carries a palette reasoned from its chemistry — methane absorbs red light, which is why the ice giants are blue — and a structure: zonal bands for a body with a fluid envelope, because a rapidly rotating atmosphere organises into them, and fractal terrain for one with a solid surface. Polar caps grow and shrink with the derived temperature, which is the clearest visible consequence of the whole chain. The generator samples three-dimensional noise along the sphere rather than a flat field, so there is no seam to stitch at the antimeridian and no pinching at the poles, and it writes into a byte array rather than a canvas — a pure function, testable, with no 2D context to be unavailable. Two things the derivation cannot do, both stated on screen next to the measurements it rests on. Equilibrium temperature ignores greenhouse warming and internal heat, so Venus comes out at 300 K against a real surface of 737 K and Io, kept molten by tides, classifies as ice. And these are illustrations: reasoned, but not observations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
This commit is contained in:
@@ -5,6 +5,7 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
|
||||
import { dateToJulianDate } from '../../shared/astro/constants';
|
||||
import { galacticCentrePositionPc, galacticToEquatorial, MILKY_WAY_ARMS, SUN_GALACTOCENTRIC_RADIUS_PC } from '../../shared/astro/galaxy';
|
||||
import { luminositySolar } from '../../shared/astro/stellar';
|
||||
import { DataLoaderService } from '../../core/data/data-loader.service';
|
||||
import { EngineService } from '../../core/engine/engine.service';
|
||||
import { BodyRecord } from '../../shared/models/body.model';
|
||||
@@ -691,7 +692,10 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
// The star's own position is the line of sight to it, which is the plane the archive
|
||||
// measures exoplanet inclinations against. The Sun sits at the origin and has no
|
||||
// exoplanets, so it has no meaningful direction and the renderer falls back.
|
||||
this.systemRenderer = new SystemOrbitsRenderer(systemBodies, systemExoplanets, { x: star.x, y: star.y, z: star.z });
|
||||
// The star's luminosity, derived from its own catalogued magnitude and distance, is what
|
||||
// decides how hot each body in the system is — and so what each of them looks like.
|
||||
const hostLuminosity = luminositySolar({ magnitude: star.magnitude, distancePc: Math.hypot(star.x, star.y, star.z), spectralType: star.spectralType });
|
||||
this.systemRenderer = new SystemOrbitsRenderer(systemBodies, systemExoplanets, { x: star.x, y: star.y, z: star.z }, hostLuminosity);
|
||||
this.systemGroup.add(this.systemRenderer.object);
|
||||
|
||||
// Sized against this system's innermost orbit, so the star never swallows its own planets.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as THREE from 'three/webgpu';
|
||||
|
||||
import { appearanceForBody, appearanceForExoplanet } from '../../shared/astro/body-appearance';
|
||||
import { gmForParent } from '../../shared/astro/constants';
|
||||
import { PlanetAppearance } from '../../shared/astro/planet-appearance';
|
||||
import { MARKER_TEXTURE_HEIGHT, MARKER_TEXTURE_WIDTH, planetTexture } from '../../shared/rendering/procedural-planet-texture';
|
||||
import { isPropagatableOrbit, orbitEllipsePoints, propagateOrbit, resolveGravitationalParameter, resolveOrbitalElements } from '../../shared/astro/kepler';
|
||||
import { CartesianCoordinates, OBLIQUITY_J2000_DEG } from '../../shared/astro/coordinates';
|
||||
import { BodyRecord, OrbitalElements } from '../../shared/models/body.model';
|
||||
@@ -110,9 +113,19 @@ function buildOrbitLine(elements: OrbitalElements, kind: SystemMemberKind, frame
|
||||
return new THREE.Line(geometry, material);
|
||||
}
|
||||
|
||||
function buildMarker(kind: SystemMemberKind, radiusKm: number | undefined, systemSpanAu: number): THREE.Mesh {
|
||||
/**
|
||||
* A marker sphere, surfaced with the body's own derived appearance rather than a flat category
|
||||
* colour — so a system reads as a set of distinct worlds at a glance, and the colour of each is
|
||||
* a consequence of its measurements rather than of which list it came from.
|
||||
*
|
||||
* The texture is tiny (see `MARKER_TEXTURE_WIDTH`): a marker is a few pixels across, so what
|
||||
* survives is essentially its average colour, and generating it costs well under a millisecond.
|
||||
*/
|
||||
function buildMarker(kind: SystemMemberKind, radiusKm: number | undefined, systemSpanAu: number, appearance: PlanetAppearance | undefined): THREE.Mesh {
|
||||
const geometry = new THREE.SphereGeometry(bodyMarkerRadiusAu(radiusKm, systemSpanAu), 16, 12);
|
||||
const material = new THREE.MeshBasicMaterial({ color: colorForKind(kind) });
|
||||
const material = appearance
|
||||
? new THREE.MeshBasicMaterial({ map: planetTexture(appearance, { width: MARKER_TEXTURE_WIDTH, height: MARKER_TEXTURE_HEIGHT }) })
|
||||
: new THREE.MeshBasicMaterial({ color: colorForKind(kind) });
|
||||
return new THREE.Mesh(geometry, material);
|
||||
}
|
||||
|
||||
@@ -172,7 +185,13 @@ export class SystemOrbitsRenderer {
|
||||
bodies: readonly BodyRecord[],
|
||||
exoplanets: readonly ExoplanetRecord[],
|
||||
/** Direction from the Sun to this system's host star, equatorial — the exoplanet line of sight. */
|
||||
hostStarDirection?: CartesianCoordinates
|
||||
hostStarDirection?: CartesianCoordinates,
|
||||
/**
|
||||
* The host star's luminosity in solar units, which is what sets how hot each body in the
|
||||
* system is and therefore what it looks like. Omitted for a host that is not in the star
|
||||
* catalogue, leaving its bodies classified on size and density alone.
|
||||
*/
|
||||
hostLuminositySolar?: number | null
|
||||
) {
|
||||
const members: SystemMember[] = [];
|
||||
const topLevelBodiesById = new Map<string, BodyRecord>();
|
||||
@@ -198,7 +217,7 @@ 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, gmForParent(undefined), body.radiusKm, ECLIPTIC_FRAME);
|
||||
const tracked = this.addTopLevelBody(body.id, kind, body.orbit, gmForParent(undefined), body.radiusKm, ECLIPTIC_FRAME, appearanceForBody(body, bodies, hostLuminositySolar));
|
||||
members.push({ id: body.id, kind, marker: tracked.marker });
|
||||
}
|
||||
|
||||
@@ -211,7 +230,7 @@ export class SystemOrbitsRenderer {
|
||||
if (!parentTracked) {
|
||||
continue; // orphaned moon reference; skip rather than crash.
|
||||
}
|
||||
const moon = this.addMoon(body.id, body.orbit, gmForParent(body.parentBodyId), body.radiusKm, parentTracked, ECLIPTIC_FRAME);
|
||||
const moon = this.addMoon(body.id, body.orbit, gmForParent(body.parentBodyId), body.radiusKm, parentTracked, ECLIPTIC_FRAME, appearanceForBody(body, bodies, hostLuminositySolar));
|
||||
members.push({ id: body.id, kind: 'moon', marker: moon.marker });
|
||||
}
|
||||
|
||||
@@ -235,7 +254,7 @@ export class SystemOrbitsRenderer {
|
||||
periodDays: exoplanet.periodDays,
|
||||
hostStarMassSolar: exoplanet.hostStarMassSolar
|
||||
});
|
||||
const tracked = this.addTopLevelBody(exoplanet.id, 'exoplanet', elements, gm, radiusKm, exoplanetFrame);
|
||||
const tracked = this.addTopLevelBody(exoplanet.id, 'exoplanet', elements, gm, radiusKm, exoplanetFrame, appearanceForExoplanet(exoplanet, hostLuminositySolar));
|
||||
members.push({ id: exoplanet.id, kind: 'exoplanet', marker: tracked.marker });
|
||||
}
|
||||
|
||||
@@ -325,10 +344,11 @@ export class SystemOrbitsRenderer {
|
||||
elements: OrbitalElements,
|
||||
gmAu3PerDay2: number,
|
||||
radiusKm: number | undefined,
|
||||
frame: THREE.Quaternion
|
||||
frame: THREE.Quaternion,
|
||||
appearance?: PlanetAppearance
|
||||
): TrackedTopLevelBody {
|
||||
const orbitLine = buildOrbitLine(elements, kind, frame);
|
||||
const marker = buildMarker(kind, radiusKm, this.maxTopLevelSemiMajorAxisAu);
|
||||
const marker = buildMarker(kind, radiusKm, this.maxTopLevelSemiMajorAxisAu, appearance);
|
||||
this.object.add(orbitLine, marker);
|
||||
this.trackDisposable(orbitLine.geometry, orbitLine.material as THREE.Material);
|
||||
this.trackDisposable(marker.geometry, marker.material as THREE.Material);
|
||||
@@ -344,11 +364,12 @@ export class SystemOrbitsRenderer {
|
||||
gmAu3PerDay2: number,
|
||||
radiusKm: number | undefined,
|
||||
parent: TrackedTopLevelBody,
|
||||
frame: THREE.Quaternion
|
||||
frame: THREE.Quaternion,
|
||||
appearance?: PlanetAppearance
|
||||
): TrackedMoon {
|
||||
const pivot = new THREE.Group();
|
||||
const orbitLine = buildOrbitLine(elements, 'moon', frame);
|
||||
const marker = buildMarker('moon', radiusKm, this.maxTopLevelSemiMajorAxisAu);
|
||||
const marker = buildMarker('moon', radiusKm, this.maxTopLevelSemiMajorAxisAu, appearance);
|
||||
pivot.add(orbitLine, marker);
|
||||
this.object.add(pivot);
|
||||
this.trackDisposable(orbitLine.geometry, orbitLine.material as THREE.Material);
|
||||
|
||||
Reference in New Issue
Block a user