mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Keep GNSS history across globe changes and share the Navara ground cell pass
This commit is contained in:
1 parent
6fe5bda028
commit
1930ca247a
20 files changed
+629
-415
No files matched your search
@@ -36,6 +36,7 @@ const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
|
|||||||
const RENDERED_LAYER_DIRS = [
|
const RENDERED_LAYER_DIRS = [
|
||||||
'src/layers/alpr/',
|
'src/layers/alpr/',
|
||||||
'src/layers/airQuality/',
|
'src/layers/airQuality/',
|
||||||
|
'src/layers/gnss/',
|
||||||
'src/layers/radio/',
|
'src/layers/radio/',
|
||||||
'src/layers/bikeshare/',
|
'src/layers/bikeshare/',
|
||||||
'src/layers/earthquakes/',
|
'src/layers/earthquakes/',
|
||||||
|
|||||||
@@ -3227,6 +3227,7 @@
|
|||||||
"src/layers/perimeters/inciweb.js",
|
"src/layers/perimeters/inciweb.js",
|
||||||
"src/app/layers/gnss.js",
|
"src/app/layers/gnss.js",
|
||||||
"src/layers/gnss/index.js",
|
"src/layers/gnss/index.js",
|
||||||
|
"src/layers/gnss/render/cesium.js",
|
||||||
"src/layers/gnss/source.js",
|
"src/layers/gnss/source.js",
|
||||||
"src/layers/gnss/records.js",
|
"src/layers/gnss/records.js",
|
||||||
"src/app/layers/ghostMaps.js",
|
"src/app/layers/ghostMaps.js",
|
||||||
@@ -3657,6 +3658,7 @@
|
|||||||
"src/layers/earthquakes/render/cesium.js",
|
"src/layers/earthquakes/render/cesium.js",
|
||||||
"src/layers/earthquakes/render/navara.js",
|
"src/layers/earthquakes/render/navara.js",
|
||||||
"src/layers/airQuality/render/navara.js",
|
"src/layers/airQuality/render/navara.js",
|
||||||
|
"src/layers/gnss/render/navara.js",
|
||||||
"src/layers/earthquakes/render/navaraGeometry.js",
|
"src/layers/earthquakes/render/navaraGeometry.js",
|
||||||
"src/layers/earthquakes/render/navaraGrid.js",
|
"src/layers/earthquakes/render/navaraGrid.js",
|
||||||
"src/layers/installations/render/navara.js",
|
"src/layers/installations/render/navara.js",
|
||||||
@@ -3683,7 +3685,8 @@
|
|||||||
"src/layers/alpr/render/navara.js",
|
"src/layers/alpr/render/navara.js",
|
||||||
"src/layers/alpr/render/navaraOverlay.js",
|
"src/layers/alpr/render/navaraOverlay.js",
|
||||||
"src/layers/radio/screenClusters.js",
|
"src/layers/radio/screenClusters.js",
|
||||||
"src/layers/firms/render/navaraGeometry.js",
|
"src/layers/shared/render/navaraCellGeometry.js",
|
||||||
|
"src/layers/shared/render/navaraGroundCells.js",
|
||||||
"src/layers/firms/render/navaraSprites.js",
|
"src/layers/firms/render/navaraSprites.js",
|
||||||
"src/layers/shared/render/navaraGroundGeometry.js",
|
"src/layers/shared/render/navaraGroundGeometry.js",
|
||||||
"src/layers/shared/render/navaraGroundGrid.js",
|
"src/layers/shared/render/navaraGroundGrid.js",
|
||||||
@@ -4104,6 +4107,7 @@
|
|||||||
"src/layers/perimeters/inciweb.js",
|
"src/layers/perimeters/inciweb.js",
|
||||||
"src/app/layers/gnss.js",
|
"src/app/layers/gnss.js",
|
||||||
"src/layers/gnss/index.js",
|
"src/layers/gnss/index.js",
|
||||||
|
"src/layers/gnss/render/cesium.js",
|
||||||
"src/layers/gnss/source.js",
|
"src/layers/gnss/source.js",
|
||||||
"src/layers/gnss/records.js",
|
"src/layers/gnss/records.js",
|
||||||
"src/app/layers/ghostMaps.js",
|
"src/app/layers/ghostMaps.js",
|
||||||
@@ -5237,6 +5241,7 @@
|
|||||||
"exports": ["./layers/gnss"],
|
"exports": ["./layers/gnss"],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/layers/gnss/index.js",
|
"src/layers/gnss/index.js",
|
||||||
|
"src/layers/gnss/render/cesium.js",
|
||||||
"src/sources/httpBody.js",
|
"src/sources/httpBody.js",
|
||||||
"src/i18n/marker.js",
|
"src/i18n/marker.js",
|
||||||
"src/layers/gnss/source.js",
|
"src/layers/gnss/source.js",
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ try {
|
|||||||
const fetch = window.fetch.bind(window);
|
const fetch = window.fetch.bind(window);
|
||||||
window.fetch = (input, o) => {
|
window.fetch = (input, o) => {
|
||||||
const url = new URL(input.url ?? input, document.baseURI);
|
const url = new URL(input.url ?? input, document.baseURI);
|
||||||
|
if (url.pathname === '/api/gnss-integrity') {
|
||||||
|
return Promise.resolve(Response.json({ageMs: 0, rows: ['a1','a2','a3'].map(hex =>
|
||||||
|
({hex, lat: 43.65, lon: -79.38, nic: 0, nacp: 0, degraded: true, gpsLost: false}))}));
|
||||||
|
}
|
||||||
if (url.hostname === 'api.weather.gc.ca') {
|
if (url.hostname === 'api.weather.gc.ca') {
|
||||||
const records = url.pathname.includes('aqhi-stations')
|
const records = url.pathname.includes('aqhi-stations')
|
||||||
? [
|
? [
|
||||||
@@ -76,12 +80,12 @@ try {
|
|||||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const capture = async (visible) => {
|
const capture = async (visible, layerId = 'air-quality') => {
|
||||||
await p.evaluate(async (show) => {
|
await p.evaluate(async (show, id) => {
|
||||||
const d = window.__godsEyeView;
|
const d = window.__godsEyeView;
|
||||||
await d.dataManager.setEnabled('air-quality', show, { origin: 'user' });
|
await d.dataManager.setEnabled(id, show, { origin: 'user' });
|
||||||
d.engine.render.request('qa-air-quality');
|
d.engine.render.request('qa-air-quality');
|
||||||
}, visible);
|
}, visible, layerId);
|
||||||
await new Promise((r) => setTimeout(r, 3900));
|
await new Promise((r) => setTimeout(r, 3900));
|
||||||
const shot = await p.screenshot();
|
const shot = await p.screenshot();
|
||||||
return sharp(shot)
|
return sharp(shot)
|
||||||
@@ -97,6 +101,7 @@ try {
|
|||||||
if (d.tileset || window.__GOOGLE_MAPS_API_KEY__)
|
if (d.tileset || window.__GOOGLE_MAPS_API_KEY__)
|
||||||
throw Error('Keyless required');
|
throw Error('Keyless required');
|
||||||
window.qaAir = d.dataManager.layers.get('air-quality').module;
|
window.qaAir = d.dataManager.layers.get('air-quality').module;
|
||||||
|
window.qaGnss = d.dataManager.layers.get('gnss-integrity').module;
|
||||||
});
|
});
|
||||||
for (const kind of ['navara', 'cesium', 'navara']) {
|
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||||
@@ -105,10 +110,12 @@ try {
|
|||||||
const same = await p.evaluate(
|
const same = await p.evaluate(
|
||||||
() =>
|
() =>
|
||||||
window.__godsEyeView.dataManager.layers.get('air-quality').module ===
|
window.__godsEyeView.dataManager.layers.get('air-quality').module ===
|
||||||
window.qaAir,
|
window.qaAir &&
|
||||||
|
window.__godsEyeView.dataManager.layers.get('gnss-integrity').module === window.qaGnss,
|
||||||
);
|
);
|
||||||
assert.ok(same);
|
assert.ok(same);
|
||||||
}
|
}
|
||||||
|
await p.evaluate(() => window.__godsEyeView.dataManager.setEnabled('gnss-integrity', false));
|
||||||
const off = await capture(false),
|
const off = await capture(false),
|
||||||
on = await capture(true);
|
on = await capture(true);
|
||||||
const values = await p.evaluate(() => ({
|
const values = await p.evaluate(() => ({
|
||||||
@@ -131,6 +138,14 @@ try {
|
|||||||
assert.equal(values.records[0].name, 'QA Toronto');
|
assert.equal(values.records[0].name, 'QA Toronto');
|
||||||
assert.equal(values.records[0].value, 9);
|
assert.equal(values.records[0].value, 9);
|
||||||
assert.equal(values.card.length, 1);
|
assert.equal(values.card.length, 1);
|
||||||
|
await capture(false);
|
||||||
|
const cellOff = await capture(false, 'gnss-integrity');
|
||||||
|
const cellOn = await capture(true, 'gnss-integrity');
|
||||||
|
assert.ok(orange(cellOn) > orange(cellOff) + 5, kind + ' must draw GNSS high cell');
|
||||||
|
const gnss = await p.evaluate(() => window.qaGnss.getStats());
|
||||||
|
assert.equal(gnss.levels.high, 1);
|
||||||
|
console.log('PASS', kind, 'GNSS high cell, view anchor', gnss.coverage.latitude, gnss.coverage.longitude);
|
||||||
|
await p.evaluate(() => window.__godsEyeView.dataManager.setEnabled('air-quality', true));
|
||||||
}
|
}
|
||||||
assert.deepEqual(errors, []);
|
assert.deepEqual(errors, []);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -196,10 +196,10 @@ export function createApplicationCatalog({
|
|||||||
[
|
[
|
||||||
'gnss-integrity',
|
'gnss-integrity',
|
||||||
() =>
|
() =>
|
||||||
createApplicationGnssIntegrity({
|
sessionModel('gnss-integrity', () => createApplicationGnssIntegrity({
|
||||||
source: sources['gnss-integrity'],
|
source: sources['gnss-integrity'],
|
||||||
windowMs: sources['gnss-integrity'].windowMs,
|
windowMs: sources['gnss-integrity'].windowMs,
|
||||||
}),
|
})),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
's2-ghostmaps',
|
's2-ghostmaps',
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import { rendererFor } from './renderers.js';
|
||||||
|
import { createGnssCesiumRenderer } from '../../layers/gnss/render/cesium.js';
|
||||||
import { createGnssIntegrityLayer } from '../../layers/gnss/index.js';
|
import { createGnssIntegrityLayer } from '../../layers/gnss/index.js';
|
||||||
|
|
||||||
/** Wire the adsb.lol GNSS navigation-integrity layer into the application catalog. */
|
/** Wire the adsb.lol GNSS navigation-integrity layer into the application catalog. */
|
||||||
export function createApplicationGnssIntegrity(options) {
|
export function createApplicationGnssIntegrity(options) {
|
||||||
return createGnssIntegrityLayer(options);
|
return createGnssIntegrityLayer({
|
||||||
|
createRenderer: ({ viewer, engine, viewAnchor }) => engine
|
||||||
|
? rendererFor('gnss-integrity', engine, { viewer, viewAnchor })
|
||||||
|
: createGnssCesiumRenderer({ viewer, viewAnchor }),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createGnssCesiumRenderer } from '../../layers/gnss/render/cesium.js';
|
||||||
import { createAirQualityCesiumRenderer } from '../../layers/airQuality/render/cesium.js';
|
import { createAirQualityCesiumRenderer } from '../../layers/airQuality/render/cesium.js';
|
||||||
import { createFirmsCesiumRenderer } from '../../layers/firms/render/cesium.js';
|
import { createFirmsCesiumRenderer } from '../../layers/firms/render/cesium.js';
|
||||||
import { createEarthquakesCesiumRenderer } from '../../layers/earthquakes/render/cesium.js';
|
import { createEarthquakesCesiumRenderer } from '../../layers/earthquakes/render/cesium.js';
|
||||||
@@ -94,6 +95,7 @@ export const layerAvailableOn = (layerId, engineKind) =>
|
|||||||
export const CESIUM_LAYER_RENDERERS = Object.freeze({
|
export const CESIUM_LAYER_RENDERERS = Object.freeze({
|
||||||
earthquakes: createEarthquakesCesiumRenderer,
|
earthquakes: createEarthquakesCesiumRenderer,
|
||||||
'air-quality': (_engine, options) => createAirQualityCesiumRenderer(options),
|
'air-quality': (_engine, options) => createAirQualityCesiumRenderer(options),
|
||||||
|
'gnss-integrity': (_engine, options) => createGnssCesiumRenderer(options),
|
||||||
'local-firms': createFirmsCesiumRenderer,
|
'local-firms': createFirmsCesiumRenderer,
|
||||||
'rocket-launches': createLaunchesCesiumRenderer,
|
'rocket-launches': createLaunchesCesiumRenderer,
|
||||||
satellites: createSatellitesCesiumRenderer,
|
satellites: createSatellitesCesiumRenderer,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ test('the aircraft, vessels, satellites, launches and earthquakes have extracted
|
|||||||
assert.deepEqual(Object.keys(CESIUM_LAYER_RENDERERS), [
|
assert.deepEqual(Object.keys(CESIUM_LAYER_RENDERERS), [
|
||||||
'earthquakes',
|
'earthquakes',
|
||||||
'air-quality',
|
'air-quality',
|
||||||
|
'gnss-integrity',
|
||||||
'local-firms',
|
'local-firms',
|
||||||
'rocket-launches',
|
'rocket-launches',
|
||||||
'satellites',
|
'satellites',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ test('the Navara mode registers migrated renderers once', async () => {
|
|||||||
'ais-live-vessels',
|
'ais-live-vessels',
|
||||||
'earthquakes',
|
'earthquakes',
|
||||||
'air-quality',
|
'air-quality',
|
||||||
|
'gnss-integrity',
|
||||||
'local-firms',
|
'local-firms',
|
||||||
'military-installations',
|
'military-installations',
|
||||||
'bikeshare',
|
'bikeshare',
|
||||||
@@ -46,6 +47,7 @@ test('the Navara mode registers migrated renderers once', async () => {
|
|||||||
'local-adsb',
|
'local-adsb',
|
||||||
'earthquakes',
|
'earthquakes',
|
||||||
'air-quality',
|
'air-quality',
|
||||||
|
'gnss-integrity',
|
||||||
'alpr-cameras',
|
'alpr-cameras',
|
||||||
'satellites',
|
'satellites',
|
||||||
'rocket-launches',
|
'rocket-launches',
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ export async function registerNavaraLayerRenderers() {
|
|||||||
...navaraViews.get(engine),
|
...navaraViews.get(engine),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
'gnss-integrity': async () => {
|
||||||
|
const { createGnssNavaraRenderer } = await import('../layers/gnss/render/navara.js');
|
||||||
|
return (engine) => createGnssNavaraRenderer(engine, navaraViews.get(engine));
|
||||||
|
},
|
||||||
'air-quality': async () => {
|
'air-quality': async () => {
|
||||||
const { createAirQualityNavaraRenderer } =
|
const { createAirQualityNavaraRenderer } =
|
||||||
await import('../layers/airQuality/render/navara.js');
|
await import('../layers/airQuality/render/navara.js');
|
||||||
|
|||||||
@@ -1,291 +1,26 @@
|
|||||||
// FIRMS terrain/tiles classification and actual engine mover glows.
|
// FIRMS terrain/tiles classification and actual engine mover glows.
|
||||||
import { Effect, EffectAttribute } from 'postprocessing';
|
|
||||||
import { Matrix4, Uniform } from 'three';
|
|
||||||
import { createEngineLayerView } from '../../shared/render/engineView.js';
|
import { createEngineLayerView } from '../../shared/render/engineView.js';
|
||||||
import { selectionDispatcherFor } from '../../../data/selectionDispatcher.js';
|
import { selectionDispatcherFor } from '../../../data/selectionDispatcher.js';
|
||||||
import { LOD_LEVELS } from '../policy.js';
|
import { LOD_LEVELS } from '../policy.js';
|
||||||
import { createFirmsSpriteBank } from './navaraSprites.js';
|
import { createFirmsSpriteBank } from './navaraSprites.js';
|
||||||
import { rectangleFrame } from './navaraGeometry.js';
|
import { createGroundCellDisplay } from '../../shared/render/navaraGroundCells.js';
|
||||||
import {
|
|
||||||
viewFrame,
|
|
||||||
groundClipBounds,
|
|
||||||
} from '../../shared/render/navaraGroundGeometry.js';
|
|
||||||
import {
|
|
||||||
createGroundGrid,
|
|
||||||
GRID_COLS,
|
|
||||||
GRID_ROWS,
|
|
||||||
} from '../../shared/render/navaraGroundGrid.js';
|
|
||||||
import {
|
|
||||||
createGroundTexture,
|
|
||||||
createGroundStorage,
|
|
||||||
groundSurfaceUniforms,
|
|
||||||
updateGroundSurface,
|
|
||||||
} from '../../shared/render/navaraGroundSurface.js';
|
|
||||||
const KEY = 'gevFirmsGround';
|
|
||||||
const registeredViews = new WeakSet();
|
|
||||||
const fragmentShader = /* glsl */ `
|
|
||||||
uniform sampler2D tAllDepth;
|
|
||||||
uniform sampler2D tSurfaceDepth;
|
|
||||||
uniform sampler2D tCells;
|
|
||||||
uniform sampler2D tGrid;
|
|
||||||
uniform sampler2D tCandidates;
|
|
||||||
uniform vec2 uCellSize;
|
|
||||||
uniform vec2 uCandidateSize;
|
|
||||||
uniform mat4 uInverseProjection;
|
|
||||||
uniform mat4 uInverseView;
|
|
||||||
uniform float uLogDepthFC;
|
|
||||||
vec4 atSlot(sampler2D image,vec2 size,float slot) {
|
|
||||||
return texture2D(image,(vec2(mod(slot,size.x),floor(slot/size.x))+0.5)/size);
|
|
||||||
}
|
|
||||||
vec3 toDisplay(vec3 c) {
|
|
||||||
return mix(12.92*c,1.055*pow(max(c,vec3(0.0)),vec3(1.0/2.4))-0.055,step(vec3(0.0031308),c));
|
|
||||||
}
|
|
||||||
vec3 toLinear(vec3 c) {
|
|
||||||
return mix(c/12.92,pow(max((c+0.055)/1.055,vec3(0.0)),vec3(2.4)),step(vec3(0.04045),c));
|
|
||||||
}
|
|
||||||
vec3 pixelPlane(float distance,vec3 normal,vec3 dx,vec3 dy) {
|
|
||||||
return vec3(distance,dot(normal,dx),dot(normal,dy));
|
|
||||||
}
|
|
||||||
float rectangleCoverage(vec3 west,vec3 east,vec3 south,vec3 north) {
|
|
||||||
vec3 edges[4];
|
|
||||||
edges[0]=west;edges[1]=east;edges[2]=south;edges[3]=north;
|
|
||||||
float coverage=1.0;
|
|
||||||
int partial=0;
|
|
||||||
for(int i=0;i<4;i++) {
|
|
||||||
float width=abs(edges[i].y)+abs(edges[i].z);
|
|
||||||
coverage=min(coverage,clamp(edges[i].x/max(width,0.001)+0.5,0.0,1.0));
|
|
||||||
if(coverage<=0.0) return 0.0;
|
|
||||||
if(edges[i].x<=width*0.5) partial++;
|
|
||||||
}
|
|
||||||
if(partial<2) return coverage;
|
|
||||||
// Intersect the SAME pixel square with all sides. Minimum soft-edge
|
|
||||||
// coverage creates long miter halos at acute/polar rectangle corners.
|
|
||||||
vec2 polygon[8],clipped[8];
|
|
||||||
polygon[0]=vec2(-0.5,-0.5);polygon[1]=vec2(0.5,-0.5);
|
|
||||||
polygon[2]=vec2(0.5,0.5);polygon[3]=vec2(-0.5,0.5);
|
|
||||||
int count=4;
|
|
||||||
for(int side=0;side<4;side++) {
|
|
||||||
int nextCount=0;
|
|
||||||
vec2 previous=polygon[count-1];
|
|
||||||
float before=edges[side].x+dot(edges[side].yz,previous);
|
|
||||||
for(int i=0;i<8;i++) {
|
|
||||||
if(i>=count) break;
|
|
||||||
vec2 current=polygon[i];
|
|
||||||
float after=edges[side].x+dot(edges[side].yz,current);
|
|
||||||
if((before>0.0&&after<0.0)||(before<0.0&&after>0.0))
|
|
||||||
clipped[nextCount++]=mix(previous,current,before/(before-after));
|
|
||||||
if(after>=0.0) clipped[nextCount++]=current;
|
|
||||||
previous=current;before=after;
|
|
||||||
}
|
|
||||||
count=nextCount;
|
|
||||||
if(count<3) return 0.0;
|
|
||||||
for(int i=0;i<8;i++) { if(i>=count) break;polygon[i]=clipped[i]; }
|
|
||||||
}
|
|
||||||
float area=0.0;
|
|
||||||
vec2 previous=polygon[count-1];
|
|
||||||
for(int i=0;i<8;i++) {
|
|
||||||
if(i>=count) break;
|
|
||||||
area+=previous.x*polygon[i].y-previous.y*polygon[i].x;
|
|
||||||
previous=polygon[i];
|
|
||||||
}
|
|
||||||
return clamp(abs(area)*0.5,0.0,1.0);
|
|
||||||
}
|
|
||||||
void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor) {
|
|
||||||
outputColor=inputColor;
|
|
||||||
float depth=unpackRGBAToDepth(texture2D(tAllDepth,uv));
|
|
||||||
float distance=exp2(2.0*depth/uLogDepthFC)-1.0;
|
|
||||||
vec4 ray=uInverseProjection*vec4(uv*2.0-1.0,-1.0,1.0);
|
|
||||||
vec3 point=ray.xyz*(distance/-ray.z);
|
|
||||||
// Evaluate derivatives before divergent candidate branches, as in quakes.
|
|
||||||
vec3 world=(uInverseView*vec4(point,1.0)).xyz;
|
|
||||||
vec3 dx=(uInverseView*vec4(dFdx(point),0.0)).xyz;
|
|
||||||
vec3 dy=(uInverseView*vec4(dFdy(point),0.0)).xyz;
|
|
||||||
// A quad crossing the sky has no surface derivative. Keep exact halfspaces
|
|
||||||
// there; otherwise its far-plane lane spreads cell AA along the globe limb.
|
|
||||||
float onSurface=1.0-step(1.0,depth);
|
|
||||||
float derivativeMask=1.0-step(0.5,abs(dFdx(onSurface))+abs(dFdy(onSurface)));
|
|
||||||
dx*=derivativeMask;
|
|
||||||
dy*=derivativeMask;
|
|
||||||
if(depth>=1.0) return;
|
|
||||||
vec2 tile=clamp(floor(uv*vec2(${GRID_COLS}.0,${GRID_ROWS}.0)),vec2(0.0),vec2(${GRID_COLS - 1}.0,${GRID_ROWS - 1}.0));
|
|
||||||
vec2 range=texture2D(tGrid,(tile+0.5)/vec2(${GRID_COLS}.0,${GRID_ROWS}.0)).xy;
|
|
||||||
if(range.y<0.5) return;
|
|
||||||
float surface=texture2D(tSurfaceDepth,uv).r;
|
|
||||||
if(depth<surface-2e-7) return;
|
|
||||||
float rho=length(world.xy);
|
|
||||||
vec2 radial=world.xy/max(rho,0.001);
|
|
||||||
vec3 color=toDisplay(inputColor.rgb);
|
|
||||||
for(int k=0;k<int(range.y);k++) {
|
|
||||||
float id=atSlot(tCandidates,uCandidateSize,range.x+float(k)).x;
|
|
||||||
vec4 longitude=atSlot(tCells,uCellSize,id*4.0);
|
|
||||||
vec3 south=atSlot(tCells,uCellSize,id*4.0+1.0).xyz;
|
|
||||||
vec3 north=atSlot(tCells,uCellSize,id*4.0+2.0).xyz;
|
|
||||||
vec3 westNormal=vec3(-longitude.x,longitude.y,0.0);
|
|
||||||
vec3 eastNormal=vec3(longitude.z,-longitude.w,0.0);
|
|
||||||
float coverage=rectangleCoverage(
|
|
||||||
pixelPlane(dot(westNormal,world),westNormal,dx,dy),
|
|
||||||
pixelPlane(dot(eastNormal,world),eastNormal,dx,dy),
|
|
||||||
pixelPlane(world.z*south.y-rho*south.x+south.z,vec3(-south.x*radial,south.y),dx,dy),
|
|
||||||
pixelPlane(rho*north.x-world.z*north.y-north.z,vec3(north.x*radial,-north.y),dx,dy));
|
|
||||||
if(coverage<=0.0) continue;
|
|
||||||
vec4 fill=atSlot(tCells,uCellSize,id*4.0+3.0);
|
|
||||||
color=mix(color,fill.rgb,fill.a*coverage);
|
|
||||||
}
|
|
||||||
outputColor=vec4(toLinear(color),inputColor.a);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
function buildState(cells) {
|
|
||||||
const frames = cells.map(rectangleFrame),
|
|
||||||
packed = createGroundStorage(cells.length * 4);
|
|
||||||
const grid = createGroundGrid(cells.length),
|
|
||||||
gridData = new Float32Array(GRID_COLS * GRID_ROWS * 4);
|
|
||||||
const gridTexture = createGroundTexture(gridData, GRID_COLS, GRID_ROWS);
|
|
||||||
let candidates = createGroundStorage(1);
|
|
||||||
const scratch = new Float64Array(12),
|
|
||||||
bounds = new Float64Array(4);
|
|
||||||
const lastView = new Float64Array(16).fill(NaN),
|
|
||||||
lastProjection = new Float64Array(16).fill(NaN);
|
|
||||||
let lastWidth = 0,
|
|
||||||
lastHeight = 0;
|
|
||||||
frames.forEach((frame, index) => {
|
|
||||||
const offset = index * 16,
|
|
||||||
color = cells[index].color;
|
|
||||||
packed.data.set(frame.bounds.longitude, offset);
|
|
||||||
packed.data.set(frame.bounds.south, offset + 4);
|
|
||||||
packed.data.set(frame.bounds.north, offset + 8);
|
|
||||||
packed.data.set(
|
|
||||||
[color.red, color.green, color.blue, color.alpha],
|
|
||||||
offset + 12,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
count: cells.length,
|
|
||||||
packed,
|
|
||||||
gridTexture,
|
|
||||||
get candidates() {
|
|
||||||
return candidates;
|
|
||||||
},
|
|
||||||
update(camera, context) {
|
|
||||||
const input = context.getInputBuffer(),
|
|
||||||
view = camera.matrixWorldInverse.elements,
|
|
||||||
projection = camera.projectionMatrix.elements;
|
|
||||||
let changed = input.width !== lastWidth || input.height !== lastHeight;
|
|
||||||
for (let i = 0; i < 16 && !changed; i++)
|
|
||||||
changed =
|
|
||||||
view[i] !== lastView[i] || projection[i] !== lastProjection[i];
|
|
||||||
if (!changed) return;
|
|
||||||
lastView.set(view);
|
|
||||||
lastProjection.set(projection);
|
|
||||||
lastWidth = input.width;
|
|
||||||
lastHeight = input.height;
|
|
||||||
frames.forEach((frame, index) => {
|
|
||||||
viewFrame(frame, camera.matrixWorldInverse, scratch);
|
|
||||||
groundClipBounds(frame, scratch, camera.projectionMatrix, bounds);
|
|
||||||
if (bounds[0] <= 1) {
|
|
||||||
bounds[0] -= 2 / Math.max(1, input.width);
|
|
||||||
bounds[2] += 2 / Math.max(1, input.width);
|
|
||||||
bounds[1] -= 2 / Math.max(1, input.height);
|
|
||||||
bounds[3] += 2 / Math.max(1, input.height);
|
|
||||||
}
|
|
||||||
grid.bounds.set(bounds, index * 4);
|
|
||||||
});
|
|
||||||
const total = grid.pack();
|
|
||||||
if (candidates.data.length / 4 < total) {
|
|
||||||
candidates.texture.dispose();
|
|
||||||
candidates = createGroundStorage(
|
|
||||||
Math.max(total, candidates.data.length / 2),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < total; i++) candidates.data[i * 4] = grid.indices[i];
|
|
||||||
for (let i = 0; i < grid.counts.length; i++) {
|
|
||||||
gridData[i * 4] = grid.offsets[i];
|
|
||||||
gridData[i * 4 + 1] = grid.counts[i];
|
|
||||||
}
|
|
||||||
gridTexture.needsUpdate = true;
|
|
||||||
candidates.texture.needsUpdate = true;
|
|
||||||
},
|
|
||||||
dispose() {
|
|
||||||
packed.texture.dispose();
|
|
||||||
gridTexture.dispose();
|
|
||||||
candidates.texture.dispose();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function groundEffectClass(EffectDesc, NavaraEffect) {
|
|
||||||
return class FirmsGroundEffectDesc extends EffectDesc {
|
|
||||||
static key = KEY;
|
|
||||||
static insertAfter = [
|
|
||||||
'gevEarthquakeGround',
|
|
||||||
'gevRecentImagery',
|
|
||||||
'toneMapping',
|
|
||||||
];
|
|
||||||
static insertBefore = [
|
|
||||||
'gevPreSharpenOverlay',
|
|
||||||
'gevSharpen',
|
|
||||||
'smaa',
|
|
||||||
'fxaa',
|
|
||||||
'gevOverlay',
|
|
||||||
'final',
|
|
||||||
];
|
|
||||||
constructor(view, context, config) {
|
|
||||||
super(view, context, config);
|
|
||||||
this.owner = config[KEY].owner;
|
|
||||||
}
|
|
||||||
createPass() {
|
|
||||||
this.effect = new Effect('GevFirmsGround', fragmentShader, {
|
|
||||||
attributes: EffectAttribute.CONVOLUTION,
|
|
||||||
uniforms: new Map([
|
|
||||||
...groundSurfaceUniforms(),
|
|
||||||
['tCells', new Uniform(null)],
|
|
||||||
['tGrid', new Uniform(null)],
|
|
||||||
['tCandidates', new Uniform(null)],
|
|
||||||
['uCellSize', new Uniform([1, 1])],
|
|
||||||
['uCandidateSize', new Uniform([1, 1])],
|
|
||||||
['uInverseView', new Uniform(new Matrix4())],
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
return new NavaraEffect(this.view.camera.raw, this.effect);
|
|
||||||
}
|
|
||||||
update() {
|
|
||||||
const state = this.owner.state,
|
|
||||||
camera = this.view.camera.raw;
|
|
||||||
if (!this.effect || !state || !camera) return;
|
|
||||||
updateGroundSurface(this.effect, camera, this.ctx, this.find('mrt'));
|
|
||||||
state.update(camera, this.ctx);
|
|
||||||
const u = this.effect.uniforms;
|
|
||||||
u.get('tCells').value = state.packed.texture;
|
|
||||||
u.get('uCellSize').value[0] = state.packed.width;
|
|
||||||
u.get('uCellSize').value[1] = state.packed.height;
|
|
||||||
u.get('tGrid').value = state.gridTexture;
|
|
||||||
u.get('tCandidates').value = state.candidates.texture;
|
|
||||||
u.get('uCandidateSize').value[0] = state.candidates.width;
|
|
||||||
u.get('uCandidateSize').value[1] = state.candidates.height;
|
|
||||||
u.get('uInverseView').value.copy(camera.matrixWorld);
|
|
||||||
}
|
|
||||||
onDestroy() {
|
|
||||||
this.effect?.dispose();
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export function createFirmsNavaraRenderer(
|
export function createFirmsNavaraRenderer(
|
||||||
engine,
|
engine,
|
||||||
{ view: nativeView, EffectDesc, NavaraEffect, id = 'local-firms' } = {},
|
{ view: nativeView, EffectDesc, NavaraEffect, id = 'local-firms' } = {},
|
||||||
) {
|
) {
|
||||||
if (!engine?.movers || !nativeView?.camera || !EffectDesc || !NavaraEffect)
|
if (!engine?.movers || !nativeView?.camera || !EffectDesc || !NavaraEffect)
|
||||||
throw new TypeError('FIRMS requires a Navara view');
|
throw new TypeError('FIRMS requires a Navara view');
|
||||||
if (!registeredViews.has(nativeView)) {
|
const display = createGroundCellDisplay(
|
||||||
nativeView.registerEffect(KEY, groundEffectClass(EffectDesc, NavaraEffect));
|
engine,
|
||||||
registeredViews.add(nativeView);
|
{ view: nativeView, EffectDesc, NavaraEffect },
|
||||||
}
|
1,
|
||||||
|
);
|
||||||
let destroyed = false,
|
let destroyed = false,
|
||||||
visible = false,
|
visible = false,
|
||||||
cells = [],
|
cells = [],
|
||||||
queued = false,
|
queued = false,
|
||||||
handle = null,
|
|
||||||
sprites = null;
|
sprites = null;
|
||||||
const owner = { state: buildState([]) },
|
const dataSource = { show: false },
|
||||||
dataSource = { show: false },
|
|
||||||
handlers = new Set();
|
handlers = new Set();
|
||||||
const adapter = createEngineLayerView(engine, {
|
const adapter = createEngineLayerView(engine, {
|
||||||
layerId: id,
|
layerId: id,
|
||||||
@@ -293,24 +28,13 @@ export function createFirmsNavaraRenderer(
|
|||||||
});
|
});
|
||||||
adapter.view.camera.moveEnd = engine.camera.moveEnd;
|
adapter.view.camera.moveEnd = engine.camera.moveEnd;
|
||||||
adapter.view.scene.preRender = engine.frames.preRender;
|
adapter.view.scene.preRender = engine.frames.preRender;
|
||||||
const sync = () => {
|
|
||||||
const show = visible && owner.state.count > 0;
|
|
||||||
if (show && !handle)
|
|
||||||
handle = nativeView.addEffect({ [KEY]: { owner }, visible: true });
|
|
||||||
else handle?.update({ visible: show });
|
|
||||||
};
|
|
||||||
const scheduleCells = () => {
|
const scheduleCells = () => {
|
||||||
if (queued || destroyed) return;
|
if (queued || destroyed) return;
|
||||||
queued = true;
|
queued = true;
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
queued = false;
|
queued = false;
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
const next = buildState(cells),
|
display.replace(cells);
|
||||||
previous = owner.state;
|
|
||||||
owner.state = next;
|
|
||||||
sync();
|
|
||||||
previous.dispose();
|
|
||||||
if (visible) engine.render.request('firms-cells');
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const removeSprites = () => {
|
const removeSprites = () => {
|
||||||
@@ -366,7 +90,7 @@ export function createFirmsNavaraRenderer(
|
|||||||
visible = next;
|
visible = next;
|
||||||
dataSource.show = next;
|
dataSource.show = next;
|
||||||
sprites?.setVisible(next);
|
sprites?.setVisible(next);
|
||||||
sync();
|
display.setVisible(visible);
|
||||||
engine.render.request('firms-visibility');
|
engine.render.request('firms-visibility');
|
||||||
},
|
},
|
||||||
clearCells() {
|
clearCells() {
|
||||||
@@ -406,8 +130,7 @@ export function createFirmsNavaraRenderer(
|
|||||||
cells = [];
|
cells = [];
|
||||||
for (const handler of [...handlers]) handler.destroy();
|
for (const handler of [...handlers]) handler.destroy();
|
||||||
removeSprites();
|
removeSprites();
|
||||||
handle?.delete();
|
display.destroy();
|
||||||
owner.state.dispose();
|
|
||||||
adapter.destroy();
|
adapter.destroy();
|
||||||
engine.render.request('firms-destroy');
|
engine.render.request('firms-destroy');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createGroundCellDisplay } from '../../shared/render/navaraGroundCells.js';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createEngineEvent } from '../../../globe/contract.js';
|
import { createEngineEvent } from '../../../globe/contract.js';
|
||||||
@@ -130,6 +131,32 @@ test('ground changes are atomic, lazy and dispose replaced and destroyed GPU tex
|
|||||||
second.destroy();
|
second.destroy();
|
||||||
assert.equal(h.registered.length, 1);
|
assert.equal(h.registered.length, 1);
|
||||||
});
|
});
|
||||||
|
test('GNSS and FIRMS share one ground pass without hiding or destroying one another', async () => {
|
||||||
|
const h = harness();
|
||||||
|
const gnss = createGroundCellDisplay(h.engine, h.options, 0);
|
||||||
|
const fire = h.renderer;
|
||||||
|
try {
|
||||||
|
fire.setVisible(true);
|
||||||
|
fire.addCell(cell);
|
||||||
|
await settle();
|
||||||
|
gnss.replace([cell, cell]);
|
||||||
|
gnss.setVisible(true);
|
||||||
|
const owner = h.effects[0].config.gevFirmsGround.owner;
|
||||||
|
assert.equal(h.effects.length, 1);
|
||||||
|
assert.equal(owner.state.count, 3);
|
||||||
|
gnss.setVisible(false);
|
||||||
|
assert.equal(owner.state.count, 1);
|
||||||
|
gnss.setVisible(true);
|
||||||
|
fire.destroy();
|
||||||
|
assert.equal(owner.state.count, 2);
|
||||||
|
assert.equal(h.effects[0].deleted, 0);
|
||||||
|
} finally {
|
||||||
|
fire.destroy();
|
||||||
|
gnss.destroy();
|
||||||
|
}
|
||||||
|
assert.equal(h.effects[0].deleted, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('FIRMS selection uses actual engine event picks and releases its dispatcher subscriptions', () => {
|
test('FIRMS selection uses actual engine event picks and releases its dispatcher subscriptions', () => {
|
||||||
const h = harness(),
|
const h = harness(),
|
||||||
r = h.renderer;
|
r = h.renderer;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
rectangleBoundaries,
|
rectangleBoundaries,
|
||||||
rectangleDistances,
|
rectangleDistances,
|
||||||
rectangleFrame,
|
rectangleFrame,
|
||||||
} from './navaraGeometry.js';
|
} from '../../shared/render/navaraCellGeometry.js';
|
||||||
const cases = [];
|
const cases = [];
|
||||||
for (const lat of [-90, -80, -45, 0, 45, 80, 88])
|
for (const lat of [-90, -80, -45, 0, 45, 80, 88])
|
||||||
for (const west of [-180, -123, 0, 178])
|
for (const west of [-180, -123, 0, 178])
|
||||||
|
|||||||
+53
-116
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import { createGnssCesiumRenderer } from './render/cesium.js';
|
||||||
|
export { defaultGnssViewAnchor } from './render/cesium.js';
|
||||||
import { formatNumber, msg, t } from '../../i18n/index.js';
|
import { formatNumber, msg, t } from '../../i18n/index.js';
|
||||||
import { translateFeedError } from '../../ui/statusText.js';
|
import { translateFeedError } from '../../ui/statusText.js';
|
||||||
import {
|
import {
|
||||||
@@ -13,17 +14,6 @@ import {
|
|||||||
export * from './records.js';
|
export * from './records.js';
|
||||||
export { createAdsbGnssSource } from './source.js';
|
export { createAdsbGnssSource } from './source.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* GroundPrimitive allowPicking:false suppresses ids but still submits stencil
|
|
||||||
* commands in Cesium's pick pass, erasing picks of overlapping ground geometry.
|
|
||||||
* This fill participates only in rendering, so sibling polygons remain pickable.
|
|
||||||
*/
|
|
||||||
class GnssGroundFill extends Cesium.GroundPrimitive {
|
|
||||||
update(frameState) {
|
|
||||||
if (!frameState.passes.pick) super.update(frameState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Low stays visible over land so coverage reads apart from no data. */
|
/** Low stays visible over land so coverage reads apart from no data. */
|
||||||
const FILL_ALPHA = Object.freeze({ low: 0.3, medium: 0.42, high: 0.55 });
|
const FILL_ALPHA = Object.freeze({ low: 0.3, medium: 0.42, high: 0.55 });
|
||||||
const LEVEL_LABELS = Object.freeze({
|
const LEVEL_LABELS = Object.freeze({
|
||||||
@@ -32,34 +22,6 @@ const LEVEL_LABELS = Object.freeze({
|
|||||||
high: msg('Low-accuracy score > 10%'),
|
high: msg('Low-accuracy score > 10%'),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Degrees at the centre of the current view, or null before the camera settles. */
|
|
||||||
export function defaultGnssViewAnchor(viewer) {
|
|
||||||
const canvas = viewer?.scene?.canvas;
|
|
||||||
const ellipsoid = viewer?.scene?.globe?.ellipsoid;
|
|
||||||
// At globe scale the visible rectangle can be the whole world, whose
|
|
||||||
// midpoint is unrelated to the place under the screen centre.
|
|
||||||
const centre =
|
|
||||||
canvas?.clientWidth > 0 &&
|
|
||||||
canvas.clientHeight > 0 &&
|
|
||||||
viewer.camera?.pickEllipsoid?.(
|
|
||||||
new Cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2),
|
|
||||||
ellipsoid,
|
|
||||||
);
|
|
||||||
const rectangle =
|
|
||||||
!centre && viewer?.camera?.computeViewRectangle?.(ellipsoid);
|
|
||||||
const carto = centre
|
|
||||||
? (ellipsoid || Cesium.Ellipsoid.WGS84).cartesianToCartographic(centre)
|
|
||||||
: rectangle
|
|
||||||
? Cesium.Rectangle.center(rectangle)
|
|
||||||
: viewer?.camera?.positionCartographic;
|
|
||||||
if (!carto) return null;
|
|
||||||
const latitude = Cesium.Math.toDegrees(carto.latitude);
|
|
||||||
const longitude = Cesium.Math.toDegrees(carto.longitude);
|
|
||||||
return Number.isFinite(latitude) && Number.isFinite(longitude)
|
|
||||||
? { latitude, longitude }
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Own one GNSS navigation-integrity display: rolling integrity observations
|
* Own one GNSS navigation-integrity display: rolling integrity observations
|
||||||
* around the views the user visits, binned into cells coloured by the
|
* around the views the user visits, binned into cells coloured by the
|
||||||
@@ -67,7 +29,8 @@ export function defaultGnssViewAnchor(viewer) {
|
|||||||
*/
|
*/
|
||||||
export function createGnssIntegrityLayer({
|
export function createGnssIntegrityLayer({
|
||||||
source,
|
source,
|
||||||
viewAnchor = defaultGnssViewAnchor,
|
viewAnchor,
|
||||||
|
createRenderer = createGnssCesiumRenderer,
|
||||||
now = () => Date.now(),
|
now = () => Date.now(),
|
||||||
monotonicNow = () => performance.now(),
|
monotonicNow = () => performance.now(),
|
||||||
windowMs: requestedWindowMs,
|
windowMs: requestedWindowMs,
|
||||||
@@ -80,8 +43,9 @@ export function createGnssIntegrityLayer({
|
|||||||
Number.isFinite(requestedWindowMs) && requestedWindowMs > 0
|
Number.isFinite(requestedWindowMs) && requestedWindowMs > 0
|
||||||
? requestedWindowMs
|
? requestedWindowMs
|
||||||
: GNSS_WINDOW_MS;
|
: GNSS_WINDOW_MS;
|
||||||
let _viewer = null;
|
let initialized = false;
|
||||||
let _primitives = null;
|
let renderer = null;
|
||||||
|
let lastAnchor = null;
|
||||||
let _request = null;
|
let _request = null;
|
||||||
let _enabled = false;
|
let _enabled = false;
|
||||||
let _cells = [];
|
let _cells = [];
|
||||||
@@ -109,9 +73,9 @@ export function createGnssIntegrityLayer({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
function scheduleCameraRefresh() {
|
function scheduleCameraRefresh() {
|
||||||
if (!_enabled || !_primitives) return;
|
if (!_enabled || !renderer) return;
|
||||||
if (now() < _retryAt) return;
|
if (now() < _retryAt) return;
|
||||||
const key = queryKey(viewAnchor(_viewer));
|
const key = queryKey(renderer?.anchor());
|
||||||
if (!key || key === _lastQueryKey) return;
|
if (!key || key === _lastQueryKey) return;
|
||||||
clearTimeout(_cameraTimer);
|
clearTimeout(_cameraTimer);
|
||||||
// Debounce drags and bound moving/follow cameras to at most 24 requests
|
// Debounce drags and bound moving/follow cameras to at most 24 requests
|
||||||
@@ -120,7 +84,7 @@ export function createGnssIntegrityLayer({
|
|||||||
async () => {
|
async () => {
|
||||||
_cameraTimer = null;
|
_cameraTimer = null;
|
||||||
if (!_enabled || _backgroundRefresh || _request) return;
|
if (!_enabled || _backgroundRefresh || _request) return;
|
||||||
const key = queryKey(viewAnchor(_viewer));
|
const key = queryKey(renderer?.anchor());
|
||||||
if (!key || key === _lastQueryKey) return;
|
if (!key || key === _lastQueryKey) return;
|
||||||
await refreshInBackground();
|
await refreshInBackground();
|
||||||
},
|
},
|
||||||
@@ -174,45 +138,8 @@ export function createGnssIntegrityLayer({
|
|||||||
_cells.map(({ id, level }) => [id, level]),
|
_cells.map(({ id, level }) => [id, level]),
|
||||||
);
|
);
|
||||||
if (signature === _signature) return;
|
if (signature === _signature) return;
|
||||||
|
renderer?.replace(_cells, FILL_ALPHA);
|
||||||
_signature = signature;
|
_signature = signature;
|
||||||
_primitives.removeAll();
|
|
||||||
if (_cells.length) {
|
|
||||||
_primitives.add(
|
|
||||||
new GnssGroundFill({
|
|
||||||
geometryInstances: _cells.map(
|
|
||||||
(cell) =>
|
|
||||||
new Cesium.GeometryInstance({
|
|
||||||
id: `${layer.id}:${cell.id}`,
|
|
||||||
geometry: new Cesium.RectangleGeometry({
|
|
||||||
rectangle: Cesium.Rectangle.fromDegrees(
|
|
||||||
cell.west,
|
|
||||||
cell.south,
|
|
||||||
cell.east,
|
|
||||||
cell.north,
|
|
||||||
),
|
|
||||||
vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
|
|
||||||
}),
|
|
||||||
attributes: {
|
|
||||||
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
|
||||||
Cesium.Color.fromCssColorString(
|
|
||||||
GNSS_LEVEL_COLORS[cell.level],
|
|
||||||
).withAlpha(FILL_ALPHA[cell.level]),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
appearance: new Cesium.PerInstanceColorAppearance({
|
|
||||||
flat: true,
|
|
||||||
translucent: true,
|
|
||||||
}),
|
|
||||||
classificationType: Cesium.ClassificationType.BOTH,
|
|
||||||
allowPicking: false,
|
|
||||||
// Retain the compact rectangle descriptors for inspection, not Entity visualizers.
|
|
||||||
releaseGeometryInstances: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_viewer?.scene?.requestRender?.();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const layer = {
|
const layer = {
|
||||||
@@ -228,29 +155,45 @@ export function createGnssIntegrityLayer({
|
|||||||
_manager = manager;
|
_manager = manager;
|
||||||
},
|
},
|
||||||
|
|
||||||
init(viewer) {
|
init(viewer, { engine = null } = {}) {
|
||||||
if (_viewer)
|
if (initialized)
|
||||||
throw new Error('GNSS integrity layer is already initialized');
|
throw new Error('GNSS integrity layer is already initialized');
|
||||||
_viewer = viewer;
|
initialized = true;
|
||||||
_primitives = new Cesium.PrimitiveCollection();
|
try {
|
||||||
_primitives.id = layer.id;
|
if (viewer || engine) this.attachRenderer(viewer, { engine });
|
||||||
_primitives.show = false;
|
} catch (error) {
|
||||||
viewer.scene.groundPrimitives.add(_primitives);
|
this.destroy();
|
||||||
_removeCameraEnd = viewer.camera?.moveEnd?.addEventListener(
|
throw error;
|
||||||
scheduleCameraRefresh,
|
}
|
||||||
);
|
},
|
||||||
_removeCameraChanged = viewer.camera?.changed?.addEventListener(
|
|
||||||
scheduleCameraRefresh,
|
attachRenderer(viewer, { engine = null } = {}) {
|
||||||
);
|
if (!initialized || renderer)
|
||||||
|
throw new Error('Invalid GNSS scene attachment');
|
||||||
|
renderer = createRenderer({ viewer, engine, viewAnchor });
|
||||||
|
_removeCameraEnd = renderer.camera?.moveEnd?.addEventListener(scheduleCameraRefresh);
|
||||||
|
_removeCameraChanged = renderer.camera?.changed?.addEventListener(scheduleCameraRefresh);
|
||||||
|
_signature = null;
|
||||||
|
if (_enabled) expire();
|
||||||
|
renderer.setVisible(_enabled);
|
||||||
|
},
|
||||||
|
|
||||||
|
detachRenderer() {
|
||||||
|
clearTimeout(_cameraTimer);
|
||||||
|
_cameraTimer = null;
|
||||||
|
_removeCameraEnd?.();
|
||||||
|
_removeCameraChanged?.();
|
||||||
|
_removeCameraEnd = null;
|
||||||
|
_removeCameraChanged = null;
|
||||||
|
renderer?.destroy();
|
||||||
|
renderer = null;
|
||||||
|
_signature = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
_enabled = true;
|
_enabled = true;
|
||||||
// Cells left from before a long disable must not reappear past the window.
|
expire();
|
||||||
if (_primitives) {
|
renderer?.setVisible(true);
|
||||||
expire();
|
|
||||||
_primitives.show = true;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
@@ -263,11 +206,11 @@ export function createGnssIntegrityLayer({
|
|||||||
_request?.abort();
|
_request?.abort();
|
||||||
_request = null;
|
_request = null;
|
||||||
_enabled = false;
|
_enabled = false;
|
||||||
if (_primitives) _primitives.show = false;
|
renderer?.setVisible(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
async update(_viewerArg, { signal } = {}) {
|
async update(_viewerArg, { signal } = {}) {
|
||||||
if (!_enabled || !_primitives || signal?.aborted) return false;
|
if (!_enabled || signal?.aborted) return false;
|
||||||
if (now() < _retryAt) {
|
if (now() < _retryAt) {
|
||||||
expire();
|
expire();
|
||||||
scheduleRetry();
|
scheduleRetry();
|
||||||
@@ -276,7 +219,8 @@ export function createGnssIntegrityLayer({
|
|||||||
_retryAt = 0;
|
_retryAt = 0;
|
||||||
clearTimeout(_retryTimer);
|
clearTimeout(_retryTimer);
|
||||||
_retryTimer = null;
|
_retryTimer = null;
|
||||||
const anchor = viewAnchor(_viewer);
|
const anchor = renderer ? renderer.anchor() : lastAnchor;
|
||||||
|
lastAnchor = anchor;
|
||||||
// No anchor yet (camera still settling) is not a failure: returning
|
// No anchor yet (camera still settling) is not a failure: returning
|
||||||
// false would make the manager reject the enable. The next interval
|
// false would make the manager reject the enable. The next interval
|
||||||
// retries; cells already drawn still age out of the window.
|
// retries; cells already drawn still age out of the window.
|
||||||
@@ -352,20 +296,13 @@ export function createGnssIntegrityLayer({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
destroy(viewer = _viewer) {
|
destroy() {
|
||||||
layer.disable();
|
layer.disable();
|
||||||
_removeCameraEnd?.();
|
initialized = false;
|
||||||
_removeCameraChanged?.();
|
layer.detachRenderer();
|
||||||
_removeCameraEnd = null;
|
|
||||||
_removeCameraChanged = null;
|
|
||||||
_observations.clear();
|
_observations.clear();
|
||||||
_cells = [];
|
_cells = [];
|
||||||
_signature = null;
|
lastAnchor = null;
|
||||||
if (_primitives) {
|
|
||||||
viewer.scene.groundPrimitives.remove(_primitives);
|
|
||||||
_primitives = null;
|
|
||||||
}
|
|
||||||
_viewer = null;
|
|
||||||
_lastUpdate = null;
|
_lastUpdate = null;
|
||||||
_lastError = null;
|
_lastError = null;
|
||||||
_stale = false;
|
_stale = false;
|
||||||
|
|||||||
@@ -698,3 +698,24 @@ test('view anchor falls back to rectangle, then nadir, then no camera', () => {
|
|||||||
assert.ok(Math.abs(nadirAnchor.latitude - 41) < 1e-10);
|
assert.ok(Math.abs(nadirAnchor.latitude - 41) < 1e-10);
|
||||||
assert.ok(Math.abs(nadirAnchor.longitude - 21) < 1e-10);
|
assert.ok(Math.abs(nadirAnchor.longitude - 21) < 1e-10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an in-flight regional response supplies the next renderer without a new request', async () => {
|
||||||
|
let resolve, calls = 0;
|
||||||
|
const h = harness(() => { calls++; return new Promise(done => { resolve = done; }); });
|
||||||
|
try {
|
||||||
|
h.layer.enable();
|
||||||
|
const pending = h.layer.update();
|
||||||
|
h.layer.detachRenderer();
|
||||||
|
assert.equal(h.viewer.camera.changed.numberOfListeners, 0);
|
||||||
|
resolve({ rows: threeHealthy() });
|
||||||
|
assert.equal(await pending, true);
|
||||||
|
assert.equal(h.sources.length, 0);
|
||||||
|
assert.equal(h.layer.getStats().levels.low, 1);
|
||||||
|
h.layer.attachRenderer(h.viewer);
|
||||||
|
assert.equal(h.sources[0].length, 1);
|
||||||
|
assert.equal(calls, 1);
|
||||||
|
assert.equal(h.viewer.camera.changed.numberOfListeners, 1);
|
||||||
|
} finally {
|
||||||
|
h.layer.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import { GNSS_LEVEL_COLORS } from '../records.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GroundPrimitive allowPicking:false suppresses ids but still submits stencil
|
||||||
|
* commands in Cesium's pick pass, erasing picks of overlapping ground geometry.
|
||||||
|
* This fill participates only in rendering, so sibling polygons remain pickable.
|
||||||
|
*/
|
||||||
|
class GnssGroundFill extends Cesium.GroundPrimitive {
|
||||||
|
update(frameState) {
|
||||||
|
if (!frameState.passes.pick) super.update(frameState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Degrees at the centre of the current view, or null before the camera settles. */
|
||||||
|
export function defaultGnssViewAnchor(viewer) {
|
||||||
|
const canvas = viewer?.scene?.canvas;
|
||||||
|
const ellipsoid = viewer?.scene?.globe?.ellipsoid;
|
||||||
|
// At globe scale the visible rectangle can be the whole world, whose
|
||||||
|
// midpoint is unrelated to the place under the screen centre.
|
||||||
|
const centre =
|
||||||
|
canvas?.clientWidth > 0 &&
|
||||||
|
canvas.clientHeight > 0 &&
|
||||||
|
viewer.camera?.pickEllipsoid?.(
|
||||||
|
new Cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2),
|
||||||
|
ellipsoid,
|
||||||
|
);
|
||||||
|
const rectangle =
|
||||||
|
!centre && viewer?.camera?.computeViewRectangle?.(ellipsoid);
|
||||||
|
const carto = centre
|
||||||
|
? (ellipsoid || Cesium.Ellipsoid.WGS84).cartesianToCartographic(centre)
|
||||||
|
: rectangle
|
||||||
|
? Cesium.Rectangle.center(rectangle)
|
||||||
|
: viewer?.camera?.positionCartographic;
|
||||||
|
if (!carto) return null;
|
||||||
|
const latitude = Cesium.Math.toDegrees(carto.latitude);
|
||||||
|
const longitude = Cesium.Math.toDegrees(carto.longitude);
|
||||||
|
return Number.isFinite(latitude) && Number.isFinite(longitude)
|
||||||
|
? { latitude, longitude }
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGnssCesiumRenderer({
|
||||||
|
viewer,
|
||||||
|
viewAnchor = defaultGnssViewAnchor,
|
||||||
|
}) {
|
||||||
|
const primitives = new Cesium.PrimitiveCollection();
|
||||||
|
primitives.id = 'gnss-integrity';
|
||||||
|
primitives.show = false;
|
||||||
|
viewer.scene.groundPrimitives.add(primitives);
|
||||||
|
return {
|
||||||
|
camera: viewer.camera,
|
||||||
|
anchor: () => viewAnchor(viewer),
|
||||||
|
setVisible(show) {
|
||||||
|
primitives.show = show;
|
||||||
|
},
|
||||||
|
replace(cells, alpha) {
|
||||||
|
primitives.removeAll();
|
||||||
|
if (cells.length) {
|
||||||
|
primitives.add(
|
||||||
|
new GnssGroundFill({
|
||||||
|
geometryInstances: cells.map(
|
||||||
|
(cell) =>
|
||||||
|
new Cesium.GeometryInstance({
|
||||||
|
id: `gnss-integrity:${cell.id}`,
|
||||||
|
geometry: new Cesium.RectangleGeometry({
|
||||||
|
rectangle: Cesium.Rectangle.fromDegrees(
|
||||||
|
cell.west,
|
||||||
|
cell.south,
|
||||||
|
cell.east,
|
||||||
|
cell.north,
|
||||||
|
),
|
||||||
|
vertexFormat:
|
||||||
|
Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
|
||||||
|
}),
|
||||||
|
attributes: {
|
||||||
|
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
||||||
|
Cesium.Color.fromCssColorString(
|
||||||
|
GNSS_LEVEL_COLORS[cell.level],
|
||||||
|
).withAlpha(alpha[cell.level]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
appearance: new Cesium.PerInstanceColorAppearance({
|
||||||
|
flat: true,
|
||||||
|
translucent: true,
|
||||||
|
}),
|
||||||
|
classificationType: Cesium.ClassificationType.BOTH,
|
||||||
|
allowPicking: false,
|
||||||
|
// Retain the compact rectangle descriptors for inspection, not Entity visualizers.
|
||||||
|
releaseGeometryInstances: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
viewer?.scene?.requestRender?.();
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
viewer.scene.groundPrimitives.remove(primitives);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
import { GNSS_LEVEL_COLORS } from '../records.js';
|
||||||
|
import { rgbaFromCss } from '../../shared/color.js';
|
||||||
|
import { createGroundCellDisplay } from '../../shared/render/navaraGroundCells.js';
|
||||||
|
|
||||||
|
export function createGnssNavaraRenderer(engine, options) {
|
||||||
|
const cells = createGroundCellDisplay(engine, options, 0);
|
||||||
|
const { camera } = engine;
|
||||||
|
function anchor() {
|
||||||
|
const { width, height } = engine.host.size();
|
||||||
|
const centre =
|
||||||
|
width && height && camera.pickEllipsoid({ x: width / 2, y: height / 2 });
|
||||||
|
let position = centre && geo.cartographic.fromCartesian(centre);
|
||||||
|
if (!position) {
|
||||||
|
const rect = camera.viewRectangle();
|
||||||
|
if (rect) {
|
||||||
|
const east =
|
||||||
|
rect.east < rect.west ? rect.east + 2 * Math.PI : rect.east;
|
||||||
|
const longitude = (rect.west + east) / 2;
|
||||||
|
position = {
|
||||||
|
latitude: (rect.south + rect.north) / 2,
|
||||||
|
longitude: longitude > Math.PI ? longitude - 2 * Math.PI : longitude,
|
||||||
|
};
|
||||||
|
} else position = camera.pose().cartographic;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
latitude: geo.math.toDegrees(position.latitude),
|
||||||
|
longitude: geo.math.toDegrees(position.longitude),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
camera,
|
||||||
|
anchor,
|
||||||
|
setVisible: cells.setVisible,
|
||||||
|
destroy: cells.destroy,
|
||||||
|
replace(rows, alpha) {
|
||||||
|
cells.replace(
|
||||||
|
rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
color: rgbaFromCss(GNSS_LEVEL_COLORS[row.level]).withAlpha(
|
||||||
|
alpha[row.level],
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,320 @@
|
|||||||
|
// Classify visible terrain and 3D tiles. Multiple cell layers share one non-pickable pass.
|
||||||
|
import { Effect, EffectAttribute } from 'postprocessing';
|
||||||
|
import { Matrix4, Uniform } from 'three';
|
||||||
|
import { rectangleFrame } from './navaraCellGeometry.js';
|
||||||
|
import {
|
||||||
|
viewFrame,
|
||||||
|
groundClipBounds,
|
||||||
|
} from './navaraGroundGeometry.js';
|
||||||
|
import {
|
||||||
|
createGroundGrid,
|
||||||
|
GRID_COLS,
|
||||||
|
GRID_ROWS,
|
||||||
|
} from './navaraGroundGrid.js';
|
||||||
|
import {
|
||||||
|
createGroundTexture,
|
||||||
|
createGroundStorage,
|
||||||
|
groundSurfaceUniforms,
|
||||||
|
updateGroundSurface,
|
||||||
|
} from './navaraGroundSurface.js';
|
||||||
|
const KEY = 'gevFirmsGround';
|
||||||
|
const registeredViews = new WeakSet();
|
||||||
|
const fragmentShader = /* glsl */ `
|
||||||
|
uniform sampler2D tAllDepth;
|
||||||
|
uniform sampler2D tSurfaceDepth;
|
||||||
|
uniform sampler2D tCells;
|
||||||
|
uniform sampler2D tGrid;
|
||||||
|
uniform sampler2D tCandidates;
|
||||||
|
uniform vec2 uCellSize;
|
||||||
|
uniform vec2 uCandidateSize;
|
||||||
|
uniform mat4 uInverseProjection;
|
||||||
|
uniform mat4 uInverseView;
|
||||||
|
uniform float uLogDepthFC;
|
||||||
|
vec4 atSlot(sampler2D image,vec2 size,float slot) {
|
||||||
|
return texture2D(image,(vec2(mod(slot,size.x),floor(slot/size.x))+0.5)/size);
|
||||||
|
}
|
||||||
|
vec3 toDisplay(vec3 c) {
|
||||||
|
return mix(12.92*c,1.055*pow(max(c,vec3(0.0)),vec3(1.0/2.4))-0.055,step(vec3(0.0031308),c));
|
||||||
|
}
|
||||||
|
vec3 toLinear(vec3 c) {
|
||||||
|
return mix(c/12.92,pow(max((c+0.055)/1.055,vec3(0.0)),vec3(2.4)),step(vec3(0.04045),c));
|
||||||
|
}
|
||||||
|
vec3 pixelPlane(float distance,vec3 normal,vec3 dx,vec3 dy) {
|
||||||
|
return vec3(distance,dot(normal,dx),dot(normal,dy));
|
||||||
|
}
|
||||||
|
float rectangleCoverage(vec3 west,vec3 east,vec3 south,vec3 north) {
|
||||||
|
vec3 edges[4];
|
||||||
|
edges[0]=west;edges[1]=east;edges[2]=south;edges[3]=north;
|
||||||
|
float coverage=1.0;
|
||||||
|
int partial=0;
|
||||||
|
for(int i=0;i<4;i++) {
|
||||||
|
float width=abs(edges[i].y)+abs(edges[i].z);
|
||||||
|
coverage=min(coverage,clamp(edges[i].x/max(width,0.001)+0.5,0.0,1.0));
|
||||||
|
if(coverage<=0.0) return 0.0;
|
||||||
|
if(edges[i].x<=width*0.5) partial++;
|
||||||
|
}
|
||||||
|
if(partial<2) return coverage;
|
||||||
|
// Intersect the SAME pixel square with all sides. Minimum soft-edge
|
||||||
|
// coverage creates long miter halos at acute/polar rectangle corners.
|
||||||
|
vec2 polygon[8],clipped[8];
|
||||||
|
polygon[0]=vec2(-0.5,-0.5);polygon[1]=vec2(0.5,-0.5);
|
||||||
|
polygon[2]=vec2(0.5,0.5);polygon[3]=vec2(-0.5,0.5);
|
||||||
|
int count=4;
|
||||||
|
for(int side=0;side<4;side++) {
|
||||||
|
int nextCount=0;
|
||||||
|
vec2 previous=polygon[count-1];
|
||||||
|
float before=edges[side].x+dot(edges[side].yz,previous);
|
||||||
|
for(int i=0;i<8;i++) {
|
||||||
|
if(i>=count) break;
|
||||||
|
vec2 current=polygon[i];
|
||||||
|
float after=edges[side].x+dot(edges[side].yz,current);
|
||||||
|
if((before>0.0&&after<0.0)||(before<0.0&&after>0.0))
|
||||||
|
clipped[nextCount++]=mix(previous,current,before/(before-after));
|
||||||
|
if(after>=0.0) clipped[nextCount++]=current;
|
||||||
|
previous=current;before=after;
|
||||||
|
}
|
||||||
|
count=nextCount;
|
||||||
|
if(count<3) return 0.0;
|
||||||
|
for(int i=0;i<8;i++) { if(i>=count) break;polygon[i]=clipped[i]; }
|
||||||
|
}
|
||||||
|
float area=0.0;
|
||||||
|
vec2 previous=polygon[count-1];
|
||||||
|
for(int i=0;i<8;i++) {
|
||||||
|
if(i>=count) break;
|
||||||
|
area+=previous.x*polygon[i].y-previous.y*polygon[i].x;
|
||||||
|
previous=polygon[i];
|
||||||
|
}
|
||||||
|
return clamp(abs(area)*0.5,0.0,1.0);
|
||||||
|
}
|
||||||
|
void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor) {
|
||||||
|
outputColor=inputColor;
|
||||||
|
float depth=unpackRGBAToDepth(texture2D(tAllDepth,uv));
|
||||||
|
float distance=exp2(2.0*depth/uLogDepthFC)-1.0;
|
||||||
|
vec4 ray=uInverseProjection*vec4(uv*2.0-1.0,-1.0,1.0);
|
||||||
|
vec3 point=ray.xyz*(distance/-ray.z);
|
||||||
|
// Evaluate derivatives before divergent candidate branches, as in quakes.
|
||||||
|
vec3 world=(uInverseView*vec4(point,1.0)).xyz;
|
||||||
|
vec3 dx=(uInverseView*vec4(dFdx(point),0.0)).xyz;
|
||||||
|
vec3 dy=(uInverseView*vec4(dFdy(point),0.0)).xyz;
|
||||||
|
// A quad crossing the sky has no surface derivative. Keep exact halfspaces
|
||||||
|
// there; otherwise its far-plane lane spreads cell AA along the globe limb.
|
||||||
|
float onSurface=1.0-step(1.0,depth);
|
||||||
|
float derivativeMask=1.0-step(0.5,abs(dFdx(onSurface))+abs(dFdy(onSurface)));
|
||||||
|
dx*=derivativeMask;
|
||||||
|
dy*=derivativeMask;
|
||||||
|
if(depth>=1.0) return;
|
||||||
|
vec2 tile=clamp(floor(uv*vec2(${GRID_COLS}.0,${GRID_ROWS}.0)),vec2(0.0),vec2(${GRID_COLS - 1}.0,${GRID_ROWS - 1}.0));
|
||||||
|
vec2 range=texture2D(tGrid,(tile+0.5)/vec2(${GRID_COLS}.0,${GRID_ROWS}.0)).xy;
|
||||||
|
if(range.y<0.5) return;
|
||||||
|
float surface=texture2D(tSurfaceDepth,uv).r;
|
||||||
|
if(depth<surface-2e-7) return;
|
||||||
|
float rho=length(world.xy);
|
||||||
|
vec2 radial=world.xy/max(rho,0.001);
|
||||||
|
vec3 color=toDisplay(inputColor.rgb);
|
||||||
|
for(int k=0;k<int(range.y);k++) {
|
||||||
|
float id=atSlot(tCandidates,uCandidateSize,range.x+float(k)).x;
|
||||||
|
vec4 longitude=atSlot(tCells,uCellSize,id*4.0);
|
||||||
|
vec3 south=atSlot(tCells,uCellSize,id*4.0+1.0).xyz;
|
||||||
|
vec3 north=atSlot(tCells,uCellSize,id*4.0+2.0).xyz;
|
||||||
|
vec3 westNormal=vec3(-longitude.x,longitude.y,0.0);
|
||||||
|
vec3 eastNormal=vec3(longitude.z,-longitude.w,0.0);
|
||||||
|
float coverage=rectangleCoverage(
|
||||||
|
pixelPlane(dot(westNormal,world),westNormal,dx,dy),
|
||||||
|
pixelPlane(dot(eastNormal,world),eastNormal,dx,dy),
|
||||||
|
pixelPlane(world.z*south.y-rho*south.x+south.z,vec3(-south.x*radial,south.y),dx,dy),
|
||||||
|
pixelPlane(rho*north.x-world.z*north.y-north.z,vec3(north.x*radial,-north.y),dx,dy));
|
||||||
|
if(coverage<=0.0) continue;
|
||||||
|
vec4 fill=atSlot(tCells,uCellSize,id*4.0+3.0);
|
||||||
|
color=mix(color,fill.rgb,fill.a*coverage);
|
||||||
|
}
|
||||||
|
outputColor=vec4(toLinear(color),inputColor.a);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
function buildState(cells) {
|
||||||
|
const frames = cells.map(rectangleFrame),
|
||||||
|
packed = createGroundStorage(cells.length * 4);
|
||||||
|
const grid = createGroundGrid(cells.length),
|
||||||
|
gridData = new Float32Array(GRID_COLS * GRID_ROWS * 4);
|
||||||
|
const gridTexture = createGroundTexture(gridData, GRID_COLS, GRID_ROWS);
|
||||||
|
let candidates = createGroundStorage(1);
|
||||||
|
const scratch = new Float64Array(12),
|
||||||
|
bounds = new Float64Array(4);
|
||||||
|
const lastView = new Float64Array(16).fill(NaN),
|
||||||
|
lastProjection = new Float64Array(16).fill(NaN);
|
||||||
|
let lastWidth = 0,
|
||||||
|
lastHeight = 0;
|
||||||
|
frames.forEach((frame, index) => {
|
||||||
|
const offset = index * 16,
|
||||||
|
color = cells[index].color;
|
||||||
|
packed.data.set(frame.bounds.longitude, offset);
|
||||||
|
packed.data.set(frame.bounds.south, offset + 4);
|
||||||
|
packed.data.set(frame.bounds.north, offset + 8);
|
||||||
|
packed.data.set(
|
||||||
|
[color.red, color.green, color.blue, color.alpha],
|
||||||
|
offset + 12,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
count: cells.length,
|
||||||
|
packed,
|
||||||
|
gridTexture,
|
||||||
|
get candidates() {
|
||||||
|
return candidates;
|
||||||
|
},
|
||||||
|
update(camera, context) {
|
||||||
|
const input = context.getInputBuffer(),
|
||||||
|
view = camera.matrixWorldInverse.elements,
|
||||||
|
projection = camera.projectionMatrix.elements;
|
||||||
|
let changed = input.width !== lastWidth || input.height !== lastHeight;
|
||||||
|
for (let i = 0; i < 16 && !changed; i++)
|
||||||
|
changed =
|
||||||
|
view[i] !== lastView[i] || projection[i] !== lastProjection[i];
|
||||||
|
if (!changed) return;
|
||||||
|
lastView.set(view);
|
||||||
|
lastProjection.set(projection);
|
||||||
|
lastWidth = input.width;
|
||||||
|
lastHeight = input.height;
|
||||||
|
frames.forEach((frame, index) => {
|
||||||
|
viewFrame(frame, camera.matrixWorldInverse, scratch);
|
||||||
|
groundClipBounds(frame, scratch, camera.projectionMatrix, bounds);
|
||||||
|
if (bounds[0] <= 1) {
|
||||||
|
bounds[0] -= 2 / Math.max(1, input.width);
|
||||||
|
bounds[2] += 2 / Math.max(1, input.width);
|
||||||
|
bounds[1] -= 2 / Math.max(1, input.height);
|
||||||
|
bounds[3] += 2 / Math.max(1, input.height);
|
||||||
|
}
|
||||||
|
grid.bounds.set(bounds, index * 4);
|
||||||
|
});
|
||||||
|
const total = grid.pack();
|
||||||
|
if (candidates.data.length / 4 < total) {
|
||||||
|
candidates.texture.dispose();
|
||||||
|
candidates = createGroundStorage(
|
||||||
|
Math.max(total, candidates.data.length / 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < total; i++) candidates.data[i * 4] = grid.indices[i];
|
||||||
|
for (let i = 0; i < grid.counts.length; i++) {
|
||||||
|
gridData[i * 4] = grid.offsets[i];
|
||||||
|
gridData[i * 4 + 1] = grid.counts[i];
|
||||||
|
}
|
||||||
|
gridTexture.needsUpdate = true;
|
||||||
|
candidates.texture.needsUpdate = true;
|
||||||
|
},
|
||||||
|
dispose() {
|
||||||
|
packed.texture.dispose();
|
||||||
|
gridTexture.dispose();
|
||||||
|
candidates.texture.dispose();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function groundEffectClass(EffectDesc, NavaraEffect) {
|
||||||
|
return class FirmsGroundEffectDesc extends EffectDesc {
|
||||||
|
static key = KEY;
|
||||||
|
static insertAfter = [
|
||||||
|
'gevEarthquakeGround',
|
||||||
|
'gevRecentImagery',
|
||||||
|
'toneMapping',
|
||||||
|
];
|
||||||
|
static insertBefore = [
|
||||||
|
'gevPreSharpenOverlay',
|
||||||
|
'gevSharpen',
|
||||||
|
'smaa',
|
||||||
|
'fxaa',
|
||||||
|
'gevOverlay',
|
||||||
|
'final',
|
||||||
|
];
|
||||||
|
constructor(view, context, config) {
|
||||||
|
super(view, context, config);
|
||||||
|
this.owner = config[KEY].owner;
|
||||||
|
}
|
||||||
|
createPass() {
|
||||||
|
this.effect = new Effect('GevFirmsGround', fragmentShader, {
|
||||||
|
attributes: EffectAttribute.CONVOLUTION,
|
||||||
|
uniforms: new Map([
|
||||||
|
...groundSurfaceUniforms(),
|
||||||
|
['tCells', new Uniform(null)],
|
||||||
|
['tGrid', new Uniform(null)],
|
||||||
|
['tCandidates', new Uniform(null)],
|
||||||
|
['uCellSize', new Uniform([1, 1])],
|
||||||
|
['uCandidateSize', new Uniform([1, 1])],
|
||||||
|
['uInverseView', new Uniform(new Matrix4())],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
return new NavaraEffect(this.view.camera.raw, this.effect);
|
||||||
|
}
|
||||||
|
update() {
|
||||||
|
const state = this.owner.state,
|
||||||
|
camera = this.view.camera.raw;
|
||||||
|
if (!this.effect || !state || !camera) return;
|
||||||
|
updateGroundSurface(this.effect, camera, this.ctx, this.find('mrt'));
|
||||||
|
state.update(camera, this.ctx);
|
||||||
|
const u = this.effect.uniforms;
|
||||||
|
u.get('tCells').value = state.packed.texture;
|
||||||
|
u.get('uCellSize').value[0] = state.packed.width;
|
||||||
|
u.get('uCellSize').value[1] = state.packed.height;
|
||||||
|
u.get('tGrid').value = state.gridTexture;
|
||||||
|
u.get('tCandidates').value = state.candidates.texture;
|
||||||
|
u.get('uCandidateSize').value[0] = state.candidates.width;
|
||||||
|
u.get('uCandidateSize').value[1] = state.candidates.height;
|
||||||
|
u.get('uInverseView').value.copy(camera.matrixWorld);
|
||||||
|
}
|
||||||
|
onDestroy() {
|
||||||
|
this.effect?.dispose();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// The SDK allows one active effect per key. Compose enabled datasets in
|
||||||
|
// catalog order so both can be drawn without replacing one another.
|
||||||
|
const views = new WeakMap();
|
||||||
|
export function createGroundCellDisplay(engine, { view, EffectDesc, NavaraEffect }, order) {
|
||||||
|
if (!registeredViews.has(view)) {
|
||||||
|
view.registerEffect(KEY, groundEffectClass(EffectDesc, NavaraEffect));
|
||||||
|
registeredViews.add(view);
|
||||||
|
}
|
||||||
|
let pass = views.get(view);
|
||||||
|
if (!pass) {
|
||||||
|
pass = { inputs: new Set(), owner: { state: buildState([]) }, handle: null };
|
||||||
|
views.set(view, pass);
|
||||||
|
}
|
||||||
|
const input = { cells: [], visible: false, order };
|
||||||
|
pass.inputs.add(input);
|
||||||
|
function sync() {
|
||||||
|
const cells = [...pass.inputs].filter(item => item.visible)
|
||||||
|
.sort((a,b) => a.order - b.order).flatMap(item => item.cells);
|
||||||
|
const previous = pass.owner.state;
|
||||||
|
pass.owner.state = buildState(cells);
|
||||||
|
try {
|
||||||
|
if (!pass.handle && cells.length) {
|
||||||
|
pass.handle = view.addEffect({ [KEY]: { owner: pass.owner }, visible: true });
|
||||||
|
} else {
|
||||||
|
pass.handle?.update({ visible: cells.length > 0 });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
previous.dispose();
|
||||||
|
}
|
||||||
|
engine.render.request('ground-cells');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
replace(cells) {
|
||||||
|
input.cells = cells;
|
||||||
|
if (input.visible) sync();
|
||||||
|
},
|
||||||
|
setVisible(show) {
|
||||||
|
if (input.visible === show) return;
|
||||||
|
input.visible = show;
|
||||||
|
sync();
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
if (!pass.inputs.delete(input)) return;
|
||||||
|
if (pass.inputs.size) {
|
||||||
|
if (input.visible) sync();
|
||||||
|
} else {
|
||||||
|
pass.handle?.delete();
|
||||||
|
pass.owner.state.dispose();
|
||||||
|
views.delete(view);
|
||||||
|
engine.render.request('ground-cells-destroy');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import fs from 'node:fs/promises';
|
|||||||
const declarations = [
|
const declarations = [
|
||||||
['gevRecentImagery', '../../recentImagery/render/navaraGround.js'],
|
['gevRecentImagery', '../../recentImagery/render/navaraGround.js'],
|
||||||
['gevEarthquakeGround', '../../earthquakes/render/navara.js'],
|
['gevEarthquakeGround', '../../earthquakes/render/navara.js'],
|
||||||
['gevFirmsGround', '../../firms/render/navara.js'],
|
['gevFirmsGround', './navaraGroundCells.js'],
|
||||||
['gevPreSharpenOverlay', '../../../globe/navara/overlay.js'],
|
['gevPreSharpenOverlay', '../../../globe/navara/overlay.js'],
|
||||||
];
|
];
|
||||||
const specs = await Promise.all(
|
const specs = await Promise.all(
|
||||||
|
|||||||
@@ -499,7 +499,7 @@ export function createGroundFloor({ terrain, signal }) {
|
|||||||
return h != null ? h : null;
|
return h != null ? h : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test hook: clears mesh cells (never called at runtime — cells are valid all session). */
|
/** Discard measurements from the outgoing scene while keeping the DEM cache. */
|
||||||
function clearMeshFloorCells() {
|
function clearMeshFloorCells() {
|
||||||
_meshCells.clear();
|
_meshCells.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user