Hide Navara infrastructure stems and data pack meshes behind walls, terrain and the Earth

Navara draws its transparent scene after the post effects into a target without the scene depth, so stems and pack polygons and images painted through 3D tiles. A scene-depth test now drops each of their fragments behind the depth Navara packs after its opaque pass; they stay in the transparent pass, so their look in the open is unchanged (scripts/qa-edge-occlusion.mjs compares it with the previous build). Wind flow was already hidden and is kept as a guard in the same scenario.
This commit is contained in:
German Khokhlov committed 2026-11-03 21:15:03 +04:00
1 parent c392d6fb50
commit eee4f48745
6 files changed
+298 -5

No files matched your search

+5 -1
View File
@@ -71,7 +71,11 @@ const renderedLayer = (file) =>
RENDERED_LAYER_DIRS.some((dir) => file.startsWith(dir)); RENDERED_LAYER_DIRS.some((dir) => file.startsWith(dir));
const cesiumLayerRenderer = (file) => const cesiumLayerRenderer = (file) =>
/\/render\/cesium(?:[^/]*|\/[^/]+)\.js$/.test(file); /\/render\/cesium(?:[^/]*|\/[^/]+)\.js$/.test(file);
const navaraLayerRenderer = (file) => /\/render\/navara[^/]*\.js$/.test(file); // Navara renderers outside a layer's render/ directory: like those, they
// reach the app only through the Navara mode's import().
const NAVARA_SCENE_RENDERERS = new Set(['src/scenes/dataPacks/navara.js']);
const navaraLayerRenderer = (file) =>
/\/render\/navara[^/]*\.js$/.test(file) || NAVARA_SCENE_RENDERERS.has(file);
const renderer = (file) => const renderer = (file) =>
/^src\/(?:ui|app|standalone|overlays)\//.test(file) || /^src\/(?:ui|app|standalone|overlays)\//.test(file) ||
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test( /^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
+1
View File
@@ -3706,6 +3706,7 @@
"src/layers/awareness/render/navara.js", "src/layers/awareness/render/navara.js",
"src/layers/infrastructure/render/navara.js", "src/layers/infrastructure/render/navara.js",
"src/layers/shared/render/navaraStems.js", "src/layers/shared/render/navaraStems.js",
"src/layers/shared/render/navaraSceneDepth.js",
"src/layers/submarineCables/render/navara.js", "src/layers/submarineCables/render/navara.js",
"src/layers/directions/render/navara.js", "src/layers/directions/render/navara.js",
"src/layers/traffic/render/navara.js", "src/layers/traffic/render/navara.js",
@@ -0,0 +1,109 @@
// Navara draws its transparent scene after the post effects, into a target
// that does not hold the scene's depth, so a transparent mesh behind a wall,
// terrain or the Earth painted through them. Moving such a mesh into the
// opaque scene (as CCTV did) would also run it through the effects and change
// its look. This test keeps the mesh in the transparent pass and drops every
// fragment that lies behind the depth Navara packs after its opaque pass
// (globe, 3D tiles, MRT and opaque meshes): an unhidden fragment is drawn
// exactly as before. scripts/qa-edge-occlusion.mjs checks it in a browser.
import { RGBADepthPacking, Vector2 } from 'three';
const KEY = 'gevSceneDepth';
// Log-depth units (about 0.01 % of the distance): a stem's foot or a ground
// overlay on the very surface it stands on is not cut by its own ground.
export const SCENE_DEPTH_BIAS = 5e-6;
const PARS = /* glsl */ `
uniform sampler2D tGevSceneDepth;
uniform vec2 uGevTargetSize;
uniform float uGevSceneDepthOn;
`;
const TEST = /* glsl */ `
if (uGevSceneDepthOn > 0.5) {
vec2 gevDepthSize = vec2(textureSize(tGevSceneDepth, 0));
ivec2 gevTexel = ivec2(gl_FragCoord.xy * gevDepthSize / uGevTargetSize);
float gevScene = unpackRGBAToDepth(texelFetch(tGevSceneDepth, gevTexel, 0));
#if defined( USE_LOGARITHMIC_DEPTH_BUFFER )
float gevDepth = vIsPerspective == 0.0
? gl_FragCoord.z
: log2(vFragDepth) * logDepthBufFC * 0.5;
#else
float gevDepth = gl_FragCoord.z;
#endif
if (gevScene < 1.0 && gevDepth > gevScene + ${SCENE_DEPTH_BIAS.toExponential()})
discard;
}
`;
/** Patch a fragment shader source with the scene-depth test. */
export function withSceneDepthTest(fragmentShader) {
const anchor = '#include <logdepthbuf_fragment>';
if (!fragmentShader.includes(anchor))
throw new Error('A scene-depth test needs logdepthbuf_fragment');
const pars = fragmentShader.includes('#include <packing>')
? PARS
: `#include <packing>\n${PARS}`;
return fragmentShader
.replace('void main() {', `${pars}\nvoid main() {`)
.replace(anchor, `${anchor}\n${TEST}`);
}
/**
* One scene-depth test for a set of transparent meshes: `material()` patches
* each material, `object()` binds the depth before each draw, and the mesh
* descriptor calls `attach(ctx)` with its view context once created.
*/
export function createSceneDepthTest() {
const uniforms = {
tGevSceneDepth: { value: null },
uGevTargetSize: { value: new Vector2(1, 1) },
uGevSceneDepthOn: { value: 0 },
};
let ctx = null;
const bind = (renderer) => {
const mrt = ctx?.findEffect?.('mrt');
const texture =
mrt?.depthBufferPacking === RGBADepthPacking ? mrt.depthBuffer : null;
uniforms.tGevSceneDepth.value = texture ?? null;
uniforms.uGevSceneDepthOn.value = texture ? 1 : 0;
const target = renderer.getRenderTarget?.();
if (target) uniforms.uGevTargetSize.value.set(target.width, target.height);
else renderer.getDrawingBufferSize?.(uniforms.uGevTargetSize.value);
};
const test = {
uniforms,
material(material) {
const previousCompile = material.onBeforeCompile;
const previousKey = material.customProgramCacheKey;
material.onBeforeCompile = (shader, renderer) => {
Object.assign(shader.uniforms, uniforms);
shader.fragmentShader = withSceneDepthTest(shader.fragmentShader);
previousCompile?.call(material, shader, renderer);
};
material.customProgramCacheKey = () =>
`${previousKey ? previousKey.call(material) : ''}_gevSceneDepth`;
material.needsUpdate = true;
return material;
},
object(object) {
const previous = object.onBeforeRender;
object.onBeforeRender = function onBeforeRender(renderer, ...rest) {
bind(renderer);
return previous?.call(this, renderer, ...rest);
};
object.userData[KEY] = test;
return object;
},
attach(next) {
ctx = next ?? null;
},
};
return test;
}
/** Give an object's scene-depth test (if any) its descriptor's context. */
export function attachSceneDepth(object, ctx) {
object?.userData?.[KEY]?.attach(ctx);
}
@@ -0,0 +1,157 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
BasicDepthPacking,
DataTexture,
MeshBasicMaterial,
RGBADepthPacking,
ShaderLib,
} from 'three';
import {
SCENE_DEPTH_BIAS,
createSceneDepthTest,
withSceneDepthTest,
} from './navaraSceneDepth.js';
import { createStemBatch } from './navaraStems.js';
import { createNavaraPackRenderer } from '../../../scenes/dataPacks/navara.js';
const count = (text, part) => text.split(part).length - 1;
test('the scene-depth test goes after the log depth and brings packing once', () => {
for (const source of [
ShaderLib.basic.fragmentShader,
'#include <common>\n#include <packing>\nvoid main() {\n#include <logdepthbuf_fragment>\n}',
]) {
const patched = withSceneDepthTest(source);
assert.equal(count(patched, '#include <packing>'), 1);
assert.equal(count(patched, 'uniform sampler2D tGevSceneDepth;'), 1);
const depth = patched.indexOf('#include <logdepthbuf_fragment>');
const test = patched.indexOf('if (uGevSceneDepthOn > 0.5)');
assert.ok(depth >= 0 && test > depth, 'tested after gl_FragDepth is set');
assert.ok(
patched.indexOf('uniform float uGevSceneDepthOn;') <
patched.indexOf('void main() {'),
);
assert.match(patched, /gevDepth > gevScene \+ 5e-6\)\s*discard;/);
}
assert.ok(SCENE_DEPTH_BIAS > 0 && SCENE_DEPTH_BIAS < 1e-4);
assert.throws(() => withSceneDepthTest('void main() {}'), /logdepthbuf/);
});
test('the test binds the MRT pass packed depth before each draw, and only packed RGBA depth', () => {
const depth = createSceneDepthTest();
const material = depth.material(new MeshBasicMaterial());
const shader = {
uniforms: {},
vertexShader: ShaderLib.basic.vertexShader,
fragmentShader: ShaderLib.basic.fragmentShader,
};
material.onBeforeCompile(shader, null);
assert.match(material.customProgramCacheKey(), /_gevSceneDepth$/);
assert.equal(shader.uniforms.tGevSceneDepth, depth.uniforms.tGevSceneDepth);
assert.match(shader.fragmentShader, /discard;/);
const drawn = [];
const object = depth.object({
userData: {},
onBeforeRender: (renderer) => drawn.push(renderer),
});
const texture = new DataTexture();
const mrt = { depthBuffer: texture, depthBufferPacking: RGBADepthPacking };
const renderer = { getRenderTarget: () => ({ width: 640, height: 360 }) };
// Before the descriptor attaches its context the test stays off.
object.onBeforeRender(renderer);
assert.equal(depth.uniforms.uGevSceneDepthOn.value, 0);
depth.attach({ findEffect: (key) => (key === 'mrt' ? mrt : undefined) });
object.onBeforeRender(renderer);
assert.equal(depth.uniforms.tGevSceneDepth.value, texture);
assert.equal(depth.uniforms.uGevSceneDepthOn.value, 1);
assert.deepEqual(depth.uniforms.uGevTargetSize.value.toArray(), [640, 360]);
assert.deepEqual(drawn, [renderer, renderer], 'the mesh hook still runs');
// Another packing would be read wrong: the mesh draws as before.
mrt.depthBufferPacking = BasicDepthPacking;
object.onBeforeRender(renderer);
assert.equal(depth.uniforms.uGevSceneDepthOn.value, 0);
assert.equal(depth.uniforms.tGevSceneDepth.value, null);
});
/** A Navara view that builds each descriptor as the SDK does. */
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 = {
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 };
}
/** Every renderable under an object compiles with the test and binds depth. */
function assertTested(object, mrt) {
const renderables = [];
object.traverse?.((child) => {
if (child.material) renderables.push(child);
});
if (!object.traverse && object.material) renderables.push(object);
assert.ok(renderables.length > 0);
for (const child of renderables) {
const shader = {
uniforms: { ...(child.material.uniforms ?? {}) },
vertexShader: '',
fragmentShader:
child.material.fragmentShader ?? ShaderLib.basic.fragmentShader,
};
child.material.onBeforeCompile(shader, null);
assert.match(shader.fragmentShader, /if \(uGevSceneDepthOn > 0\.5\)/);
assert.ok(shader.uniforms.tGevSceneDepth);
child.onBeforeRender(
{
getRenderTarget: () => ({ width: 8, height: 8 }),
getViewport: (target) => target.set(0, 0, 8, 8),
},
null,
null,
child.geometry,
child.material,
);
assert.equal(shader.uniforms.tGevSceneDepth.value, mrt.depthBuffer);
assert.equal(shader.uniforms.uGevSceneDepthOn.value, 1);
}
}
test('stems and pack meshes stay transparent and test the scene depth', () => {
const stems = fakeView();
createStemBatch(stems, 0xffffff, 4);
const packs = fakeView();
const owned = [];
const engine = { render: { request() {} } };
createNavaraPackRenderer(engine, packs).image(
{ id: 'p', placement: { bounds: [0, 0, 0.01, 0.01], height: 300 } },
{},
{ own: (fn) => owned.push(fn) },
);
for (const { created, mrt } of [stems, packs]) {
assert.equal(created.length, 1);
assert.equal(created[0].pass, 'transparent', created[0].kind);
assertTested(created[0].object, mrt);
}
for (const release of owned) release();
});
+14 -2
View File
@@ -1,11 +1,18 @@
import { LineSegments2 } from 'three/addons/lines/LineSegments2.js'; import { LineSegments2 } from 'three/addons/lines/LineSegments2.js';
import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js'; import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
import { attachSceneDepth, createSceneDepthTest } from './navaraSceneDepth.js';
const KEY = 'gevInfrastructureStems'; const KEY = 'gevInfrastructureStems';
const views = new WeakSet(); const views = new WeakSet();
/** All visible stems use one draw and one fixed buffer; tips are a point bank. */ /** All visible stems use one draw and one fixed buffer; tips are a point bank. */
export function createStemBatch({ view, MeshDesc }, color, capacity, width = 3.5, opacity = 1) { export function createStemBatch(
{ view, MeshDesc },
color,
capacity,
width = 3.5,
opacity = 1,
) {
if (!views.has(view)) { if (!views.has(view)) {
view.registerMesh( view.registerMesh(
KEY, KEY,
@@ -14,10 +21,13 @@ export function createStemBatch({ view, MeshDesc }, color, capacity, width = 3.5
super(host, ctx, config); super(host, ctx, config);
this.batch = config[KEY]; this.batch = config[KEY];
} }
// Transparent, so the effects leave its look alone; the scene-depth
// test hides a stem behind terrain, 3D tiles and the Earth.
getPassKey() { getPassKey() {
return 'transparent'; return 'transparent';
} }
createMesh() { createMesh() {
attachSceneDepth(this.batch.line, this.ctx);
return this.batch.line; return this.batch.line;
} }
onDestroy() { onDestroy() {
@@ -39,7 +49,9 @@ export function createStemBatch({ view, MeshDesc }, color, capacity, width = 3.5
transparent: true, transparent: true,
toneMapped: false, toneMapped: false,
}); });
const line = new LineSegments2(geometry, material); const depthTest = createSceneDepthTest();
depthTest.material(material);
const line = depthTest.object(new LineSegments2(geometry, material));
line.frustumCulled = false; line.frustumCulled = false;
const holder = view.addMesh({ const holder = view.addMesh({
[KEY]: { [KEY]: {
+12 -2
View File
@@ -11,6 +11,10 @@ import {
SRGBColorSpace, SRGBColorSpace,
Texture, Texture,
} from 'three'; } from 'three';
import {
attachSceneDepth,
createSceneDepthTest,
} from '../../layers/shared/render/navaraSceneDepth.js';
const KEY = 'gevScenePack', const KEY = 'gevScenePack',
registered = new WeakSet(); registered = new WeakSet();
const cyan = [0, 1, 1, 1], const cyan = [0, 1, 1, 1],
@@ -24,10 +28,13 @@ export function createNavaraPackRenderer(engine, { view, MeshDesc }) {
super(host, ctx, config); super(host, ctx, config);
this.object = config[KEY]; this.object = config[KEY];
} }
// Transparent, so the effects leave its look alone; the scene-depth
// test hides a pack behind terrain, 3D tiles and the Earth.
getPassKey() { getPassKey() {
return 'transparent'; return 'transparent';
} }
createMesh() { createMesh() {
attachSceneDepth(this.object.group, this.ctx);
return this.object.group; return this.object.group;
} }
onDestroy() { onDestroy() {
@@ -39,7 +46,8 @@ export function createNavaraPackRenderer(engine, { view, MeshDesc }) {
registered.add(view); registered.add(view);
} }
const meshGroup = (handle) => { const meshGroup = (handle) => {
const group = new Group(), const depthTest = createSceneDepthTest();
const group = depthTest.object(new Group()),
owned = []; owned = [];
let released = false; let released = false;
const dispose = () => { const dispose = () => {
@@ -62,7 +70,7 @@ export function createNavaraPackRenderer(engine, { view, MeshDesc }) {
dispose(); dispose();
} }
}); });
return { group, owned }; return { group, owned, depthTest };
}; };
function addMesh(batch, shape, properties, id, texture) { function addMesh(batch, shape, properties, id, texture) {
if (!shape) return; if (!shape) return;
@@ -99,6 +107,8 @@ export function createNavaraPackRenderer(engine, { view, MeshDesc }) {
.copy(pigment) .copy(pigment)
.multiplyScalar(1 / Math.max(1, view.toneMappingExposure ?? 1)); .multiplyScalar(1 / Math.max(1, view.toneMappingExposure ?? 1));
}; };
batch.depthTest.material(material);
batch.depthTest.object(mesh);
mesh.position.set(origin.x, origin.y, origin.z); mesh.position.set(origin.x, origin.y, origin.z);
mesh.userData.pickId = id; mesh.userData.pickId = id;
batch.group.add(mesh); batch.group.add(mesh);