-
-
Notifications
You must be signed in to change notification settings - Fork 735
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* first prototype * edited map center so that example looks nice on small screens * updated comments to match style spec * manually created screenshot * updated styling to match lint rules * scene orientation unchanged, objects in scene rotated to match maplibres orientation * leaving models unchanged, transforming whole scene to match maplibre * using , which simplified calculations * removed trailing whitespace * Added info to CHANGELOG * updated image to match size of existing images * Fixed grammar
- Loading branch information
1 parent
8edbac0
commit a9ffa17
Showing
3 changed files
with
216 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
|
||
<head> | ||
<title>Adding 3D models with three.js on terrain</title> | ||
<meta property="og:description" | ||
content="Use a custom style layer with three.js to add 3D models to a map with 3d terrain." /> | ||
<meta charset='utf-8'> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
<link rel='stylesheet' href='../../dist/maplibre-gl.css' /> | ||
<script src='../../dist/maplibre-gl-dev.js'></script> | ||
<style> | ||
body { | ||
margin: 0; | ||
padding: 0; | ||
} | ||
|
||
html, | ||
body, | ||
#map { | ||
height: 100%; | ||
} | ||
</style> | ||
</head> | ||
|
||
<body> | ||
<script src="https://unpkg.com/three@0.147.0/build/three.min.js"></script> | ||
<script src="https://unpkg.com/three@0.147.0/examples/js/loaders/GLTFLoader.js"></script> | ||
<div id="map"></div> | ||
<script> | ||
/** | ||
* Objective: | ||
* Given two known world-locations `model1Location` and `model2Location`, | ||
* place two three.js objects on those locations at the appropriate height of | ||
* the terrain. | ||
*/ | ||
|
||
async function main() { | ||
|
||
const THREE = window.THREE; | ||
|
||
const map = new maplibregl.Map({ | ||
container: 'map', | ||
center: [11.53, 47.668], | ||
zoom: 15, | ||
pitch: 60, | ||
bearing: -45, | ||
antialias: true, | ||
style: { | ||
version: 8, | ||
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf', | ||
layers: [ | ||
{ | ||
id: 'baseColor', // Hides edges of terrain tiles, which have 'walls' going down to 0. | ||
type: 'background', | ||
paint: { | ||
'background-color': '#fff', | ||
'background-opacity': 1.0, | ||
}, | ||
}, { | ||
id: 'hills', | ||
type: 'hillshade', | ||
source: 'hillshadeSource', | ||
layout: {visibility: 'visible'}, | ||
paint: {'hillshade-shadow-color': '#473B24'} | ||
} | ||
], | ||
terrain: { | ||
source: 'terrainSource', | ||
exaggeration: 1, | ||
}, | ||
sources: { | ||
terrainSource: { | ||
type: 'raster-dem', | ||
url: 'https://demotiles.maplibre.org/terrain-tiles/tiles.json', | ||
tileSize: 256 | ||
}, | ||
hillshadeSource: { | ||
type: 'raster-dem', | ||
url: 'https://demotiles.maplibre.org/terrain-tiles/tiles.json', | ||
tileSize: 256 | ||
} | ||
}, | ||
} | ||
}); | ||
|
||
/* | ||
* Helper function used to get threejs-scene-coordinates from mercator coordinates. | ||
* This is just a quick and dirty solution - it won't work if points are far away from each other | ||
* because a meter near the north-pole covers more mercator-units | ||
* than a meter near the equator. | ||
*/ | ||
function calculateDistanceMercatorToMeters(from, to) { | ||
const mercatorPerMeter = from.meterInMercatorCoordinateUnits(); | ||
// mercator x: 0=west, 1=east | ||
const dEast = to.x - from.x; | ||
const dEastMeter = dEast / mercatorPerMeter; | ||
// mercator y: 0=north, 1=south | ||
const dNorth = from.y - to.y; | ||
const dNorthMeter = dNorth / mercatorPerMeter; | ||
return {dEastMeter, dNorthMeter}; | ||
} | ||
|
||
async function loadModel() { | ||
const loader = new THREE.GLTFLoader(); | ||
const gltf = await loader.loadAsync('https://maplibre.org/maplibre-gl-js/docs/assets/34M_17/34M_17.gltf'); | ||
const model = gltf.scene; | ||
return model; | ||
} | ||
|
||
const model1 = await loadModel(); | ||
const model2 = model1.clone(); | ||
|
||
// Known locations. We'll infer the elevation of those locations once terrain is loaded. | ||
const sceneOrigin = new maplibregl.LngLat(11.53, 47.67); | ||
const model1Location = new maplibregl.LngLat(11.531, 47.67); | ||
const model2Location = new maplibregl.LngLat(11.5245, 47.6675); | ||
|
||
// Configuration of the custom layer for a 3D model, implementing `CustomLayerInterface`. | ||
const customLayer = { | ||
id: '3d-model', | ||
type: 'custom', | ||
renderingMode: '3d', | ||
|
||
onAdd(map, gl) { | ||
/** | ||
* Setting up three.js scene. | ||
* We're placing model1 and model2 in such a way that the whole scene fits over the terrain. | ||
*/ | ||
|
||
this.camera = new THREE.Camera(); | ||
this.scene = new THREE.Scene(); | ||
// In threejs, y points up - we're rotating the scene such that it's y points along maplibre's up. | ||
this.scene.rotateX(Math.PI / 2); | ||
// In threejs, z points toward the viewer - mirroring it such that z points along maplibre's north. | ||
this.scene.scale.multiply(new THREE.Vector3(1, 1, -1)); | ||
// We now have a scene with (x=east, y=up, z=north) | ||
|
||
const light = new THREE.DirectionalLight(0xffffff); | ||
// Making it just before noon - light coming from south-east. | ||
light.position.set(50, 70, -30).normalize(); | ||
this.scene.add(light); | ||
|
||
// Axes helper to show how threejs scene is oriented. | ||
const axesHelper = new THREE.AxesHelper(100); | ||
this.scene.add(axesHelper); | ||
|
||
// Getting model elevations (in meters) relative to scene origin from maplibre's terrain. | ||
const sceneElevation = map.queryTerrainElevation(sceneOrigin) || 0; | ||
const model1Elevation = map.queryTerrainElevation(model1Location) || 0; | ||
const model2Elevation = map.queryTerrainElevation(model2Location) || 0; | ||
const model1up = model1Elevation - sceneElevation; | ||
const model2up = model2Elevation - sceneElevation; | ||
|
||
// Getting model x and y (in meters) relative to scene origin. | ||
const sceneOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(sceneOrigin); | ||
const model1Mercator = maplibregl.MercatorCoordinate.fromLngLat(model1Location); | ||
const model2Mercator = maplibregl.MercatorCoordinate.fromLngLat(model2Location); | ||
const {dEastMeter: model1east, dNorthMeter: model1north} = calculateDistanceMercatorToMeters(sceneOriginMercator, model1Mercator); | ||
const {dEastMeter: model2east, dNorthMeter: model2north} = calculateDistanceMercatorToMeters(sceneOriginMercator, model2Mercator); | ||
|
||
model1.position.set(model1east, model1up, model1north); | ||
model2.position.set(model2east, model2up, model2north); | ||
|
||
this.scene.add(model1); | ||
this.scene.add(model2); | ||
|
||
// Use the MapLibre GL JS map canvas for three.js. | ||
this.renderer = new THREE.WebGLRenderer({ | ||
canvas: map.getCanvas(), | ||
context: gl, | ||
antialias: true | ||
}); | ||
|
||
this.renderer.autoClear = false; | ||
}, | ||
|
||
render(gl, mercatorMatrix) { | ||
|
||
// `queryTerrainElevation` gives us the elevation of a point on the terrain | ||
// **relative to the elevation of `center`**, | ||
// where `center` is the point on the terrain that the middle of the camera points at. | ||
// If we didn't account for that offset, and the scene lay on a point on the terrain that is | ||
// below `center`, then the scene would appear to float in the air. | ||
const offsetFromCenterElevation = map.queryTerrainElevation(sceneOrigin) || 0; | ||
const sceneOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(sceneOrigin, offsetFromCenterElevation); | ||
|
||
const sceneTransform = { | ||
translateX: sceneOriginMercator.x, | ||
translateY: sceneOriginMercator.y, | ||
translateZ: sceneOriginMercator.z, | ||
scale: sceneOriginMercator.meterInMercatorCoordinateUnits() | ||
}; | ||
|
||
const m = new THREE.Matrix4().fromArray(mercatorMatrix); | ||
const l = new THREE.Matrix4() | ||
.makeTranslation(sceneTransform.translateX, sceneTransform.translateY, sceneTransform.translateZ) | ||
.scale(new THREE.Vector3(sceneTransform.scale, -sceneTransform.scale, sceneTransform.scale)); | ||
|
||
this.camera.projectionMatrix = m.multiply(l); | ||
this.renderer.resetState(); | ||
this.renderer.render(this.scene, this.camera); | ||
map.triggerRepaint(); | ||
} | ||
}; | ||
|
||
await map.once('load'); | ||
map.addLayer(customLayer); | ||
} | ||
|
||
main(); | ||
</script> | ||
</body> | ||
|
||
</html> |