mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(globe): Navara sprites, points and lines drawn after tone mapping
Review of Tasks 2–3: on Navara the overlays went through the look's tone mapping and grading (magenta came out as 247/40/228, green as 147/228/89 in the cinematic look), where Cesium draws billboards and polylines in the colours asked for. src/globe/navara/overlay.js: a public Navara EffectDesc (gevOverlay) inserted after toneMapping and before the gev sharpening, SMAA/FXAA and final, whose pass renders one three scene over the tone-mapped input buffer (in Navara's own Pass wrapper: Navara inserts a bare pass only when it is an instance of its own postprocessing copy); it costs nothing while the scene is empty. The chain has no depth buffer, so each overlay material tests the scene depth itself (overlayDepth.js): its log depth against Navara's MRT depth (RGBA packed), modes always / normal / depth-fail, disableWithinM as a per-vertex escape. Sprites and points (moverMesh.js) and line ribbons (lineMesh.js) live in that scene with their per-frame work as overlay hooks; models stay Navara meshes, lit and graded like the tiles. The sharpening now sits after the overlay (Cesium's sharpen stage sharpens billboards too). Also (review): Navara pixel dashes are measured in CSS pixels (gl_FragCoord over uGevPixelRatio), and the line viewport uniform is updated in place. Measured: a magenta sprite draws 255/0/255. The contract suite's colour checks are now within 40 of the asked colour on every channel on both engines (were: dominant channels); Cesium 68/68, Navara 74/74 (its sharpen-order check accepts the overlay between tone mapping and the sharpening). Shell: cesium 23/23, cross 4/4; navara 23/25 — the two map stack pictures from space fail on stddev 7.2/7.6 against 8, as on the Task 3 commit run at the same hour (the cinematic sun follows the clock into night over the reset view), not from this change.
This commit is contained in:
1 parent
3c2e924363
commit
e804351205
11 files changed
+388
-125
No files matched your search
@@ -15,6 +15,7 @@ import { buildNavaraLook } from './look.js';
|
||||
import { createNavaraSharpen } from './sharpen.js';
|
||||
import { createNavaraMovers } from './movers/index.js';
|
||||
import { createNavaraLines } from './lines/index.js';
|
||||
import { createNavaraOverlay } from './overlay.js';
|
||||
import {
|
||||
NAVARA_CONTEXT_ATTRIBUTES,
|
||||
navaraScenePlan,
|
||||
@@ -98,6 +99,7 @@ export async function createNavaraGlobe({
|
||||
const canvas = preservedCanvas(container);
|
||||
let view;
|
||||
let engine;
|
||||
let overlay = null;
|
||||
let removeLook = null;
|
||||
try {
|
||||
view = new ThreeView(navaraViewOptions({ container, canvas, pixelRatio }));
|
||||
@@ -118,13 +120,15 @@ export async function createNavaraGlobe({
|
||||
);
|
||||
// The look's shadowed sun leaked a light per frame (internals.js).
|
||||
keepCascadedLightIds(view);
|
||||
// Sprites, points and lines, drawn after tone mapping (overlay.js).
|
||||
overlay = createNavaraOverlay(view);
|
||||
engine = createNavaraEngine(view, {
|
||||
hooks: navaraHooks(view),
|
||||
container,
|
||||
...(appClock ? { appClock } : {}),
|
||||
sharpen: createNavaraSharpen(view),
|
||||
movers: createNavaraMovers(view),
|
||||
lines: createNavaraLines(view),
|
||||
movers: createNavaraMovers(view, { overlay }),
|
||||
lines: createNavaraLines(view, { overlay }),
|
||||
});
|
||||
const camera = engine.camera;
|
||||
let ground = Number.NaN;
|
||||
@@ -166,6 +170,7 @@ export async function createNavaraGlobe({
|
||||
} catch (error) {
|
||||
removeLook?.();
|
||||
engine?.destroy();
|
||||
overlay?.destroy();
|
||||
view?.dispose();
|
||||
canvas.remove();
|
||||
throw error;
|
||||
@@ -179,6 +184,7 @@ export async function createNavaraGlobe({
|
||||
destroyed = true;
|
||||
removeLook();
|
||||
engine.destroy();
|
||||
overlay.destroy();
|
||||
view.dispose();
|
||||
canvas.remove();
|
||||
},
|
||||
|
||||
@@ -1,56 +1,25 @@
|
||||
// engine.lines on Navara: each line is one Navara mesh descriptor (public
|
||||
// MeshDesc API, the transparent pass, unlit) holding its ribbon
|
||||
// (lineMesh.js). Geodesic arcs are densified on the CPU (src/globe/lines.js)
|
||||
// engine.lines on Navara: each line is a ribbon (lineMesh.js) in gev's
|
||||
// overlay (overlay.js: drawn after tone mapping, so its colour is exact). Geodesic arcs are densified on the CPU (src/globe/lines.js)
|
||||
// since the ribbon draws straight segments. Every change asks for one frame;
|
||||
// nothing animates on its own, so an idle view stays idle. Lines are not
|
||||
// pickable on Navara yet (trails never take a click; see phase 3 Task 4).
|
||||
|
||||
import { MeshDesc } from '@navaramap/three';
|
||||
import { createLineRegistry, densifyGeodesic } from '../../lines.js';
|
||||
import { createLineMesh } from './lineMesh.js';
|
||||
|
||||
const DESC_NAME = 'gevLine';
|
||||
const registered = new WeakSet();
|
||||
|
||||
class GevLineDesc extends MeshDesc {
|
||||
constructor(view, ctx, config) {
|
||||
super(view, ctx, config);
|
||||
this.gev = config[DESC_NAME];
|
||||
}
|
||||
|
||||
getPassKey() {
|
||||
return 'transparent';
|
||||
}
|
||||
|
||||
createMesh() {
|
||||
return this.gev.line.group;
|
||||
}
|
||||
|
||||
update() {
|
||||
const canvas = this.ctx.getRenderer().domElement;
|
||||
this.gev.line.update(canvas.clientWidth, canvas.clientHeight);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.gev.line.dispose();
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} view an initialised Navara ThreeView
|
||||
* @param {{ overlay: ReturnType<import('../overlay.js').createNavaraOverlay> }} ports
|
||||
* @returns {{ lines: import('../../contract.js').GlobeLines, destroy: () => void }}
|
||||
*/
|
||||
export function createNavaraLines(view) {
|
||||
if (!registered.has(view)) {
|
||||
view.registerMesh(DESC_NAME, GevLineDesc);
|
||||
registered.add(view);
|
||||
}
|
||||
export function createNavaraLines(view, { overlay }) {
|
||||
const request = () => view.forceUpdate();
|
||||
// What CPU picks need of each line: its drawn positions and matrix.
|
||||
const drawn = new Map();
|
||||
const registry = createLineRegistry((options) => {
|
||||
const line = createLineMesh(options);
|
||||
const line = createLineMesh(options, {
|
||||
overlayUniforms: overlay.uniforms,
|
||||
});
|
||||
const state = {
|
||||
options,
|
||||
flat: new Float64Array(0),
|
||||
@@ -59,8 +28,14 @@ export function createNavaraLines(view) {
|
||||
};
|
||||
drawn.set(options.id, state);
|
||||
const canvas = view.canvas;
|
||||
if (canvas) line.update(canvas.clientWidth || 1, canvas.clientHeight || 1);
|
||||
const handle = view.addMesh({ lit: false, [DESC_NAME]: { line } });
|
||||
const viewport = () =>
|
||||
line.update(
|
||||
canvas?.clientWidth || 1,
|
||||
canvas?.clientHeight || 1,
|
||||
overlay.ctx?.getRenderer().getPixelRatio() ?? 1,
|
||||
);
|
||||
viewport();
|
||||
overlay.add(line.group, viewport);
|
||||
return {
|
||||
setPositions(flat) {
|
||||
state.flat = options.arc === 'geodesic' ? densifyGeodesic(flat) : flat;
|
||||
@@ -83,7 +58,8 @@ export function createNavaraLines(view) {
|
||||
},
|
||||
destroy() {
|
||||
drawn.delete(options.id);
|
||||
handle.delete();
|
||||
overlay.remove(line.group);
|
||||
line.dispose();
|
||||
request();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// The three side of a Navara line: a ribbon of one quad per segment whose
|
||||
// vertices are pushed apart in the vertex shader to the line's width in CSS
|
||||
// pixels (projected ends, the screen-space normal), so the width holds at any
|
||||
// distance. A second copy of the ribbon, drawn only where the scene hides it
|
||||
// (depthFunc GreaterDepth) with the alpha scaled, is depthFailAlpha. Dashes
|
||||
// in pixels are cut in the fragment shader along the segment's screen
|
||||
// direction (as Cesium's PolylineDash); the tactical dash is the launches'
|
||||
// distance. Drawn after tone mapping (overlay.js) with the depth test in the
|
||||
// shader (overlayDepth.js); a second copy of the ribbon, drawn only where the
|
||||
// scene hides it with the alpha scaled, is depthFailAlpha. Dashes in CSS
|
||||
// pixels are cut in the fragment shader along the segment's screen
|
||||
// direction (as Cesium's PolylineDash; gl_FragCoord over the pixel ratio, so
|
||||
// the period is the same on any screen); the tactical dash is the launches'
|
||||
// pattern over the fraction along the whole line. Positions are relative to
|
||||
// the first point (the group sits there, in doubles), the model matrix
|
||||
// multiplies the group's transform (an orbit ring turned by GMST).
|
||||
@@ -13,13 +15,16 @@ import {
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
DoubleSide,
|
||||
GreaterDepth,
|
||||
Group,
|
||||
LessEqualDepth,
|
||||
Matrix4,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
} from 'three';
|
||||
import {
|
||||
OVERLAY_DEPTH_FRAGMENT,
|
||||
OVERLAY_DEPTH_MODES,
|
||||
OVERLAY_DEPTH_VERTEX,
|
||||
} from '../overlayDepth.js';
|
||||
|
||||
const VERTEX_HEAD = /* glsl */ `
|
||||
attribute vec3 gevEnd;
|
||||
@@ -30,6 +35,7 @@ uniform float uGevWidthPx;
|
||||
varying float vGevS;
|
||||
varying float vGevT;
|
||||
varying vec2 vGevDirection;
|
||||
${OVERLAY_DEPTH_VERTEX}
|
||||
`;
|
||||
|
||||
const VERTEX_PROJECT = /* glsl */ `
|
||||
@@ -50,11 +56,14 @@ vec4 mvPosition = modelViewMatrix * vec4(gevSide.x < 0.5 ? position : gevEnd, 1.
|
||||
vGevS = gevS;
|
||||
vGevT = gevSide.y * 0.5 + 0.5;
|
||||
vGevDirection = gevDirection;
|
||||
vGevDepthFree = 0.0;
|
||||
`;
|
||||
|
||||
const FRAGMENT_HEAD = /* glsl */ `
|
||||
uniform vec4 uGevColor;
|
||||
uniform float uGevAlphaScale;
|
||||
uniform float uGevPixelRatio;
|
||||
${OVERLAY_DEPTH_FRAGMENT}
|
||||
uniform float uGevDash;
|
||||
uniform vec2 uGevDashPx;
|
||||
uniform vec2 uGevTactical;
|
||||
@@ -80,12 +89,13 @@ float gevTactical() {
|
||||
|
||||
const FRAGMENT_COLOR = /* glsl */ `
|
||||
#include <color_fragment>
|
||||
if (gevOverlayHidden()) discard;
|
||||
diffuseColor = uGevColor;
|
||||
diffuseColor.a *= uGevAlphaScale;
|
||||
if (uGevDash > 1.5) {
|
||||
diffuseColor.a *= gevTactical();
|
||||
} else if (uGevDash > 0.5) {
|
||||
float gevAlong = dot(gl_FragCoord.xy, vGevDirection);
|
||||
float gevAlong = dot(gl_FragCoord.xy / uGevPixelRatio, vGevDirection);
|
||||
if (mod(gevAlong, uGevDashPx.x + uGevDashPx.y) > uGevDashPx.x) discard;
|
||||
}
|
||||
`;
|
||||
@@ -170,11 +180,23 @@ export function buildRibbon(flat, origin) {
|
||||
return geometry;
|
||||
}
|
||||
|
||||
// Overlay uniforms of a line drawn outside an overlay (tests): no depth.
|
||||
const DETACHED_OVERLAY = Object.freeze({
|
||||
tGevSceneDepth: { value: null },
|
||||
uGevResolution: { value: [0, 0] },
|
||||
});
|
||||
|
||||
/** @param {ReturnType<import('../../lines.js').normalizeLineOptions>} options */
|
||||
export function createLineMesh(options) {
|
||||
export function createLineMesh(
|
||||
options,
|
||||
{ overlayUniforms = DETACHED_OVERLAY } = {},
|
||||
) {
|
||||
const dash = options.dash;
|
||||
const shared = {
|
||||
uGevViewport: { value: [1, 1] },
|
||||
uGevPixelRatio: { value: 1 },
|
||||
tGevSceneDepth: overlayUniforms.tGevSceneDepth,
|
||||
uGevResolution: overlayUniforms.uGevResolution,
|
||||
uGevWidthPx: { value: options.widthPx },
|
||||
uGevColor: { value: [...options.color] },
|
||||
uGevDash: { value: dash?.kind === 'tactical' ? 2 : dash ? 1 : 0 },
|
||||
@@ -185,14 +207,19 @@ export function createLineMesh(options) {
|
||||
value: dash?.kind === 'tactical' ? [dash.groups, dash.dashes] : [1, 1],
|
||||
},
|
||||
};
|
||||
const material = (alphaScale, depthFunc) => {
|
||||
const material = (alphaScale, depthMode) => {
|
||||
const value = new MeshBasicMaterial({
|
||||
transparent: true,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
depthFunc,
|
||||
side: DoubleSide,
|
||||
});
|
||||
const uniforms = { ...shared, uGevAlphaScale: { value: alphaScale } };
|
||||
const uniforms = {
|
||||
...shared,
|
||||
uGevAlphaScale: { value: alphaScale },
|
||||
uGevDepthMode: { value: depthMode },
|
||||
};
|
||||
value.userData.gevDepthMode = depthMode;
|
||||
value.onBeforeCompile = (shader) => patchLineShader(shader, uniforms);
|
||||
value.customProgramCacheKey = () => 'gev-line';
|
||||
return value;
|
||||
@@ -200,7 +227,10 @@ export function createLineMesh(options) {
|
||||
const group = new Group();
|
||||
group.matrixAutoUpdate = false;
|
||||
group.frustumCulled = false;
|
||||
const main = new Mesh(new BufferGeometry(), material(1, LessEqualDepth));
|
||||
const main = new Mesh(
|
||||
new BufferGeometry(),
|
||||
material(1, OVERLAY_DEPTH_MODES.normal),
|
||||
);
|
||||
main.frustumCulled = false;
|
||||
main.renderOrder = options.order;
|
||||
group.add(main);
|
||||
@@ -208,7 +238,7 @@ export function createLineMesh(options) {
|
||||
if (options.depthFailAlpha > 0) {
|
||||
behind = new Mesh(
|
||||
main.geometry,
|
||||
material(options.depthFailAlpha, GreaterDepth),
|
||||
material(options.depthFailAlpha, OVERLAY_DEPTH_MODES.fail),
|
||||
);
|
||||
behind.frustumCulled = false;
|
||||
behind.renderOrder = options.order;
|
||||
@@ -247,9 +277,16 @@ export function createLineMesh(options) {
|
||||
show(visible) {
|
||||
group.visible = visible;
|
||||
},
|
||||
/** Each drawn frame: the viewport the pixel width is measured in. */
|
||||
update(width, height) {
|
||||
shared.uGevViewport.value = [width, height];
|
||||
/**
|
||||
* Each drawn frame: the viewport (CSS px) the width is measured in and
|
||||
* the device pixel ratio the dashes are measured with. In place: no
|
||||
* allocation per frame.
|
||||
*/
|
||||
update(width, height, pixelRatio = 1) {
|
||||
const viewport = shared.uGevViewport.value;
|
||||
viewport[0] = width;
|
||||
viewport[1] = height;
|
||||
shared.uGevPixelRatio.value = pixelRatio;
|
||||
},
|
||||
dispose() {
|
||||
main.geometry.dispose();
|
||||
|
||||
@@ -67,7 +67,17 @@ test('a line places its group at the first point under its model matrix', () =>
|
||||
// (6378137, 0, 0) turned 90° about z.
|
||||
assert.ok(Math.abs(origin.x) < 1e-6 && Math.abs(origin.y - 6_378_137) < 1e-6);
|
||||
const [main, behind] = line.meshes();
|
||||
assert.equal(behind.material.depthFunc, GreaterDepth);
|
||||
// The overlay tests depth in the shader: normal for the line, drawn
|
||||
// only where the scene is nearer for its depth-fail copy.
|
||||
assert.equal(main.material.depthTest, false);
|
||||
assert.equal(main.material.userData.gevDepthMode, 1);
|
||||
assert.equal(behind.material.userData.gevDepthMode, 2);
|
||||
// Viewport and pixel ratio update in place (no allocation per frame).
|
||||
const viewport = line.uniforms.uGevViewport.value;
|
||||
line.update(1280, 800, 2);
|
||||
assert.equal(line.uniforms.uGevViewport.value, viewport);
|
||||
assert.deepEqual(viewport, [1280, 800]);
|
||||
assert.equal(line.uniforms.uGevPixelRatio.value, 2);
|
||||
assert.equal(main.geometry, behind.geometry);
|
||||
assert.equal(line.uniforms.uGevDash.value, 2);
|
||||
assert.deepEqual(line.uniforms.uGevTactical.value, [4, 100]);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// engine.movers on Navara: each set is one Navara mesh descriptor (public
|
||||
// MeshDesc API) holding its instanced meshes (moverMesh.js, modelMover.js).
|
||||
// Pickable sets register a PickableInstancedMeshWrapper so Navara's pick
|
||||
// pass sees each instance; pickIds.js turns its batch ids back into
|
||||
// instances. commit() uploads and asks for one frame (view.forceUpdate): a
|
||||
// descriptor's update() runs only on frames the view draws anyway (S5), so
|
||||
// an idle view stays idle.
|
||||
// engine.movers on Navara. Sprites and points are one instanced quad mesh
|
||||
// per set (moverMesh.js) in gev's overlay (overlay.js: drawn after tone
|
||||
// mapping, so their colours are exact); models are a Navara mesh descriptor
|
||||
// (public MeshDesc API) holding one InstancedMesh per glTF mesh
|
||||
// (modelMover.js), lit and graded like the scene. Pickable sets register a
|
||||
// PickableInstancedMeshWrapper so Navara's pick pass sees each instance;
|
||||
// pickIds.js turns its batch ids back into instances. commit() uploads and
|
||||
// asks for one frame (view.forceUpdate); per-frame work runs only on frames
|
||||
// the view draws anyway (S5), so an idle view stays idle.
|
||||
|
||||
import {
|
||||
MeshDesc,
|
||||
@@ -28,13 +30,8 @@ class GevMoverSetDesc extends MeshDesc {
|
||||
this.wrappers = [];
|
||||
}
|
||||
|
||||
// Sprites and points are screen overlays: the transparent pass, unlit.
|
||||
getPassKey() {
|
||||
return this.gev?.kind === 'model' ? super.getPassKey() : 'transparent';
|
||||
}
|
||||
|
||||
createMesh() {
|
||||
const { mover, pickable, onPickable, kind } = this.gev;
|
||||
const { mover, pickable, onPickable } = this.gev;
|
||||
const register = (mesh, key) => {
|
||||
if (!pickable) return;
|
||||
const wrapper = new PickableInstancedMeshWrapper(
|
||||
@@ -46,10 +43,6 @@ class GevMoverSetDesc extends MeshDesc {
|
||||
this.wrappers.push(key);
|
||||
onPickable(wrapper);
|
||||
};
|
||||
if (kind !== 'model') {
|
||||
register(mover.mesh, this.id);
|
||||
return mover.mesh;
|
||||
}
|
||||
const group = new Group();
|
||||
group.frustumCulled = false;
|
||||
// Meshes arrive with the glTF; they join the group (and the pick pass).
|
||||
@@ -65,21 +58,7 @@ class GevMoverSetDesc extends MeshDesc {
|
||||
}
|
||||
|
||||
update() {
|
||||
const raw = this.view.camera.raw;
|
||||
const height = this.ctx.getRenderer().domElement.clientHeight;
|
||||
if (!raw || !(height > 0)) return;
|
||||
raw.updateMatrixWorld();
|
||||
const e = raw.matrixWorld.elements;
|
||||
const cameraPosition = { x: e[12], y: e[13], z: e[14] };
|
||||
const cameraHeight =
|
||||
Math.hypot(cameraPosition.x, cameraPosition.y, cameraPosition.z) -
|
||||
EARTH_RADIUS_M;
|
||||
this.gev.mover.update({
|
||||
cameraPosition,
|
||||
cameraHeight,
|
||||
fovDeg: raw.fov,
|
||||
viewportHeightPx: height,
|
||||
});
|
||||
updateMover(this.view, this.ctx, this.gev.mover);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
@@ -90,13 +69,38 @@ class GevMoverSetDesc extends MeshDesc {
|
||||
}
|
||||
}
|
||||
|
||||
// The camera a mover needs each drawn frame (scratch: no allocation).
|
||||
const frameCamera = {
|
||||
cameraPosition: { x: 0, y: 0, z: 0 },
|
||||
cameraHeight: 0,
|
||||
fovDeg: 60,
|
||||
viewportHeightPx: 0,
|
||||
};
|
||||
function updateMover(view, ctx, mover) {
|
||||
const raw = view.camera.raw;
|
||||
const height = ctx?.getRenderer().domElement.clientHeight;
|
||||
if (!raw || !(height > 0)) return;
|
||||
raw.updateMatrixWorld();
|
||||
const e = raw.matrixWorld.elements;
|
||||
const position = frameCamera.cameraPosition;
|
||||
position.x = e[12];
|
||||
position.y = e[13];
|
||||
position.z = e[14];
|
||||
frameCamera.cameraHeight =
|
||||
Math.hypot(position.x, position.y, position.z) - EARTH_RADIUS_M;
|
||||
frameCamera.fovDeg = raw.fov;
|
||||
frameCamera.viewportHeightPx = height;
|
||||
mover.update(frameCamera);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} view an initialised Navara ThreeView
|
||||
* @param {{ overlay: ReturnType<import('../overlay.js').createNavaraOverlay> }} ports
|
||||
* @returns {{ movers: import('../../contract.js').GlobeMovers,
|
||||
* resolvePick: (batchId: number) => ({ set: string, instance: number, pickId: any } | null),
|
||||
* destroy: () => void }}
|
||||
*/
|
||||
export function createNavaraMovers(view) {
|
||||
export function createNavaraMovers(view, { overlay }) {
|
||||
if (!registered.has(view)) {
|
||||
view.registerMesh(DESC_NAME, GevMoverSetDesc);
|
||||
registered.add(view);
|
||||
@@ -108,13 +112,43 @@ export function createNavaraMovers(view) {
|
||||
return loader.loadAsync(url);
|
||||
};
|
||||
const request = () => view.forceUpdate();
|
||||
const overlaySet = (options, store) => {
|
||||
const mover = createMoverMesh(options, store, {
|
||||
onTextureLoad: request,
|
||||
overlayUniforms: overlay.uniforms,
|
||||
});
|
||||
const key = `gevMovers:${options.id}`;
|
||||
if (options.pickable) {
|
||||
const wrapper = new PickableInstancedMeshWrapper(
|
||||
mover.mesh,
|
||||
options.capacity,
|
||||
overlay.ctx,
|
||||
);
|
||||
overlay.registerPickable(key, wrapper);
|
||||
pickIndex.add(options.id, wrapper, (index) => store.pickIds[index]);
|
||||
}
|
||||
overlay.add(mover.mesh, () => updateMover(view, overlay.ctx, mover));
|
||||
return {
|
||||
commit() {
|
||||
mover.commit(store.dirty());
|
||||
request();
|
||||
},
|
||||
destroy() {
|
||||
pickIndex.remove(options.id);
|
||||
if (options.pickable) overlay.unregisterPickable(key);
|
||||
overlay.remove(mover.mesh);
|
||||
mover.dispose();
|
||||
request();
|
||||
},
|
||||
};
|
||||
};
|
||||
const registry = createMoverRegistry((options, store) => {
|
||||
const mover =
|
||||
options.kind === 'model'
|
||||
? createModelMover(options, store, { load: loadGltf, onLoad: request })
|
||||
: createMoverMesh(options, store, { onTextureLoad: request });
|
||||
if (options.kind !== 'model') return overlaySet(options, store);
|
||||
const mover = createModelMover(options, store, {
|
||||
load: loadGltf,
|
||||
onLoad: request,
|
||||
});
|
||||
const handle = view.addMesh({
|
||||
...(options.kind === 'model' ? {} : { lit: false }),
|
||||
[DESC_NAME]: {
|
||||
mover,
|
||||
kind: options.kind,
|
||||
|
||||
@@ -23,6 +23,11 @@ import {
|
||||
originFor,
|
||||
packInstances,
|
||||
} from './pack.js';
|
||||
import {
|
||||
OVERLAY_DEPTH_FRAGMENT,
|
||||
OVERLAY_DEPTH_MODES,
|
||||
OVERLAY_DEPTH_VERTEX,
|
||||
} from '../overlayDepth.js';
|
||||
|
||||
const VERTEX_HEAD = /* glsl */ `
|
||||
attribute vec4 gevStyle;
|
||||
@@ -37,6 +42,7 @@ uniform float uGevOutlinePx;
|
||||
varying vec4 vGevColor;
|
||||
varying vec2 vGevLocal;
|
||||
varying float vGevOutline;
|
||||
${OVERLAY_DEPTH_VERTEX}
|
||||
`;
|
||||
|
||||
const VERTEX_PROJECT = /* glsl */ `
|
||||
@@ -66,6 +72,7 @@ gl_Position = projectionMatrix * mvPosition;
|
||||
vGevColor = gevColor;
|
||||
vGevLocal = gevCorner;
|
||||
vGevOutline = uGevOutlinePx / max(gevSizePx, 1e-3);
|
||||
vGevDepthFree = gevDistance < uGevDisableWithinM ? 1.0 : 0.0;
|
||||
`;
|
||||
|
||||
const VERTEX_UV = /* glsl */ `
|
||||
@@ -75,23 +82,18 @@ vMapUv = mix(gevUv.xy, gevUv.zw, uv);
|
||||
#endif
|
||||
`;
|
||||
|
||||
const VERTEX_DEPTH = /* glsl */ `
|
||||
#include <logdepthbuf_vertex>
|
||||
#ifdef USE_LOGARITHMIC_DEPTH_BUFFER
|
||||
if (gevDistance < uGevDisableWithinM) vFragDepth = 1.0;
|
||||
#endif
|
||||
`;
|
||||
|
||||
const FRAGMENT_HEAD = /* glsl */ `
|
||||
varying vec4 vGevColor;
|
||||
varying vec2 vGevLocal;
|
||||
varying float vGevOutline;
|
||||
${OVERLAY_DEPTH_FRAGMENT}
|
||||
`;
|
||||
|
||||
const fragmentColor = (point) =>
|
||||
point
|
||||
? /* glsl */ `
|
||||
#include <color_fragment>
|
||||
if (gevOverlayHidden()) discard;
|
||||
float gevRadius = length(vGevLocal) * 2.0;
|
||||
if (gevRadius > 1.0) discard;
|
||||
diffuseColor *= vGevColor;
|
||||
@@ -99,9 +101,16 @@ if (gevRadius > 1.0 - vGevOutline) diffuseColor.rgb = vec3(0.0);
|
||||
`
|
||||
: /* glsl */ `
|
||||
#include <color_fragment>
|
||||
if (gevOverlayHidden()) discard;
|
||||
diffuseColor *= vGevColor;
|
||||
`;
|
||||
|
||||
// Overlay uniforms of a mesh drawn outside an overlay (tests): no depth.
|
||||
const DETACHED_OVERLAY = Object.freeze({
|
||||
tGevSceneDepth: { value: null },
|
||||
uGevResolution: { value: [0, 0] },
|
||||
});
|
||||
|
||||
/** Patches three's MeshBasicMaterial shaders into the screen-space quad. */
|
||||
export function patchMoverShader(shader, uniforms, { point = false } = {}) {
|
||||
Object.assign(shader.uniforms, uniforms);
|
||||
@@ -117,7 +126,6 @@ export function patchMoverShader(shader, uniforms, { point = false } = {}) {
|
||||
);
|
||||
vertex = replace(vertex, '#include <project_vertex>', VERTEX_PROJECT);
|
||||
vertex = replace(vertex, '#include <uv_vertex>', VERTEX_UV);
|
||||
vertex = replace(vertex, '#include <logdepthbuf_vertex>', VERTEX_DEPTH);
|
||||
let fragment = replace(
|
||||
shader.fragmentShader,
|
||||
'void main() {',
|
||||
@@ -161,7 +169,7 @@ function atlasTexture(atlas, onLoad) {
|
||||
export function createMoverMesh(
|
||||
options,
|
||||
store,
|
||||
{ onTextureLoad = () => {} } = {},
|
||||
{ onTextureLoad = () => {}, overlayUniforms = DETACHED_OVERLAY } = {},
|
||||
) {
|
||||
const { capacity, kind, depthTest } = options;
|
||||
const point = kind === 'point';
|
||||
@@ -192,12 +200,23 @@ export function createMoverMesh(
|
||||
value: typeof depthTest === 'object' ? depthTest.disableWithinM : -1,
|
||||
},
|
||||
uGevOutlinePx: { value: point ? options.point.outlinePx : 0 },
|
||||
uGevDepthMode: {
|
||||
value:
|
||||
depthTest === 'always'
|
||||
? OVERLAY_DEPTH_MODES.always
|
||||
: OVERLAY_DEPTH_MODES.normal,
|
||||
},
|
||||
// Shared by every overlay: the scene depth and the target's size.
|
||||
tGevSceneDepth: overlayUniforms.tGevSceneDepth,
|
||||
uGevResolution: overlayUniforms.uGevResolution,
|
||||
};
|
||||
const material = new MeshBasicMaterial({
|
||||
map: point ? null : atlasTexture(options.sprite.atlas, onTextureLoad),
|
||||
transparent: true,
|
||||
alphaTest: 0.02,
|
||||
depthTest: depthTest !== 'always',
|
||||
// Drawn after tone mapping, with no depth buffer: the shader tests the
|
||||
// scene depth itself (overlayDepth.js).
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
side: DoubleSide,
|
||||
});
|
||||
|
||||
@@ -32,10 +32,14 @@ test('the shader patch sizes the quad in pixels and colours it per instance', ()
|
||||
/uGevFootprint \* max\(-mvCenter\.z, 0\.0\)/,
|
||||
);
|
||||
assert.match(shader.vertexShader, /vMapUv = mix\(gevUv\.xy, gevUv\.zw, uv\)/);
|
||||
assert.match(shader.vertexShader, /vFragDepth = 1\.0;/);
|
||||
assert.match(
|
||||
shader.vertexShader,
|
||||
/vGevDepthFree = gevDistance < uGevDisableWithinM/,
|
||||
);
|
||||
assert.match(shader.fragmentShader, /if \(gevOverlayHidden\(\)\) discard;/);
|
||||
assert.doesNotMatch(shader.vertexShader, /#include <project_vertex>/);
|
||||
assert.match(shader.fragmentShader, /diffuseColor \*= vGevColor;/);
|
||||
assert.doesNotMatch(shader.fragmentShader, /discard/);
|
||||
assert.doesNotMatch(shader.fragmentShader, /gevRadius > 1\.0\) discard/);
|
||||
const point = patchMoverShader(basicShader(), {}, { point: true });
|
||||
assert.match(point.fragmentShader, /if \(gevRadius > 1\.0\) discard;/);
|
||||
});
|
||||
@@ -46,7 +50,9 @@ test('a sprite mesh uploads committed instances relative to its origin', () => {
|
||||
const { mesh, uniforms } = mover;
|
||||
assert.equal(mesh.renderOrder, 3);
|
||||
assert.equal(mesh.count, 0);
|
||||
assert.equal(mesh.material.depthTest, true);
|
||||
// The overlay tests depth in the shader, against the scene's.
|
||||
assert.equal(mesh.material.depthTest, false);
|
||||
assert.equal(uniforms.uGevDepthMode.value, 1);
|
||||
assert.equal(uniforms.uGevDisableWithinM.value, 2000);
|
||||
assert.deepEqual(uniforms.uGevScaleByDistance.value, [1000, 1, 100000, 0.5]);
|
||||
store.set(0, { position: { x: 6_378_137, y: 10, z: 20 } });
|
||||
@@ -98,6 +104,7 @@ test('points have no atlas and draw a disc', () => {
|
||||
const mover = createMoverMesh(options, createMoverStore(options));
|
||||
assert.equal(mover.mesh.material.map, null);
|
||||
assert.equal(mover.mesh.material.depthTest, false);
|
||||
assert.equal(mover.uniforms.uGevDepthMode.value, 0, "'always': no test");
|
||||
assert.equal(mover.uniforms.uGevOutlinePx.value, 1);
|
||||
mover.dispose();
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
// gev's screen overlays on Navara: sprites, points and lines (engine.movers
|
||||
// and engine.lines) drawn into the frame after Navara's tone mapping and
|
||||
// before its 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 the scene is empty. Models stay in Navara's
|
||||
// scene, lit and graded like the 3D tiles.
|
||||
|
||||
import { EffectDesc, Pass as NavaraPass } from '@navaramap/three';
|
||||
import { Pass } from 'postprocessing';
|
||||
import { RGBADepthPacking, Scene } from 'three';
|
||||
|
||||
export const OVERLAY_EFFECT_KEY = 'gevOverlay';
|
||||
|
||||
class GevOverlayPass extends Pass {
|
||||
constructor(scene, camera, overlay) {
|
||||
super('GevOverlayPass', scene, camera);
|
||||
this.needsSwap = false;
|
||||
this.overlay = overlay;
|
||||
}
|
||||
|
||||
render(renderer, inputBuffer) {
|
||||
if (!this.scene.children.length) return;
|
||||
const resolution = this.overlay.uniforms.uGevResolution.value;
|
||||
resolution[0] = inputBuffer?.width ?? 0;
|
||||
resolution[1] = inputBuffer?.height ?? 0;
|
||||
const autoClear = renderer.autoClear;
|
||||
renderer.autoClear = false;
|
||||
renderer.setRenderTarget(this.renderToScreen ? null : inputBuffer);
|
||||
renderer.render(this.scene, this.camera);
|
||||
renderer.autoClear = autoClear;
|
||||
}
|
||||
}
|
||||
|
||||
class GevOverlayEffectDesc extends EffectDesc {
|
||||
static key = OVERLAY_EFFECT_KEY;
|
||||
static insertAfter = ['toneMapping'];
|
||||
static insertBefore = ['gevSharpen', 'smaa', 'fxaa', 'final'];
|
||||
|
||||
constructor(view, ctx, config) {
|
||||
super(view, ctx, config);
|
||||
this.overlay = config[OVERLAY_EFFECT_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,
|
||||
);
|
||||
}
|
||||
|
||||
// Each drawn frame: the scene depth the overlays test against, then each
|
||||
// overlay's own per-frame work (pixel footprint, origin, viewport).
|
||||
update(time) {
|
||||
const mrt = this.find('mrt');
|
||||
this.overlay.uniforms.tGevSceneDepth.value =
|
||||
mrt?.depthBufferPacking === RGBADepthPacking ? mrt.depthBuffer : null;
|
||||
this.overlay.beforeFrame(time);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
view.registerEffect(OVERLAY_EFFECT_KEY, GevOverlayEffectDesc);
|
||||
const scene = new Scene();
|
||||
const frameHooks = new Map();
|
||||
let ctx = null;
|
||||
const pendingPicks = new Map();
|
||||
const overlay = {
|
||||
scene,
|
||||
uniforms: {
|
||||
tGevSceneDepth: { value: null },
|
||||
uGevResolution: { value: [0, 0] },
|
||||
},
|
||||
attach(context) {
|
||||
ctx = context;
|
||||
for (const [key, wrapper] of pendingPicks)
|
||||
ctx.registerPickableMesh(key, wrapper);
|
||||
pendingPicks.clear();
|
||||
},
|
||||
beforeFrame(time) {
|
||||
for (const hook of frameHooks.values()) hook(time);
|
||||
},
|
||||
get ctx() {
|
||||
return ctx;
|
||||
},
|
||||
};
|
||||
const handle = view.addEffect({
|
||||
[OVERLAY_EFFECT_KEY]: { overlay },
|
||||
visible: true,
|
||||
});
|
||||
return {
|
||||
scene,
|
||||
uniforms: overlay.uniforms,
|
||||
add(object, onFrame) {
|
||||
scene.add(object);
|
||||
if (onFrame) frameHooks.set(object, onFrame);
|
||||
},
|
||||
remove(object) {
|
||||
scene.remove(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();
|
||||
frameHooks.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// The depth test of gev's screen overlays on Navara (sprites, points and
|
||||
// lines drawn after tone mapping, overlay.js): there is no depth buffer in
|
||||
// the post-processing chain, so each fragment compares its own logarithmic
|
||||
// depth with the scene's, read from Navara's MRT pass (depth packed to RGBA,
|
||||
// as the sharpening reads it). Modes: 0 always drawn; 1 hidden where the
|
||||
// scene is nearer (normal); 2 drawn only where the scene is nearer (a
|
||||
// line's depth-fail copy). `vGevDepthFree` (set by the vertex shader) lets
|
||||
// an instance skip the test (disableWithinM). No three imports: GLSL only.
|
||||
|
||||
export const OVERLAY_DEPTH_MODES = Object.freeze({
|
||||
always: 0,
|
||||
normal: 1,
|
||||
fail: 2,
|
||||
});
|
||||
|
||||
/** Fragment declarations: uniforms, the varying and gevOverlayHidden(). */
|
||||
export const OVERLAY_DEPTH_FRAGMENT = /* glsl */ `
|
||||
uniform sampler2D tGevSceneDepth;
|
||||
uniform vec2 uGevResolution;
|
||||
uniform float uGevDepthMode;
|
||||
varying float vGevDepthFree;
|
||||
bool gevOverlayHidden() {
|
||||
if (uGevDepthMode < 0.5 || vGevDepthFree > 0.5 || uGevResolution.x < 1.0)
|
||||
return false;
|
||||
// three's unpackRGBAToDepth (RGBADepthPacking).
|
||||
float scene = dot(
|
||||
texture2D(tGevSceneDepth, gl_FragCoord.xy / uGevResolution),
|
||||
vec4(255.0 / 256.0, 255.0 / 65536.0, 255.0 / 16777216.0, 1.0 / 16777216.0)
|
||||
);
|
||||
#ifdef USE_LOGARITHMIC_DEPTH_BUFFER
|
||||
float mine = log2(vFragDepth) * logDepthBufFC * 0.5;
|
||||
#else
|
||||
float mine = gl_FragCoord.z;
|
||||
#endif
|
||||
bool occluded = mine > scene + 2e-6;
|
||||
return uGevDepthMode < 1.5 ? occluded : !occluded;
|
||||
}
|
||||
`;
|
||||
|
||||
/** Vertex declaration of the per-vertex escape from the depth test. */
|
||||
export const OVERLAY_DEPTH_VERTEX = /* glsl */ `
|
||||
varying float vGevDepthFree;
|
||||
`;
|
||||
@@ -3,8 +3,10 @@
|
||||
// (src/ui/visualPresets.js SHARPEN_SHADER): a 3×3 box blur, then
|
||||
// center + (center − blur) × amount, clamped to [0, 1].
|
||||
//
|
||||
// Where it sits: right after tone mapping and before SMAA or FXAA (before
|
||||
// `final` in a look without them), as Cesium runs its custom stages after
|
||||
// Where it sits: right after tone mapping (after gev's overlay of sprites,
|
||||
// points and lines, which Cesium's sharpen stage also sharpens) and before
|
||||
// SMAA or FXAA (before `final` in a look without them), as Cesium runs its
|
||||
// custom stages after
|
||||
// its tone mapping and before FXAA. After tone mapping the colours are the
|
||||
// display's [0, 1] range, so highlights do not blow up; anti-aliasing after
|
||||
// it smooths the stair steps sharpening brings out. Cesium sharpens
|
||||
@@ -84,7 +86,7 @@ class UnsharpMaskEffect extends Effect {
|
||||
/** config.gevSharpen: { amount } (Cesium's `amount` uniform). */
|
||||
export class SharpenEffectDesc extends EffectDesc {
|
||||
static key = SHARPEN_EFFECT_KEY;
|
||||
static insertAfter = ['toneMapping'];
|
||||
static insertAfter = ['gevOverlay', 'toneMapping'];
|
||||
static insertBefore = ['smaa', 'fxaa', 'final'];
|
||||
|
||||
constructor(view, ctx, config) {
|
||||
|
||||
@@ -510,17 +510,13 @@ export function createContractSuite(
|
||||
);
|
||||
// Pixels close to a colour in a 64 px box around a point: their
|
||||
// count and centroid.
|
||||
// A pure colour as drawn through either engine's tone mapping and
|
||||
// grading (Navara's cinematic look desaturates): every full
|
||||
// channel above 170 and at least 60 over every empty one.
|
||||
const like = (colour, data, i) => {
|
||||
let low = 0;
|
||||
let high = 255;
|
||||
for (let k = 0; k < 3; k++)
|
||||
if (colour[k] > 0.5) high = Math.min(high, data[i + k]);
|
||||
else low = Math.max(low, data[i + k]);
|
||||
return high > 170 && high - low >= 60;
|
||||
};
|
||||
// A colour as drawn: within 40 of the asked one on every channel
|
||||
// (both engines draw overlays after tone mapping, unlit).
|
||||
const like = (colour, data, i) =>
|
||||
colour.every(
|
||||
(channel, k) =>
|
||||
k > 2 || Math.abs(data[i + k] - channel * 255) <= 40,
|
||||
);
|
||||
const find = (screen, colour) => {
|
||||
const half = 32;
|
||||
const x0 = Math.round(screen.x) - half;
|
||||
|
||||
Reference in new issue
Block a user