From 6d6ad20ad3d171ade71911b9542d94d72855194d Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:41:30 -0700 Subject: [PATCH 1/5] fix(infrastructure): ground-sample only stem bases the camera sees scene.sampleHeight reads only surface rendered for the current view. For a data centre or dam base outside the frustum or over the horizon it read through the Earth (about -6,344 km over Ashburn on Esri), failed the 9,000 m check and retried every 2 s indefinitely, one offscreen render per record. Over Ashburn that held rest frames at 58-589 ms. Unseen bases are now skipped without being stamped, so each is sampled on the first walk that sees it, and they no longer arm the retry render. Co-Authored-By: Claude Opus 5.5 --- docs/CURRENT-STATE.md | 4 ++- src/data/localGeojson.test.mjs | 53 +++++++++++++++++++++++++++++++++- src/data/localGeojsonCore.js | 44 +++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 09ae87f1a..e5202e55c 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1959,7 +1959,9 @@ during continuous motion. Disable/re-enable resets selection cleanly. Close-up stem sizing uses camera-to-feature distance without a 5 km minimum. Unchanged overlay entries do not request another frame; moved positions, changed membership and re-enabling still publish. Ground sampling waits for -visible globe tiles to settle and uses valid loaded terrain as a floor while +visible globe tiles to settle, samples only markers whose base is in the +camera's view and above the horizon (an unseen base reads through the Earth +and would retry every 2 s), and uses valid loaded terrain as a floor while retaining rooftop and below-sea-level elevations. A hidden globe does not constrain photoreal geometry, and retry work remains bounded. Already sampled nearby markers follow higher settled terrain on existing bounded walks, without diff --git a/src/data/localGeojson.test.mjs b/src/data/localGeojson.test.mjs index 13cde65a5..89de41529 100644 --- a/src/data/localGeojson.test.mjs +++ b/src/data/localGeojson.test.mjs @@ -90,6 +90,8 @@ async function createRealLocalLayerHarness({ globalThis.window = { dispatchEvent() {} }; let sampleHeightImpl = sampleHeight; const sampleCalls = { count: 0 }; + // The camera frustum, reduced to the one answer ground sampling asks of it. + let baseVisibility = Cesium.Intersect.INSIDE; const overlayHost = { setVisible: (...args) => hostCalls.push(['visible', ...args]), setEntries: (...args) => hostCalls.push(['entries', ...args]), @@ -107,7 +109,14 @@ async function createRealLocalLayerHarness({ }, camera: { positionWC: Cesium.Cartesian3.fromDegrees(-97.695, 30.205, 100_000), - frustum: { fov: Math.PI / 3 }, + directionWC: new Cesium.Cartesian3(), + upWC: new Cesium.Cartesian3(), + frustum: { + fov: Math.PI / 3, + computeCullingVolume: () => ({ + computeVisibility: () => baseVisibility, + }), + }, moveEnd, flyTo() {}, }, @@ -151,6 +160,10 @@ async function createRealLocalLayerHarness({ sampleCalls, /** Swap scene.sampleHeight mid-test (e.g. tiles finally arrive). */ setSampleHeight(next) { sampleHeightImpl = next; }, + /** Put the feature's base inside (true) or outside the camera frustum. */ + setBaseInView(inView) { + baseVisibility = inView ? Cesium.Intersect.INSIDE : Cesium.Intersect.OUTSIDE; + }, cleanup() { if (originalWindow === undefined) delete globalThis.window; else globalThis.window = originalWindow; @@ -736,6 +749,44 @@ test('ground sampling waits for visible globe tiles before caching a height', as assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01); }); +test('an unseen base is not sampled and arms no retry until it is in view', async (t) => { + // Out of view the scene has no surface under the base, so the depth sample + // would pass through the Earth (about -6,344 km) and fail on every retry. + const env = await createRealLocalLayerHarness({ + sampleHeightSupported: true, sampleHeight: () => 117, + }); + _resetRenderGovernorForTest(); + const governorScene = { renders: 0, requestRender() { this.renders += 1; } }; + installRenderGovernor({ scene: governorScene }); + governorScene.renders = 0; // discard the governor's own install settling frame + t.mock.timers.enable({ apis: ['setTimeout'] }); + const clock = installFakeClock(t); + t.after(() => { + env.layer.destroy(env.viewer); + env.cleanup(); + _resetRenderGovernorForTest(); + }); + env.viewer.scene.globe = { show: true, tilesLoaded: true, getHeight: () => 117 }; + setCameraAltitude(env, 20_000); + env.setBaseInView(false); + runArmedRetryChain(env, t, clock, 5); + assert.equal(env.sampleCalls.count, 0, 'an off-screen base must not be sampled'); + assert.equal(governorScene.renders, 0, 'an off-screen base must not keep a parked camera rendering'); + assert.ok(Math.abs(baseHeightM(env)) < 0.01); + + // In the frustum but over the horizon: 50 m up, about 67 km away. + env.setBaseInView(true); + env.viewer.camera.positionWC = Cesium.Cartesian3.fromDegrees(-97.0, 30.205, 50); + runArmedRetryChain(env, t, clock, 5); + assert.equal(env.sampleCalls.count, 0, 'a base over the horizon must not be sampled'); + assert.equal(governorScene.renders, 0); + + setCameraAltitude(env, 20_000); + env.preRender.raise(); + assert.equal(env.sampleCalls.count, 1, 'the base is sampled on the first walk that sees it'); + assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01); +}); + test('settled terrain refinement lifts an already sampled stem without another GPU sample', async (t) => { const env = await createRealLocalLayerHarness({ sampleHeightSupported: true, sampleHeight: () => 117, diff --git a/src/data/localGeojsonCore.js b/src/data/localGeojsonCore.js index 1d04fa46c..9d97df61c 100644 --- a/src/data/localGeojsonCore.js +++ b/src/data/localGeojsonCore.js @@ -1000,9 +1000,9 @@ export function createLocalGeoJsonLayer( // governor the retry's preRender never arrives on a parked camera // and the stem stays at ellipsoid height (buried/floating) until // the user happens to move. Schedule the frame the retry needs. - // Gated on a sampleable scene and in-range records only, so a far - // camera (or a keyless scene) stays fully idle; the distance is - // only computed for still-unsampled stems. + // Gated on a sampleable scene and in-range, in-view records only, + // so a far camera (or a keyless scene) stays fully idle; the + // distance is only computed for still-unsampled stems. if ( canSampleGround && !record.groundSampled && @@ -1010,7 +1010,8 @@ export function createLocalGeoJsonLayer( Cesium.Cartesian3.distance( viewer.camera.positionWC, record.base, - ) < GROUND_SAMPLE_MAX_DISTANCE_M + ) < GROUND_SAMPLE_MAX_DISTANCE_M && + groundSampleInView(viewer, record) ) { groundRetryPending = true; } @@ -1130,9 +1131,44 @@ function refreshLocalTerrainFloor(viewer, record) { return true; } +const groundSampleSphere = new Cesium.BoundingSphere( + new Cesium.Cartesian3(), + 1, +); + +const groundSampleOccluder = new Cesium.EllipsoidalOccluder( + Cesium.Ellipsoid.WGS84, +); + +/** + * Whether the camera sees the record's base: inside the view frustum and + * above the horizon. scene.sampleHeight reads only surface rendered for the + * current view, so a sample of an unseen base passes through the Earth (about + * -6,344 km near Ashburn on Esri) and fails on every 2 s retry, each one a + * full offscreen render: 110 unseen data centres there held the page near 2 s + * a frame indefinitely. An unseen base keeps its stem at the unsampled height + * and is sampled on the first walk that sees it. + */ +function groundSampleInView(viewer, record) { + const camera = viewer.camera; + const culling = camera.frustum.computeCullingVolume( + camera.positionWC, + camera.directionWC, + camera.upWC, + ); + Cesium.Cartesian3.clone(record.base, groundSampleSphere.center); + if ( + culling.computeVisibility(groundSampleSphere) === Cesium.Intersect.OUTSIDE + ) + return false; + groundSampleOccluder.cameraPosition = camera.positionWC; + return groundSampleOccluder.isPointVisible(record.base); +} + function sampleLocalGroundHeight(viewer, record, now) { if (record.groundSampled || !viewer.scene.sampleHeightSupported) return false; if (now - record.lastGroundSampleMs < GROUND_SAMPLE_RETRY_MS) return false; + if (!groundSampleInView(viewer, record)) return false; record.lastGroundSampleMs = now; const globe = viewer.scene.globe; if (globe?.show && globe.tilesLoaded === false) return false; From 189b30b5459a1a476fed3833325d2f460ce07862 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:43:40 -0700 Subject: [PATCH 2/5] docs(changelog): add the infrastructure ground-sampling entry Co-Authored-By: Claude Opus 5.5 --- changelog.d/1464-infrastructure-ground-sample-in-view.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/1464-infrastructure-ground-sample-in-view.md diff --git a/changelog.d/1464-infrastructure-ground-sample-in-view.md b/changelog.d/1464-infrastructure-ground-sample-in-view.md new file mode 100644 index 000000000..8ab5d1d88 --- /dev/null +++ b/changelog.d/1464-infrastructure-ground-sample-in-view.md @@ -0,0 +1,4 @@ +- Ground-sample Data Centers and Dams markers only when the camera sees their + base. An off-screen base read through the Earth and retried every 2 s, which + held the page near 2 s a frame over Ashburn, Virginia on the Esri map + (quanticsoul4772, #1464). From ecc0ced47c4f765627910bf089e303251cd5e9b4 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:25:57 -0700 Subject: [PATCH 3/5] fix(infrastructure): test the ground, not the ellipsoid, for in-view sampling An unsampled base sits at ellipsoid height 0. Over high terrain seen at an angle that point projects below the screen while the ground above it is in view, so the gate skipped ground the camera could see and never sampled it. The gate now tests loaded globe terrain when there is some, and otherwise the whole span of heights a sample can accept (0 to 9,000 m). Co-Authored-By: Claude Opus 5.5 --- ...64-infrastructure-ground-sample-in-view.md | 6 +- docs/CURRENT-STATE.md | 8 ++- src/data/localGeojson.test.mjs | 58 +++++++++++++++++++ src/data/localGeojsonCore.js | 49 +++++++++++++--- 4 files changed, 106 insertions(+), 15 deletions(-) diff --git a/changelog.d/1464-infrastructure-ground-sample-in-view.md b/changelog.d/1464-infrastructure-ground-sample-in-view.md index 8ab5d1d88..b563c86b4 100644 --- a/changelog.d/1464-infrastructure-ground-sample-in-view.md +++ b/changelog.d/1464-infrastructure-ground-sample-in-view.md @@ -1,4 +1,4 @@ -- Ground-sample Data Centers and Dams markers only when the camera sees their - base. An off-screen base read through the Earth and retried every 2 s, which - held the page near 2 s a frame over Ashburn, Virginia on the Esri map +- Ground-sample Data Centers and Dams markers only when the camera sees the + ground under them. Unseen ground read through the Earth and retried every + 2 s; over Ashburn, Virginia on the Esri map, slow frames reached 2 s (quanticsoul4772, #1464). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index e5202e55c..065d67112 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1959,9 +1959,11 @@ during continuous motion. Disable/re-enable resets selection cleanly. Close-up stem sizing uses camera-to-feature distance without a 5 km minimum. Unchanged overlay entries do not request another frame; moved positions, changed membership and re-enabling still publish. Ground sampling waits for -visible globe tiles to settle, samples only markers whose base is in the -camera's view and above the horizon (an unseen base reads through the Earth -and would retry every 2 s), and uses valid loaded terrain as a floor while +visible globe tiles to settle, samples only markers whose ground is in the +camera's view and above the horizon (unseen ground reads through the Earth +and would retry every 2 s; until it is seen, a stem keeps its unsampled +height, which can show for a stem whose base is just off screen), and uses +valid loaded terrain as a floor while retaining rooftop and below-sea-level elevations. A hidden globe does not constrain photoreal geometry, and retry work remains bounded. Already sampled nearby markers follow higher settled terrain on existing bounded walks, without diff --git a/src/data/localGeojson.test.mjs b/src/data/localGeojson.test.mjs index 89de41529..1c47cb8c1 100644 --- a/src/data/localGeojson.test.mjs +++ b/src/data/localGeojson.test.mjs @@ -787,6 +787,64 @@ test('an unseen base is not sampled and arms no retry until it is in view', asyn assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01); }); +for (const [label, globe] of [ + ['loaded terrain', { show: true, tilesLoaded: true, getHeight: () => 2_500 }], + ['a hidden globe', { show: false, tilesLoaded: true, getHeight: () => undefined }], +]) { + test(`high ground in view is sampled with ${label} although its unsampled base projects off screen`, async (t) => { + // A base on 2,500 m terrain, seen from 3 km away at a shallow angle: its + // ellipsoid-height point is far below the bottom of the screen. + const env = await createRealLocalLayerHarness({ + sampleHeightSupported: true, sampleHeight: () => 2_500, + }); + const clock = installFakeClock(t); + t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); }); + env.viewer.scene.globe = globe; + const entity = env.dataSources[0].entities.values[0]; + const base = Cesium.Cartographic.fromCartesian(entity.__localBaseCartesian); + const ground = Cesium.Cartesian3.fromRadians(base.longitude, base.latitude, 2_500); + const position = Cesium.Cartesian3.fromRadians( + base.longitude, + base.latitude - 3_000 / 6_371_000, + 2_500 + 1_000, + ); + const direction = Cesium.Cartesian3.normalize( + Cesium.Cartesian3.subtract(ground, position, new Cesium.Cartesian3()), + new Cesium.Cartesian3(), + ); + const right = Cesium.Cartesian3.normalize( + Cesium.Cartesian3.cross( + direction, + Cesium.Ellipsoid.WGS84.geodeticSurfaceNormal(position), + new Cesium.Cartesian3(), + ), + new Cesium.Cartesian3(), + ); + const up = Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()); + const frustum = new Cesium.PerspectiveFrustum({ + fov: Math.PI / 3, + aspectRatio: 800 / 600, + near: 1, + far: 1e8, + }); + Object.assign(env.viewer.camera, { + positionWC: position, + directionWC: direction, + upWC: up, + frustum, + }); + const offScreen = frustum + .computeCullingVolume(position, direction, up) + .computeVisibility(new Cesium.BoundingSphere(entity.__localBaseCartesian, 1)); + assert.equal(offScreen, Cesium.Intersect.OUTSIDE, 'precondition: the ellipsoid-height base is off screen'); + + clock.advance(2_100); + env.preRender.raise(); + assert.equal(env.sampleCalls.count, 1, 'the ground in view is sampled'); + assert.ok(Math.abs(baseHeightM(env) - 2_500) < 0.01); + }); +} + test('settled terrain refinement lifts an already sampled stem without another GPU sample', async (t) => { const env = await createRealLocalLayerHarness({ sampleHeightSupported: true, sampleHeight: () => 117, diff --git a/src/data/localGeojsonCore.js b/src/data/localGeojsonCore.js index 9d97df61c..ec8fc824e 100644 --- a/src/data/localGeojsonCore.js +++ b/src/data/localGeojsonCore.js @@ -1140,29 +1140,60 @@ const groundSampleOccluder = new Cesium.EllipsoidalOccluder( Cesium.Ellipsoid.WGS84, ); +const groundSampleCarto = new Cesium.Cartographic(); +const groundSampleTop = new Cesium.Cartesian3(); + /** - * Whether the camera sees the record's base: inside the view frustum and - * above the horizon. scene.sampleHeight reads only surface rendered for the - * current view, so a sample of an unseen base passes through the Earth (about - * -6,344 km near Ashburn on Esri) and fails on every 2 s retry, each one a - * full offscreen render: 110 unseen data centres there held the page near 2 s - * a frame indefinitely. An unseen base keeps its stem at the unsampled height - * and is sampled on the first walk that sees it. + * Whether the camera can see the ground under the record: inside the view + * frustum and above the horizon. scene.sampleHeight reads only surface + * rendered for the current view, so a sample of unseen ground passes through + * the Earth (about -6,344 km near Ashburn on Esri) and fails on every 2 s + * retry, each one a full offscreen render; 110 unseen data centres there held + * rest frames at 58-589 ms. Unsampled bases sit on the ellipsoid, so the test + * uses loaded globe terrain when there is some, and otherwise every height a + * sample can accept (0 to GROUND_SAMPLE_MAX_ABS_HEIGHT_M): high ground in + * view is never mistaken for an unseen base. Unseen ground is sampled on the + * first walk that sees it. */ function groundSampleInView(viewer, record) { const camera = viewer.camera; + const globe = viewer.scene.globe; + const terrain = globe?.show ? globe.getHeight?.(record.carto) : undefined; + const known = + Number.isFinite(terrain) && + Math.abs(terrain) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M; + const span = GROUND_SAMPLE_MAX_ABS_HEIGHT_M / 2; + groundSampleCarto.longitude = record.carto.longitude; + groundSampleCarto.latitude = record.carto.latitude; + groundSampleCarto.height = known ? terrain : span; + Cesium.Cartographic.toCartesian( + groundSampleCarto, + Cesium.Ellipsoid.WGS84, + groundSampleSphere.center, + ); + groundSampleSphere.radius = known ? 1 : span; const culling = camera.frustum.computeCullingVolume( camera.positionWC, camera.directionWC, camera.upWC, ); - Cesium.Cartesian3.clone(record.base, groundSampleSphere.center); if ( culling.computeVisibility(groundSampleSphere) === Cesium.Intersect.OUTSIDE ) return false; + // The highest ground the sample could find is the last to drop below the + // horizon. + if (!known) groundSampleCarto.height = GROUND_SAMPLE_MAX_ABS_HEIGHT_M; groundSampleOccluder.cameraPosition = camera.positionWC; - return groundSampleOccluder.isPointVisible(record.base); + return groundSampleOccluder.isPointVisible( + known + ? groundSampleSphere.center + : Cesium.Cartographic.toCartesian( + groundSampleCarto, + Cesium.Ellipsoid.WGS84, + groundSampleTop, + ), + ); } function sampleLocalGroundHeight(viewer, record, now) { From f392eecbd969a0f8941b6ee2748bc58362a51865 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:47:05 -0700 Subject: [PATCH 4/5] fix(infrastructure): test ground below 0 m and unknown terrain correctly The ellipsoidal horizon test hid ground below the ellipsoid (low-geoid coasts, the Dead Sea) from a camera near it, so it is now tested at 0 m. With the globe hidden the ground is tested as the vertical from 0 to 9,000 m against the frustum planes, in place of a 4.5 km sphere that let in much unseen ground. Co-Authored-By: Claude Opus 5.5 --- docs/CURRENT-STATE.md | 11 +-- src/data/localGeojson.test.mjs | 128 +++++++++++++++++++++------------ src/data/localGeojsonCore.js | 77 +++++++++++--------- 3 files changed, 130 insertions(+), 86 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 065d67112..4815e0fd1 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1959,11 +1959,12 @@ during continuous motion. Disable/re-enable resets selection cleanly. Close-up stem sizing uses camera-to-feature distance without a 5 km minimum. Unchanged overlay entries do not request another frame; moved positions, changed membership and re-enabling still publish. Ground sampling waits for -visible globe tiles to settle, samples only markers whose ground is in the -camera's view and above the horizon (unseen ground reads through the Earth -and would retry every 2 s; until it is seen, a stem keeps its unsampled -height, which can show for a stem whose base is just off screen), and uses -valid loaded terrain as a floor while +visible globe tiles to settle, and skips markers whose ground the camera +cannot see (unseen ground reads through the Earth and would retry every 2 s). +Ground is tested at loaded globe terrain, or across 0-9,000 m when the globe +is hidden, so photoreal views skip only ground well out of view. Until it is +seen, a stem keeps its unsampled height, which can show for a stem whose base +is just off screen. Sampling uses valid loaded terrain as a floor while retaining rooftop and below-sea-level elevations. A hidden globe does not constrain photoreal geometry, and retry work remains bounded. Already sampled nearby markers follow higher settled terrain on existing bounded walks, without diff --git a/src/data/localGeojson.test.mjs b/src/data/localGeojson.test.mjs index 1c47cb8c1..3e104592f 100644 --- a/src/data/localGeojson.test.mjs +++ b/src/data/localGeojson.test.mjs @@ -113,8 +113,13 @@ async function createRealLocalLayerHarness({ upWC: new Cesium.Cartesian3(), frustum: { fov: Math.PI / 3, + // No planes: everything is in view. One plane no point satisfies: + // nothing is. computeCullingVolume: () => ({ - computeVisibility: () => baseVisibility, + planes: + baseVisibility === Cesium.Intersect.OUTSIDE + ? [new Cesium.Cartesian4(0, 0, 0, -1)] + : [], }), }, moveEnd, @@ -787,64 +792,95 @@ test('an unseen base is not sampled and arms no retry until it is in view', asyn assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01); }); -for (const [label, globe] of [ - ['loaded terrain', { show: true, tilesLoaded: true, getHeight: () => 2_500 }], - ['a hidden globe', { show: false, tilesLoaded: true, getHeight: () => undefined }], +/** + * Give the harness a real Cesium camera `southM` metres south of the base at + * `cameraM` metres above the ellipsoid, looking at the base's ground at + * `groundM` (or directly away from it). + */ +function aimRealCamera(env, { groundM, cameraM, southM, away = false }) { + const entity = env.dataSources[0].entities.values[0]; + const base = Cesium.Cartographic.fromCartesian(entity.__localBaseCartesian); + const ground = Cesium.Cartesian3.fromRadians(base.longitude, base.latitude, groundM); + const position = Cesium.Cartesian3.fromRadians( + base.longitude, + base.latitude - southM / 6_371_000, + cameraM, + ); + const direction = Cesium.Cartesian3.normalize( + away + ? Cesium.Cartesian3.subtract(position, ground, new Cesium.Cartesian3()) + : Cesium.Cartesian3.subtract(ground, position, new Cesium.Cartesian3()), + new Cesium.Cartesian3(), + ); + const right = Cesium.Cartesian3.normalize( + Cesium.Cartesian3.cross( + direction, + Cesium.Ellipsoid.WGS84.geodeticSurfaceNormal(position), + new Cesium.Cartesian3(), + ), + new Cesium.Cartesian3(), + ); + const up = Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()); + const frustum = new Cesium.PerspectiveFrustum({ + fov: Math.PI / 3, + aspectRatio: 800 / 600, + near: 1, + far: 1e8, + }); + Object.assign(env.viewer.camera, { + positionWC: position, + directionWC: direction, + upWC: up, + frustum, + }); + return frustum + .computeCullingVolume(position, direction, up) + .computeVisibility(new Cesium.BoundingSphere(entity.__localBaseCartesian, 1)); +} + +const loadedTerrain = (height) => ({ show: true, tilesLoaded: true, getHeight: () => height }); +const hiddenGlobe = { show: false, tilesLoaded: true, getHeight: () => undefined }; + +for (const { label, globe, groundM, cameraM, southM, baseOffScreen } of [ + // 2,500 m terrain from 3 km away at a shallow angle: the ellipsoid-height + // base is far below the bottom of the screen. + { label: 'high ground with loaded terrain', globe: loadedTerrain(2_500), groundM: 2_500, cameraM: 3_500, southM: 3_000, baseOffScreen: true }, + { label: 'high ground under a hidden globe', globe: hiddenGlobe, groundM: 2_500, cameraM: 3_500, southM: 3_000, baseOffScreen: true }, + // A low-geoid coast (about -63 m near Mumbai) from a camera 50 m above the + // ellipsoid: the ellipsoidal horizon would hide the ground itself. + { label: 'ground below the ellipsoid', globe: loadedTerrain(-63), groundM: -63, cameraM: 50, southM: 2_000, baseOffScreen: false }, ]) { - test(`high ground in view is sampled with ${label} although its unsampled base projects off screen`, async (t) => { - // A base on 2,500 m terrain, seen from 3 km away at a shallow angle: its - // ellipsoid-height point is far below the bottom of the screen. + test(`${label} in view is sampled`, async (t) => { const env = await createRealLocalLayerHarness({ - sampleHeightSupported: true, sampleHeight: () => 2_500, + sampleHeightSupported: true, sampleHeight: () => groundM, }); const clock = installFakeClock(t); t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); }); env.viewer.scene.globe = globe; - const entity = env.dataSources[0].entities.values[0]; - const base = Cesium.Cartographic.fromCartesian(entity.__localBaseCartesian); - const ground = Cesium.Cartesian3.fromRadians(base.longitude, base.latitude, 2_500); - const position = Cesium.Cartesian3.fromRadians( - base.longitude, - base.latitude - 3_000 / 6_371_000, - 2_500 + 1_000, - ); - const direction = Cesium.Cartesian3.normalize( - Cesium.Cartesian3.subtract(ground, position, new Cesium.Cartesian3()), - new Cesium.Cartesian3(), - ); - const right = Cesium.Cartesian3.normalize( - Cesium.Cartesian3.cross( - direction, - Cesium.Ellipsoid.WGS84.geodeticSurfaceNormal(position), - new Cesium.Cartesian3(), - ), - new Cesium.Cartesian3(), - ); - const up = Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()); - const frustum = new Cesium.PerspectiveFrustum({ - fov: Math.PI / 3, - aspectRatio: 800 / 600, - near: 1, - far: 1e8, - }); - Object.assign(env.viewer.camera, { - positionWC: position, - directionWC: direction, - upWC: up, - frustum, - }); - const offScreen = frustum - .computeCullingVolume(position, direction, up) - .computeVisibility(new Cesium.BoundingSphere(entity.__localBaseCartesian, 1)); - assert.equal(offScreen, Cesium.Intersect.OUTSIDE, 'precondition: the ellipsoid-height base is off screen'); + const baseVisibility = aimRealCamera(env, { groundM, cameraM, southM }); + if (baseOffScreen) + assert.equal(baseVisibility, Cesium.Intersect.OUTSIDE, 'precondition: the ellipsoid-height base is off screen'); clock.advance(2_100); env.preRender.raise(); assert.equal(env.sampleCalls.count, 1, 'the ground in view is sampled'); - assert.ok(Math.abs(baseHeightM(env) - 2_500) < 0.01); + assert.ok(Math.abs(baseHeightM(env) - groundM) < 0.01); }); } +test('under a hidden globe, ground behind the camera is not sampled', async (t) => { + const env = await createRealLocalLayerHarness({ + sampleHeightSupported: true, sampleHeight: () => 2_500, + }); + const clock = installFakeClock(t); + t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); }); + env.viewer.scene.globe = hiddenGlobe; + aimRealCamera(env, { groundM: 2_500, cameraM: 3_500, southM: 3_000, away: true }); + clock.advance(2_100); + env.preRender.raise(); + assert.equal(env.sampleCalls.count, 0); +}); + test('settled terrain refinement lifts an already sampled stem without another GPU sample', async (t) => { const env = await createRealLocalLayerHarness({ sampleHeightSupported: true, sampleHeight: () => 117, diff --git a/src/data/localGeojsonCore.js b/src/data/localGeojsonCore.js index ec8fc824e..596b78ed9 100644 --- a/src/data/localGeojsonCore.js +++ b/src/data/localGeojsonCore.js @@ -1131,17 +1131,25 @@ function refreshLocalTerrainFloor(viewer, record) { return true; } -const groundSampleSphere = new Cesium.BoundingSphere( - new Cesium.Cartesian3(), - 1, -); - const groundSampleOccluder = new Cesium.EllipsoidalOccluder( Cesium.Ellipsoid.WGS84, ); const groundSampleCarto = new Cesium.Cartographic(); -const groundSampleTop = new Cesium.Cartesian3(); +const groundSampleLow = new Cesium.Cartesian3(); +const groundSampleHigh = new Cesium.Cartesian3(); + +/** The record's vertical at `height` metres above the ellipsoid. */ +function groundSamplePoint(record, height, result) { + groundSampleCarto.longitude = record.carto.longitude; + groundSampleCarto.latitude = record.carto.latitude; + groundSampleCarto.height = height; + return Cesium.Cartographic.toCartesian( + groundSampleCarto, + Cesium.Ellipsoid.WGS84, + result, + ); +} /** * Whether the camera can see the ground under the record: inside the view @@ -1150,10 +1158,10 @@ const groundSampleTop = new Cesium.Cartesian3(); * the Earth (about -6,344 km near Ashburn on Esri) and fails on every 2 s * retry, each one a full offscreen render; 110 unseen data centres there held * rest frames at 58-589 ms. Unsampled bases sit on the ellipsoid, so the test - * uses loaded globe terrain when there is some, and otherwise every height a - * sample can accept (0 to GROUND_SAMPLE_MAX_ABS_HEIGHT_M): high ground in - * view is never mistaken for an unseen base. Unseen ground is sampled on the - * first walk that sees it. + * uses loaded globe terrain when there is some. Otherwise (a hidden globe) it + * tests the vertical from 0 to GROUND_SAMPLE_MAX_ABS_HEIGHT_M, every height a + * sample can accept, so high ground in view is never mistaken for an unseen + * base. Unseen ground is sampled on the first walk that sees it. */ function groundSampleInView(viewer, record) { const camera = viewer.camera; @@ -1162,37 +1170,36 @@ function groundSampleInView(viewer, record) { const known = Number.isFinite(terrain) && Math.abs(terrain) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M; - const span = GROUND_SAMPLE_MAX_ABS_HEIGHT_M / 2; - groundSampleCarto.longitude = record.carto.longitude; - groundSampleCarto.latitude = record.carto.latitude; - groundSampleCarto.height = known ? terrain : span; - Cesium.Cartographic.toCartesian( - groundSampleCarto, - Cesium.Ellipsoid.WGS84, - groundSampleSphere.center, - ); - groundSampleSphere.radius = known ? 1 : span; - const culling = camera.frustum.computeCullingVolume( + const low = groundSamplePoint(record, known ? terrain : 0, groundSampleLow); + const high = known + ? low + : groundSamplePoint( + record, + GROUND_SAMPLE_MAX_ABS_HEIGHT_M, + groundSampleHigh, + ); + // The vertical is unseen only when both ends are outside the same plane. + const { planes } = camera.frustum.computeCullingVolume( camera.positionWC, camera.directionWC, camera.upWC, ); - if ( - culling.computeVisibility(groundSampleSphere) === Cesium.Intersect.OUTSIDE - ) - return false; - // The highest ground the sample could find is the last to drop below the - // horizon. - if (!known) groundSampleCarto.height = GROUND_SAMPLE_MAX_ABS_HEIGHT_M; + for (const plane of planes) { + if ( + Cesium.Cartesian3.dot(plane, low) + plane.w < 0 && + Cesium.Cartesian3.dot(plane, high) + plane.w < 0 + ) + return false; + } + // The ellipsoidal occluder hides every point below the ellipsoid from a + // camera near it, so ground below 0 m (low-geoid coasts, the Dead Sea) is + // tested at 0 m; unknown ground at the top of the vertical. + const horizonHeight = known + ? Math.max(terrain, 0) + : GROUND_SAMPLE_MAX_ABS_HEIGHT_M; groundSampleOccluder.cameraPosition = camera.positionWC; return groundSampleOccluder.isPointVisible( - known - ? groundSampleSphere.center - : Cesium.Cartographic.toCartesian( - groundSampleCarto, - Cesium.Ellipsoid.WGS84, - groundSampleTop, - ), + groundSamplePoint(record, horizonHeight, groundSampleHigh), ); } From 65f0467ad8955d6db2b02d27cbb2d8fb517ab0d7 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 3 Nov 2026 15:10:15 +0400 Subject: [PATCH 5/5] fix(infrastructure): sample visible ground below the ellipsoid --- docs/CURRENT-STATE.md | 4 ++-- src/data/localGeojson.test.mjs | 11 +++++++---- src/data/localGeojsonCore.js | 13 +++++++++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 718ba460e..9108ed50d 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2048,8 +2048,8 @@ Unchanged overlay entries do not request another frame; moved positions, changed membership and re-enabling still publish. Ground sampling waits for visible globe tiles to settle, and skips markers whose ground the camera cannot see (unseen ground reads through the Earth and would retry every 2 s). -Ground is tested at loaded globe terrain, or across 0-9,000 m when the globe -is hidden, so photoreal views skip only ground well out of view. Until it is +Ground is tested at loaded globe terrain, or across -9,000 to 9,000 m when the +globe is hidden, so photoreal views skip only ground well out of view. Until it is seen, a stem keeps its unsampled height, which can show for a stem whose base is just off screen. Sampling uses valid loaded terrain as a floor while retaining rooftop and below-sea-level elevations. A hidden globe does not diff --git a/src/data/localGeojson.test.mjs b/src/data/localGeojson.test.mjs index 3e104592f..6eae458cb 100644 --- a/src/data/localGeojson.test.mjs +++ b/src/data/localGeojson.test.mjs @@ -797,7 +797,7 @@ test('an unseen base is not sampled and arms no retry until it is in view', asyn * `cameraM` metres above the ellipsoid, looking at the base's ground at * `groundM` (or directly away from it). */ -function aimRealCamera(env, { groundM, cameraM, southM, away = false }) { +function aimRealCamera(env, { groundM, cameraM, southM, away = false, fov = Math.PI / 3 }) { const entity = env.dataSources[0].entities.values[0]; const base = Cesium.Cartographic.fromCartesian(entity.__localBaseCartesian); const ground = Cesium.Cartesian3.fromRadians(base.longitude, base.latitude, groundM); @@ -822,7 +822,7 @@ function aimRealCamera(env, { groundM, cameraM, southM, away = false }) { ); const up = Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()); const frustum = new Cesium.PerspectiveFrustum({ - fov: Math.PI / 3, + fov, aspectRatio: 800 / 600, near: 1, far: 1e8, @@ -841,7 +841,10 @@ function aimRealCamera(env, { groundM, cameraM, southM, away = false }) { const loadedTerrain = (height) => ({ show: true, tilesLoaded: true, getHeight: () => height }); const hiddenGlobe = { show: false, tilesLoaded: true, getHeight: () => undefined }; -for (const { label, globe, groundM, cameraM, southM, baseOffScreen } of [ +for (const { label, globe, groundM, cameraM, southM, fov, baseOffScreen } of [ + // The Dead Sea (about -430 m) under a hidden globe, seen steeply down + // through a narrow view: both 0 m and 9,000 m above it are off screen. + { label: 'hidden-globe depression', globe: hiddenGlobe, groundM: -430, cameraM: 20, southM: 180, fov: Math.PI / 12, baseOffScreen: true }, // 2,500 m terrain from 3 km away at a shallow angle: the ellipsoid-height // base is far below the bottom of the screen. { label: 'high ground with loaded terrain', globe: loadedTerrain(2_500), groundM: 2_500, cameraM: 3_500, southM: 3_000, baseOffScreen: true }, @@ -857,7 +860,7 @@ for (const { label, globe, groundM, cameraM, southM, baseOffScreen } of [ const clock = installFakeClock(t); t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); }); env.viewer.scene.globe = globe; - const baseVisibility = aimRealCamera(env, { groundM, cameraM, southM }); + const baseVisibility = aimRealCamera(env, { groundM, cameraM, southM, fov }); if (baseOffScreen) assert.equal(baseVisibility, Cesium.Intersect.OUTSIDE, 'precondition: the ellipsoid-height base is off screen'); diff --git a/src/data/localGeojsonCore.js b/src/data/localGeojsonCore.js index 596b78ed9..811dfab57 100644 --- a/src/data/localGeojsonCore.js +++ b/src/data/localGeojsonCore.js @@ -1159,9 +1159,10 @@ function groundSamplePoint(record, height, result) { * retry, each one a full offscreen render; 110 unseen data centres there held * rest frames at 58-589 ms. Unsampled bases sit on the ellipsoid, so the test * uses loaded globe terrain when there is some. Otherwise (a hidden globe) it - * tests the vertical from 0 to GROUND_SAMPLE_MAX_ABS_HEIGHT_M, every height a - * sample can accept, so high ground in view is never mistaken for an unseen - * base. Unseen ground is sampled on the first walk that sees it. + * tests the vertical across +/-GROUND_SAMPLE_MAX_ABS_HEIGHT_M, every height a + * sample can accept, so high or below-ellipsoid ground in view is never + * mistaken for an unseen base. Unseen ground is sampled on the first walk that + * sees it. */ function groundSampleInView(viewer, record) { const camera = viewer.camera; @@ -1170,7 +1171,11 @@ function groundSampleInView(viewer, record) { const known = Number.isFinite(terrain) && Math.abs(terrain) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M; - const low = groundSamplePoint(record, known ? terrain : 0, groundSampleLow); + const low = groundSamplePoint( + record, + known ? terrain : -GROUND_SAMPLE_MAX_ABS_HEIGHT_M, + groundSampleLow, + ); const high = known ? low : groundSamplePoint(