Skip to content

Commit

Permalink
Example threejs terrain (#3429)
Browse files Browse the repository at this point in the history
* 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
MichaelLangbein authored Dec 1, 2023
1 parent 8edbac0 commit a9ffa17
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- ⚠️ Removed callback usage from `map.loadImage` in continue to below change ([#3422](https://github.com/maplibre/maplibre-gl-js/pull/3422))
- ⚠️ Changed the `GeoJSONSource`'s `getClusterExpansionZoom`, `getClusterChildren`, `getClusterLeaves` methods to return a `Promise` instead of a callback usage ([#3421](https://github.com/maplibre/maplibre-gl-js/pull/3421))
- ⚠️ Changed the `setRTLTextPlugin` function to return a promise instead of using callback ([#3418](https://github.com/maplibre/maplibre-gl-js/pull/3418)) this also changed how the RTL pluing code is handled internally by splitting the main thread and worker thread code.
- Created a new example showing how to place a threejs scene as a `CustomLayer` over maplibre 3d-terrain ([#3429](https://github.com/maplibre/maplibre-gl-js/pull/3429))
- _...Add new stuff here..._

### 🐞 Bug fixes
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
215 changes: 215 additions & 0 deletions test/examples/add-3d-model-with-terrain.html
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>

0 comments on commit a9ffa17

Please sign in to comment.