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.1.css" /> <script src="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.1.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("68B06901AF7DA34884CE5FB7A202A743"); const THREE = window.THREE; const map = new TrimbleMaps.Map({ container: "map", center: [11.5257, 47.668], zoom: 16.27, 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, }); } 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, }); } 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("/docs/assets/34M_17/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.1.css" /> <script src="https://maps-sdk.trimblemaps.com/v4/trimblemaps-4.2.1.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("68B06901AF7DA34884CE5FB7A202A743"); const THREE = window.THREE; const map = new TrimbleMaps.Map({ container: "map", center: [11.5257, 47.668], zoom: 16.27, 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, }); } 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, }); } 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("/docs/assets/34M_17/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>