diff --git a/app/three/SceneView.ts b/app/three/SceneView.ts index 1f1481a..f769b64 100644 --- a/app/three/SceneView.ts +++ b/app/three/SceneView.ts @@ -15,13 +15,8 @@ import { WebGLRenderer, } from "three"; import { buildMeshes } from "./utils/build-meshes"; -import { Extent, buildScene } from "./utils/build-scene"; -import { - getFrustumIntersections, - getMetadata, - tileBounds, - transform, -} from "./utils/utils"; +import { Extent, animate, buildScene } from "./utils/build-scene"; +import { getMetadata, tileBounds, transform } from "./utils/utils"; import { MODEL_ID, SERVICE_URL } from "./config"; import { Orientation, @@ -177,8 +172,10 @@ export class SceneView extends EventTarget { } toggleTopography() { - const topo = this._scene.getObjectByName("topography"); - if (topo) { + const osmTopo = this._scene.getObjectByName("osm-topography"); + const topo = this._scene.getObjectByName("Topography"); + if (osmTopo && topo) { + osmTopo.visible = !osmTopo.visible; topo.visible = !topo.visible; } } @@ -471,7 +468,14 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { // Build the 3D model const meshes = await buildMeshes(mappedFeatures); 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"; scene.add(model); @@ -494,27 +498,18 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { // Create the map view for OSM topography const lod = new LODFrustum(); + lod.simplifyDistance = 200; + lod.subdivideDistance = 120; const map = new MapView(MapView.PLANAR, provider); map.lod = lod; map.rotateX(Math.PI / 2); - map.name = "topography"; + map.name = "osm-topography"; map.visible = false; scene.add(map); - controls.addEventListener("change", () => { - 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); - }); + renderer.setAnimationLoop(animate(rendererCallback(map, extent))); return { scene, @@ -526,12 +521,19 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { }; } -function traverse( - node: MapPlaneNode, - extent: Extent, - tiles: TileData[], - frustumPoints: Vector3[] -) { +function rendererCallback(map: MapView, extent: Extent) { + return () => { + if (map.visible) { + const tiles: TileData[] = []; + 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 xmin = bounds[0]; @@ -545,28 +547,22 @@ function traverse( ((ymax >= extent.ymin && ymax <= 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; - if (texture) { - tiles.push({ - xmin, - ymin, - xmax, - ymax, - zoom: node.level, - texture, - }); - } + const texture = (node.material as MeshPhongMaterial).map; + if (texture) { + tiles.push({ + xmin, + ymin, + xmax, + ymax, + x: node.x, + y: node.y, + zoom: node.level, + texture, + }); } } for (const c of node.children) { - traverse(c as MapPlaneNode, extent, tiles, frustumPoints); + traverse(c as MapPlaneNode, extent, tiles); } } diff --git a/app/three/ShaderMaterial.ts b/app/three/ShaderMaterial.ts index 9cb41a4..deeef9f 100644 --- a/app/three/ShaderMaterial.ts +++ b/app/three/ShaderMaterial.ts @@ -13,23 +13,42 @@ export interface TileData { ymin: number; xmax: number; ymax: number; + x: number; + y: number; zoom: number; texture: Texture; } -const maxTiles = 24; +const maxTiles = 48; +const width = 256; +const height = 256; +const size = width * height; -// Initialize empty texture slots -const dummyTexture = new Texture(); -dummyTexture.image = document.createElement("canvas"); -dummyTexture.needsUpdate = true; +const canvas = new OffscreenCanvas(width, height); +const ctx = canvas.getContext("2d"); + +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 export const shaderMaterial = new ShaderMaterial({ uniforms: { - tileBounds: { value: Array(maxTiles).fill(new Vector4(0, 0, 0, 0)) }, - tileCount: { value: 0 }, - tiles: { value: null }, + tileBounds: { value: tileBounds }, + tileCount: { value: maxTiles }, + tiles: { value: dataArrayTexture }, }, vertexShader: ShaderChunk.common + @@ -91,54 +110,42 @@ export function updateTiles(newTiles: TileData[]) { newTiles = newTiles.slice(0, maxTiles); } - const textures = newTiles.map((t) => t.texture); - const bounds = newTiles.map( - (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)); + for (let i = 0; i < newTiles.length; i++) { + updateDataArrayTexture(newTiles[i], i); } - // Update shader uniforms - shaderMaterial.uniforms.tileBounds.value = bounds; - shaderMaterial.uniforms.tileCount.value = newTiles.length; - shaderMaterial.uniforms.tiles.value = createDataArrayTexture(textures); + dataArrayTexture.needsUpdate = true; } -// Create a buffer with color data -const width = 256; -const height = 256; -const size = width * height; -function createDataArrayTexture(textures: Texture[]) { - const depth = textures.length; +// Update buffer +function updateDataArrayTexture(tileData: TileData, index: number) { + const k = getTileDataKey(tileData); + const cachedData = tileCache[k]?.imageData; - const data = new Uint8Array(4 * size * depth); - - for (let i = 0; i < depth; i++) { - const texture = textures[i]; - const imageData = getImageData(texture); + if (cachedData) { + tileBounds[index] = getTileBounds(tileData); + data.set(cachedData, index * size * 4); + } else { + const imageData = getImageData(tileData.texture); 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 - const texture = new DataArrayTexture(data, width, height, depth); - texture.format = RGBAFormat; - texture.generateMipmaps = false; - texture.magFilter = LinearFilter; - texture.minFilter = LinearFilter; - texture.needsUpdate = true; - return texture; +function getTileDataKey(t: TileData) { + return `${t.zoom}/${t.x}/${t.y}`; +} + +function getTileBounds(t: TileData) { + return new Vector4(t.xmin, t.xmax, t.ymin, t.ymax); } // Create a canvas and draw the image on it -const canvas = new OffscreenCanvas(width, height); -const ctx = canvas.getContext("2d"); function getImageData(texture: Texture) { const image = texture.source.data; diff --git a/app/three/utils/build-scene.ts b/app/three/utils/build-scene.ts index a33fec2..805ea9b 100644 --- a/app/three/utils/build-scene.ts +++ b/app/three/utils/build-scene.ts @@ -62,7 +62,7 @@ export function buildScene(container: HTMLElement, extent: Extent) { renderer.setSize(width, height); renderer.localClippingEnabled = true; renderer.autoClear = false; - renderer.setAnimationLoop(animate); + // renderer.setAnimationLoop(animate); // Handle window resize event to adapt the aspect ratio window.addEventListener("resize", () => onWindowResize(container)); @@ -121,14 +121,19 @@ function onWindowResize(container: HTMLElement) { controls.update(); } -function animate() { - // Update controls for main camera - controls.update(); +// Callback for animation loop +export function animate(cb: () => void) { + return () => { + // Update controls for main camera + controls.update(); - renderer.render(scene, camera); + renderer.render(scene, camera); - // Render the UI overlay - renderOverlay(); + // Render the UI overlay + renderOverlay(); + + cb(); + }; } // Render the overlay scene as an overlay diff --git a/app/three/utils/utils.ts b/app/three/utils/utils.ts index ccf60bf..3815546 100644 --- a/app/three/utils/utils.ts +++ b/app/three/utils/utils.ts @@ -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 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 raycaster = new Raycaster(); @@ -112,13 +117,13 @@ export const getFrustumIntersections = (camera: PerspectiveCamera) => { if (points.length > 1) { return [ new Vector3( - Math.min(points[0].x, points[1].x), - Math.min(points[0].y, points[1].y), + Math.min(points[0].x, points[1].x, points[2].x, points[3].x), + Math.min(points[0].y, points[1].y, points[2].y, points[3].y), 0 ), new Vector3( - Math.max(points[0].x, points[1].x), - Math.max(points[0].y, points[1].y), + Math.max(points[0].x, points[1].x, points[2].x, points[3].x), + Math.max(points[0].y, points[1].y, points[2].y, points[3].y), 0 ), ];