// gev's screen overlays on Navara: sprites, points and lines (engine.movers // and engine.lines) drawn into the frame after Navara's tone mapping, // anti-aliasing (SMAA/FXAA) and the gev sharpening, so their // colours are the colours asked for (as Cesium draws billboards and // polylines), not tone-mapped or graded with the look. A public Navara // EffectDesc whose pass renders one three scene over the input buffer; the // post-processing chain has no depth buffer, so each overlay material tests // the scene depth from Navara's MRT pass itself (overlayDepth.js). The // pass costs nothing while no geometry is visible. Models retain scene // depth; their lit display colour is resolved here so HDR effects and MSAA // cannot spread a tint into neighbouring pixels. // FIRMS opts into a separate native-stage overlay before sharpening/AA. // Only that stage owns an encoded-RGBA8 blend target; W1 stays in place. import { withFrameUpdates } from './frameDescriptor.js'; import { EffectDesc, Pass as NavaraPass } from '@navaramap/three'; import { GevOverlayPass } from './overlayPass.js'; import { RGBADepthPacking, Scene } from 'three'; export const OVERLAY_EFFECT_KEY = 'gevOverlay'; const PRE_SHARPEN_KEY = 'gevPreSharpenOverlay'; const registeredViews = new WeakMap(); class GevOverlayEffectDesc extends withFrameUpdates(EffectDesc) { static key = OVERLAY_EFFECT_KEY; // Navara takes the first existing anchor, so the latest pass comes first. static insertAfter = ['fxaa', 'smaa', 'gevSharpen', 'toneMapping']; static insertBefore = ['final']; constructor(view, ctx, config) { super(view, ctx, config); this.overlay = config[this.constructor.key].overlay; this.overlay.attach(ctx); } createPass() { const camera = this.view.camera.raw; if (!camera) throw new Error('Camera not available for the overlay'); // In Navara's own Pass wrapper: Navara inserts a bare pass only when // it is an instance of its own postprocessing copy. return new NavaraPass( new GevOverlayPass(this.overlay.scene, camera, this.overlay), null, ); } onDestroy() { // Navara removes the pass from its composer; our pass owns its GPU target // and context event handlers until it is explicitly disposed. const pass = this.raw?.rawPass; try { super.onDestroy(); } finally { pass?.dispose(); } } // Each drawn frame: the scene depth the overlays test against, then each // overlay's own per-frame work (pixel footprint, origin, viewport). updateFrame(time) { const mrt = this.find('mrt'); this.overlay.uniforms.tGevSceneDepth.value = mrt?.depthBufferPacking === RGBADepthPacking ? mrt.depthBuffer : null; this.overlay.beforeFrame(time); } } class GevPreSharpenOverlayEffectDesc extends GevOverlayEffectDesc { static key = PRE_SHARPEN_KEY; // Ground classifiers also insert after tone mapping. Anchor to the next // stage so glyphs stay above them regardless of layer activation order. static insertAfter = []; static insertBefore = [ 'gevSharpen', 'smaa', 'fxaa', OVERLAY_EFFECT_KEY, 'final', ]; } /** * The overlay of one view: add(object, onFrame) puts a three object in the * overlay scene (onFrame runs each drawn frame before it draws); * registerPickable/unregisterPickable reach Navara's pick pass. * @param {object} view an initialised ThreeView */ export function createNavaraOverlay(view, { beforeSharpen = false } = {}) { const key = beforeSharpen ? PRE_SHARPEN_KEY : OVERLAY_EFFECT_KEY; let registered = registeredViews.get(view); if (!registered) registeredViews.set(view, (registered = new Set())); if (!registered.has(key)) { view.registerEffect( key, beforeSharpen ? GevPreSharpenOverlayEffectDesc : GevOverlayEffectDesc, ); registered.add(key); } const scene = new Scene(); const frameHooks = new Map(); const lights = new Map(); let ctx = null; const pendingPicks = new Map(); const overlay = { scene, displayBlend: beforeSharpen, modelTints: new Set(), uniforms: { uGevModelExposure: { value: 1 }, uGevDisplayBlend: { value: 0 }, tGevSceneDepth: { value: null }, uGevResolution: { value: [0, 0] }, }, attach(context) { ctx = context; for (const [key, wrapper] of pendingPicks) ctx.registerPickableMesh(key, wrapper); pendingPicks.clear(); }, hasDrawables() { for (const child of scene.children) if ( !child.isLight && child.visible && (child.count === undefined || child.count > 0) ) return true; return false; }, beforeFrame(time) { overlay.uniforms.uGevModelExposure.value = view.toneMappingExposure ?? 1; // Public descriptor lights are copied, never reparented from Navara. if (overlay.modelTints.size && ctx) { for (const light of ctx.scenes.light.children) { if (!light.isLight) continue; let copy = lights.get(light); if (!copy) { copy = light.clone(false); copy.castShadow = false; copy.matrixAutoUpdate = false; lights.set(light, copy); scene.add(copy); } copy.color.copy(light.color); copy.intensity = light.intensity; copy.matrix.copy(light.matrixWorld); copy.sh?.copy(light.sh); copy.groundColor?.copy(light.groundColor); if (copy.target && light.target) { copy.target.matrixAutoUpdate = false; copy.target.matrix.copy(light.target.matrixWorld); copy.target.updateMatrixWorld(true); } } } for (const hook of frameHooks.values()) hook(time); }, get ctx() { return ctx; }, }; const handle = view.addEffect({ [key]: { overlay }, visible: true, }); return { scene, uniforms: overlay.uniforms, add(object, onFrame) { scene.add(object); if (object.userData.gevModelTint) overlay.modelTints.add(object); if (onFrame) frameHooks.set(object, onFrame); }, remove(object) { scene.remove(object); overlay.modelTints.delete(object); frameHooks.delete(object); }, registerPickable(key, wrapper) { if (ctx) ctx.registerPickableMesh(key, wrapper); else pendingPicks.set(key, wrapper); }, unregisterPickable(key) { pendingPicks.delete(key); ctx?.unregisterPickableMesh(key); }, get ctx() { return ctx; }, destroy() { handle.delete(); scene.clear(); overlay.modelTints.clear(); for (const light of lights.values()) light.dispose?.(); lights.clear(); frameHooks.clear(); }, }; }