Merge branch 'german/refactor-e5-navara-mesh-colour' into german/navara-refactor

This commit is contained in:
German Khokhlov committed 2026-11-10 15:45:37 +04:00
commit 5b27f26e14
7 files changed
+129 -107

No files matched your search

+4 -3
View File
@@ -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.
-10
View File
@@ -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),
});
}
+1 -1
View File
@@ -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) =>
+7 -42
View File
@@ -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();
}
+102 -26
View File
@@ -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);
+2 -2
View File
@@ -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
+13 -23
View File
@@ -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();
});