From 618979ad52506d670e75366ed36fa92e2e404e43 Mon Sep 17 00:00:00 2001 From: Thomas Fuhrmann Date: Tue, 8 Apr 2025 09:17:40 +0200 Subject: [PATCH] Add clipping planes to topography --- app/components/Form.tsx | 74 +++---- app/three/CustomMapHeightNodeShader.ts | 237 ----------------------- app/three/SceneView.ts | 17 +- app/three/ShaderMaterial.ts | 13 ++ app/three/utils/build-clipping-planes.ts | 8 +- app/three/utils/build-scene.ts | 7 +- 6 files changed, 65 insertions(+), 291 deletions(-) delete mode 100644 app/three/CustomMapHeightNodeShader.ts diff --git a/app/components/Form.tsx b/app/components/Form.tsx index 00862fc..ab47784 100644 --- a/app/components/Form.tsx +++ b/app/components/Form.tsx @@ -258,43 +258,47 @@ export function Form() { > {
- {sceneView?.model.children.map((child) => { - const key = `toggle-visibility-${child.name}`; - let color = "transparent"; - if ((child as Mesh).material instanceof MeshStandardMaterial) { - color = `#${( - (child as Mesh).material as MeshStandardMaterial - ).color.getHexString()}`; - } - const visible = (child as Mesh).visible; + {sceneView?.model.children + .filter((c) => c.name !== "Topography") + .map((child) => { + const key = `toggle-visibility-${child.name}`; + let color = "transparent"; + if ( + (child as Mesh).material instanceof MeshStandardMaterial + ) { + color = `#${( + (child as Mesh).material as MeshStandardMaterial + ).color.getHexString()}`; + } + const visible = (child as Mesh).visible; - return ( -
- - handleCheckboxChange(child.name)} - className="hover:cursor-pointer" - defaultChecked={visible ? true : false} - /> - -
- ); - })} + + handleCheckboxChange(child.name)} + className="hover:cursor-pointer" + defaultChecked={visible ? true : false} + /> + +
+ ); + })} } diff --git a/app/three/CustomMapHeightNodeShader.ts b/app/three/CustomMapHeightNodeShader.ts deleted file mode 100644 index 240c62e..0000000 --- a/app/three/CustomMapHeightNodeShader.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { - BufferGeometry, - Intersection, - Material, - MeshPhongMaterial, - NearestFilter, - Raycaster, - RGBAFormat, - Texture, - Vector3, -} from "three"; - -import { - MapHeightNode, - MapNodeGeometry, - MapPlaneNode, - UnitsUtils, - MapNode, - QuadTreePosition, - TextureUtils, - MapView, -} from "geo-three"; - -/** - * Map height node that uses GPU height calculation to generate the deformed plane mesh. - * - * This solution is faster if no mesh interaction is required since all trasnformations are done in the GPU the transformed mesh cannot be accessed for CPU operations (e.g. raycasting). - * - * @param parentNode - The parent node of this node. - * @param mapView - Map view object where this node is placed. - * @param location - Position in the node tree relative to the parent. - * @param level - Zoom level in the tile tree of the node. - * @param x - X position of the node in the tile tree. - * @param y - Y position of the node in the tile tree. - */ -export class CustomMapHeightNodeShader extends MapHeightNode { - /** - * Default height texture applied when tile load fails. - * - * This tile sets the height to sea level where it is common for the data sources to be missing height data. - */ - public static defaultHeightTexture = - TextureUtils.createFillTexture("#0186C0"); - - /** - * Size of the grid of the geometry displayed on the scene for each tile. - */ - public static geometrySize: number = 256; - - /** - * Map node plane geometry. - */ - public static geometry: BufferGeometry = new MapNodeGeometry( - 1.0, - 1.0, - CustomMapHeightNodeShader.geometrySize, - CustomMapHeightNodeShader.geometrySize, - true - ); - - /** - * Base geometry of the map node. - */ - public static baseGeometry: BufferGeometry = MapPlaneNode.geometry; - - /** - * Base scale of the map node. - */ - public static baseScale: Vector3 = new Vector3( - UnitsUtils.EARTH_PERIMETER, - 1, - UnitsUtils.EARTH_PERIMETER - ); - - public constructor( - parentNode: MapHeightNode | undefined, - mapView: MapView, - location: number = QuadTreePosition.root, - level: number = 0, - x: number = 0, - y: number = 0 - ) { - const material: Material = CustomMapHeightNodeShader.prepareMaterial( - new MeshPhongMaterial({ - map: MapNode.defaultTexture, - color: 0xffffff, - }) - ); - - super( - parentNode, - mapView, - location, - level, - x, - y, - CustomMapHeightNodeShader.geometry, - material - ); - - this.frustumCulled = true; - } - - /** - * Prepare the three.js material to be used in the map tile. - * - * @param material - Material to be transformed. - */ - public static prepareMaterial(material: Material): Material { - material.userData = { - heightMap: { value: CustomMapHeightNodeShader.defaultHeightTexture }, - }; - - material.onBeforeCompile = (shader) => { - // Pass uniforms from userData - for (const i in material.userData) { - shader.uniforms[i] = material.userData[i]; - } - - // Vertex variables - shader.vertexShader = - ` - uniform sampler2D heightMap; - ` + shader.vertexShader; - - // Vertex depth logic - // elevation = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1) - // heightMap stores normalized values in the range [0, 1] - // multiply by 255.0 to obtain values in the range [0, 255] - shader.vertexShader = shader.vertexShader.replace( - "#include ", - ` - #include - - // Calculate height of the tile - vec4 _theight = texture(heightMap, vMapUv); - float _height = ((_theight.r * 255.0 * 65536.0 + _theight.g * 255.0 * 256.0 + _theight.b * 255.0) * 0.1) - 10000.0; - - // Apply height displacement - vec3 _transformed = position + _height * normal; - - - gl_Position = projectionMatrix * modelViewMatrix * vec4(_transformed, 1.0); - ` - ); - }; - - return material; - } - - public async loadData(): Promise { - await super.loadData(); - - this.textureLoaded = true; - } - - public async loadHeightGeometry(): Promise { - if (this.mapView.heightProvider === null) { - throw new Error("GeoThree: MapView.heightProvider provider is null."); - } - - if ( - this.level < this.mapView.heightProvider.minZoom || - this.level > this.mapView.heightProvider.maxZoom - ) { - console.warn("Geo-Three: Loading tile outside of provider range: ", this); - - (this.material as MeshPhongMaterial).map = - CustomMapHeightNodeShader.defaultTexture; - (this.material as MeshPhongMaterial).needsUpdate = true; - return; - } - - try { - const image = await this.mapView.heightProvider.fetchTile( - this.level, - this.x, - this.y - ); - - if (this.disposed) { - return; - } - - const texture = new Texture(image as HTMLImageElement); - texture.generateMipmaps = false; - texture.format = RGBAFormat; - texture.magFilter = NearestFilter; - texture.minFilter = NearestFilter; - texture.needsUpdate = true; - - (this.material as Material).userData.heightMap.value = texture; - } catch (e) { - console.warn("Could not fetch tile: ", e); - if (this.disposed) { - return; - } - - console.warn("Geo-Three: Failed to load height data: ", this); - - // Water level texture (assume that missing texture will be water level) - (this.material as Material).userData.heightMap.value = - CustomMapHeightNodeShader.defaultHeightTexture; - } - - (this.material as Material).needsUpdate = true; - - this.heightLoaded = true; - } - - /** - * Overrides normal raycasting, to avoid raycasting when isMesh is set to false. - * - * Switches the geometry for a simpler one for faster raycasting. - */ - public raycast(raycaster: Raycaster, intersects: Intersection[]): void { - if (this.isMesh === true) { - this.geometry = MapPlaneNode.geometry; - - super.raycast(raycaster, intersects); - - this.geometry = CustomMapHeightNodeShader.geometry; - } - } - - public dispose(): void { - super.dispose(); - - if ( - (this.material as Material).userData.heightMap.value && - (this.material as Material).userData.heightMap.value !== - CustomMapHeightNodeShader.defaultHeightTexture - ) { - (this.material as Material).userData.heightMap.value.dispose(); - } - } -} diff --git a/app/three/SceneView.ts b/app/three/SceneView.ts index a642b2c..02de088 100644 --- a/app/three/SceneView.ts +++ b/app/three/SceneView.ts @@ -172,10 +172,8 @@ export class SceneView extends EventTarget { } toggleTopography() { - const osmTopo = this._scene.getObjectByName("osm-topography"); const topo = this._scene.getObjectByName("Topography"); - if (osmTopo && topo) { - // osmTopo.visible = !osmTopo.visible; + if (topo) { topo.visible = !topo.visible; } } @@ -468,14 +466,7 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { // Build the 3D model const meshes = await buildMeshes(mappedFeatures); const model = new Group(); - for (const mesh of meshes) { - if (mesh.name !== "Topography") { - model.add(mesh); - } else { - // Add the topography as a separate layer - scene.add(mesh); - } - } + model.add(...meshes); model.name = "geologic-model"; scene.add(model); @@ -498,8 +489,8 @@ 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; + lod.simplifyDistance = 225; + lod.subdivideDistance = 80; const map = new MapView(MapView.PLANAR, provider); map.lod = lod; diff --git a/app/three/ShaderMaterial.ts b/app/three/ShaderMaterial.ts index deeef9f..11f61ca 100644 --- a/app/three/ShaderMaterial.ts +++ b/app/three/ShaderMaterial.ts @@ -1,4 +1,5 @@ import { + Color, DataArrayTexture, LinearFilter, RGBAFormat, @@ -45,10 +46,12 @@ dataArrayTexture.needsUpdate = true; // Create shader material export const shaderMaterial = new ShaderMaterial({ + clipping: true, uniforms: { tileBounds: { value: tileBounds }, tileCount: { value: maxTiles }, tiles: { value: dataArrayTexture }, + color: { value: new Color(1, 1, 1) }, }, vertexShader: ShaderChunk.common + @@ -58,10 +61,16 @@ export const shaderMaterial = new ShaderMaterial({ varying vec3 vWorldPosition; varying float fragDepth; + #include + void main() { + #include vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); fragDepth = (gl_Position.z / gl_Position.w + 1.0) * 0.5; + + #include + #include ` + ShaderChunk.logdepthbuf_vertex + @@ -77,7 +86,11 @@ export const shaderMaterial = new ShaderMaterial({ varying vec3 vWorldPosition; varying float fragDepth; + #include + void main() { + #include + vec4 color = vec4(191.0/255.0, 209.0/255.0, 229.0/255.0, 1.0); // Default color for (int i = 0; i < ${maxTiles}; i++) { diff --git a/app/three/utils/build-clipping-planes.ts b/app/three/utils/build-clipping-planes.ts index cc823e2..4c93d9f 100644 --- a/app/three/utils/build-clipping-planes.ts +++ b/app/three/utils/build-clipping-planes.ts @@ -1,6 +1,7 @@ import { BufferAttribute, BufferGeometry, + Color, DoubleSide, EdgesGeometry, Group, @@ -649,8 +650,13 @@ function generateCapMeshes( ? 1 : -1; + const color = + mesh.material instanceof MeshStandardMaterial + ? mesh.material.color + : new Color(1, 1, 1); + const material = new MeshStandardMaterial({ - color: (mesh.material as MeshStandardMaterial).color, + color, side: DoubleSide, metalness: 0.0, roughness: 1.0, diff --git a/app/three/utils/build-scene.ts b/app/three/utils/build-scene.ts index 805ea9b..063eee1 100644 --- a/app/three/utils/build-scene.ts +++ b/app/three/utils/build-scene.ts @@ -37,8 +37,8 @@ let overlayCamera: OrthographicCamera; let overlayScene: Scene; let maxSize = 0; const compass = new Group(); -const UI_WIDTH = 200; -const UI_HEIGHT = 200; +const UI_WIDTH = 150; +const UI_HEIGHT = 150; export function buildScene(container: HTMLElement, extent: Extent) { maxSize = getMaxSize(extent); @@ -152,11 +152,8 @@ function renderOverlay() { ); // Render the overlay scene to the screen (position it in the bottom left) - renderer.setScissorTest(true); - renderer.setScissor(10, 10, UI_WIDTH, UI_HEIGHT); renderer.setViewport(10, 10, UI_WIDTH, UI_HEIGHT); renderer.render(overlayScene, overlayCamera); - renderer.setScissorTest(false); // Disable scissor testing for the rest of the scene renderer.setViewport( 0, 0,