Skip to content

Add a 3D model with babylon.js

Use a custom style layer with babylon.js to add a 3D model to the map.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Add a 3D model with babylon.js</title>
    <meta property="og:description" content="Use a custom style layer with babylon.js to add a 3D model to the map." />
    <meta charset='utf-8'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='stylesheet' href='https://unpkg.com/maplibre-gl@4.1.3/dist/maplibre-gl.css' />
    <script src='https://unpkg.com/maplibre-gl@4.1.3/dist/maplibre-gl.js'></script>
    <style>
        body { margin: 0; padding: 0; }
        html, body, #map { height: 100%; }
    </style>
</head>
<body>
<script src="https://unpkg.com/babylonjs@5.42.2/babylon.js"></script>
<script src="https://unpkg.com/babylonjs-loaders@5.42.2/babylonjs.loaders.min.js"></script>
<div id="map"></div>
<script>
    const BABYLON = window.BABYLON;

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

    // World matrix parameters
    const worldOrigin = [148.9819, -35.39847];
    const worldAltitude = 0;

    // Maplibre.js default coordinate system (no rotations)
    // +x east, -y north, +z up
    //var worldRotate = [0, 0, 0];

    // Babylon.js default coordinate system
    // +x east, +y up, +z north
    const worldRotate = [Math.PI / 2, 0, 0];

    // Calculate mercator coordinates and scale
    const worldOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(
        worldOrigin,
        worldAltitude
    );
    const worldScale = worldOriginMercator.meterInMercatorCoordinateUnits();

    // Calculate world matrix
    const worldMatrix = BABYLON.Matrix.Compose(
        new BABYLON.Vector3(worldScale, worldScale, worldScale),
        BABYLON.Quaternion.FromEulerAngles(
            worldRotate[0],
            worldRotate[1],
            worldRotate[2]
        ),
        new BABYLON.Vector3(
            worldOriginMercator.x,
            worldOriginMercator.y,
            worldOriginMercator.z
        )
    );

    // configuration of the custom layer for a 3D model per the CustomLayerInterface
    const customLayer = {
        id: '3d-model',
        type: 'custom',
        renderingMode: '3d',
        onAdd (map, gl) {
            this.engine = new BABYLON.Engine(
                gl,
                true,
                {
                    useHighPrecisionMatrix: true // Important to prevent jitter at mercator scale
                },
                true
            );
            this.scene = new BABYLON.Scene(this.engine);
            /**
            * optionally add
            * this.scene.autoClearDepthAndStencil = false
            * and for renderingGroupIds set this individually via
            * this.scene.setRenderingAutoClearDepthStencil(1,false)
            * to allow blending with maplibre scene
            * as documented in https://doc.babylonjs.com/features/featuresDeepDive/scene/optimize_your_scene#reducing-calls-to-glclear
            */
            this.scene.autoClear = false;
            /**
            * use detachControl if you only want to interact with maplibre-gl and do not need pointer events of babylonjs.
            * alternatively exchange this.scene.detachControl() with the following two lines, they will allow bubbling up events to maplibre-gl.
            * this.scene.preventDefaultOnPointerDown = false
            * this.scene.preventDefaultOnPointerUp = false
            * https://doc.babylonjs.com/typedoc/classes/BABYLON.Scene#preventDefaultOnPointerDown
            */
            this.scene.detachControl();

            this.scene.beforeRender = () => {
                this.engine.wipeCaches(true);
            };

            // create simple camera (will have its project matrix manually calculated)
            this.camera = new BABYLON.Camera(
                'Camera',
                new BABYLON.Vector3(0, 0, 0),
                this.scene
            );

            // create simple light
            const light = new BABYLON.HemisphericLight(
                'light1',
                new BABYLON.Vector3(0, 0, 100),
                this.scene
            );
            light.intensity = 0.7;

            // Add debug axes viewer, positioned at origin, 10 meter axis lengths
            new BABYLON.AxesViewer(this.scene, 10);

            // load GLTF model in to the scene
            BABYLON.SceneLoader.LoadAssetContainerAsync(
                'https://maplibre.org/maplibre-gl-js/docs/assets/34M_17/34M_17.gltf',
                '',
                this.scene
            ).then((modelContainer) => {
                modelContainer.addAllToScene();

                const rootMesh = modelContainer.createRootMesh();

                // If using maplibre.js coordinate system (+z up)
                //rootMesh.rotation.x = Math.PI/2

                // Create a second mesh
                const rootMesh2 = rootMesh.clone();

                // Position in babylon.js coordinate system
                rootMesh2.position.x = 25; // +east, meters
                rootMesh2.position.z = 25; // +north, meters
            });

            this.map = map;
        },
        render (gl, matrix) {
            const cameraMatrix = BABYLON.Matrix.FromArray(matrix);

            // world-view-projection matrix
            const wvpMatrix = worldMatrix.multiply(cameraMatrix);

            this.camera.freezeProjectionMatrix(wvpMatrix);

            this.scene.render(false);
            this.map.triggerRepaint();
        }
    };

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