Skip to content

Add a 3D model to globe using three.js

Use a custom style layer with three.js to add a 3D model to a globe.

<!DOCTYPE html>
<html lang="en">

<head>
    <title>Add a 3D model to globe using three.js</title>
    <meta property="og:description" content="Use a custom style layer with three.js to add a 3D model to a globe." />
    <meta charset='utf-8'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='stylesheet' href='https://unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.css' />
    <script src='https://unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js'></script>
    <style>
        body {
            margin: 0;
            padding: 0;
        }

        html,
        body,
        #map {
            height: 100%;
        }
        #project {
            display: block;
            position: absolute;
            top: 20px;
            left: 50%;
            transform: translate(-50%);
            width: 50%;
            height: 40px;
            padding: 10px;
            border: none;
            border-radius: 3px;
            font-size: 12px;
            text-align: center;
            color: #fff;
            background: #ee8a65;
        }
    </style>
</head>

<body>
    <script type="importmap">
        {
            "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.169.0/examples/jsm/"
            }
        }
    </script>

    <div id="map"></div>
    <br />
    <button id="project">Toggle projection</button>
    <script type="module">

        import * as THREE from 'three';
        import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

        const map = new maplibregl.Map({
            container: 'map',
            style: 'https://api.maptiler.com/maps/basic/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
            zoom: 5.5,
            center: [150.16546137527212, -35.017179237129994],
            maxPitch: 80,
            pitch: 70,
            canvasContextAttributes: {antialias: true} // create the gl context with MSAA antialiasing, so custom layers are antialiased
        });

        map.on('style.load', () => {
            map.setProjection({
                type: 'globe', // Set projection to globe
            });
        });

        // The API demonstrated in this example will work regardless of projection.
        // Click this button to toggle it.
        document.getElementById('project').addEventListener('click', () => {
            // Toggle projection
            const currentProjection = map.getProjection();
            map.setProjection({
                type: currentProjection.type === 'globe' ? 'mercator' : 'globe',
            });
        });

        // configuration of the custom layer for a 3D model per the CustomLayerInterface
        const customLayer = {
            id: '3d-model',
            type: 'custom',
            renderingMode: '3d', // The layer MUST be marked as 3D in order to get the proper depth buffer with globe depths in it.
            onAdd(map, gl) {
                this.camera = new THREE.Camera();
                this.scene = new THREE.Scene();

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

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

                // use the three.js GLTF loader to add the 3D model to the three.js scene
                const loader = new GLTFLoader();
                loader.load(
                    'https://maplibre.org/maplibre-gl-js/docs/assets/34M_17/34M_17.gltf',
                    (gltf) => {
                        this.scene.add(gltf.scene);
                    }
                );
                this.map = map;

                // 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, args) {
                // parameters to ensure the model is georeferenced correctly on the map
                const modelOrigin = [148.9819, -35.39847];
                const modelAltitude = 0;

                // Make the object ~10s of km tall to make it visible at planetary scale.
                const scaling = 10_000.0;

                // We can use this API to get the correct model matrix.
                // It will work regardless of current projection.
                // See MapLibre source code, file "mercator_transform.ts" or "vertical_perspective_transform.ts".
                const modelMatrix = map.transform.getMatrixForModel(modelOrigin, modelAltitude);
                const m = new THREE.Matrix4().fromArray(args.defaultProjectionData.mainMatrix);
                const l = new THREE.Matrix4().fromArray(modelMatrix).scale(
                    new THREE.Vector3(
                        scaling,
                        scaling,
                        scaling
                    )
                );

                this.camera.projectionMatrix = m.multiply(l);
                this.renderer.resetState();
                this.renderer.render(this.scene, this.camera);
                this.map.triggerRepaint();
            }
        };

        map.on('style.load', () => {
            map.addLayer(customLayer);
        });
    </script>
</body>

</html>