Merge pull request #26 from avalon-vanguard/perf/drawn-set-one-walk
Choose the drawn stars in one walk of the brightness order 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
|
(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.
|
// 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.starFieldFocus = GALAXY_OVERVIEW_TARGET.clone();
|
||||||
this.galaxyGroup.add(this.starField.object);
|
this.galaxyGroup.add(this.starField.object);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as THREE from 'three/webgpu';
|
import * as THREE from 'three/webgpu';
|
||||||
import { float, instancedBufferAttribute, mix, modelViewMatrix, smoothstep, uniform, uv, vec2, vec4 } from 'three/tsl';
|
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 { spectralTypeToColorIndex } from '../../shared/astro/spectral';
|
||||||
import { SceneCamera } from '../../core/engine/engine.service';
|
import { SceneCamera } from '../../core/engine/engine.service';
|
||||||
import { StarRecord } from '../../shared/models/star.model';
|
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 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.
|
* 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
|
* Returns indices into the original list, in the order they were chosen. `index` is the
|
||||||
* catalogue's brightness order, passed in when the caller already has it rather than sorted again
|
* catalogue's brightness index, passed in when the caller already has it rather than sorted again
|
||||||
* on every call.
|
* on every call.
|
||||||
*/
|
*/
|
||||||
export function selectDrawnStars(
|
export function selectDrawnStars(
|
||||||
stars: readonly StarRecord[],
|
stars: readonly StarRecord[],
|
||||||
budget = STAR_RENDER_BUDGET,
|
budget = STAR_RENDER_BUDGET,
|
||||||
focus: DrawFocus = {},
|
focus: DrawFocus = {},
|
||||||
order: Uint32Array = brightnessOrder(stars)
|
index: BrightnessIndex = brightnessIndex(stars)
|
||||||
): Uint32Array {
|
): Uint32Array {
|
||||||
if (stars.length <= budget) {
|
if (stars.length <= budget) {
|
||||||
return Uint32Array.from(stars.keys());
|
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 chosen = new Uint8Array(stars.length);
|
||||||
const selected: number[] = [];
|
const selected: number[] = [];
|
||||||
const take = (index: number): void => {
|
const take = (index: number): void => {
|
||||||
@@ -160,40 +186,14 @@ export function selectDrawnStars(
|
|||||||
selected.push(index);
|
selected.push(index);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
for (const pinned of focus.pinned ?? []) {
|
||||||
for (const index of focus.pinned ?? []) {
|
if (pinned >= 0 && pinned < stars.length) {
|
||||||
if (index >= 0 && index < stars.length) {
|
take(pinned);
|
||||||
take(index);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
nearCentre.forEach(take);
|
||||||
// Both neighbourhoods in one pass in catalogue order, then each sorted brightest first. They hold
|
nearSun.forEach(take);
|
||||||
// a few thousand stars between them, so sorting them costs far less than walking the whole
|
rest.forEach(take);
|
||||||
// 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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uint32Array.from(selected);
|
return Uint32Array.from(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ export class StarFieldRenderer {
|
|||||||
private readonly geometry: THREE.InstancedBufferGeometry;
|
private readonly geometry: THREE.InstancedBufferGeometry;
|
||||||
private readonly material: THREE.SpriteNodeMaterial;
|
private readonly material: THREE.SpriteNodeMaterial;
|
||||||
private readonly budget: number;
|
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
|
* 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.
|
* 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 catalogue: readonly StarRecord[],
|
||||||
private readonly cataloguePositions: Float32Array,
|
private readonly cataloguePositions: Float32Array,
|
||||||
budget = STAR_RENDER_BUDGET,
|
budget = STAR_RENDER_BUDGET,
|
||||||
order?: Uint32Array
|
brightness?: BrightnessIndex
|
||||||
) {
|
) {
|
||||||
this.budget = budget;
|
this.budget = budget;
|
||||||
this.order = order ?? brightnessOrder(catalogue);
|
this.brightness = brightness ?? brightnessIndex(catalogue);
|
||||||
const capacity = Math.min(budget, catalogue.length);
|
const capacity = Math.min(budget, catalogue.length);
|
||||||
this.geometry = createQuadGeometry(0);
|
this.geometry = createQuadGeometry(0);
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ export class StarFieldRenderer {
|
|||||||
* with them. See {@link selectDrawnStars}.
|
* with them. See {@link selectDrawnStars}.
|
||||||
*/
|
*/
|
||||||
refocus(focus: DrawFocus): void {
|
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
|
// 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.
|
// 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])) {
|
if (drawn.length === this.drawn.length && drawn.every((index, instance) => index === this.drawn[instance])) {
|
||||||
|
|||||||
Reference in New Issue
Block a user