Drape a 'tiles' ground set only on 3D tiles on Navara

Navara's ground pass took a pixel for a tile whenever its depth lay in
front of the saved globe depth, without checking which buffer the visible
depth came from; the scene buffer can be cleared apart from the globe
depth, so a set draped on 'tiles' also drew on bare terrain, which Cesium
never does. The pass now matches the visible depth to the globe or the
tiles before it drapes. A contract check compares a 'tiles' set hidden,
shown and hidden again over terrain, with a mutant.
This commit is contained in:
German Khokhlov committed 2026-11-10 19:12:25 +04:00
1 parent facc9541ab
commit b3daed4a9b
5 files changed
+207 -42

No files matched your search

+14 -9
View File
@@ -25,7 +25,11 @@ import {
Uniform,
} from 'three';
import { DASH_EDGE, groundSeconds } from '../../ground.js';
import { groundSurfaceUniforms, updateGroundSurface } from './surface.js';
import {
GROUND_SURFACE_GLSL,
groundSurfaceUniforms,
updateGroundSurface,
} from './surface.js';
import { withFrameUpdates, withPassDisposal } from '../frameDescriptor.js';
import { REVEAL_OFFSET, ITEM_TEXELS, LOOK, MARGIN_RATIO } from './corridors.js';
import {
@@ -64,6 +68,7 @@ vec4 atSlot(highp sampler2D image, int slot) {
}
${DISPLAY_COLOR_GLSL}
${VIEW_POSITION_GLSL}
${GROUND_SURFACE_GLSL}
// A corridor item over the colour, nearest metres from its line and along
// metres from its start: its width, glow, pulse and dash, in LDR and
// premultiplied, as Cesium writes ground polylines.
@@ -119,7 +124,9 @@ vec3 blendCorridor(vec3 color, int item, float nearest, vec2 normal, float along
}
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
outputColor = inputColor;
float depth = unpackRGBAToDepth(texture2D(tAllDepth, uv));
// All three depths must name one pixel, including at tile boundaries.
ivec2 depthPixel = ivec2(uv * vec2(textureSize(tAllDepth, 0)));
float depth = unpackRGBAToDepth(texelFetch(tAllDepth, depthPixel, 0));
float distance = viewDistance(depth, uLogDepthFC);
vec4 ray = uInverseProjection * vec4(uv * 2.0 - 1.0, -1.0, 1.0);
vec3 point = viewPosition(ray, distance);
@@ -135,11 +142,10 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor)
ivec2 tile = clamp(ivec2(floor(uv * vec2(${GRID_COLS}.0, ${GRID_ROWS}.0))), ivec2(0), ivec2(${GRID_COLS - 1}, ${GRID_ROWS - 1}));
vec2 range = texelFetch(tGrid, tile, 0).xy;
if (range.y < 0.5) return;
float surface = texture2D(tSurfaceDepth, uv).r;
// The packed depth also holds forward meshes, which are not ground.
if (depth < surface - 2e-7) return;
float globeDepth = unpackRGBAToDepth(texture2D(tGlobeDepth, uv));
bool onTiles = surface < globeDepth - 2e-7;
float surface = texelFetch(tSurfaceDepth, depthPixel, 0).r;
float globeDepth = unpackRGBAToDepth(texelFetch(tGlobeDepth, depthPixel, 0));
float surfaceType = groundSurfaceType(depth, surface, globeDepth);
if (surfaceType == 0.0) return;
vec3 color = toDisplay(inputColor.rgb);
// The pixel's ground in world space, once: every corridor tile tests the
// same value, so exactly one tile of a set holds it.
@@ -154,7 +160,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor)
vec4 east = atSlot(tFrames, id * ${TEXELS} + 1);
vec4 north = atSlot(tFrames, id * ${TEXELS} + 2);
vec4 surfaceFrame = atSlot(tFrames, id * ${TEXELS} + 3);
if ((surfaceFrame.w == 1.0 && onTiles) || (surfaceFrame.w == 2.0 && !onTiles)) continue;
if (surfaceFrame.w != 0.0 && surfaceFrame.w != surfaceType) continue;
vec3 up = surfaceFrame.xyz;
vec4 span = atSlot(tStyles, id * ${STYLE_TEXELS} + 3);
vec3 delta = point - origin.xyz;
@@ -453,7 +459,6 @@ class GroundEffectDesc extends withPassDisposal(withFrameUpdates(EffectDesc)) {
textures.candidates.needsUpdate = true;
}
const u = this.effect.uniforms;
u.get('tGlobeDepth').value = mrt.globeDepthBuffer;
u.get('uViewToWorld').value.copy(camera.matrixWorld);
u.get('uTime').value = groundSeconds();
u.get('uPixelRatio').value = pixelRatio;
+18
View File
@@ -1,5 +1,18 @@
// Public depth inputs shared by actual terrain/tiles classifiers.
import { Matrix4, Uniform, RGBADepthPacking } from 'three';
export const GROUND_SURFACE_GLSL = /* glsl */ `
float groundSurfaceType(float depth, float surface, float globeDepth) {
if (depth >= 1.0) return 0.0;
// The scene target can be cleared after the globe is saved. Classify
// the visible depth, not a difference between the two source buffers.
if (globeDepth < 1.0 && abs(depth - globeDepth) <= 2e-7) return 1.0;
if (surface < 1.0 && abs(depth - surface) <= 2e-7) return 2.0;
// A forward mesh has depth in neither ground buffer.
return 0.0;
}
`;
export function groundSurfaceUniforms({ surface = true } = {}) {
return [
['tAllDepth', new Uniform(null)],
@@ -15,6 +28,11 @@ export function updateGroundSurface(effect, camera, context, mrt) {
u.get('tAllDepth').value = mrt.depthBuffer;
if (u.has('tSurfaceDepth'))
u.get('tSurfaceDepth').value = context.getRenderTarget().depthTexture;
if (u.has('tGlobeDepth')) {
if (mrt.globeDepthBufferPacking !== RGBADepthPacking)
throw new Error('Ground classification requires packed globe depth');
u.get('tGlobeDepth').value = mrt.globeDepthBuffer;
}
u.get('uInverseProjection').value.copy(camera.projectionMatrixInverse);
u.get('uLogDepthFC').value = 2 / Math.log2(camera.far + 1);
}
+77
View File
@@ -0,0 +1,77 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { BasicDepthPacking, Matrix4, RGBADepthPacking, Uniform } from 'three';
import {
GROUND_SURFACE_GLSL,
groundSurfaceUniforms,
updateGroundSurface,
} from './surface.js';
// Execute the scalar shader decision with the same inputs as a depth texel.
const classify = new Function(
'abs',
`${GROUND_SURFACE_GLSL.replace('float groundSurfaceType', 'function groundSurfaceType').replaceAll(/float (\w+)/g, '$1')}
return groundSurfaceType;`,
)(Math.abs);
for (const [name, depth, surface, globe, expected] of [
['only globe depth after the scene depth is cleared', 0.6, 1, 0.6, 1],
['only tile depth', 0.4, 0.4, 1, 2],
['tiles in front of the globe', 0.4, 0.4, 0.6, 2],
['globe in front of tiles', 0.4, 0.6, 0.4, 1],
['globe retained in the scene depth', 0.6, 0.6, 0.6, 1],
['visible globe with a different scene depth', 0.6, 0.4, 0.6, 1],
[
'globe with scene depth differing by one depth step',
0.6,
0.6 - 6e-8,
0.6,
1,
],
['forward mesh in front of the globe', 0.2, 0.6, 0.6, 0],
['forward mesh in front of tiles', 0.2, 0.4, 0.6, 0],
['no ground depth', 1, 1, 1, 0],
['forward mesh without ground', 0.2, 1, 1, 0],
]) {
test(`ground classification: ${name}`, () => {
assert.equal(classify(depth, surface, globe), expected);
});
}
test('ground depth bindings refresh together and require the globe packing they decode', () => {
const effect = {
uniforms: new Map([
...groundSurfaceUniforms(),
['tGlobeDepth', new Uniform(null)],
]),
};
const camera = { projectionMatrixInverse: new Matrix4(), far: 1000 };
const target = { depthTexture: {} };
const context = { getRenderTarget: () => target };
const mrt = {
depthBuffer: {},
depthBufferPacking: RGBADepthPacking,
globeDepthBuffer: {},
globeDepthBufferPacking: RGBADepthPacking,
};
for (let frame = 0; frame < 2; frame++) {
mrt.depthBuffer = {};
mrt.globeDepthBuffer = {};
target.depthTexture = {};
updateGroundSurface(effect, camera, context, mrt);
assert.equal(effect.uniforms.get('tAllDepth').value, mrt.depthBuffer);
assert.equal(
effect.uniforms.get('tGlobeDepth').value,
mrt.globeDepthBuffer,
);
assert.equal(
effect.uniforms.get('tSurfaceDepth').value,
target.depthTexture,
);
}
mrt.globeDepthBufferPacking = BasicDepthPacking;
assert.throws(
() => updateGroundSurface(effect, camera, context, mrt),
/packed globe depth/,
);
});
+62 -32
View File
@@ -1097,44 +1097,74 @@ export function groundBehaviourChecks(
},
],
];
if (fixtures?.tiles)
checks.push([
'ground.createSet: tiles drape ignores terrain and picks only tiles',
async () => {
await nadirView(10000);
const set = engine.ground.createSet({
id: 'contract-tiles-drape',
kind: 'cell',
drape: 'tiles',
pickable: true,
});
try {
set.replace([
{
west: 19.98,
south: 9.98,
east: 20.02,
north: 10.02,
fill: [1, 0, 1, 1],
pickId: 'tiles-drape',
},
]);
set.commit();
await engine.maps.setPhotorealTiles(null);
ensure(
engine.picker.pick(centre())?.id !== 'tiles-drape',
'tiles drape picked on terrain',
checks.push([
'ground.createSet: tiles drape ignores terrain and picks only tiles',
async () => {
await engine.maps.setPhotorealTiles(null);
await nadirView(10000);
const set = engine.ground.createSet({
id: 'contract-tiles-drape',
kind: 'cell',
drape: 'tiles',
pickable: true,
});
try {
set.replace([
{
west: 19.98,
south: 9.98,
east: 20.02,
north: 10.02,
fill: [1, 0, 1, 1],
pickId: 'tiles-drape',
},
]);
set.commit();
await until(() => set.ready(), 'tiles drape ready');
ensure(
engine.picker.pick(centre())?.id !== 'tiles-drape',
'tiles drape picked on terrain',
);
if (readRegion) {
set.show(false);
await until(
() => engine.surface.tilesLoaded?.() !== false,
'terrain settles',
);
await settle(5);
const sample = () => meanAt(engine, readRegion, 20, 10, 2);
const baseline = sample();
ensure(
!nearColour(baseline, MAGENTA, 30),
'terrain is already magenta',
);
set.show(true);
await settle(5);
const shown = sample();
set.show(false);
await settle(3);
ensure(
nearColour(sample(), baseline, 10),
'terrain baseline changed',
);
ensure(
nearColour(shown, baseline, 10),
'tiles drape changed terrain pixels',
);
set.show(true);
}
if (fixtures?.tiles) {
await engine.maps.setPhotorealTiles(mapSources.photoreal);
ensure(
engine.picker.pick(centre())?.id === 'tiles-drape',
'tiles drape missed tiles',
);
} finally {
set.destroy();
await engine.maps.setPhotorealTiles(null);
}
},
]);
} finally {
set.destroy();
await engine.maps.setPhotorealTiles(null);
}
},
]);
return checks;
}