mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'german/refactor-e5-navara-mesh-colour' into german/navara-refactor
This commit is contained in:
commit
5b27f26e14
7 files changed
+129
-107
No files matched your search
@@ -538,9 +538,10 @@ Cesium draws a synchronous Primitive with a flat material, no back-face
|
||||
culling and no depth writes. Colour, image and visibility changes retain the
|
||||
primitive; geometry, pick-id and depth-test changes replace it. The engine
|
||||
requests frames while the primitive or its image is preparing. Navara draws
|
||||
local coordinates relative to a double-precision world origin in its
|
||||
transparent scene pass after tone mapping, with scene depth and triangle
|
||||
picking. Their authored colours are blended in display space.
|
||||
local coordinates relative to a double-precision world origin in the shared
|
||||
overlay pass after tone mapping and anti-aliasing, the pass that draws sprites
|
||||
and lines, with scene depth and triangle picking. Their authored colours are
|
||||
blended in display space.
|
||||
Both engines own geometry, material and texture disposal. The fake engine
|
||||
keeps the same state and tests triangle intersections for picks.
|
||||
|
||||
|
||||
@@ -237,12 +237,6 @@ async function runOn(browser, kind) {
|
||||
outcome.push({ name, skipped: owner[1] });
|
||||
continue;
|
||||
}
|
||||
const knownFailure =
|
||||
engine.kind === 'navara' &&
|
||||
name ===
|
||||
'meshes.createMesh: a single colour draws as asked within 10 per channel'
|
||||
? 'Meshes are drawn before tone mapping'
|
||||
: null;
|
||||
try {
|
||||
await run();
|
||||
outcome.push({ name, ok: true });
|
||||
@@ -250,10 +244,6 @@ async function runOn(browser, kind) {
|
||||
outcome.push({
|
||||
name,
|
||||
ok: false,
|
||||
...(knownFailure &&
|
||||
String(error?.message).startsWith(
|
||||
'mesh colour within 10 per channel',
|
||||
) && { knownFailure }),
|
||||
error: String(error?.message ?? error),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ export async function createNavaraGlobe({ container, search, hd = false }) {
|
||||
),
|
||||
}),
|
||||
lines: createNavaraLines(view, { overlay }),
|
||||
meshes: (parts) => createNavaraMeshes(view, { MeshDesc, ...parts }),
|
||||
meshes: (parts) => createNavaraMeshes(view, { overlay, ...parts }),
|
||||
ground: createNavaraGround(view, {
|
||||
createPass: createGroundPass,
|
||||
pulseField: (options) =>
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
CanvasTexture,
|
||||
Color,
|
||||
DoubleSide,
|
||||
Group,
|
||||
Mesh,
|
||||
@@ -14,39 +13,11 @@ import {
|
||||
} from 'three';
|
||||
import { createMeshRegistry } from '../meshes.js';
|
||||
import { cartesian3 } from '../../geo/index.js';
|
||||
import { attachSceneDepth, createSceneDepthTest } from './sceneDepth.js';
|
||||
import { createSceneDepthTest } from './sceneDepth.js';
|
||||
|
||||
const KIND = 'gevWorldMesh';
|
||||
const NEAR_SURFACE_M = 2;
|
||||
const registered = new WeakSet();
|
||||
|
||||
export function createNavaraMeshes(view, { MeshDesc, camera, picker }) {
|
||||
if (!registered.has(view)) {
|
||||
view.registerMesh(
|
||||
KIND,
|
||||
class extends MeshDesc {
|
||||
constructor(owner, ctx, config) {
|
||||
super(owner, ctx, config);
|
||||
this.resource = config[KIND];
|
||||
}
|
||||
// Transparent, drawn after the effects: the colours are the ones
|
||||
// asked for (as Cesium draws them), not tone-mapped with the look.
|
||||
// The scene-depth test hides a mesh behind terrain, tiles and Earth.
|
||||
getPassKey() {
|
||||
return 'transparent';
|
||||
}
|
||||
createMesh() {
|
||||
attachSceneDepth(this.resource.group, this.ctx);
|
||||
return this.resource.group;
|
||||
}
|
||||
onDestroy() {
|
||||
this.resource.dispose();
|
||||
super.onDestroy();
|
||||
}
|
||||
},
|
||||
);
|
||||
registered.add(view);
|
||||
}
|
||||
export function createNavaraMeshes(view, { overlay, camera, picker }) {
|
||||
const registry = createMeshRegistry(
|
||||
(state) => {
|
||||
const group = new Group();
|
||||
@@ -60,18 +31,14 @@ export function createNavaraMeshes(view, { MeshDesc, camera, picker }) {
|
||||
const mesh = new Mesh(geometry, material);
|
||||
const sceneDepth = createSceneDepthTest();
|
||||
sceneDepth.material(material);
|
||||
sceneDepth.object(group);
|
||||
sceneDepth.object(mesh);
|
||||
let depthFree = false;
|
||||
const pigment = new Color();
|
||||
const bindDepth = mesh.onBeforeRender;
|
||||
mesh.onBeforeRender = function onBeforeRender(...args) {
|
||||
// The overlay context can arrive after the mesh is created.
|
||||
sceneDepth.attach(overlay.ctx);
|
||||
bindDepth.apply(this, args);
|
||||
if (depthFree) sceneDepth.uniforms.uGevSceneDepthOn.value = 0;
|
||||
// The pass still scales by the view's exposure: undo it.
|
||||
material.color
|
||||
.copy(pigment)
|
||||
.multiplyScalar(1 / Math.max(1, view.toneMappingExposure ?? 1));
|
||||
};
|
||||
group.frustumCulled = mesh.frustumCulled = false;
|
||||
group.add(mesh);
|
||||
@@ -116,17 +83,15 @@ export function createNavaraMeshes(view, { MeshDesc, camera, picker }) {
|
||||
material.needsUpdate = true;
|
||||
} else if (texture && !texture.isVideoTexture && 'image' in patch)
|
||||
texture.needsUpdate = true;
|
||||
pigment.setRGB(...next.color.slice(0, 3), SRGBColorSpace);
|
||||
material.color.copy(pigment);
|
||||
material.color.setRGB(...next.color.slice(0, 3), SRGBColorSpace);
|
||||
material.opacity = next.color[3];
|
||||
depthFree = next.depthTest === 'always';
|
||||
material.depthTest = !depthFree;
|
||||
group.visible = next.show;
|
||||
}
|
||||
let holder;
|
||||
try {
|
||||
update(state, state);
|
||||
holder = view.addMesh({ [KIND]: { group, dispose } });
|
||||
overlay.add(group);
|
||||
} catch (error) {
|
||||
dispose();
|
||||
throw error;
|
||||
@@ -137,7 +102,7 @@ export function createNavaraMeshes(view, { MeshDesc, camera, picker }) {
|
||||
update,
|
||||
destroy() {
|
||||
try {
|
||||
holder.delete();
|
||||
overlay.remove(group);
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { Group, Vector3 } from 'three';
|
||||
import {
|
||||
Color,
|
||||
Group,
|
||||
RGBADepthPacking,
|
||||
ShaderLib,
|
||||
SRGBColorSpace,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createNavaraMeshes } from './meshes.js';
|
||||
import { createCctvMeshes } from '../../layers/cctv/render/meshes.js';
|
||||
@@ -8,40 +15,30 @@ import { viewshedColors } from '../../data/cctvViewshed.js';
|
||||
|
||||
function harness() {
|
||||
const scene = new Group();
|
||||
const kinds = new Map();
|
||||
const handles = new Set();
|
||||
let requests = 0;
|
||||
let fail = false;
|
||||
let failedGeometryDisposals = 0;
|
||||
class MeshDesc {
|
||||
onDestroy() {}
|
||||
}
|
||||
const view = {
|
||||
forceUpdate: () => requests++,
|
||||
registerMesh: (kind, desc) => kinds.set(kind, desc),
|
||||
addMesh(config) {
|
||||
toneMappingExposure: 1,
|
||||
};
|
||||
const overlay = {
|
||||
ctx: null,
|
||||
add(object) {
|
||||
if (fail) {
|
||||
config.gevWorldMesh.group.children[0].geometry.addEventListener(
|
||||
object.children[0].geometry.addEventListener(
|
||||
'dispose',
|
||||
() => failedGeometryDisposals++,
|
||||
);
|
||||
throw new Error('descriptor failed');
|
||||
throw new Error('overlay failed');
|
||||
}
|
||||
const kind = Object.keys(config)[0];
|
||||
const desc = new (kinds.get(kind))(view, {}, config);
|
||||
// After the effects, depth-tested against the scene's packed depth.
|
||||
assert.equal(desc.getPassKey(), 'transparent');
|
||||
const object = desc.createMesh();
|
||||
scene.add(object);
|
||||
const handle = {
|
||||
delete() {
|
||||
handles.delete(handle);
|
||||
scene.remove(object);
|
||||
desc.onDestroy();
|
||||
},
|
||||
};
|
||||
handles.add(handle);
|
||||
return handle;
|
||||
handles.add(object);
|
||||
},
|
||||
remove(object) {
|
||||
scene.remove(object);
|
||||
handles.delete(object);
|
||||
},
|
||||
};
|
||||
const pickers = new Set();
|
||||
@@ -57,7 +54,7 @@ function harness() {
|
||||
},
|
||||
};
|
||||
const port = createNavaraMeshes(view, {
|
||||
MeshDesc,
|
||||
overlay,
|
||||
camera: engine.camera,
|
||||
picker: engine.picker,
|
||||
});
|
||||
@@ -66,6 +63,8 @@ function harness() {
|
||||
return {
|
||||
owner,
|
||||
port,
|
||||
view,
|
||||
overlay,
|
||||
drawn: () => scene.children.at(-1)?.children[0],
|
||||
engine,
|
||||
scene,
|
||||
@@ -82,6 +81,83 @@ const positions = { capCenter: { x: 6378137, y: 1234, z: 5678 } };
|
||||
const identity = { x: 0, y: 0, z: 0, w: 1 };
|
||||
const canvas = () => ({ width: 8, height: 8, tagName: 'CANVAS' });
|
||||
|
||||
test('solid and textured overlay meshes keep authored colours through landscape exposure changes', () => {
|
||||
const h = harness();
|
||||
for (const image of [null, canvas()]) {
|
||||
const handle = h.engine.meshes.createMesh({
|
||||
id: image ? 'textured' : 'solid',
|
||||
positions: [6378137, 0, 0, 6378137, 10, 0, 6378137, 0, 10],
|
||||
indices: [0, 1, 2],
|
||||
uv: [0, 0, 1, 0, 0, 1],
|
||||
image,
|
||||
});
|
||||
const mesh = h.drawn();
|
||||
assert.equal(mesh.parent.parent, h.scene);
|
||||
assert.equal(mesh.material.toneMapped, false);
|
||||
assert.equal(mesh.material.transparent, true);
|
||||
assert.equal(mesh.material.depthWrite, false);
|
||||
if (image) assert.equal(mesh.material.map.colorSpace, SRGBColorSpace);
|
||||
for (const color of [
|
||||
[0, 1, 0, 1],
|
||||
[0.25, 0.5, 0.75, 0.4],
|
||||
]) {
|
||||
handle.update({ color });
|
||||
const expected = new Color().setRGB(...color.slice(0, 3), SRGBColorSpace);
|
||||
for (const exposure of [1, 10, 1]) {
|
||||
h.view.toneMappingExposure = exposure;
|
||||
mesh.onBeforeRender({});
|
||||
assert.deepEqual(mesh.material.color, expected);
|
||||
assert.equal(mesh.material.opacity, color[3]);
|
||||
}
|
||||
}
|
||||
handle.destroy();
|
||||
assert.equal(h.scene.children.length, 0);
|
||||
}
|
||||
h.port.destroy();
|
||||
});
|
||||
|
||||
test('overlay meshes bind late and replaced scene depth and honour depth-test updates', () => {
|
||||
const h = harness();
|
||||
const plane = h.owner.createPlane(
|
||||
'depth',
|
||||
h.owner.createMaterial(canvas()),
|
||||
shape,
|
||||
positions,
|
||||
identity,
|
||||
);
|
||||
const mesh = h.drawn();
|
||||
const shader = {
|
||||
uniforms: {},
|
||||
fragmentShader: ShaderLib.basic.fragmentShader,
|
||||
};
|
||||
mesh.material.onBeforeCompile(shader, null);
|
||||
assert.match(shader.fragmentShader, /packedDepthOccluded/);
|
||||
const renderer = { getRenderTarget: () => ({ width: 640, height: 360 }) };
|
||||
const { tGevSceneDepth, uGevSceneDepthOn, uGevTargetSize } = shader.uniforms;
|
||||
mesh.onBeforeRender(renderer);
|
||||
assert.equal(uGevSceneDepthOn.value, 0);
|
||||
const mrt = { depthBuffer: {}, depthBufferPacking: RGBADepthPacking };
|
||||
h.overlay.ctx = { findEffect: (key) => (key === 'mrt' ? mrt : null) };
|
||||
mesh.onBeforeRender(renderer);
|
||||
assert.equal(tGevSceneDepth.value, mrt.depthBuffer);
|
||||
assert.equal(uGevSceneDepthOn.value, 1);
|
||||
assert.deepEqual(uGevTargetSize.value.toArray(), [640, 360]);
|
||||
plane.mesh.update({ depthTest: 'always' });
|
||||
mesh.onBeforeRender(renderer);
|
||||
assert.equal(uGevSceneDepthOn.value, 0);
|
||||
assert.equal(mesh.material.depthTest, false);
|
||||
plane.mesh.update({ depthTest: 'normal' });
|
||||
mrt.depthBuffer = {};
|
||||
mesh.onBeforeRender({ getDrawingBufferSize: (size) => size.set(1280, 720) });
|
||||
assert.equal(tGevSceneDepth.value, mrt.depthBuffer);
|
||||
assert.equal(uGevSceneDepthOn.value, 1);
|
||||
assert.equal(mesh.material.depthTest, true);
|
||||
assert.deepEqual(uGevTargetSize.value.toArray(), [1280, 720]);
|
||||
h.owner.destroy();
|
||||
h.port.destroy();
|
||||
assert.equal(h.scene.children.length, 0);
|
||||
});
|
||||
|
||||
test('Navara CCTV plane preserves native rotated geometry, UVs and metre precision', (t) => {
|
||||
const h = harness();
|
||||
t.after(() => {
|
||||
@@ -213,7 +289,7 @@ test('world meshes release replaced images and video callbacks exactly once', ()
|
||||
assert.equal(finalDisposed, 1);
|
||||
});
|
||||
|
||||
test('world mesh descriptor failure releases geometry and rejects later creates after teardown', () => {
|
||||
test('world mesh overlay failure releases geometry and rejects later creates after teardown', () => {
|
||||
const h = harness();
|
||||
h.fail();
|
||||
assert.throws(
|
||||
@@ -225,7 +301,7 @@ test('world mesh descriptor failure releases geometry and rejects later creates
|
||||
positions,
|
||||
identity,
|
||||
),
|
||||
/descriptor failed/,
|
||||
/overlay failed/,
|
||||
);
|
||||
assert.equal(h.failedGeometryDisposals(), 1);
|
||||
assert.equal(h.handles.size, 0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// gev's screen overlays on Navara: sprites, points and lines (engine.movers
|
||||
// and engine.lines) drawn into the frame after Navara's tone mapping,
|
||||
// gev's screen overlays on Navara: sprites, points, lines and world meshes
|
||||
// (engine.movers, engine.lines and engine.meshes) drawn after 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
|
||||
|
||||
@@ -78,34 +78,25 @@ test('the test binds the MRT pass packed depth before each draw, and only packed
|
||||
assert.equal(depth.uniforms.tGevSceneDepth.value, null);
|
||||
});
|
||||
|
||||
/** A Navara view that builds each descriptor as the SDK does. */
|
||||
/** An overlay that exposes the scene's packed depth to its meshes. */
|
||||
function fakeView() {
|
||||
const kinds = new Map();
|
||||
const created = [];
|
||||
const mrt = {
|
||||
depthBuffer: new DataTexture(),
|
||||
depthBufferPacking: RGBADepthPacking,
|
||||
};
|
||||
const ctx = { findEffect: (key) => (key === 'mrt' ? mrt : undefined) };
|
||||
class MeshDesc {
|
||||
constructor(view, context, config) {
|
||||
this.ctx = context;
|
||||
this.config = config;
|
||||
}
|
||||
onDestroy() {}
|
||||
}
|
||||
const view = {
|
||||
forceUpdate() {},
|
||||
registerMesh: (kind, desc) => kinds.set(kind, desc),
|
||||
addMesh(config) {
|
||||
const kind = Object.keys(config)[0];
|
||||
const desc = new (kinds.get(kind))(view, ctx, config);
|
||||
const object = desc.createMesh();
|
||||
created.push({ kind, pass: desc.getPassKey(), object });
|
||||
return { delete: () => desc.onDestroy() };
|
||||
},
|
||||
};
|
||||
return { view, MeshDesc, created, mrt };
|
||||
const overlay = {
|
||||
ctx,
|
||||
add(object) {
|
||||
created.push(object);
|
||||
},
|
||||
remove() {},
|
||||
};
|
||||
return { view, overlay, created, mrt };
|
||||
}
|
||||
|
||||
/** Every renderable under an object compiles with the test and binds depth. */
|
||||
@@ -141,11 +132,11 @@ function assertTested(object, mrt) {
|
||||
}
|
||||
}
|
||||
|
||||
test('pack meshes stay transparent and test the scene depth', () => {
|
||||
test('pack meshes draw in the overlay and test the scene depth', () => {
|
||||
const packs = fakeView();
|
||||
const owned = [];
|
||||
const port = createNavaraMeshes(packs.view, {
|
||||
MeshDesc: packs.MeshDesc,
|
||||
overlay: packs.overlay,
|
||||
camera: {},
|
||||
picker: { registerMeshPicker: () => () => {} },
|
||||
});
|
||||
@@ -156,9 +147,8 @@ test('pack meshes stay transparent and test the scene depth', () => {
|
||||
{ own: (fn) => owned.push(fn) },
|
||||
);
|
||||
assert.equal(packs.created.length, 1);
|
||||
assert.equal(packs.created[0].pass, 'transparent');
|
||||
assertTested(packs.created[0].object, packs.mrt);
|
||||
assert.equal(packs.created[0].object.children[0].material.depthTest, true);
|
||||
assertTested(packs.created[0], packs.mrt);
|
||||
assert.equal(packs.created[0].children[0].material.depthTest, true);
|
||||
for (const release of owned) release();
|
||||
port.destroy();
|
||||
});
|
||||
Reference in new issue
Block a user