diff --git a/app/components/Form.tsx b/app/components/Form.tsx index e7a72c5..d6b4c7d 100644 --- a/app/components/Form.tsx +++ b/app/components/Form.tsx @@ -118,10 +118,7 @@ export function Form() { function handleCheckboxChange(name: string) { if (!sceneView) return; - const mesh = sceneView.model.getObjectByName(name); - if (mesh) { - mesh.visible = !mesh.visible; - } + sceneView.toggleLayerVisibility(name); } function handleChangeTopography() { diff --git a/app/three/CustomMapHeightNodeShader.ts b/app/three/CustomMapHeightNodeShader.ts index 460f233..47abcb8 100644 --- a/app/three/CustomMapHeightNodeShader.ts +++ b/app/three/CustomMapHeightNodeShader.ts @@ -84,9 +84,6 @@ export class CustomMapHeightNodeShader extends MapHeightNode { new MeshPhongMaterial({ map: MapNode.defaultTexture, color: 0xffffff, - transparent: true, - opacity: 1.0, - depthTest: true, }) ); @@ -109,7 +106,7 @@ export class CustomMapHeightNodeShader extends MapHeightNode { material ); - this.frustumCulled = false; + this.frustumCulled = true; } /** @@ -147,9 +144,10 @@ export class CustomMapHeightNodeShader extends MapHeightNode { 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; - - // Vertex position based on height + + gl_Position = projectionMatrix * modelViewMatrix * vec4(_transformed, 1.0); ` ); diff --git a/app/three/SceneView.ts b/app/three/SceneView.ts index 6ab5ab4..c438baf 100644 --- a/app/three/SceneView.ts +++ b/app/three/SceneView.ts @@ -1,4 +1,4 @@ -import { Group, Mesh, MeshStandardMaterial, Scene } from "three"; +import { Group, Material, Mesh, MeshStandardMaterial, Scene } from "three"; import { buildMeshes } from "./utils/build-meshes"; import { Extent, buildScene } from "./utils/build-scene"; import { getCenter3D, getMetadata, transform } from "./utils/utils"; @@ -12,6 +12,7 @@ import { DragControls } from "three/examples/jsm/Addons.js"; import { DebugProvider, HeightDebugProvider, + MapHeightNode, MapTilerProvider, MapView, OpenStreetMapsProvider, @@ -61,6 +62,20 @@ export class SceneView { if (mesh) { mesh.visible = !mesh.visible; } + + // Set visibility for any existing cap meshes + for (const key of Object.values(Orientation)) { + const name = `cap-mesh-group-${key}`; + const capMeshGroup = this._scene.getObjectByName(name); + + if (capMeshGroup) { + for (const m of capMeshGroup.children) { + if (m.name === layerName && m) { + m.visible = !m.visible; + } + } + } + } } toggleCoordinateGrid() { @@ -166,9 +181,10 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { // Create the map view for OSM topography const map = new MapView(MapView.PLANAR, provider, heightProvider); - const customNode = new CustomMapHeightNodeShader(null, map); + const customNode = new CustomMapHeightNodeShader(undefined, map); map.setRoot(customNode); map.rotateX(Math.PI / 2); + map.name = "topography"; scene.add(map); diff --git a/app/three/utils/build-clipping-planes.ts b/app/three/utils/build-clipping-planes.ts index ec2d5ba..eb64f2b 100644 --- a/app/three/utils/build-clipping-planes.ts +++ b/app/three/utils/build-clipping-planes.ts @@ -470,94 +470,84 @@ function generateCapMeshes( // Iterate over the list of geologic meshes for (const mesh of meshes) { // Slice visible meshes only - if (mesh.visible) { - const position = mesh.geometry.attributes.position.array; - const indices = mesh.geometry.index ? mesh.geometry.index.array : null; - const edges: Array<[Vector3, Vector3]> = []; - for ( - let i = 0; - i < (indices ? indices.length : position.length / 3); - i += 3 - ) { - const i1 = indices ? indices[i] * 3 : i * 3; - const i2 = indices ? indices[i + 1] * 3 : (i + 1) * 3; - const i3 = indices ? indices[i + 2] * 3 : (i + 2) * 3; + const position = mesh.geometry.attributes.position.array; + const indices = mesh.geometry.index ? mesh.geometry.index.array : null; + const edges: Array<[Vector3, Vector3]> = []; - const v1 = new Vector3( - position[i1], - position[i1 + 1], - position[i1 + 2] - ); - const v2 = new Vector3( - position[i2], - position[i2 + 1], - position[i2 + 2] - ); - const v3 = new Vector3( - position[i3], - position[i3 + 1], - position[i3 + 2] - ); + for ( + let i = 0; + i < (indices ? indices.length : position.length / 3); + i += 3 + ) { + const i1 = indices ? indices[i] * 3 : i * 3; + const i2 = indices ? indices[i + 1] * 3 : (i + 1) * 3; + const i3 = indices ? indices[i + 2] * 3 : (i + 2) * 3; - // Check if the triangle is cut by the plane - const d1 = plane.distanceToPoint(v1); - const d2 = plane.distanceToPoint(v2); - const d3 = plane.distanceToPoint(v3); + const v1 = new Vector3(position[i1], position[i1 + 1], position[i1 + 2]); + const v2 = new Vector3(position[i2], position[i2 + 1], position[i2 + 2]); + const v3 = new Vector3(position[i3], position[i3 + 1], position[i3 + 2]); - // Compute intersection points - const intersections = []; + // Check if the triangle is cut by the plane + const d1 = plane.distanceToPoint(v1); + const d2 = plane.distanceToPoint(v2); + const d3 = plane.distanceToPoint(v3); - if (d1 * d2 < 0) intersections.push(intersectEdge(v1, v2, d1, d2)); - if (d2 * d3 < 0) intersections.push(intersectEdge(v2, v3, d2, d3)); - if (d3 * d1 < 0) intersections.push(intersectEdge(v3, v1, d3, d1)); + // Compute intersection points + const intersections = []; - if (intersections.length === 2) { - edges.push([intersections[0], intersections[1]]); - } + if (d1 * d2 < 0) intersections.push(intersectEdge(v1, v2, d1, d2)); + if (d2 * d3 < 0) intersections.push(intersectEdge(v2, v3, d2, d3)); + if (d3 * d1 < 0) intersections.push(intersectEdge(v3, v1, d3, d1)); + + if (intersections.length === 2) { + edges.push([intersections[0], intersections[1]]); } - - // Intersection surface can be a multipolygon consisting of disconnected polygons - const polygons: Vector3[][] = buildPolygons(edges); - - // Clip cap surfaces with clipping planes - const clippingPlanes = planes.filter( - (p) => !p.normal.equals(plane.normal) - ); - - const offset = orientation === Orientation.Z ? 1 : -1; - const material = new MeshStandardMaterial({ - color: (mesh.material as MeshStandardMaterial).color, - side: DoubleSide, - polygonOffset: true, - polygonOffsetFactor: offset, - polygonOffsetUnits: offset, - clippingPlanes, - wireframe: scene.userData.wireframe, - }); - - const localMeshes = polygons.map((polygon) => { - const geometry = triangulatePolygon(polygon, plane); - - const capMesh = new Mesh(geometry, material); - - // Offset mesh to avoid flickering - const normal = plane.normal.clone().multiplyScalar(offset); - - const positionAttr = capMesh.geometry.attributes.position; - for (let i = 0; i < positionAttr.count; i++) { - const x = positionAttr.getX(i) + normal.x; - const y = positionAttr.getY(i) + normal.y; - const z = positionAttr.getZ(i) + normal.z; - positionAttr.setXYZ(i, x, y, z); - } - positionAttr.needsUpdate = true; - - return capMesh; - }); - - capMeshes.push(...localMeshes); } + + // Intersection surface can be a multipolygon consisting of disconnected polygons + const polygons: Vector3[][] = buildPolygons(edges); + + // Clip cap surfaces with clipping planes + const clippingPlanes = planes.filter((p) => !p.normal.equals(plane.normal)); + + const offset = orientation === Orientation.Z ? 1 : -1; + const material = new MeshStandardMaterial({ + color: (mesh.material as MeshStandardMaterial).color, + side: DoubleSide, + metalness: 0.0, + roughness: 0.75, + flatShading: true, + polygonOffset: true, + polygonOffsetFactor: offset, + polygonOffsetUnits: offset, + clippingPlanes, + wireframe: scene.userData.wireframe, + }); + + const localMeshes = polygons.map((polygon) => { + const geometry = triangulatePolygon(polygon, plane); + + const capMesh = new Mesh(geometry, material); + capMesh.visible = mesh.visible; + capMesh.name = mesh.name; + + // Offset mesh to avoid flickering + const normal = plane.normal.clone().multiplyScalar(offset); + + const positionAttr = capMesh.geometry.attributes.position; + for (let i = 0; i < positionAttr.count; i++) { + const x = positionAttr.getX(i) + normal.x; + const y = positionAttr.getY(i) + normal.y; + const z = positionAttr.getZ(i) + normal.z; + positionAttr.setXYZ(i, x, y, z); + } + positionAttr.needsUpdate = true; + + return capMesh; + }); + + capMeshes.push(...localMeshes); } return capMeshes; diff --git a/app/three/utils/build-scene.ts b/app/three/utils/build-scene.ts index 5560e81..4db9e21 100644 --- a/app/three/utils/build-scene.ts +++ b/app/three/utils/build-scene.ts @@ -7,6 +7,7 @@ import { Group, Object3D, Vector3, + HemisphereLight, } from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; @@ -62,7 +63,8 @@ export function buildScene(container: HTMLElement, extent: Extent) { controls = new OrbitControls(camera, renderer.domElement); controls.target.set(center.x, center.y, center.z); // Focus on the center controls.enableDamping = true; // Smooth camera movement - controls.maxDistance = maxSize * 5; + controls.maxDistance = maxSize * 3; + controls.minDistance = maxSize / 5; controls.update(); // Scene @@ -97,17 +99,24 @@ function animate() { function buildDefaultLights(scene: Scene, extent: Extent) { const center = getCenter3D(extent); + const lightPosition = { + x: center.x, + y: center.y - 200000, + z: extent.zmax + 100000, + }; + const lights = []; + // Ambient light - const ambient = new AmbientLight(0xffffff, 1.0); + const ambient = new AmbientLight(0xffffff, 2); lights.push(ambient); // Directional lights - const directionalLight = new DirectionalLight(0xffffff, 1); + const directionalLight = new DirectionalLight(0xffffff, 2); directionalLight.position.set( - center.x, - center.y - 200000, - extent.zmax + 100000 + lightPosition.x, + lightPosition.y, + lightPosition.z ); // Create a target for the ligth