Add clipping planes to topography

This commit is contained in:
Fuhrmann 2025-04-08 09:17:40 +02:00
parent 961c2f79cc
commit 618979ad52
6 changed files with 65 additions and 291 deletions

View file

@ -258,43 +258,47 @@ export function Form() {
>
{
<div className="flex flex-col gap-2">
{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 (
<div
key={key}
className="flex items-center justify-start gap-2.5 border-b border-gray-200 dark:border-gray-400 py-1 dark:text-gray-400"
>
<span
className="inline-block w-5 h-5 flex-none rounded"
style={{
backgroundColor: color,
}}
></span>
<input
id={key}
type="checkbox"
onChange={() => handleCheckboxChange(child.name)}
className="hover:cursor-pointer"
defaultChecked={visible ? true : false}
/>
<label
htmlFor={key}
className="font-light text-gray-500 dark:text-gray-400"
return (
<div
key={key}
className="flex items-center justify-start gap-2.5 border-b border-gray-200 dark:border-gray-400 py-1 dark:text-gray-400"
>
{child.name}
</label>
</div>
);
})}
<span
className="inline-block w-5 h-5 flex-none rounded"
style={{
backgroundColor: color,
}}
></span>
<input
id={key}
type="checkbox"
onChange={() => handleCheckboxChange(child.name)}
className="hover:cursor-pointer"
defaultChecked={visible ? true : false}
/>
<label
htmlFor={key}
className="font-light text-gray-500 dark:text-gray-400"
>
{child.name}
</label>
</div>
);
})}
</div>
}
</Accordion>

View file

@ -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 <fog_vertex>",
`
#include <fog_vertex>
// 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<void> {
await super.loadData();
this.textureLoaded = true;
}
public async loadHeightGeometry(): Promise<void> {
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();
}
}
}

View file

@ -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;

View file

@ -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 <clipping_planes_pars_vertex>
void main() {
#include <begin_vertex>
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 <project_vertex>
#include <clipping_planes_vertex>
` +
ShaderChunk.logdepthbuf_vertex +
@ -77,7 +86,11 @@ export const shaderMaterial = new ShaderMaterial({
varying vec3 vWorldPosition;
varying float fragDepth;
#include <clipping_planes_pars_fragment>
void main() {
#include <clipping_planes_fragment>
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++) {

View file

@ -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,

View file

@ -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,