Add 3D Model With Terrain
Use a custom style layer with three.js to add 3D models to a map with 3D terrain. Requires Trimble Maps v4.0.0 or later.
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Adding 3D models with three.js on terrain</title>
<link rel="stylesheet" href="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.5.css" />
<script src="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.5.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" class="trimblemaps-map">
<div class="trimblemaps-canvas-container trimblemaps-interactive trimblemaps-touch-drag-pan trimblemaps-touch-zoom-rotate">
<canvas class="trimblemaps-canvas" tabindex="0" aria-label="Map" role="region" width="935" height="1316" style="width: 935px; height: 1316px"></canvas>
</div>
<div class="trimblemaps-control-container">
<div class="trimblemaps-ctrl-top-left"></div>
<div class="trimblemaps-ctrl-top-right"></div>
<div class="trimblemaps-ctrl-bottom-left"></div>
<div class="trimblemaps-ctrl-bottom-right">
<div class="trimblemaps-ctrl trimblemaps-ctrl-attrib trimblemaps-compact trimblemaps-compact-show" open="">
<div class="trimblemaps-ctrl-attrib-inner">
<a href="https://maps.trimble.com/" target="_blank">TrimbleMaps</a> | <a href="https://developer.trimblemaps.com">© Trimble Inc.</a> - Certain POI data by Infogroup
© 2022. | <a href="https://openmaptiles.org/" rel="nofollow">© OpenMapTiles</a>
<a href="https://www.openstreetmap.org/copyright" rel="nofollow">© OpenStreetMap contributors</a>
</div>
</div>
</div>
</div>
</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() {
TrimbleMaps.setAPIKey('YOUR_API_KEY_HERE');
const THREE = window.THREE;
const map = new TrimbleMaps.Map({
container: "map",
center: [11.5257, 47.668],
zoom: 13.9,
pitch: 60,
bearing: -28.5,
antialias: true,
style: TrimbleMaps.Common.Style.TRANSPORTATION,
});
map.on("sourcedata", (e) => {
if (e.sourceId === "terrainSource" && e.isSourceLoaded) {
//enable the terrain control once terrain sources are loaded.
map.setTerrain({
source: "terrainSource",
exaggeration: 1,
});
}
});
map.on("load", () => {
// Add new sources and layers
if (!map.getSource("hillshadeSource")) {
map.addSource("hillshadeSource", {
type: "raster-dem",
tiles: ["https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"],
encoding: "terrarium",
tileSize: 256,
maxzoom: 14
});
}
if (!map.getSource("terrainSource")) {
map.addSource("terrainSource", {
type: "raster-dem",
tiles: ["https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"],
encoding: "terrarium",
tileSize: 256,
maxzoom: 14
});
}
if (!map.getLayer("hills")) {
map.addLayer(
{
id: "hills",
type: "hillshade",
source: "hillshadeSource",
layout: { visibility: "visible" },
paint: { "hillshade-shadow-color": "#473B24" },
},
"admin_boundary",
);
}
});
/*
* 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://developer.trimblemaps.com/maps-sdk/3d-model-demo/34M_17.gltf");
const model = gltf.scene;
return model;
}
// Known locations. We'll infer the elevation of those locations once terrain is loaded.
const sceneOrigin = new TrimbleMaps.LngLat(11.5255, 47.6677);
const model1Location = new TrimbleMaps.LngLat(11.527, 47.6678);
const model2Location = new TrimbleMaps.LngLat(11.5249, 47.6676);
// 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(60);
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 = TrimbleMaps.MercatorCoordinate.fromLngLat(sceneOrigin);
const model1Mercator = TrimbleMaps.MercatorCoordinate.fromLngLat(model1Location);
const model2Mercator = TrimbleMaps.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 TrimbleMaps 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 = TrimbleMaps.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();
},
};
const results = await Promise.all([map.once("load"), loadModel()]);
const model1 = results[1];
const model2 = model1.clone();
map.addLayer(customLayer);
}
main();
</script>
</body>
</html>
Add 3D Model With Terrain
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Adding 3D models with three.js on terrain</title>
<link rel="stylesheet" href="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.5.css" />
<script src="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.5.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" class="trimblemaps-map">
<div class="trimblemaps-canvas-container trimblemaps-interactive trimblemaps-touch-drag-pan trimblemaps-touch-zoom-rotate">
<canvas class="trimblemaps-canvas" tabindex="0" aria-label="Map" role="region" width="935" height="1316" style="width: 935px; height: 1316px"></canvas>
</div>
<div class="trimblemaps-control-container">
<div class="trimblemaps-ctrl-top-left"></div>
<div class="trimblemaps-ctrl-top-right"></div>
<div class="trimblemaps-ctrl-bottom-left"></div>
<div class="trimblemaps-ctrl-bottom-right">
<div class="trimblemaps-ctrl trimblemaps-ctrl-attrib trimblemaps-compact trimblemaps-compact-show" open="">
<div class="trimblemaps-ctrl-attrib-inner">
<a href="https://maps.trimble.com/" target="_blank">TrimbleMaps</a> | <a href="https://developer.trimblemaps.com">© Trimble Inc.</a> - Certain POI data by Infogroup
© 2022. | <a href="https://openmaptiles.org/" rel="nofollow">© OpenMapTiles</a>
<a href="https://www.openstreetmap.org/copyright" rel="nofollow">© OpenStreetMap contributors</a>
</div>
</div>
</div>
</div>
</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() {
TrimbleMaps.setAPIKey('YOUR_API_KEY_HERE');
const THREE = window.THREE;
const map = new TrimbleMaps.Map({
container: "map",
center: [11.5257, 47.668],
zoom: 13.9,
pitch: 60,
bearing: -28.5,
antialias: true,
style: TrimbleMaps.Common.Style.TRANSPORTATION,
});
map.on("sourcedata", (e) => {
if (e.sourceId === "terrainSource" && e.isSourceLoaded) {
//enable the terrain control once terrain sources are loaded.
map.setTerrain({
source: "terrainSource",
exaggeration: 1,
});
}
});
map.on("load", () => {
// Add new sources and layers
if (!map.getSource("hillshadeSource")) {
map.addSource("hillshadeSource", {
type: "raster-dem",
tiles: ["https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"],
encoding: "terrarium",
tileSize: 256,
maxzoom: 14
});
}
if (!map.getSource("terrainSource")) {
map.addSource("terrainSource", {
type: "raster-dem",
tiles: ["https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"],
encoding: "terrarium",
tileSize: 256,
maxzoom: 14
});
}
if (!map.getLayer("hills")) {
map.addLayer(
{
id: "hills",
type: "hillshade",
source: "hillshadeSource",
layout: { visibility: "visible" },
paint: { "hillshade-shadow-color": "#473B24" },
},
"admin_boundary",
);
}
});
/*
* 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://developer.trimblemaps.com/maps-sdk/3d-model-demo/34M_17.gltf");
const model = gltf.scene;
return model;
}
// Known locations. We'll infer the elevation of those locations once terrain is loaded.
const sceneOrigin = new TrimbleMaps.LngLat(11.5255, 47.6677);
const model1Location = new TrimbleMaps.LngLat(11.527, 47.6678);
const model2Location = new TrimbleMaps.LngLat(11.5249, 47.6676);
// 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(60);
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 = TrimbleMaps.MercatorCoordinate.fromLngLat(sceneOrigin);
const model1Mercator = TrimbleMaps.MercatorCoordinate.fromLngLat(model1Location);
const model2Mercator = TrimbleMaps.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 TrimbleMaps 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 = TrimbleMaps.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();
},
};
const results = await Promise.all([map.once("load"), loadModel()]);
const model1 = results[1];
const model2 = model1.clone();
map.addLayer(customLayer);
}
main();
</script>
</body>
</html>