Bỏ qua

Thêm mô hình 3D bằng three.js trên địa hình

Sử dụng lớp kiểu tùy chỉnh với three.js để thêm các mô hình 3D vào bản đồ với địa hình 3D.

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

<head>
    <title>Adding 3D models using three.js on terrain</title>
    <meta property="og:description" content="Use a custom style layer with three.js to add 3D models to a map with 3d terrain." />
    <meta charset='utf-8'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='stylesheet' href='https://unpkg.com/[email protected]/dist/trackasia-gl.css' />
    <script src='https://unpkg.com/[email protected]/dist/trackasia-gl.js'></script>
    <style>
        body {
            margin: 0;
            padding: 0;
        }

        html,
        body,
        #map {
            height: 100%;
        }
    </style>
    <script type="importmap">
        {
            "imports": {
            "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
            }
        }
    </script>
</head>
<body>
<div id="map"></div>

<script type="module">
    import * as THREE from 'three';
    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

        /**
         * 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() {

            const map = new trackasiagl.Map({
                container: 'map',
                center: [11.5257, 47.668],
                zoom: 16.27,
                pitch: 60,
                bearing: -28.5,
                canvasContextAttributes: {antialias: true},
                style: {
                    version: 8,
                    layers: [
                        {
                            id: 'baseColor', // Hides edges of terrain tiles, which have 'walls' going down to 0.
                            type: 'background',
                            paint: {
                                'background-color': '#fff',
                                'background-opacity': 1.0,
                            },
                        }, {
                            id: 'hills',
                            type: 'hillshade',
                            source: 'hillshadeSource',
                            layout: {visibility: 'visible'},
                            paint: {'hillshade-shadow-color': '#473B24'}
                        }
                    ],
                    terrain: {
                        source: 'terrainSource',
                        exaggeration: 1,
                    },
                    sources: {
                        terrainSource: {
                            type: 'raster-dem',
                            url: 'https://static.track-asia.com/demotiles/terrain-tiles/tiles.json',
                            tileSize: 256
                        },
                        hillshadeSource: {
                            type: 'raster-dem',
                            url: 'https://static.track-asia.com/demotiles/terrain-tiles/tiles.json',
                            tileSize: 256
                        }
                    },
                }
            });

            /*
            * 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 GLTFLoader();
                const gltf = await loader.loadAsync('https://docs.track-asia.com/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 trackasiagl.LngLat(11.5255, 47.6677);
            const model1Location = new trackasiagl.LngLat(11.527, 47.6678);
            const model2Location = new trackasiagl.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 trackasia's up.
                    this.scene.rotateX(Math.PI / 2);
                    // In threejs, z points toward the viewer - mirroring it such that z points along trackasia'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 above sea level
                    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 = trackasiagl.MercatorCoordinate.fromLngLat(sceneOrigin);
                    const model1Mercator = trackasiagl.MercatorCoordinate.fromLngLat(model1Location);
                    const model2Mercator = trackasiagl.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 TrackAsia 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) {
                    const offsetFromCenterElevation = map.queryTerrainElevation(sceneOrigin) || 0;
                    const sceneOriginMercator = trackasiagl.MercatorCoordinate.fromLngLat(sceneOrigin, offsetFromCenterElevation);

                    const sceneTransform = {
                        translateX: sceneOriginMercator.x,
                        translateY: sceneOriginMercator.y,
                        translateZ: sceneOriginMercator.z,
                        scale: sceneOriginMercator.meterInMercatorCoordinateUnits()
                    };

                    const m = new THREE.Matrix4().fromArray(args.defaultProjectionData.mainMatrix);
                    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>