Choose the drawn stars in one walk of the brightness order
Same stars in the same order, for less: one walk of the brightness index, reading positions laid out in that order, sorts the view's neighbourhood, the Sun's and the rest as it goes, instead of gathering both neighbourhoods in catalogue order and sorting them. A refocus in the page drops from 11.3 ms to 4.6 ms (median; worst 19.1 to 7.1). This comes before the drawn set follows the camera's turns, which makes refocusing far more frequent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
@@ -554,7 +554,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
(id): id is number => id !== null && id !== undefined
|
||||
));
|
||||
|
||||
this.starField = new StarFieldRenderer(stars, positions, starRenderBudgetFromUrl(window.location.search), this.starsByBrightness.order);
|
||||
this.starField = new StarFieldRenderer(stars, positions, starRenderBudgetFromUrl(window.location.search), this.starsByBrightness);
|
||||
// It has just chosen around the Sun, which is where the view opens: the first label pass need not choose again.
|
||||
this.starFieldFocus = GALAXY_OVERVIEW_TARGET.clone();
|
||||
this.galaxyGroup.add(this.starField.object);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as THREE from 'three/webgpu';
|
||||
import { float, instancedBufferAttribute, mix, modelViewMatrix, smoothstep, uniform, uv, vec2, vec4 } from 'three/tsl';
|
||||
|
||||
import { brightnessOrder, Positioned } from '../../shared/astro/brightest';
|
||||
import { BrightnessIndex, brightnessIndex, Positioned } from '../../shared/astro/brightest';
|
||||
import { spectralTypeToColorIndex } from '../../shared/astro/spectral';
|
||||
import { SceneCamera } from '../../core/engine/engine.service';
|
||||
import { StarRecord } from '../../shared/models/star.model';
|
||||
@@ -138,20 +138,46 @@ export function starRenderBudgetFromUrl(search: string, fallback = STAR_RENDER_B
|
||||
* is centred, everything within it of the Sun, then the brightest of the rest. Each neighbourhood
|
||||
* is taken brightest first, so a budget too small to hold one whole keeps its most visible part.
|
||||
*
|
||||
* Returns indices into the original list, in the order they were chosen. `order` is the
|
||||
* catalogue's brightness order, passed in when the caller already has it rather than sorted again
|
||||
* Returns indices into the original list, in the order they were chosen. `index` is the
|
||||
* catalogue's brightness index, passed in when the caller already has it rather than sorted again
|
||||
* on every call.
|
||||
*/
|
||||
export function selectDrawnStars(
|
||||
stars: readonly StarRecord[],
|
||||
budget = STAR_RENDER_BUDGET,
|
||||
focus: DrawFocus = {},
|
||||
order: Uint32Array = brightnessOrder(stars)
|
||||
index: BrightnessIndex = brightnessIndex(stars)
|
||||
): Uint32Array {
|
||||
if (stars.length <= budget) {
|
||||
return Uint32Array.from(stars.keys());
|
||||
}
|
||||
|
||||
// One walk of the brightness order, reading positions laid out in that order, sorts each tier
|
||||
// brightest first as it goes: 2.4-3.1 ms on the real catalogue in Node, against 6.4-8.5 ms
|
||||
// gathering both neighbourhoods in catalogue order and sorting them. Beyond both neighbourhoods
|
||||
// only the first `budget` stars can ever be taken, so past those it only looks for members.
|
||||
const { order, positions } = index;
|
||||
const radiusSq = FOCUS_RADIUS_PC * FOCUS_RADIUS_PC;
|
||||
const centre = focus.centre ?? SUN;
|
||||
const nearCentre: number[] = [];
|
||||
const nearSun: number[] = [];
|
||||
const rest: number[] = [];
|
||||
for (let at = 0; at < order.length; at++) {
|
||||
const x = positions[at * 3];
|
||||
const y = positions[at * 3 + 1];
|
||||
const z = positions[at * 3 + 2];
|
||||
const dx = x - centre.x;
|
||||
const dy = y - centre.y;
|
||||
const dz = z - centre.z;
|
||||
if (dx * dx + dy * dy + dz * dz <= radiusSq) {
|
||||
nearCentre.push(order[at]);
|
||||
} else if (x * x + y * y + z * z <= radiusSq) {
|
||||
nearSun.push(order[at]);
|
||||
} else if (rest.length < budget) {
|
||||
rest.push(order[at]);
|
||||
}
|
||||
}
|
||||
|
||||
const chosen = new Uint8Array(stars.length);
|
||||
const selected: number[] = [];
|
||||
const take = (index: number): void => {
|
||||
@@ -160,40 +186,14 @@ export function selectDrawnStars(
|
||||
selected.push(index);
|
||||
}
|
||||
};
|
||||
|
||||
for (const index of focus.pinned ?? []) {
|
||||
if (index >= 0 && index < stars.length) {
|
||||
take(index);
|
||||
for (const pinned of focus.pinned ?? []) {
|
||||
if (pinned >= 0 && pinned < stars.length) {
|
||||
take(pinned);
|
||||
}
|
||||
}
|
||||
|
||||
// Both neighbourhoods in one pass in catalogue order, then each sorted brightest first. They hold
|
||||
// a few thousand stars between them, so sorting them costs far less than walking the whole
|
||||
// brightness order once per neighbourhood, which reads the catalogue out of order: 26 ms a
|
||||
// refocus on the real catalogue, against 4 ms this way.
|
||||
const radiusSq = FOCUS_RADIUS_PC * FOCUS_RADIUS_PC;
|
||||
const centres = focus.centre ? [focus.centre, SUN] : [SUN];
|
||||
const members = centres.map(() => [] as number[]);
|
||||
for (let index = 0; index < stars.length; index++) {
|
||||
const star = stars[index];
|
||||
for (let which = 0; which < centres.length; which++) {
|
||||
const dx = star.x - centres[which].x;
|
||||
const dy = star.y - centres[which].y;
|
||||
const dz = star.z - centres[which].z;
|
||||
if (dx * dx + dy * dy + dz * dz <= radiusSq) {
|
||||
members[which].push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const neighbourhood of members) {
|
||||
// Ties in catalogue order, as in the brightness order itself.
|
||||
neighbourhood.sort((a, b) => stars[a].magnitude - stars[b].magnitude || a - b).forEach(take);
|
||||
}
|
||||
|
||||
for (let at = 0; at < order.length && selected.length < budget; at++) {
|
||||
take(order[at]);
|
||||
}
|
||||
|
||||
nearCentre.forEach(take);
|
||||
nearSun.forEach(take);
|
||||
rest.forEach(take);
|
||||
return Uint32Array.from(selected);
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ export class StarFieldRenderer {
|
||||
private readonly geometry: THREE.InstancedBufferGeometry;
|
||||
private readonly material: THREE.SpriteNodeMaterial;
|
||||
private readonly budget: number;
|
||||
private readonly order: Uint32Array;
|
||||
private readonly brightness: BrightnessIndex;
|
||||
/**
|
||||
* Colour and angular size of every star in the catalogue, worked out once: a refocus then only
|
||||
* copies them into the instances, 0.7 ms for the budget rather than 5.6 ms computing them again.
|
||||
@@ -246,10 +246,10 @@ export class StarFieldRenderer {
|
||||
private readonly catalogue: readonly StarRecord[],
|
||||
private readonly cataloguePositions: Float32Array,
|
||||
budget = STAR_RENDER_BUDGET,
|
||||
order?: Uint32Array
|
||||
brightness?: BrightnessIndex
|
||||
) {
|
||||
this.budget = budget;
|
||||
this.order = order ?? brightnessOrder(catalogue);
|
||||
this.brightness = brightness ?? brightnessIndex(catalogue);
|
||||
const capacity = Math.min(budget, catalogue.length);
|
||||
this.geometry = createQuadGeometry(0);
|
||||
|
||||
@@ -319,7 +319,7 @@ export class StarFieldRenderer {
|
||||
* with them. See {@link selectDrawnStars}.
|
||||
*/
|
||||
refocus(focus: DrawFocus): void {
|
||||
const drawn = selectDrawnStars(this.catalogue, this.budget, focus, this.order);
|
||||
const drawn = selectDrawnStars(this.catalogue, this.budget, focus, this.brightness);
|
||||
// The same stars in the same instances: the buffers already hold them, and a rewrite would
|
||||
// upload 2 MB to the GPU for nothing — which a pan across empty space would do every pass.
|
||||
if (drawn.length === this.drawn.length && drawn.every((index, instance) => index === this.drawn[instance])) {
|
||||
|
||||
Reference in New Issue
Block a user