refactor(overlays): layer labels through the engine

The world overlay host already projects through engine.projector and tests
the horizon with projector.horizonVisible; the last Cesium value on its
path, the snapshot of an entry's cullPosition, is now a geo Cartesian3.

Navara's horizonVisible is Cesium's EllipsoidalOccluder.isPointVisible:
tested against Cesium's own on 8 000 eye/point pairs (600 m to 20 000 km,
points under, on and over the ellipsoid; no disagreement) and at the limb
(a label 40 m under the ellipsoid just past the horizon is hidden, with its
cullPosition lifted 100 m it is shown). It ran per label per frame with
three new vectors each call; it now uses scratch vectors. Micro-measure
(500 labels per frame, no scavenge during the run): 7 941 B per frame
before, 51 after.

The W1 sources that projected with Cesium's SceneTransforms themselves -
the icon orientation's exact projection and the launch replay overlay - go
through the engine's projector when their scene or viewer has a registered
engine. Satellites, vessel labels and aircraft recession project nothing
themselves (the host does).

scripts/qa-overlay-engines.mjs publishes the same labels on both engines at
a city, an orbital and a limb pose and compares the painted rectangles:
worst 0 / 2 / 0 px; on both, the far-side label is not painted, the lifted
limb label is and the buried one is not.
This commit is contained in:
German Khokhlov committed 2026-10-16 21:14:52 +04:00
1 parent 311722e6de
commit 7d5a484796
9 files changed
+139 -37

No files matched your search

+2 -1
View File
@@ -1095,5 +1095,6 @@
"scripts/qa-surface-heights.mjs", "scripts/qa-surface-heights.mjs",
"src/globe/navara/followGestures.js", "src/globe/navara/followGestures.js",
"src/globe/navara/followGestures.test.mjs", "src/globe/navara/followGestures.test.mjs",
"src/globe/cpuPicks.test.mjs" "src/globe/cpuPicks.test.mjs",
"scripts/qa-overlay-engines.mjs"
] ]
+27 -9
View File
@@ -893,7 +893,9 @@
"src/geo/check.js", "src/geo/check.js",
"src/geo/debug.js", "src/geo/debug.js",
"src/geo/developerError.js", "src/geo/developerError.js",
"src/time/clock.js" "src/time/clock.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -947,7 +949,9 @@
"src/geo/check.js", "src/geo/check.js",
"src/geo/debug.js", "src/geo/debug.js",
"src/geo/developerError.js", "src/geo/developerError.js",
"src/time/clock.js" "src/time/clock.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -988,7 +992,9 @@
"src/geo/scaleToGeodeticSurface.js", "src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js", "src/geo/check.js",
"src/geo/debug.js", "src/geo/debug.js",
"src/geo/developerError.js" "src/geo/developerError.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1028,7 +1034,9 @@
"src/geo/scaleToGeodeticSurface.js", "src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js", "src/geo/check.js",
"src/geo/debug.js", "src/geo/debug.js",
"src/geo/developerError.js" "src/geo/developerError.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1088,7 +1096,9 @@
"src/geo/scaleToGeodeticSurface.js", "src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js", "src/geo/check.js",
"src/geo/debug.js", "src/geo/debug.js",
"src/geo/developerError.js" "src/geo/developerError.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1171,7 +1181,9 @@
"src/geo/check.js", "src/geo/check.js",
"src/geo/debug.js", "src/geo/debug.js",
"src/geo/developerError.js", "src/geo/developerError.js",
"src/time/clock.js" "src/time/clock.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1263,7 +1275,9 @@
"src/geo/scaleToGeodeticSurface.js", "src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js", "src/geo/check.js",
"src/geo/debug.js", "src/geo/debug.js",
"src/geo/developerError.js" "src/geo/developerError.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1414,7 +1428,9 @@
"src/geo/scaleToGeodeticSurface.js", "src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js", "src/geo/check.js",
"src/geo/debug.js", "src/geo/debug.js",
"src/geo/developerError.js" "src/geo/developerError.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
], ],
"external": ["hls.js", "cesium"] "external": ["hls.js", "cesium"]
}, },
@@ -1557,7 +1573,9 @@
"src/geo/scaleToGeodeticSurface.js", "src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js", "src/geo/check.js",
"src/geo/debug.js", "src/geo/debug.js",
"src/geo/developerError.js" "src/geo/developerError.js",
"src/globe/contract.js",
"src/globe/cesium/engineFromViewer.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
+4 -2
View File
@@ -226,8 +226,10 @@ test('isOverlayPointVisible: horizon test prefers entry.cullPosition when presen
test('normalizeOverlayEntry: carries a valid cullPosition, nulls a junk one', () => { test('normalizeOverlayEntry: carries a valid cullPosition, nulls a junk one', () => {
const position = { x: 1, y: 2, z: 3 }; const position = { x: 1, y: 2, z: 3 };
const kept = normalizeOverlayEntry('firms', { id: 'a', position, cullPosition: { x: 4, y: 5, z: 6 } }); const source = { x: 4, y: 5, z: 6 };
assert.ok(kept.cullPosition instanceof Cesium.Cartesian3, 'stored as a host-owned Cartesian3'); const kept = normalizeOverlayEntry('firms', { id: 'a', position, cullPosition: source });
assert.notEqual(kept.cullPosition, source, 'stored as a host-owned copy (a geo Cartesian3)');
assert.ok(!(kept.cullPosition instanceof Cesium.Cartesian3), 'not a Cesium value');
assert.deepEqual( assert.deepEqual(
[kept.cullPosition.x, kept.cullPosition.y, kept.cullPosition.z], [kept.cullPosition.x, kept.cullPosition.y, kept.cullPosition.z],
[4, 5, 6], [4, 5, 6],
+12 -10
View File
@@ -23,6 +23,16 @@
*/ */
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from '../geo/index.js'; import * as geo from '../geo/index.js';
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
// A window position through the engine's projector when `scene` has a
// registered engine (engineFromViewer), else Cesium's SceneTransforms.
function worldToWindow(scene, position, result) {
const projector = engineFromViewer(scene)?.projector;
return projector
? projector.worldToWindow(position, result)
: Cesium.SceneTransforms.worldToWindowCoordinates(scene, position, result);
}
/** Meters used to give the course vector a stable projection magnitude. */ /** Meters used to give the course vector a stable projection magnitude. */
const FORWARD_PROBE_M = 2000; const FORWARD_PROBE_M = 2000;
@@ -331,16 +341,8 @@ export function perspectiveProjectedRotation(
); );
geo.cartesian3.add(position, _scratchWorldForward, _scratchProbe); geo.cartesian3.add(position, _scratchWorldForward, _scratchProbe);
const at = Cesium.SceneTransforms.worldToWindowCoordinates( const at = worldToWindow(scene, position, _scratchWindowAt);
scene, const ahead = worldToWindow(scene, _scratchProbe, _scratchWindowAhead);
position,
_scratchWindowAt,
);
const ahead = Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
_scratchProbe,
_scratchWindowAhead,
);
if (!at || !ahead) { if (!at || !ahead) {
return screenProjectedRotation(scene, position, courseDeg, previous); return screenProjectedRotation(scene, position, courseDeg, previous);
} }
+24
View File
@@ -745,3 +745,27 @@ test('the perspective helper is continuous through the sub-pixel band, not a 23
`exact alone at ${s.separation.toFixed(2)} px`, `exact alone at ${s.separation.toFixed(2)} px`,
); );
}); });
test('the exact projection asks the engine projector of a registered scene', async () => {
const geo = await import('../geo/index.js');
const { registerViewerEngine } = await import('../globe/cesium/engineFromViewer.js');
const { perspectiveProjectedRotation: rotation } = await import('./iconOrientation.js');
const scene = { camera: { rightWC: { x: 0, y: 1, z: 0 }, upWC: { x: 0, y: 0, z: 1 } } };
const asked = [];
const unregister = registerViewerEngine({ scene }, {
projector: {
// Screen-right for every point further east (the probe), so 90° on screen.
worldToWindow(position, result = geo.cartesian2.create()) {
asked.push(position);
return Object.assign(result, { x: asked.length === 1 ? 100 : 120, y: 100 });
},
},
});
try {
const value = rotation(scene, geo.cartesian3.fromDegrees(0, 0, 0), 90, null);
assert.equal(asked.length, 2, 'the anchor and the probe');
assert.ok(Math.abs(value - -Math.PI / 2) < 1e-9, `rotation ${value}`);
} finally {
unregister();
}
});
+10 -3
View File
@@ -221,20 +221,27 @@ export function pickEllipsoid(camera, screen) {
return intersectEllipsoid(camera.position, pickRay(camera, screen).direction); return intersectEllipsoid(camera.position, pickRay(camera, screen).direction);
} }
// horizonVisible runs per label per frame (worldOverlay): no allocation.
const horizonCamera = cartesian3.create();
const horizonTarget = cartesian3.create();
const horizonVt = cartesian3.create();
/** Cesium's EllipsoidalOccluder.isPointVisible from `eye`. */ /** Cesium's EllipsoidalOccluder.isPointVisible from `eye`. */
export function horizonVisible(eye, position) { export function horizonVisible(eye, position) {
const camera = cartesian3.create( const camera = cartesian3.fromElements(
eye.x / RADII.x, eye.x / RADII.x,
eye.y / RADII.y, eye.y / RADII.y,
eye.z / RADII.z, eye.z / RADII.z,
horizonCamera,
); );
const limbSquared = cartesian3.magnitudeSquared(camera) - 1; const limbSquared = cartesian3.magnitudeSquared(camera) - 1;
const target = cartesian3.create( const target = cartesian3.fromElements(
position.x / RADII.x, position.x / RADII.x,
position.y / RADII.y, position.y / RADII.y,
position.z / RADII.z, position.z / RADII.z,
horizonTarget,
); );
const vt = cartesian3.subtract(target, camera, cartesian3.create()); const vt = cartesian3.subtract(target, camera, horizonVt);
const vtDotVc = -cartesian3.dot(vt, camera); const vtDotVc = -cartesian3.dot(vt, camera);
const occluded = const occluded =
limbSquared < 0 limbSquared < 0
+45
View File
@@ -313,3 +313,48 @@ test('cartographic heights of the math agree with src/geo', () => {
'height', 'height',
); );
}); });
test("horizonVisible agrees with Cesium's EllipsoidalOccluder, also at the limb", async () => {
const Cesium = await import('cesium');
const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84);
let seed = 7;
const random = () => {
seed = (seed * 16807) % 2147483647;
return seed / 2147483647;
};
let disagreements = 0;
let checked = 0;
for (let i = 0; i < 400; i++) {
const eye = cartesian3.fromDegrees(
random() * 360 - 180,
random() * 160 - 80,
[600, 15_000, 400_000, 20_000_000][i % 4],
);
occluder.cameraPosition = new Cesium.Cartesian3(eye.x, eye.y, eye.z);
for (let j = 0; j < 20; j++) {
// Points on the ground and above it, many near the horizon of the eye.
const point = cartesian3.fromDegrees(
random() * 360 - 180,
random() * 160 - 80,
[0, -40, 50, 10_000][j % 4],
);
checked++;
if (
horizonVisible(eye, point) !==
occluder.isPointVisible(
new Cesium.Cartesian3(point.x, point.y, point.z),
)
)
disagreements++;
}
}
assert.equal(disagreements, 0, `${disagreements}/${checked} disagree`);
// At the limb: a point 40 m under the ellipsoid just past the horizon is
// hidden, its cullPosition lifted 100 m is visible (worldOverlay's rule).
const eye = cartesian3.fromDegrees(0, 0, 10_000);
const horizonDeg = math.toDegrees(Math.acos(6378137 / (6378137 + 10_000)));
const buried = cartesian3.fromDegrees(horizonDeg + 0.02, 0, -40);
const lifted = cartesian3.fromDegrees(horizonDeg + 0.02, 0, 100);
assert.equal(horizonVisible(eye, buried), false);
assert.equal(horizonVisible(eye, lifted), true);
});
+12 -8
View File
@@ -1,3 +1,4 @@
import { engineFromViewer } from '../../globe/cesium/engineFromViewer.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { import {
createCyberSonarSampler, createCyberSonarSampler,
@@ -483,10 +484,16 @@ export function createOverlays({ state: layerState, services, parts, source }) {
hideReplayVehicleOverlay(); hideReplayVehicleOverlay();
return null; return null;
} }
const windowPosition = Cesium.SceneTransforms.worldToWindowCoordinates( // The engine's projector (engineFromViewer), as the world overlay.
layerState._viewer.scene, const projector = engineFromViewer(layerState._viewer)?.projector;
position, const project = (point) =>
); projector
? projector.worldToWindow(point)
: Cesium.SceneTransforms.worldToWindowCoordinates(
layerState._viewer.scene,
point,
);
const windowPosition = project(position);
if (!windowPosition) { if (!windowPosition) {
hideReplayVehicleOverlay(); hideReplayVehicleOverlay();
return null; return null;
@@ -548,10 +555,7 @@ export function createOverlays({ state: layerState, services, parts, source }) {
useForward ? forwardProgress : backwardProgress, useForward ? forwardProgress : backwardProgress,
); );
const tangentWindowPosition = tangentPosition const tangentWindowPosition = tangentPosition
? Cesium.SceneTransforms.worldToWindowCoordinates( ? project(tangentPosition)
layerState._viewer.scene,
tangentPosition,
)
: null; : null;
if (tangentWindowPosition) { if (tangentWindowPosition) {
const screenDelta = Math.hypot( const screenDelta = Math.hypot(
+3 -4
View File
@@ -1,4 +1,3 @@
import * as Cesium from 'cesium';
import * as geo from '../geo/index.js'; import * as geo from '../geo/index.js';
import { isGlobeEngine } from '../globe/contract.js'; import { isGlobeEngine } from '../globe/contract.js';
import { import {
@@ -416,7 +415,8 @@ export function paintLaneForOverlayEntry(entry = {}) {
* point) to the per-frame occluder. * point) to the per-frame occluder.
* *
* @param {object} entry - Raw source entry. * @param {object} entry - Raw source entry.
* @returns {?Cesium.Cartesian3} Host-owned lifted anchor, or null. * @returns {?{x: number, y: number, z: number}} Host-owned lifted anchor
* (a geo Cartesian3), or null.
*/ */
function snapshotCullPosition(entry) { function snapshotCullPosition(entry) {
let candidate; let candidate;
@@ -429,8 +429,7 @@ function snapshotCullPosition(entry) {
const { x, y, z } = candidate; const { x, y, z } = candidate;
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
return null; return null;
// Still the sources' own vector type until they move off Cesium (phase 1c). return geo.cartesian3.create(x, y, z);
return new Cesium.Cartesian3(x, y, z);
} }
export function normalizeOverlayEntry(sourceId, entry) { export function normalizeOverlayEntry(sourceId, entry) {