Finish topography overlay

This commit is contained in:
Fuhrmann 2025-04-07 14:47:24 +02:00
parent f2cb1ce9e2
commit 832c958fba
4 changed files with 117 additions and 104 deletions

View file

@ -15,13 +15,8 @@ import {
WebGLRenderer, WebGLRenderer,
} from "three"; } from "three";
import { buildMeshes } from "./utils/build-meshes"; import { buildMeshes } from "./utils/build-meshes";
import { Extent, buildScene } from "./utils/build-scene"; import { Extent, animate, buildScene } from "./utils/build-scene";
import { import { getMetadata, tileBounds, transform } from "./utils/utils";
getFrustumIntersections,
getMetadata,
tileBounds,
transform,
} from "./utils/utils";
import { MODEL_ID, SERVICE_URL } from "./config"; import { MODEL_ID, SERVICE_URL } from "./config";
import { import {
Orientation, Orientation,
@ -177,8 +172,10 @@ export class SceneView extends EventTarget {
} }
toggleTopography() { toggleTopography() {
const topo = this._scene.getObjectByName("topography"); const osmTopo = this._scene.getObjectByName("osm-topography");
if (topo) { const topo = this._scene.getObjectByName("Topography");
if (osmTopo && topo) {
osmTopo.visible = !osmTopo.visible;
topo.visible = !topo.visible; topo.visible = !topo.visible;
} }
} }
@ -471,7 +468,14 @@ async function init(container: HTMLElement, modelId = MODEL_ID) {
// Build the 3D model // Build the 3D model
const meshes = await buildMeshes(mappedFeatures); const meshes = await buildMeshes(mappedFeatures);
const model = new Group(); const model = new Group();
model.add(...meshes); for (const mesh of meshes) {
if (mesh.name !== "Topography") {
model.add(mesh);
} else {
// Add the topography as a separate layer
scene.add(mesh);
}
}
model.name = "geologic-model"; model.name = "geologic-model";
scene.add(model); scene.add(model);
@ -494,27 +498,18 @@ async function init(container: HTMLElement, modelId = MODEL_ID) {
// Create the map view for OSM topography // Create the map view for OSM topography
const lod = new LODFrustum(); const lod = new LODFrustum();
lod.simplifyDistance = 200;
lod.subdivideDistance = 120;
const map = new MapView(MapView.PLANAR, provider); const map = new MapView(MapView.PLANAR, provider);
map.lod = lod; map.lod = lod;
map.rotateX(Math.PI / 2); map.rotateX(Math.PI / 2);
map.name = "topography"; map.name = "osm-topography";
map.visible = false; map.visible = false;
scene.add(map); scene.add(map);
controls.addEventListener("change", () => { renderer.setAnimationLoop(animate(rendererCallback(map, extent)));
const tiles: TileData[] = [];
camera.updateMatrix();
const frustumPoints = getFrustumIntersections(camera);
map.lod.updateLOD(map, camera, renderer, scene);
traverse(map.root, extent, tiles, frustumPoints);
tiles.sort((a, b) => b.zoom - a.zoom);
updateTiles(tiles);
});
return { return {
scene, scene,
@ -526,12 +521,19 @@ async function init(container: HTMLElement, modelId = MODEL_ID) {
}; };
} }
function traverse( function rendererCallback(map: MapView, extent: Extent) {
node: MapPlaneNode, return () => {
extent: Extent, if (map.visible) {
tiles: TileData[], const tiles: TileData[] = [];
frustumPoints: Vector3[] traverse(map.root, extent, tiles);
) { tiles.sort((a, b) => b.zoom - a.zoom);
updateTiles(tiles);
}
};
}
function traverse(node: MapPlaneNode, extent: Extent, tiles: TileData[]) {
const bounds = tileBounds(node.level, node.x, node.y); const bounds = tileBounds(node.level, node.x, node.y);
const xmin = bounds[0]; const xmin = bounds[0];
@ -544,13 +546,6 @@ function traverse(
(xmin >= extent.xmin && xmin <= extent.xmax)) && (xmin >= extent.xmin && xmin <= extent.xmax)) &&
((ymax >= extent.ymin && ymax <= extent.ymax) || ((ymax >= extent.ymin && ymax <= extent.ymax) ||
(ymin >= extent.ymin && ymin <= extent.ymax)) (ymin >= extent.ymin && ymin <= extent.ymax))
) {
if (
frustumPoints.length < 2 ||
(((xmax >= frustumPoints[0].x && xmax <= frustumPoints[1].x) ||
(xmin >= frustumPoints[0].x && xmin <= frustumPoints[1].x)) &&
((ymax >= frustumPoints[0].y && ymax <= frustumPoints[1].y) ||
(ymin >= frustumPoints[0].y && ymin <= frustumPoints[1].y)))
) { ) {
const texture = (node.material as MeshPhongMaterial).map; const texture = (node.material as MeshPhongMaterial).map;
if (texture) { if (texture) {
@ -559,14 +554,15 @@ function traverse(
ymin, ymin,
xmax, xmax,
ymax, ymax,
x: node.x,
y: node.y,
zoom: node.level, zoom: node.level,
texture, texture,
}); });
} }
} }
}
for (const c of node.children) { for (const c of node.children) {
traverse(c as MapPlaneNode, extent, tiles, frustumPoints); traverse(c as MapPlaneNode, extent, tiles);
} }
} }

View file

@ -13,23 +13,42 @@ export interface TileData {
ymin: number; ymin: number;
xmax: number; xmax: number;
ymax: number; ymax: number;
x: number;
y: number;
zoom: number; zoom: number;
texture: Texture; texture: Texture;
} }
const maxTiles = 24; const maxTiles = 48;
const width = 256;
const height = 256;
const size = width * height;
// Initialize empty texture slots const canvas = new OffscreenCanvas(width, height);
const dummyTexture = new Texture(); const ctx = canvas.getContext("2d");
dummyTexture.image = document.createElement("canvas");
dummyTexture.needsUpdate = true; const tileBounds = Array(maxTiles).fill(new Vector4(0, 0, 0, 0));
const data = new Uint8Array(4 * size * maxTiles);
const tileCache: {
[key: string]: {
imageData: Uint8ClampedArray;
};
} = {};
const dataArrayTexture = new DataArrayTexture(data, width, height, maxTiles);
dataArrayTexture.format = RGBAFormat;
dataArrayTexture.generateMipmaps = false;
dataArrayTexture.magFilter = LinearFilter;
dataArrayTexture.minFilter = LinearFilter;
dataArrayTexture.needsUpdate = true;
// Create shader material // Create shader material
export const shaderMaterial = new ShaderMaterial({ export const shaderMaterial = new ShaderMaterial({
uniforms: { uniforms: {
tileBounds: { value: Array(maxTiles).fill(new Vector4(0, 0, 0, 0)) }, tileBounds: { value: tileBounds },
tileCount: { value: 0 }, tileCount: { value: maxTiles },
tiles: { value: null }, tiles: { value: dataArrayTexture },
}, },
vertexShader: vertexShader:
ShaderChunk.common + ShaderChunk.common +
@ -91,54 +110,42 @@ export function updateTiles(newTiles: TileData[]) {
newTiles = newTiles.slice(0, maxTiles); newTiles = newTiles.slice(0, maxTiles);
} }
const textures = newTiles.map((t) => t.texture); for (let i = 0; i < newTiles.length; i++) {
const bounds = newTiles.map( updateDataArrayTexture(newTiles[i], i);
(t) => new Vector4(t.xmin, t.xmax, t.ymin, t.ymax)
);
// Fill remaining slots with dummy data to maintain uniform array size
while (textures.length < maxTiles) {
textures.push(dummyTexture);
bounds.push(new Vector4(0, 0, 0, 0));
} }
// Update shader uniforms dataArrayTexture.needsUpdate = true;
shaderMaterial.uniforms.tileBounds.value = bounds;
shaderMaterial.uniforms.tileCount.value = newTiles.length;
shaderMaterial.uniforms.tiles.value = createDataArrayTexture(textures);
} }
// Create a buffer with color data // Update buffer
const width = 256; function updateDataArrayTexture(tileData: TileData, index: number) {
const height = 256; const k = getTileDataKey(tileData);
const size = width * height; const cachedData = tileCache[k]?.imageData;
function createDataArrayTexture(textures: Texture[]) {
const depth = textures.length;
const data = new Uint8Array(4 * size * depth); if (cachedData) {
tileBounds[index] = getTileBounds(tileData);
for (let i = 0; i < depth; i++) { data.set(cachedData, index * size * 4);
const texture = textures[i]; } else {
const imageData = getImageData(texture); const imageData = getImageData(tileData.texture);
if (imageData) { if (imageData) {
data.set(imageData, i * size * 4); // Update cache and buffer
tileCache[k] = { imageData };
tileBounds[index] = getTileBounds(tileData);
data.set(imageData, index * size * 4);
}
} }
} }
// Use the buffer to create a DataArrayTexture function getTileDataKey(t: TileData) {
const texture = new DataArrayTexture(data, width, height, depth); return `${t.zoom}/${t.x}/${t.y}`;
texture.format = RGBAFormat; }
texture.generateMipmaps = false;
texture.magFilter = LinearFilter; function getTileBounds(t: TileData) {
texture.minFilter = LinearFilter; return new Vector4(t.xmin, t.xmax, t.ymin, t.ymax);
texture.needsUpdate = true;
return texture;
} }
// Create a canvas and draw the image on it // Create a canvas and draw the image on it
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d");
function getImageData(texture: Texture) { function getImageData(texture: Texture) {
const image = texture.source.data; const image = texture.source.data;

View file

@ -62,7 +62,7 @@ export function buildScene(container: HTMLElement, extent: Extent) {
renderer.setSize(width, height); renderer.setSize(width, height);
renderer.localClippingEnabled = true; renderer.localClippingEnabled = true;
renderer.autoClear = false; renderer.autoClear = false;
renderer.setAnimationLoop(animate); // renderer.setAnimationLoop(animate);
// Handle window resize event to adapt the aspect ratio // Handle window resize event to adapt the aspect ratio
window.addEventListener("resize", () => onWindowResize(container)); window.addEventListener("resize", () => onWindowResize(container));
@ -121,7 +121,9 @@ function onWindowResize(container: HTMLElement) {
controls.update(); controls.update();
} }
function animate() { // Callback for animation loop
export function animate(cb: () => void) {
return () => {
// Update controls for main camera // Update controls for main camera
controls.update(); controls.update();
@ -129,6 +131,9 @@ function animate() {
// Render the UI overlay // Render the UI overlay
renderOverlay(); renderOverlay();
cb();
};
} }
// Render the overlay scene as an overlay // Render the overlay scene as an overlay

View file

@ -90,9 +90,14 @@ export function tileBounds(zoom: number, x: number, y: number): number[] {
const plane = new Plane(new Vector3(0, 0, 1), 0); const plane = new Plane(new Vector3(0, 0, 1), 0);
const corners = [new Vector2(-1, -1), new Vector2(1, 1)]; const corners = [
new Vector2(-1, -1),
new Vector2(1, 1),
new Vector2(-1, 1),
new Vector2(1, -1),
];
export const getFrustumIntersections = (camera: PerspectiveCamera) => { export const getFrustumBoundingBox = (camera: PerspectiveCamera) => {
const points = []; const points = [];
const raycaster = new Raycaster(); const raycaster = new Raycaster();
@ -112,13 +117,13 @@ export const getFrustumIntersections = (camera: PerspectiveCamera) => {
if (points.length > 1) { if (points.length > 1) {
return [ return [
new Vector3( new Vector3(
Math.min(points[0].x, points[1].x), Math.min(points[0].x, points[1].x, points[2].x, points[3].x),
Math.min(points[0].y, points[1].y), Math.min(points[0].y, points[1].y, points[2].y, points[3].y),
0 0
), ),
new Vector3( new Vector3(
Math.max(points[0].x, points[1].x), Math.max(points[0].x, points[1].x, points[2].x, points[3].x),
Math.max(points[0].y, points[1].y), Math.max(points[0].y, points[1].y, points[2].y, points[3].y),
0 0
), ),
]; ];