Add elevation to topography
This commit is contained in:
parent
c414b9d2d6
commit
3e6504d6b0
3 changed files with 305 additions and 39 deletions
250
app/three/CustomMapHeightNodeShader.ts
Normal file
250
app/three/CustomMapHeightNodeShader.ts
Normal file
|
@ -0,0 +1,250 @@
|
|||
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 | null,
|
||||
mapView: MapView | undefined | null,
|
||||
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,
|
||||
transparent: true,
|
||||
opacity: 1.0,
|
||||
depthTest: true,
|
||||
})
|
||||
);
|
||||
|
||||
if (parentNode === null) {
|
||||
parentNode = undefined;
|
||||
}
|
||||
|
||||
if (mapView === null) {
|
||||
mapView = undefined;
|
||||
}
|
||||
|
||||
super(
|
||||
parentNode,
|
||||
mapView,
|
||||
location,
|
||||
level,
|
||||
x,
|
||||
y,
|
||||
CustomMapHeightNodeShader.geometry,
|
||||
material
|
||||
);
|
||||
|
||||
this.frustumCulled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 to the
|
||||
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;
|
||||
|
||||
vec3 _transformed = position + _height * normal;
|
||||
|
||||
// Vertex position based on height
|
||||
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);
|
||||
|
||||
// @ts-ignore
|
||||
this.material.map = MapHeightNodeShader.defaultTexture;
|
||||
// @ts-ignore
|
||||
this.material.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 any);
|
||||
texture.generateMipmaps = false;
|
||||
texture.format = RGBAFormat;
|
||||
texture.magFilter = NearestFilter;
|
||||
texture.minFilter = NearestFilter;
|
||||
texture.needsUpdate = true;
|
||||
|
||||
// @ts-ignore
|
||||
this.material.userData.heightMap.value = texture;
|
||||
} catch (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)
|
||||
// @ts-ignore
|
||||
this.material.userData.heightMap.value =
|
||||
CustomMapHeightNodeShader.defaultHeightTexture;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
this.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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,14 @@ import {
|
|||
} from "./utils/build-clipping-planes";
|
||||
import { buildCoordinateGrid } from "./utils/build-coordinate-grid";
|
||||
import { DragControls } from "three/examples/jsm/Addons.js";
|
||||
import { MapView, OpenStreetMapsProvider } from "geo-three";
|
||||
import {
|
||||
DebugProvider,
|
||||
HeightDebugProvider,
|
||||
MapTilerProvider,
|
||||
MapView,
|
||||
OpenStreetMapsProvider,
|
||||
} from "geo-three";
|
||||
import { CustomMapHeightNodeShader } from "./CustomMapHeightNodeShader";
|
||||
|
||||
export class SceneView {
|
||||
private _scene: Scene;
|
||||
|
@ -98,6 +105,7 @@ export class SceneView {
|
|||
}
|
||||
}
|
||||
|
||||
const MAPTILER_API_KEY = "1JkD1W8u5UM5Tjd8r3Wl ";
|
||||
async function init(container: HTMLElement, modelId = MODEL_ID) {
|
||||
const modelData = await getMetadata(SERVICE_URL + modelId);
|
||||
const mappedFeatures = modelData.mappedfeatures;
|
||||
|
@ -147,16 +155,20 @@ async function init(container: HTMLElement, modelId = MODEL_ID) {
|
|||
annotationsGroup.visible = false;
|
||||
scene.add(annotationsGroup);
|
||||
|
||||
//const axesHelper = new AxesHelper(5);
|
||||
//scene.add(axesHelper);
|
||||
|
||||
// Create a map tiles provider object
|
||||
const provider = new OpenStreetMapsProvider();
|
||||
const heightProvider = new MapTilerProvider(
|
||||
MAPTILER_API_KEY,
|
||||
"tiles",
|
||||
"terrain-rgb",
|
||||
"png"
|
||||
);
|
||||
|
||||
// Create the map view of OSM topography
|
||||
const map = new MapView(MapView.PLANAR, provider);
|
||||
// Create the map view for OSM topography
|
||||
const map = new MapView(MapView.PLANAR, provider, heightProvider);
|
||||
const customNode = new CustomMapHeightNodeShader(null, map);
|
||||
map.setRoot(customNode);
|
||||
map.rotateX(Math.PI / 2);
|
||||
// map.position.setComponent(2, -100);
|
||||
map.name = "topography";
|
||||
scene.add(map);
|
||||
|
||||
|
|
|
@ -4,39 +4,14 @@ import {
|
|||
WebGLRenderer,
|
||||
AmbientLight,
|
||||
DirectionalLight,
|
||||
Group,
|
||||
Object3D,
|
||||
Vector3,
|
||||
} from "three";
|
||||
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
import { getCenter3D, getMaxSize } from "./utils";
|
||||
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
function buildDefaultLights() {
|
||||
// Ambient light
|
||||
scene.add(new AmbientLight(0xaaaaaa));
|
||||
|
||||
// Directional lights
|
||||
const opt = {
|
||||
azimuth: 220,
|
||||
altitude: 45,
|
||||
};
|
||||
|
||||
const lambda = (90 - opt.azimuth) * DEG2RAD;
|
||||
const phi = opt.altitude * DEG2RAD;
|
||||
|
||||
const x = Math.cos(phi) * Math.cos(lambda);
|
||||
const y = Math.cos(phi) * Math.sin(lambda);
|
||||
const z = Math.sin(phi);
|
||||
|
||||
const light1 = new DirectionalLight(0xffffff, 0.5);
|
||||
light1.position.set(x, y, z);
|
||||
scene.add(light1);
|
||||
|
||||
// Thin light from the opposite direction
|
||||
const light2 = new DirectionalLight(0xffffff, 0.1);
|
||||
light2.position.set(-x, -y, -z);
|
||||
return light2;
|
||||
}
|
||||
|
||||
export interface Extent {
|
||||
xmin: number;
|
||||
ymin: number;
|
||||
|
@ -90,14 +65,13 @@ export function buildScene(container: HTMLElement, extent: Extent) {
|
|||
controls.maxDistance = maxSize * 5;
|
||||
controls.update();
|
||||
|
||||
// Scene will hold all our elements such as objects, cameras and lights
|
||||
// Scene
|
||||
// set wireframe to false on initial load
|
||||
scene = new Scene();
|
||||
scene.userData.wireframe = false;
|
||||
|
||||
// Add lights to the scene
|
||||
const lights = buildDefaultLights();
|
||||
lights.name = "lights";
|
||||
scene.add(lights);
|
||||
buildDefaultLights(scene, extent);
|
||||
|
||||
return { renderer, scene, camera, controls };
|
||||
}
|
||||
|
@ -119,3 +93,33 @@ function animate() {
|
|||
// required if controls.enableDamping or controls.autoRotate are set to true
|
||||
controls.update();
|
||||
}
|
||||
|
||||
function buildDefaultLights(scene: Scene, extent: Extent) {
|
||||
const center = getCenter3D(extent);
|
||||
|
||||
const lights = [];
|
||||
// Ambient light
|
||||
const ambient = new AmbientLight(0xffffff, 1.0);
|
||||
lights.push(ambient);
|
||||
|
||||
// Directional lights
|
||||
const directionalLight = new DirectionalLight(0xffffff, 1);
|
||||
directionalLight.position.set(
|
||||
center.x,
|
||||
center.y - 200000,
|
||||
extent.zmax + 100000
|
||||
);
|
||||
|
||||
// Create a target for the ligth
|
||||
const target = new Object3D();
|
||||
target.position.set(center.x, center.y, center.z);
|
||||
scene.add(target);
|
||||
|
||||
directionalLight.target = target;
|
||||
lights.push(directionalLight);
|
||||
|
||||
const lightsGroup = new Group();
|
||||
lightsGroup.name = "lights";
|
||||
lightsGroup.add(...lights);
|
||||
scene.add(lightsGroup);
|
||||
}
|
||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue