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:
@@ -908,6 +908,14 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('holds Earth to its 3-pixel floor at the arrival framing, where its true radius is far under a pixel', async () => {
|
||||||
|
Object.defineProperty((fixture.nativeElement as HTMLElement).querySelector('canvas')!, 'clientHeight', { value: 1000 });
|
||||||
|
const component = await enterTheSun(1.6);
|
||||||
|
const earth = component.systemRenderer.members.find((member) => member.id === 'earth')!.marker as THREE.Mesh;
|
||||||
|
const pixelAu = (2 * engine.visibleHalfHeight(engine.getCamera().position.distanceTo(earth.getWorldPosition(new THREE.Vector3())))) / 1000;
|
||||||
|
expect((earth.scale.x * (earth.geometry as THREE.SphereGeometry).parameters.radius) / pixelAu).toBeCloseTo(3, 3);
|
||||||
|
});
|
||||||
|
|
||||||
it('leaves the system outwards even from a phone’s framing, which stands past the 400 AU it used to fly to', async () => {
|
it('leaves the system outwards even from a phone’s framing, which stands past the 400 AU it used to fly to', async () => {
|
||||||
const component = await enterTheSun(390 / 844);
|
const component = await enterTheSun(390 / 844);
|
||||||
const camera = engine.getCamera();
|
const camera = engine.getCamera();
|
||||||
|
|||||||
@@ -861,8 +861,12 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
const world = new THREE.Vector3();
|
const world = new THREE.Vector3();
|
||||||
const drawnRadiusAu = new Map<string, number>();
|
const drawnRadiusAu = new Map<string, number>();
|
||||||
const radiusOf = (marker: THREE.Object3D): number | undefined =>
|
// A body's marker is a unit sphere scaled to its radius, kept in `userData.radiusAu`; the
|
||||||
|
// star's is built at its own.
|
||||||
|
const sphereRadius = (marker: THREE.Object3D): number | undefined =>
|
||||||
((marker as THREE.Mesh).geometry as THREE.SphereGeometry | undefined)?.parameters?.radius;
|
((marker as THREE.Mesh).geometry as THREE.SphereGeometry | undefined)?.parameters?.radius;
|
||||||
|
const radiusOf = (marker: THREE.Object3D): number | undefined =>
|
||||||
|
(marker.userData['radiusAu'] as number | undefined) ?? sphereRadius(marker);
|
||||||
const floorFor = (marker: THREE.Object3D): number => {
|
const floorFor = (marker: THREE.Object3D): number => {
|
||||||
marker.getWorldPosition(world);
|
marker.getWorldPosition(world);
|
||||||
return (
|
return (
|
||||||
@@ -891,7 +895,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
: Number.POSITIVE_INFINITY;
|
: Number.POSITIVE_INFINITY;
|
||||||
const drawn = Math.min(Math.max(radiusAu, floorFor(marker)), Math.max(radiusAu, ceiling));
|
const drawn = Math.min(Math.max(radiusAu, floorFor(marker)), Math.max(radiusAu, ceiling));
|
||||||
drawnRadiusAu.set(id, drawn);
|
drawnRadiusAu.set(id, drawn);
|
||||||
marker.scale.setScalar(drawn / radiusAu);
|
marker.scale.setScalar(drawn / sphereRadius(marker)!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { BodyRecord, RotationalElements } from '../../shared/models/body.model';
|
|||||||
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
||||||
import { SystemOrbitsRenderer } from './system-orbits-renderer';
|
import { SystemOrbitsRenderer } from './system-orbits-renderer';
|
||||||
import { bodyTexturePath, loadCachedTexture } from '../../shared/rendering/texture-catalog';
|
import { bodyTexturePath, loadCachedTexture } from '../../shared/rendering/texture-catalog';
|
||||||
|
import { bodyMarkerRadiusAu } from './system-framing';
|
||||||
|
|
||||||
/** The clock's UT date that names a TDB one: TT - UT, which moves by under a second a year, earlier. */
|
/** The clock's UT date that names a TDB one: TT - UT, which moves by under a second a year, earlier. */
|
||||||
const utOf = (jdTdb: number): number => jdTdb - ttMinusUtSeconds(jdTdb) / 86400;
|
const utOf = (jdTdb: number): number => jdTdb - ttMinusUtSeconds(jdTdb) / 86400;
|
||||||
@@ -512,6 +513,27 @@ describe('photographs', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('markers', () => {
|
||||||
|
it('draws every body on the one sphere, scaled to its radius, and leaves that sphere when a system is left', () => {
|
||||||
|
const records: BodyRecord[] = [2500, 60000].map((radiusKm, index) => ({
|
||||||
|
id: `body-${index}`, systemStarId: 0, name: `Body ${index}`, kind: 'planet', radiusKm, orbitSource: 'test',
|
||||||
|
orbit: { semiMajorAxisAu: 1 + index, eccentricity: 0, inclinationDeg: 0, longitudeOfAscendingNodeDeg: 0, argumentOfPeriapsisDeg: 0, meanAnomalyAtEpochDeg: 0, epochJd: DEFAULT_EPOCH_JD },
|
||||||
|
rates: keplerRates(1 + index, GM_SUN_AU3_PER_DAY2)
|
||||||
|
}));
|
||||||
|
const renderer = new SystemOrbitsRenderer(records, [exoplanet({ radiusEarth: 1.1 })], undefined, 1);
|
||||||
|
const meshes = renderer.members.map((member) => member.marker as THREE.Mesh);
|
||||||
|
expect(new Set(meshes.map((mesh) => mesh.geometry)).size).toBe(1);
|
||||||
|
[2500, 60000, 1.1 * 6371].forEach((radiusKm, index) => {
|
||||||
|
const sphere = meshes[index].geometry as THREE.SphereGeometry;
|
||||||
|
expect(meshes[index].scale.x * sphere.parameters.radius).toBeCloseTo(bodyMarkerRadiusAu(radiusKm), 12);
|
||||||
|
});
|
||||||
|
const disposed = vi.fn();
|
||||||
|
meshes[0].geometry.addEventListener('dispose', disposed);
|
||||||
|
renderer.dispose();
|
||||||
|
expect(disposed).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('derived surfaces', () => {
|
describe('derived surfaces', () => {
|
||||||
const maps = (renderer: SystemOrbitsRenderer): Array<THREE.Texture | null> =>
|
const maps = (renderer: SystemOrbitsRenderer): Array<THREE.Texture | null> =>
|
||||||
renderer.members.map((member) => ((member.marker as THREE.Mesh).material as THREE.MeshStandardMaterial).map);
|
renderer.members.map((member) => ((member.marker as THREE.Mesh).material as THREE.MeshStandardMaterial).map);
|
||||||
@@ -553,7 +575,7 @@ describe('derived surfaces', () => {
|
|||||||
describe('exoplanet size without a measured radius', () => {
|
describe('exoplanet size without a measured radius', () => {
|
||||||
const radiusOf = (overrides: Partial<ExoplanetRecord>): number => {
|
const radiusOf = (overrides: Partial<ExoplanetRecord>): number => {
|
||||||
const renderer = new SystemOrbitsRenderer([], [exoplanet(overrides)], undefined, 1);
|
const renderer = new SystemOrbitsRenderer([], [exoplanet(overrides)], undefined, 1);
|
||||||
return ((renderer.members[0].marker as THREE.Mesh).geometry as THREE.SphereGeometry).parameters.radius;
|
return renderer.members[0].marker.userData['radiusAu'];
|
||||||
};
|
};
|
||||||
const EARTH_AU = 6371 / 149597870.7;
|
const EARTH_AU = 6371 / 149597870.7;
|
||||||
|
|
||||||
@@ -769,7 +791,7 @@ describe('solar-system bodies against Horizons', () => {
|
|||||||
const marker = renderer.members.find((member) => member.id === id)!.marker as THREE.Mesh;
|
const marker = renderer.members.find((member) => member.id === id)!.marker as THREE.Mesh;
|
||||||
const centre = worldPosition(id);
|
const centre = worldPosition(id);
|
||||||
const towards = point.clone().sub(centre).normalize();
|
const towards = point.clone().sub(centre).normalize();
|
||||||
const radius = (marker.geometry as THREE.SphereGeometry).parameters.radius;
|
const radius = marker.userData['radiusAu'];
|
||||||
const hit = new THREE.Raycaster(centre.clone().addScaledVector(towards, radius * 4), towards.clone().negate()).intersectObject(marker)[0];
|
const hit = new THREE.Raycaster(centre.clone().addScaledVector(towards, radius * 4), towards.clone().negate()).intersectObject(marker)[0];
|
||||||
return { eastDeg: (hit.uv!.x - 0.5) * 360, latDeg: (hit.uv!.y - 0.5) * 180 };
|
return { eastDeg: (hit.uv!.x - 0.5) * 360, latDeg: (hit.uv!.y - 0.5) * 180 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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
|
* 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).
|
* 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(
|
function buildMarker(
|
||||||
id: string | undefined,
|
id: string | undefined,
|
||||||
@@ -214,7 +219,6 @@ function buildMarker(
|
|||||||
deferSurface: (paint: () => void) => void,
|
deferSurface: (paint: () => void) => void,
|
||||||
deferPhotograph: (material: THREE.MeshStandardMaterial, texture: THREE.Texture) => void
|
deferPhotograph: (material: THREE.MeshStandardMaterial, texture: THREE.Texture) => void
|
||||||
): THREE.Mesh {
|
): THREE.Mesh {
|
||||||
const geometry = new THREE.SphereGeometry(bodyMarkerRadiusAu(radiusKm), MARKER_WIDTH_SEGMENTS, MARKER_HEIGHT_SEGMENTS);
|
|
||||||
const photograph = id ? bodyTexturePath(id) : undefined;
|
const photograph = id ? bodyTexturePath(id) : undefined;
|
||||||
// null, not undefined, until there is one: three warns "parameter 'map' has value of
|
// 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.
|
// undefined" for every body built so, eleven of them on entering the Sun's system.
|
||||||
@@ -235,7 +239,11 @@ function buildMarker(
|
|||||||
material.needsUpdate = true;
|
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_WIDTH_SEGMENTS = 64;
|
||||||
const MARKER_HEIGHT_SEGMENTS = 32;
|
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
|
* 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
|
// 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
|
// 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.
|
// 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);
|
tracked.marker.add(ring);
|
||||||
this.trackDisposable(ring.geometry, ring.material as THREE.Material);
|
this.trackDisposable(ring.geometry, ring.material as THREE.Material);
|
||||||
}
|
}
|
||||||
@@ -613,7 +624,9 @@ export class SystemOrbitsRenderer {
|
|||||||
this.grid?.dispose();
|
this.grid?.dispose();
|
||||||
this.tethers?.dispose();
|
this.tethers?.dispose();
|
||||||
for (const { geometry, material } of this.disposables) {
|
for (const { geometry, material } of this.disposables) {
|
||||||
|
if (geometry !== MARKER_SPHERE) {
|
||||||
geometry.dispose();
|
geometry.dispose();
|
||||||
|
}
|
||||||
material.dispose();
|
material.dispose();
|
||||||
}
|
}
|
||||||
// Detach as well as dispose. A star-to-star hop builds a new renderer and drops the old
|
// Detach as well as dispose. A star-to-star hop builds a new renderer and drops the old
|
||||||
|
|||||||
Reference in New Issue
Block a user