Draw every body in a system on one shared sphere, so returning to the Sun's system is no long task

Each marker built its own 64 by 32 SphereGeometry, and the Sun's system now has 38 of them: the
renderer's constructor took 15 ms, 12 of them building spheres, and with their first upload a
return to the system made a long task of 52 to 70 ms that the base's 18 bodies never did.

Every marker is now the one unit sphere, scaled to its radius, which it keeps in
userData.radiusAu. The shared sphere is never disposed; Saturn's ring is built in the sphere's
own units, since it is the marker's child; keepMarkersLegible reads the stored radius and scales
against the sphere's.

Measured on :4301, eight returns to the Sun's system each (select null, then 0, at 1600x1000):
before, a long task on 3 of 8 (52-57 ms), swapToSystemSpace 13-17 ms and the first render 30-40;
after, no long task on 8 of 8, the swap 2.7-4.4 ms and the first render 20-38. Earth is drawn at
the same 0.656 AU at arrival, and every member shares one geometry.

Tests: one sphere for every marker, each at bodyMarkerRadiusAu of its radius, and not disposed
with its system; Earth held to its 3-pixel floor at the arrival framing, which no test covered.
Guarded mutants, each failing only its named test: a sphere per marker, the shared sphere
disposed, the ring built in AU inside the scaled marker ('picks Saturn through its rings'), and
the legibility scale divided by the body's radius.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 15:14:02 +02:00
co-authored by Claude Opus 5.5
parent c2683da37b
commit 3e1bd33b6c
4 changed files with 55 additions and 8 deletions
@@ -205,6 +205,11 @@ function reshapeOrbitLine(line: THREE.Line, elements: OrbitalElements): void {
* frame: a texture is copied to the GPU in the first frame that draws it, and the 28 maps, which
* arrive within 40 ms of each other, made that one frame a 160-210 ms task on entering the Sun's
* system (copyExternalImageToTexture, about 20 megapixels of JPEG).
*
* Every marker is the one unit sphere, {@link MARKER_SPHERE}, scaled to the body's radius, which
* it also keeps as `userData.radiusAu`: built one a body, the 38 spheres of the Sun's system took
* 12 ms of the 15 ms the renderer took to build and, with their upload, made a return to the
* system a long task of 52 to 70 ms, where the 18 bodies before had made none.
*/
function buildMarker(
id: string | undefined,
@@ -214,7 +219,6 @@ function buildMarker(
deferSurface: (paint: () => void) => void,
deferPhotograph: (material: THREE.MeshStandardMaterial, texture: THREE.Texture) => void
): THREE.Mesh {
const geometry = new THREE.SphereGeometry(bodyMarkerRadiusAu(radiusKm), MARKER_WIDTH_SEGMENTS, MARKER_HEIGHT_SEGMENTS);
const photograph = id ? bodyTexturePath(id) : undefined;
// null, not undefined, until there is one: three warns "parameter 'map' has value of
// undefined" for every body built so, eleven of them on entering the Sun's system.
@@ -235,7 +239,11 @@ function buildMarker(
material.needsUpdate = true;
});
}
return new THREE.Mesh(geometry, material);
const marker = new THREE.Mesh(MARKER_SPHERE, material);
const radiusAu = bodyMarkerRadiusAu(radiusKm);
marker.scale.setScalar(radiusAu);
marker.userData = { radiusAu };
return marker;
}
/**
@@ -266,6 +274,8 @@ function starLight(): THREE.PointLight {
*/
const MARKER_WIDTH_SEGMENTS = 64;
const MARKER_HEIGHT_SEGMENTS = 32;
/** Shared by every marker of every system, so it is never disposed; see `buildMarker`. */
const MARKER_SPHERE = new THREE.SphereGeometry(1, MARKER_WIDTH_SEGMENTS, MARKER_HEIGHT_SEGMENTS);
/**
* A drawn radius, in Earth radii, for an exoplanet that has a mass and no measured radius — 1 076
@@ -434,7 +444,8 @@ export class SystemOrbitsRenderer {
// 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));
// In the sphere's own units, its radius being 1.
const ring = saturnRing(body.radiusKm, 1);
tracked.marker.add(ring);
this.trackDisposable(ring.geometry, ring.material as THREE.Material);
}
@@ -613,7 +624,9 @@ export class SystemOrbitsRenderer {
this.grid?.dispose();
this.tethers?.dispose();
for (const { geometry, material } of this.disposables) {
geometry.dispose();
if (geometry !== MARKER_SPHERE) {
geometry.dispose();
}
material.dispose();
}
// Detach as well as dispose. A star-to-star hop builds a new renderer and drops the old