Skip to main content

3D Model

Show a 3D model on the map using three.js. The model is Trimble-owned content from the SketchUp team. Configuration of the custom layer for a 3D model implements the CustomLayerInterface. Requires Trimble Maps v3.0.0 or later.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="https://maps-sdk.trimblemaps.com/v3/trimblemaps-3.20.0.css" />
    <script src="https://maps-sdk.trimblemaps.com/v3/trimblemaps-3.20.0.js"></script>
    <script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>
    <script src="https://unpkg.com/three@0.128.0/examples/js/loaders/ColladaLoader.js"></script>
    <style>
      body { margin: 0; padding: 0; }
      #map {
        position: absolute;
        top: 0;
        bottom: 0;
        width: 100%;
      }
      .note {
        position: absolute;
        top: 20px;
        left: 20px;
        background-color: rgba(255, 255, 255, 0.8);
        font-family: sans-serif;
        font-weight: bold;
        padding: 4px 8px;
        color: maroon;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <div class="note">Right-click drag to rotate</div>
    <script>
      TrimbleMaps.APIKey =  'YOUR_API_KEY_HERE';
      const myMap = new TrimbleMaps.Map({
        container: 'map',
        style: TrimbleMaps.Common.Style.TRANSPORTATION,
        zoom: 16,
        center: [-122.34929, 47.6216],
        pitch: 60,
        antialias: true, // create the gl context with MSAA antialiasing, so custom layers are antialiased
      });

      // configuration of the custom layer for a 3D model per the CustomLayerInterface
      const customLayer = {
        id: '3d-model',
        type: 'custom',
        renderingMode: '3d',
        onAdd: function (map, gl) {
          const scene = new THREE.Scene();
          // use the three.js Collada loader to add the 3D model to the three.js scene
          const loader = new THREE.ColladaLoader();
          // './data/space_needle.dae'
          loader.load(`${window.location.origin}/maps-sdk/data/space_needle.dae`, function (dae) {
            scene.add(dae.scene);
          }.bind(this));

          // create three.js lights to illuminate the model
          const directionalLight = new THREE.DirectionalLight(0x999999);
          directionalLight.position.set(0, -70, 100).normalize();
          scene.add(directionalLight);

          const directionalLight2 = new THREE.DirectionalLight(0x999999);
          directionalLight2.position.set(0, 70, 100).normalize();
          scene.add(directionalLight2);

          const light = new THREE.AmbientLight(0x666666); // soft white light
          scene.add(light);
          this.scene = scene;

          // use the GL JS map canvas for three.js
          const renderer = new THREE.WebGLRenderer({
            canvas: map.getCanvas(),
            context: gl,
            antialias: true,
          });

          renderer.autoClear = false;
          this.renderer = renderer;
        },
        render: function (gl, matrix) {
          // parameters to ensure the model is georeferenced correctly on the map
          const modelAltitude = 0;
          const modelOrigin = [-122.34929, 47.6204];
          const modelRotate = [Math.PI / 2, 0, 0];
          const modelAsMercatorCoordinate = TrimbleMaps.MercatorCoordinate.fromLngLat(
            modelOrigin,
            modelAltitude
          );
          // transformation parameters to position, rotate and scale the 3D model onto the map
          const modelTransform = {
            translateX: modelAsMercatorCoordinate.x,
            translateY: modelAsMercatorCoordinate.y,
            translateZ: modelAsMercatorCoordinate.z,
            rotateX: modelRotate[0],
            rotateY: modelRotate[1],
            rotateZ: modelRotate[2],
            /* Since our 3D model is in real world meters, a scale transform needs to be
             * applied since the CustomLayerInterface expects units in MercatorCoordinates.
             */
            scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits(),
          };

          const rotationX = new THREE.Matrix4().makeRotationAxis(
            new THREE.Vector3(1, 0, 0),
            modelTransform.rotateX
          );
          const rotationY = new THREE.Matrix4().makeRotationAxis(
            new THREE.Vector3(0, 1, 0),
            modelTransform.rotateY
          );
          const rotationZ = new THREE.Matrix4().makeRotationAxis(
            new THREE.Vector3(0, 0, 1),
            modelTransform.rotateZ
          );
          const m = new THREE.Matrix4().fromArray(matrix);
          const l = new THREE.Matrix4()
            .makeTranslation(modelTransform.translateX, modelTransform.translateY, modelTransform.translateZ)
            .scale(new THREE.Vector3(modelTransform.scale, -modelTransform.scale, modelTransform.scale))
            .multiply(rotationX)
            .multiply(rotationY)
            .multiply(rotationZ);

          const camera = new THREE.Camera();
          camera.projectionMatrix = m.multiply(l);
          this.renderer.resetState();
          this.renderer.render(this.scene, camera);
        },
      };

      myMap.on('style.load', function () {
       if(myMap.getLayer(customLayer.id) == null) {
         myMap.addLayer(customLayer);
       }
      });

    </script>
  </body>
</html>
Last updated June 15, 2023.