mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
feat(street-level): refine ground casting with the Google 3D surface
The raycast challenge on #768 showed bare-earth casting misplacing roads that are not at grade: lines floated over freeways in trenches and steep streets (rendered line pixels floating: Austin 8.5%, SF 27.1%, LA 43.8%), because a ~111 m terrain grid cannot see a 30 m-wide cut or a slope between its corners. meshSampler.js samples the rendered Google 3D surface (scene.sampleHeight, overlays excluded) once per ~11 m cell within 900 m of the camera, nearest first, in idle time within a 6 ms budget; misses are retried, not latched. groundCast.refineHeights blends it in, per point along a line: - mesh up to 3 m above bare earth is the road: follow it, 1 m above; - higher mesh (canopy, deck, roof): carry the road's offset from bare earth across from the road samples on both sides (up to 8 points); - otherwise bare earth + 2 m, as before. Lines, cones and the marker use it; tiles touched by new samples are redrawn one per idle slice with the no-blink swap. Rendered line pixels on the surface / floating, before -> after: Austin 90.0 -> 95.3% / 8.5 -> 0.7%; SF 69.0 -> 86.4% / 27.1 -> 9.1%; LA 52.3 -> 91.0% / 43.8 -> 4.0%; Innsbruck 95.0 -> 95.7% / 0.2 -> 0.2%; Sacramento 80.7 -> 79.3% / 3.8 -> 2.7%; buried lines stay under 1%. Cones floating in SF 44 -> 5%, buried at LA 59 -> 28%. Main-thread long tasks added during refinement: at most 134 ms (Austin, densest tile). Roads on elevated decks are still drawn at ground level under the deck. Unit tests cover the blend (trench, slope, canopy bridge, deck, bad probe), the finer cast with meshAt, and the sampler (once per cell, nearest first, overlays excluded, far cells skipped, misses not latched, idle when off). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
abbf69a512
commit
514788303d
13 files changed
+592
-30
No files matched your search
@@ -25,6 +25,66 @@ export const GROUND_CAST_MAX_CORNERS = 1024;
|
||||
/** Cached grid corners before the cache is dropped and refilled. */
|
||||
const GROUND_CAST_CACHE_MAX = 50_000;
|
||||
|
||||
/**
|
||||
* Mesh refinement. The bare-earth grid cannot see a freeway in a trench, a
|
||||
* steep street between grid corners, or the road under a tree, so where the
|
||||
* rendered Google 3D surface has been sampled it refines the height:
|
||||
* - mesh at most MESH_ROAD_ABOVE_M above bare earth (and not absurdly far
|
||||
* below it) is the road itself: follow it, MESH_LIFT_M above;
|
||||
* - mesh higher than that is something over the road (canopy, deck, roof):
|
||||
* carry the road's offset from bare earth across from the nearest road
|
||||
* samples on both sides, within MESH_BRIDGE_MAX_POINTS;
|
||||
* - otherwise bare earth plus GROUND_CAST_LIFT_M, as without the mesh.
|
||||
*/
|
||||
export const MESH_ROAD_ABOVE_M = 3;
|
||||
/** A sample this far below bare earth is a bad probe, not a road. */
|
||||
export const MESH_ROAD_BELOW_M = 30;
|
||||
/** Metres above the sampled road surface. */
|
||||
export const MESH_LIFT_M = 1;
|
||||
/** Longest run of covered points bridged from the road on either side. */
|
||||
export const MESH_BRIDGE_MAX_POINTS = 8;
|
||||
/** Line spacing, in degrees (~22 m), while the mesh refines heights. */
|
||||
export const MESH_DENSIFY_DEG = 0.0002;
|
||||
|
||||
/**
|
||||
* Heights for a line's points from bare earth and, where sampled, the mesh.
|
||||
* Pure, so the rule above is testable without a scene.
|
||||
* @param {Array<{dem: number, mesh?: number|null}>} points
|
||||
* @param {{lift?: number}} [options]
|
||||
* @returns {Array<number>}
|
||||
*/
|
||||
export function refineHeights(points, { lift = GROUND_CAST_LIFT_M } = {}) {
|
||||
const road = points.map(({ dem, mesh }) =>
|
||||
Number.isFinite(mesh) &&
|
||||
mesh <= dem + MESH_ROAD_ABOVE_M &&
|
||||
mesh >= dem - MESH_ROAD_BELOW_M
|
||||
? mesh - dem
|
||||
: null,
|
||||
);
|
||||
return points.map(({ dem, mesh }, i) => {
|
||||
if (road[i] !== null) return dem + road[i] + MESH_LIFT_M;
|
||||
if (Number.isFinite(mesh) && mesh > dem + MESH_ROAD_ABOVE_M) {
|
||||
let a = i - 1;
|
||||
while (a >= 0 && i - a <= MESH_BRIDGE_MAX_POINTS && road[a] === null) a--;
|
||||
let b = i + 1;
|
||||
while (
|
||||
b < points.length &&
|
||||
b - i <= MESH_BRIDGE_MAX_POINTS &&
|
||||
road[b] === null
|
||||
)
|
||||
b++;
|
||||
const left = a >= 0 && i - a <= MESH_BRIDGE_MAX_POINTS ? road[a] : null;
|
||||
const right =
|
||||
b < points.length && b - i <= MESH_BRIDGE_MAX_POINTS ? road[b] : null;
|
||||
if (left !== null && right !== null) {
|
||||
const t = (i - a) / (b - a);
|
||||
return dem + left + (right - left) * t + MESH_LIFT_M;
|
||||
}
|
||||
}
|
||||
return dem + lift;
|
||||
});
|
||||
}
|
||||
|
||||
/** Camera height (m above ground) below which Google 3D switches to terrain mode. */
|
||||
export const SURFACE_TERRAIN_ENTER_M = 1400;
|
||||
/** Camera height above which terrain mode switches back to draped. */
|
||||
@@ -177,16 +237,25 @@ export function createGroundCaster({
|
||||
|
||||
/**
|
||||
* A line as flat [lon, lat, height, ...] degrees and metres, densified;
|
||||
* null when any corner it needs is not cached yet.
|
||||
* null when any corner it needs is not cached yet. With `meshAt` (sampled
|
||||
* mesh heights, undefined or null where unknown) the line is densified
|
||||
* finer and its heights refined by `refineHeights`.
|
||||
* @param {Array<[number, number]>} coords
|
||||
* @param {{meshAt?: (lon: number, lat: number) => number|null|undefined}} [options]
|
||||
*/
|
||||
function castLine(coords) {
|
||||
const flat = [];
|
||||
for (const [lon, lat] of densifyLine(coords)) {
|
||||
const height = heightAt(lon, lat);
|
||||
if (height === null) return null;
|
||||
flat.push(lon, lat, height);
|
||||
function castLine(coords, { meshAt = null } = {}) {
|
||||
const rows = [];
|
||||
const points = meshAt
|
||||
? densifyLine(coords, MESH_DENSIFY_DEG)
|
||||
: densifyLine(coords);
|
||||
for (const [lon, lat] of points) {
|
||||
const dem = groundAt(lon, lat);
|
||||
if (dem === null) return null;
|
||||
rows.push({ lon, lat, dem, mesh: meshAt ? meshAt(lon, lat) : null });
|
||||
}
|
||||
const heights = refineHeights(rows, { lift });
|
||||
const flat = [];
|
||||
rows.forEach(({ lon, lat }, i) => flat.push(lon, lat, heights[i]));
|
||||
return flat;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user