mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'german/refactor-navara-tiles-drape-2' into german/navara-refactor
This commit is contained in:
commit
4bbf9949d5
2 files changed
+382
-7
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import * as geo from '../src/geo/index.js';
|
||||
import { GROUND_SURFACE_GLSL } from '../src/globe/navara/ground/surface.js';
|
||||
import {
|
||||
assertKeylessPreview,
|
||||
createStrict,
|
||||
@@ -237,13 +238,22 @@ await strict.run({}, async (browser) => {
|
||||
// Map imagery and classification shaders may still be arriving before ready.
|
||||
await page.evaluate(() => window.qaDrape.show(false));
|
||||
await loaded(page);
|
||||
const baseline = await pixels(page, position);
|
||||
const before = await pixels(page, position);
|
||||
await page.evaluate(() => window.qaDrape.show(true));
|
||||
const rgb = await pixels(page, position);
|
||||
const shown = await pixels(page, position);
|
||||
await page.evaluate(() => window.qaDrape.show(false));
|
||||
const restored = await pixels(page, position);
|
||||
const after = await pixels(page, position);
|
||||
const baseline = before.rgb;
|
||||
const rgb = shown.rgb;
|
||||
const restored = after.rgb;
|
||||
const drawn = rgb[0] > 180 && rgb[1] < 80 && rgb[2] > 180;
|
||||
const sample = { surface, drape, baseline, rgb, restored, drawn };
|
||||
if (report && kind === 'navara')
|
||||
sample.depths = {
|
||||
baseline: before.depths,
|
||||
shown: shown.depths,
|
||||
restored: after.depths,
|
||||
};
|
||||
drapes.push(sample);
|
||||
console.log(
|
||||
`${report ? 'REPORT' : 'DRAPE'} ${kind} ${JSON.stringify(sample)}`,
|
||||
@@ -320,13 +330,24 @@ async function loaded(page) {
|
||||
|
||||
// Read actual canvas pixels during postRender; Cesium discards them afterwards.
|
||||
async function pixels(page, position) {
|
||||
return page.evaluate(async (position) => {
|
||||
const input = {
|
||||
position,
|
||||
diagnostic: report ? readNavaraDrape.toString() : null,
|
||||
classification: GROUND_SURFACE_GLSL,
|
||||
};
|
||||
const sample = await page.evaluate(async (input) => {
|
||||
const { position, diagnostic, classification } = input;
|
||||
const { engine } = window.__godsEyeView;
|
||||
const readDepths =
|
||||
diagnostic && engine.kind === 'navara'
|
||||
? new Function(`return (${diagnostic})`)()
|
||||
: null;
|
||||
let depths;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvas.height = 5;
|
||||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
for (let frame = 0; frame < 2; frame++)
|
||||
await new Promise((resolve) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const off = engine.frames.postRender.addEventListener(() => {
|
||||
off();
|
||||
const point = engine.projector.worldToWindow(position);
|
||||
@@ -343,19 +364,147 @@ async function pixels(page, position) {
|
||||
5,
|
||||
5,
|
||||
);
|
||||
try {
|
||||
if (readDepths)
|
||||
depths = readDepths(engine, point, classification);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
engine.render.request('qa-drape-pixels');
|
||||
});
|
||||
const data = context.getImageData(0, 0, 5, 5).data;
|
||||
return [0, 1, 2].map(
|
||||
const rgb = [0, 1, 2].map(
|
||||
(channel) =>
|
||||
data.reduce(
|
||||
(sum, value, i) => sum + (i % 4 === channel ? value : 0),
|
||||
0,
|
||||
) / 25,
|
||||
);
|
||||
}, position);
|
||||
return { rgb, depths };
|
||||
}, input);
|
||||
if (sample.depths) {
|
||||
const place =
|
||||
sample.depths.position &&
|
||||
geo.cartographic.fromCartesian(sample.depths.position);
|
||||
const detail =
|
||||
place &&
|
||||
(await page.evaluate(
|
||||
(place) =>
|
||||
window.__godsEyeView.engine.surface.sampleHeightDetailed(place),
|
||||
place,
|
||||
));
|
||||
sample.depths.source = detail?.source ?? null;
|
||||
sample.depths.height = detail?.height ?? null;
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
|
||||
// Runs in the page, after rendering. Read the centre of the same 5x5 colour
|
||||
// sample, using the ground effect's actual uniforms and shader when shown.
|
||||
function readNavaraDrape(engine, point, classification) {
|
||||
const view = engine.extensions.navara.view;
|
||||
const ctx = view.viewContext;
|
||||
const renderer = ctx.getRenderer();
|
||||
const mrt = ctx.findEffect('mrt');
|
||||
const effect = ctx.findEffect('gevGround')?.effect;
|
||||
const names = ['tAllDepth', 'tSurfaceDepth', 'tGlobeDepth'];
|
||||
const textures = effect
|
||||
? names.map((name) => effect.uniforms.get(name).value)
|
||||
: [
|
||||
mrt.depthBuffer,
|
||||
ctx.getRenderTarget().depthTexture,
|
||||
mrt.globeDepthBuffer,
|
||||
];
|
||||
if (effect)
|
||||
classification = effect
|
||||
.getFragmentShader()
|
||||
.match(/float groundSurfaceType\([\s\S]*?\n}/)?.[0];
|
||||
if (!classification) throw new Error('Missing ground classification shader');
|
||||
const scale = engine.host.canvas.width / engine.host.size().width;
|
||||
const canvasPixel = [
|
||||
Math.round(point.x * scale),
|
||||
Math.round(point.y * scale),
|
||||
];
|
||||
const size = textures[0].image;
|
||||
const pixel = [
|
||||
Math.floor(
|
||||
((canvasPixel[0] + 0.5) * size.width) / engine.host.canvas.width,
|
||||
),
|
||||
Math.floor(
|
||||
((engine.host.canvas.height - canvasPixel[1] - 0.5) * size.height) /
|
||||
engine.host.canvas.height,
|
||||
),
|
||||
];
|
||||
// Use the installed SDK's depth-copy pass and Three packing. Four RGBA8
|
||||
// pixels preserve the three depths and the shader's classification result.
|
||||
const copy = new mrt.raw.globeDepthCopyPass.constructor();
|
||||
const previous = renderer.getRenderTarget();
|
||||
try {
|
||||
copy.setSize(4, 1);
|
||||
const material = copy.fullscreenMaterial;
|
||||
material.uniforms = Object.fromEntries(
|
||||
names.map((name, i) => [name, { value: textures[i] }]),
|
||||
);
|
||||
material.uniforms.pixel = { value: pixel };
|
||||
material.fragmentShader = `
|
||||
#include <packing>
|
||||
uniform sampler2D tAllDepth;
|
||||
uniform sampler2D tSurfaceDepth;
|
||||
uniform sampler2D tGlobeDepth;
|
||||
uniform ivec2 pixel;
|
||||
${classification}
|
||||
void main() {
|
||||
float depth = unpackRGBAToDepth(texelFetch(tAllDepth, pixel, 0));
|
||||
float surface = texelFetch(tSurfaceDepth, pixel, 0).r;
|
||||
float globe = unpackRGBAToDepth(texelFetch(tGlobeDepth, pixel, 0));
|
||||
int column = int(gl_FragCoord.x);
|
||||
gl_FragColor = column == 3
|
||||
? vec4(groundSurfaceType(depth, surface, globe) / 255.0, 0.0, 0.0, 1.0)
|
||||
: packDepthToRGBA(column == 0 ? depth : column == 1 ? surface : globe);
|
||||
}
|
||||
`;
|
||||
copy.render(renderer);
|
||||
const bytes = new Uint8Array(16);
|
||||
renderer.readRenderTargetPixels(copy.renderTarget, 0, 0, 4, 1, bytes);
|
||||
const gl = renderer.getContext();
|
||||
const error = gl.getError();
|
||||
if (error !== gl.NO_ERROR)
|
||||
throw new Error(`Depth diagnostic WebGL error: ${error}`);
|
||||
const unpack = (offset) =>
|
||||
bytes[offset] / 256 +
|
||||
bytes[offset + 1] / 65536 +
|
||||
bytes[offset + 2] / 16777216 +
|
||||
bytes[offset + 3] / (255 * 16777216);
|
||||
const screen = {
|
||||
x: (canvasPixel[0] + 0.5) / scale,
|
||||
y: (canvasPixel[1] + 0.5) / scale,
|
||||
};
|
||||
const ray = engine.camera.pickRay(screen);
|
||||
const hit = engine.surface.raycast({ ...ray, far: view.camera.raw.far });
|
||||
return {
|
||||
pixel,
|
||||
bindings: effect ? 'ground' : 'mrt',
|
||||
allDepth: unpack(0),
|
||||
sceneDepth: unpack(4),
|
||||
globeDepth: unpack(8),
|
||||
classification: ['none', 'terrain', 'tiles'][bytes[12]],
|
||||
buffer: ['none', 'globe', 'scene'][bytes[12]],
|
||||
position: hit?.position ?? engine.camera.pickEllipsoid(screen),
|
||||
shownTileAtRay: Boolean(hit),
|
||||
photorealActive: engine.maps.photorealActive(),
|
||||
textures: textures.map((texture) => ({
|
||||
id: texture.id,
|
||||
width: texture.image.width,
|
||||
height: texture.image.height,
|
||||
})),
|
||||
};
|
||||
} finally {
|
||||
renderer.setRenderTarget(previous);
|
||||
copy.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPoints(page, kind) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Cesium3DTileset } from 'cesium';
|
||||
import { Core, initSync } from '@navaramap/engine';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { Group, Raycaster, Vector3 } from 'three';
|
||||
import { DepthCopyPass } from 'postprocessing';
|
||||
import * as geo from '../src/geo/index.js';
|
||||
import { buildTilesFixture } from './build-tiles3d-fixture.mjs';
|
||||
import { localTilesetPath, qaTilesetUrl } from '../src/maps/qaTileset.js';
|
||||
@@ -13,6 +14,7 @@ import { MapSourceController } from '../src/maps/controller.js';
|
||||
import { createFakeEngine } from '../src/globe/testing/fakeEngine.js';
|
||||
import { createNavaraMaps } from '../src/globe/navara/maps.js';
|
||||
import { createNavaraSurface } from '../src/globe/navara/surface.js';
|
||||
import { GROUND_SURFACE_GLSL } from '../src/globe/navara/ground/surface.js';
|
||||
import {
|
||||
assertKeylessPreview,
|
||||
TILES_FIXTURE_PATH,
|
||||
@@ -374,3 +376,227 @@ test('harness exposes exactly the five binary-safe fixture routes on the preview
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Load only the page callbacks, without launching the command-line scenario.
|
||||
const scenario = readFileSync(
|
||||
new URL('./qa-tiles-fixture.mjs', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const diagnosticSource = scenario.slice(
|
||||
scenario.indexOf('function readNavaraDrape('),
|
||||
scenario.indexOf('async function checkPoints('),
|
||||
);
|
||||
const readNavaraDrape = new Function(`return (${diagnosticSource})`)();
|
||||
const pixelsSource = scenario.slice(
|
||||
scenario.indexOf('async function pixels('),
|
||||
scenario.indexOf('// Runs in the page, after rendering.'),
|
||||
);
|
||||
const reportPixels = new Function(
|
||||
'geo',
|
||||
'report',
|
||||
'readNavaraDrape',
|
||||
'GROUND_SURFACE_GLSL',
|
||||
`return (${pixelsSource})`,
|
||||
)(geo, true, readNavaraDrape, GROUND_SURFACE_GLSL);
|
||||
|
||||
function depthDiagnostic(t) {
|
||||
const copies = [];
|
||||
class Copy extends DepthCopyPass {
|
||||
constructor() {
|
||||
super();
|
||||
this.disposed = false;
|
||||
this.renderTarget.addEventListener('dispose', () => {
|
||||
this.disposed = true;
|
||||
});
|
||||
copies.push(this);
|
||||
}
|
||||
}
|
||||
const bytes = new Uint8Array(16);
|
||||
// Exact RGBA depths: 0.25 composed, 0.25 scene, 0.5 globe; tile classification.
|
||||
bytes.set([64, 0, 0, 0, 64, 0, 0, 0, 128, 0, 0, 0, 2, 0, 0, 255]);
|
||||
let error = 0;
|
||||
const gl = { NO_ERROR: 0, getError: () => error };
|
||||
const textures = Array.from({ length: 3 }, (_, id) => ({
|
||||
id,
|
||||
image: { width: 1280, height: 800 },
|
||||
}));
|
||||
const previous = {};
|
||||
const renderer = {
|
||||
getContext: () => gl,
|
||||
getRenderTarget: () => previous,
|
||||
render: t.mock.fn(),
|
||||
readRenderTargetPixels: (_, x, y, width, height, buffer) =>
|
||||
buffer.set(bytes),
|
||||
setRenderTarget: t.mock.fn(),
|
||||
};
|
||||
let effect = {
|
||||
uniforms: new Map(
|
||||
['tAllDepth', 'tSurfaceDepth', 'tGlobeDepth'].map((name, i) => [
|
||||
name,
|
||||
{ value: textures[i] },
|
||||
]),
|
||||
),
|
||||
getFragmentShader: () => GROUND_SURFACE_GLSL,
|
||||
};
|
||||
const ctx = {
|
||||
getRenderer: () => renderer,
|
||||
findEffect: (key) =>
|
||||
key === 'gevGround'
|
||||
? { effect }
|
||||
: {
|
||||
depthBuffer: textures[0],
|
||||
globeDepthBuffer: textures[2],
|
||||
raw: { globeDepthCopyPass: { constructor: Copy } },
|
||||
},
|
||||
getRenderTarget: () => ({ depthTexture: textures[1] }),
|
||||
};
|
||||
let tiles = true;
|
||||
const position = geo.cartesian3.fromDegrees(-97.7, 30.2, 50);
|
||||
let listener;
|
||||
const engine = {
|
||||
kind: 'navara',
|
||||
extensions: {
|
||||
navara: { view: { viewContext: ctx, camera: { raw: { far: 1e8 } } } },
|
||||
},
|
||||
host: {
|
||||
canvas: { width: 2560, height: 1600 },
|
||||
size: () => ({ width: 1280, height: 800 }),
|
||||
},
|
||||
projector: { worldToWindow: () => ({ x: 120, y: 70 }) },
|
||||
camera: {
|
||||
pickRay: t.mock.fn(() => ({
|
||||
origin: { x: 1, y: 2, z: 3 },
|
||||
direction: { x: 0, y: 0, z: -1 },
|
||||
})),
|
||||
pickEllipsoid: () => position,
|
||||
},
|
||||
surface: {
|
||||
raycast: t.mock.fn(() => (tiles ? { position } : undefined)),
|
||||
sampleHeightDetailed: t.mock.fn(() => ({
|
||||
height: tiles ? 50 : 0,
|
||||
source: tiles ? 'tiles' : 'terrain',
|
||||
})),
|
||||
},
|
||||
maps: { photorealActive: () => tiles },
|
||||
frames: {
|
||||
postRender: {
|
||||
addEventListener: (fn) => {
|
||||
listener = fn;
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
},
|
||||
render: { request: () => queueMicrotask(() => listener()) },
|
||||
};
|
||||
const color = Uint8ClampedArray.from(
|
||||
{ length: 100 },
|
||||
(_, i) => [231, 226, 223, 255][i % 4],
|
||||
);
|
||||
globals(t, {
|
||||
window: { __godsEyeView: { engine } },
|
||||
document: {
|
||||
createElement: () => ({
|
||||
getContext: () => ({
|
||||
clearRect() {},
|
||||
drawImage() {},
|
||||
getImageData: () => ({ data: color }),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
return {
|
||||
engine,
|
||||
copies,
|
||||
renderer,
|
||||
previous,
|
||||
position,
|
||||
textures,
|
||||
page: { evaluate: async (fn, ...args) => fn(...args) },
|
||||
hideGround() {
|
||||
effect = null;
|
||||
},
|
||||
removeTiles() {
|
||||
tiles = false;
|
||||
bytes.set([128, 0, 0, 0, 255, 255, 255, 255, 128, 0, 0, 0, 1, 0, 0, 255]);
|
||||
},
|
||||
fail() {
|
||||
error = 'INVALID_OPERATION';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('drape report distinguishes depths and shown tile rays when colour is identical', async (t) => {
|
||||
const h = depthDiagnostic(t);
|
||||
const shown = await reportPixels(h.page, h.position);
|
||||
assert.deepEqual(shown.rgb, [231, 226, 223]);
|
||||
assert.equal(shown.depths.allDepth, 0.25);
|
||||
assert.equal(shown.depths.sceneDepth, 0.25);
|
||||
assert.equal(shown.depths.globeDepth, 0.5);
|
||||
assert.equal(shown.depths.buffer, 'scene');
|
||||
assert.equal(shown.depths.classification, 'tiles');
|
||||
assert.equal(shown.depths.source, 'tiles');
|
||||
assert.equal(shown.depths.shownTileAtRay, true);
|
||||
assert.equal(shown.depths.bindings, 'ground');
|
||||
assert.deepEqual(shown.depths.pixel, [120, 729]);
|
||||
assert.deepEqual(h.engine.camera.pickRay.mock.calls[0].arguments[0], {
|
||||
x: 120.25,
|
||||
y: 70.25,
|
||||
});
|
||||
assert.equal(h.engine.surface.raycast.mock.calls[0].arguments[0].far, 1e8);
|
||||
const place =
|
||||
h.engine.surface.sampleHeightDetailed.mock.calls[0].arguments[0];
|
||||
assert.ok(Math.abs(place.longitude - geo.math.toRadians(-97.7)) < 1e-10);
|
||||
assert.ok(Math.abs(place.latitude - geo.math.toRadians(30.2)) < 1e-10);
|
||||
h.hideGround();
|
||||
h.removeTiles();
|
||||
const hidden = await reportPixels(h.page, h.position);
|
||||
assert.deepEqual(hidden.rgb, shown.rgb);
|
||||
assert.equal(hidden.depths.allDepth, 0.5);
|
||||
assert.equal(hidden.depths.sceneDepth, 1);
|
||||
assert.equal(hidden.depths.globeDepth, 0.5);
|
||||
assert.equal(hidden.depths.buffer, 'globe');
|
||||
assert.equal(hidden.depths.classification, 'terrain');
|
||||
assert.equal(hidden.depths.source, 'terrain');
|
||||
assert.equal(hidden.depths.shownTileAtRay, false);
|
||||
assert.equal(hidden.depths.photorealActive, false);
|
||||
assert.equal(hidden.depths.bindings, 'mrt');
|
||||
assert.equal(h.copies.length, 4);
|
||||
assert.ok(h.copies.every((copy) => copy.disposed));
|
||||
assert.equal(
|
||||
h.copies[0].fullscreenMaterial.uniforms.tGlobeDepth.value,
|
||||
h.textures[2],
|
||||
);
|
||||
assert.equal(
|
||||
h.renderer.setRenderTarget.mock.calls.at(-1).arguments[0],
|
||||
h.previous,
|
||||
);
|
||||
});
|
||||
|
||||
test('drape report rejects a failed GPU read and releases temporary resources', async (t) => {
|
||||
const h = depthDiagnostic(t);
|
||||
h.fail();
|
||||
await assert.rejects(
|
||||
reportPixels(h.page, h.position),
|
||||
/WebGL error: INVALID_OPERATION/,
|
||||
);
|
||||
assert.equal(h.copies.length, 1);
|
||||
assert.ok(h.copies[0].disposed);
|
||||
assert.equal(
|
||||
h.renderer.setRenderTarget.mock.calls.at(-1).arguments[0],
|
||||
h.previous,
|
||||
);
|
||||
});
|
||||
|
||||
test('drape report never reads native Navara state on Cesium', async (t) => {
|
||||
const h = depthDiagnostic(t);
|
||||
h.engine.kind = 'cesium';
|
||||
Object.defineProperty(h.engine, 'extensions', {
|
||||
get() {
|
||||
throw new Error('Native read');
|
||||
},
|
||||
});
|
||||
const sample = await reportPixels(h.page, h.position);
|
||||
assert.deepEqual(sample.rgb, [231, 226, 223]);
|
||||
assert.equal(sample.depths, undefined);
|
||||
assert.equal(h.copies.length, 0);
|
||||
});
|
||||
Reference in new issue
Block a user