Add clipping planes to topography
This commit is contained in:
parent
961c2f79cc
commit
618979ad52
6 changed files with 65 additions and 291 deletions
|
@ -258,43 +258,47 @@ export function Form() {
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{sceneView?.model.children.map((child) => {
|
{sceneView?.model.children
|
||||||
const key = `toggle-visibility-${child.name}`;
|
.filter((c) => c.name !== "Topography")
|
||||||
let color = "transparent";
|
.map((child) => {
|
||||||
if ((child as Mesh).material instanceof MeshStandardMaterial) {
|
const key = `toggle-visibility-${child.name}`;
|
||||||
color = `#${(
|
let color = "transparent";
|
||||||
(child as Mesh).material as MeshStandardMaterial
|
if (
|
||||||
).color.getHexString()}`;
|
(child as Mesh).material instanceof MeshStandardMaterial
|
||||||
}
|
) {
|
||||||
const visible = (child as Mesh).visible;
|
color = `#${(
|
||||||
|
(child as Mesh).material as MeshStandardMaterial
|
||||||
|
).color.getHexString()}`;
|
||||||
|
}
|
||||||
|
const visible = (child as Mesh).visible;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
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"
|
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"
|
|
||||||
>
|
>
|
||||||
{child.name}
|
<span
|
||||||
</label>
|
className="inline-block w-5 h-5 flex-none rounded"
|
||||||
</div>
|
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>
|
</div>
|
||||||
}
|
}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -172,10 +172,8 @@ export class SceneView extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTopography() {
|
toggleTopography() {
|
||||||
const osmTopo = this._scene.getObjectByName("osm-topography");
|
|
||||||
const topo = this._scene.getObjectByName("Topography");
|
const topo = this._scene.getObjectByName("Topography");
|
||||||
if (osmTopo && topo) {
|
if (topo) {
|
||||||
// osmTopo.visible = !osmTopo.visible;
|
|
||||||
topo.visible = !topo.visible;
|
topo.visible = !topo.visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -468,14 +466,7 @@ 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();
|
||||||
for (const mesh of meshes) {
|
model.add(...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);
|
||||||
|
|
||||||
|
@ -498,8 +489,8 @@ 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.simplifyDistance = 225;
|
||||||
lod.subdivideDistance = 120;
|
lod.subdivideDistance = 80;
|
||||||
|
|
||||||
const map = new MapView(MapView.PLANAR, provider);
|
const map = new MapView(MapView.PLANAR, provider);
|
||||||
map.lod = lod;
|
map.lod = lod;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
Color,
|
||||||
DataArrayTexture,
|
DataArrayTexture,
|
||||||
LinearFilter,
|
LinearFilter,
|
||||||
RGBAFormat,
|
RGBAFormat,
|
||||||
|
@ -45,10 +46,12 @@ dataArrayTexture.needsUpdate = true;
|
||||||
|
|
||||||
// Create shader material
|
// Create shader material
|
||||||
export const shaderMaterial = new ShaderMaterial({
|
export const shaderMaterial = new ShaderMaterial({
|
||||||
|
clipping: true,
|
||||||
uniforms: {
|
uniforms: {
|
||||||
tileBounds: { value: tileBounds },
|
tileBounds: { value: tileBounds },
|
||||||
tileCount: { value: maxTiles },
|
tileCount: { value: maxTiles },
|
||||||
tiles: { value: dataArrayTexture },
|
tiles: { value: dataArrayTexture },
|
||||||
|
color: { value: new Color(1, 1, 1) },
|
||||||
},
|
},
|
||||||
vertexShader:
|
vertexShader:
|
||||||
ShaderChunk.common +
|
ShaderChunk.common +
|
||||||
|
@ -58,10 +61,16 @@ export const shaderMaterial = new ShaderMaterial({
|
||||||
varying vec3 vWorldPosition;
|
varying vec3 vWorldPosition;
|
||||||
varying float fragDepth;
|
varying float fragDepth;
|
||||||
|
|
||||||
|
#include <clipping_planes_pars_vertex>
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
#include <begin_vertex>
|
||||||
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
fragDepth = (gl_Position.z / gl_Position.w + 1.0) * 0.5;
|
fragDepth = (gl_Position.z / gl_Position.w + 1.0) * 0.5;
|
||||||
|
|
||||||
|
#include <project_vertex>
|
||||||
|
#include <clipping_planes_vertex>
|
||||||
|
|
||||||
` +
|
` +
|
||||||
ShaderChunk.logdepthbuf_vertex +
|
ShaderChunk.logdepthbuf_vertex +
|
||||||
|
@ -77,7 +86,11 @@ export const shaderMaterial = new ShaderMaterial({
|
||||||
varying vec3 vWorldPosition;
|
varying vec3 vWorldPosition;
|
||||||
varying float fragDepth;
|
varying float fragDepth;
|
||||||
|
|
||||||
|
#include <clipping_planes_pars_fragment>
|
||||||
|
|
||||||
void main() {
|
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
|
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++) {
|
for (int i = 0; i < ${maxTiles}; i++) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
BufferAttribute,
|
BufferAttribute,
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
|
Color,
|
||||||
DoubleSide,
|
DoubleSide,
|
||||||
EdgesGeometry,
|
EdgesGeometry,
|
||||||
Group,
|
Group,
|
||||||
|
@ -649,8 +650,13 @@ function generateCapMeshes(
|
||||||
? 1
|
? 1
|
||||||
: -1;
|
: -1;
|
||||||
|
|
||||||
|
const color =
|
||||||
|
mesh.material instanceof MeshStandardMaterial
|
||||||
|
? mesh.material.color
|
||||||
|
: new Color(1, 1, 1);
|
||||||
|
|
||||||
const material = new MeshStandardMaterial({
|
const material = new MeshStandardMaterial({
|
||||||
color: (mesh.material as MeshStandardMaterial).color,
|
color,
|
||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
metalness: 0.0,
|
metalness: 0.0,
|
||||||
roughness: 1.0,
|
roughness: 1.0,
|
||||||
|
|
|
@ -37,8 +37,8 @@ let overlayCamera: OrthographicCamera;
|
||||||
let overlayScene: Scene;
|
let overlayScene: Scene;
|
||||||
let maxSize = 0;
|
let maxSize = 0;
|
||||||
const compass = new Group();
|
const compass = new Group();
|
||||||
const UI_WIDTH = 200;
|
const UI_WIDTH = 150;
|
||||||
const UI_HEIGHT = 200;
|
const UI_HEIGHT = 150;
|
||||||
|
|
||||||
export function buildScene(container: HTMLElement, extent: Extent) {
|
export function buildScene(container: HTMLElement, extent: Extent) {
|
||||||
maxSize = getMaxSize(extent);
|
maxSize = getMaxSize(extent);
|
||||||
|
@ -152,11 +152,8 @@ function renderOverlay() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render the overlay scene to the screen (position it in the bottom left)
|
// 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.setViewport(10, 10, UI_WIDTH, UI_HEIGHT);
|
||||||
renderer.render(overlayScene, overlayCamera);
|
renderer.render(overlayScene, overlayCamera);
|
||||||
renderer.setScissorTest(false); // Disable scissor testing for the rest of the scene
|
|
||||||
renderer.setViewport(
|
renderer.setViewport(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue