Finish topography overlay
This commit is contained in:
parent
f2cb1ce9e2
commit
832c958fba
4 changed files with 117 additions and 104 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue