mapbox-gl | 3.3 添加模型

1,089 阅读5分钟

mapbox-gl | 3.3 添加模型

简述

随着版本的更新,mapbox逐渐支持更多的三维效果,加载模型是很多场景中常见的功能,本篇文章带来三种官方加载模型的方式。

1. mapbox加载

该方法为mapbox3.x进行支持,并处于实验性阶段

 map.on('style.load', () => {
        // set the light preset to be in dusk mode.
        map.setConfigProperty('basemap', 'lightPreset', 'dusk');

        // add a geojson source with a polygon to be used in the clip layer.
        map.addSource('eraser', {
            'type': 'geojson',
            'data': {
                'type': 'FeatureCollection',
                'features': [
                    {
                        'type': 'Feature',
                        'properties': {},
                        'geometry': {
                            'coordinates': [
                                [
                                    [-0.12573, 51.53222],
                                    [-0.12458, 51.53219],
                                    [-0.12358, 51.53492],
                                    [-0.12701, 51.53391],
                                    [-0.12573, 51.53222]
                                ]
                            ],
                            'type': 'Polygon'
                        }
                    }
                ]
            }
        });

        // add a geojson source which specifies the custom model to be used by the model layer.
        map.addSource('model', {
            'type': 'geojson',
            'data': {
                'type': 'Feature',
                'properties': {
                    'model-uri':
                        'https://docs.mapbox.com/mapbox-gl-js/assets/tower.glb'
                },
                'geometry': {
                    'coordinates': [-0.12501974, 51.5332374],
                    'type': 'Point'
                }
            }
        });

        // add the clip layer and configure it to also remove symbols and trees.
        // `clip-layer-scope` layout property is used to specify that only models from the Mapbox Standard Style should be clipped.
        // this will prevent the newly added model from getting clipped.
        map.addLayer({
            'id': 'eraser',
            'type': 'clip',
            'source': 'eraser',
            'layout': {
                // specify the layer types to be removed by this clip layer
                'clip-layer-types': ['symbol', 'model'],
                'clip-layer-scope': ['basemap']
            }
        });

        // add the model layer and specify the appropriate `slot` to ensure the symbols are rendered correctly.
        map.addLayer({
            'id': 'tower',
            'type': 'model',
            'slot': 'middle',
            'source': 'model',
            'minzoom': 15,
            'layout': {
                'model-id': ['get', 'model-uri']
            },
            'paint': {
                'model-opacity': 1,
                'model-rotation': [0.0, 0.0, 35.0],
                'model-scale': [0.8, 0.8, 1.2],
                'model-color-mix-intensity': 0,
                'model-cast-shadows': true,
                'model-emissive-strength': 0.8
            }
        });
    });

代码结构很简单,添加了两个数据源和两个图层,分别对应着裁剪面和模型。

先说一下裁剪面,同样是3.x新的特性,自3.x版本开始,地图样式开始增加更多的3d样式,如:树、标志性建筑等,当我们想将一部分默认模型替换成自己更加精细的模型时,便可以使用裁剪面将默认模型裁剪掉,所以上述代码中,添加一个面的数据源,然后以此创建裁剪面图层,裁剪默认模型。

添加模型,首先添加了一个点数据源,其中属性包含模型地址,然后添加图层,typemodel,设置mode-id为模型地址,然后设置其基本属性,如缩放,旋转,阴影等。

从上面的逻辑来看,mapbox对模型的态度,更希望它是一种丰富地图的元素,和点线面地位相同,对于模型动画这类目前没看到要支持的意思,所以对于大部分城市模型来说,这样足够业务使用了,但并不能覆盖所有场景。

2. three加载

    // parameters to ensure the model is georeferenced correctly on the map
    const modelOrigin = [148.9819, -35.39847];
    const modelAltitude = 0;
    const modelRotate = [Math.PI / 2, 0, 0];

    const modelAsMercatorCoordinate = mapboxgl.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 the 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 THREE = window.THREE;

    // 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) {
            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 THREE.GLTFLoader();
            loader.load(
                'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf',
                (gltf) => {
                    this.scene.add(gltf.scene);
                }
            );
            this.map = map;

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

            this.renderer.autoClear = false;
        },
        render: function (gl, matrix) {
            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);

            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, 'waterway-label');
    });

CustomLayer是mapbox提供的使用webgl的上层入口,大部分情况下,以这种形式结合three去使用,如果webgl熟练度高的话,也可以使用原生去写。

CustomLayerr有两种写法,一种是上述这种Object配置文件的形式,另一种是Class类的形式,殊途同归,没有太大区别,关键在于onAddrender两个方法,即初始化和每次渲染要执行的内容。

首先看一下声明的一些变量 modelOrigin modelAltitude modelRotate 定义了模型的坐标、海拔和旋转,然后通过模型mapboxgl.MercatorCoordinate.fromLngLat 将坐标转换为墨卡托坐标,需要注意的是,这个墨卡托坐标范围是[0,0]~[1,1].

modelTransform设置了平移、旋转和缩放,缩放,meterInMercatorCoordinateUnits是获取当前坐标下,像素与米的比例.

onAdd中是初始化Three场景,添加一个模型到场景,模型没有重新设置位置,默认在场景中心,render中设置了矩阵,如果不太熟悉这一块的内容也没关系,你只需要知道这样设置Three.Camera的投影矩阵后,就可以让两个场景叠加展示了。

需要注意的是,这样的矩阵转换是不完善的,虽然模型展示正常,但如果按Three的思路去设置点击(raycast)或精灵图等与相机相关的功能时,你会发现它并不会正常工作,所以这样的方式,要比第一种应用面更广一些,但仍不会覆盖全场景应用。

3.threebox加载

    const tb = (window.tb = new Threebox(
        map,
        map.getCanvas().getContext('webgl'),
        {
            defaultLights: true
        }
    ));

    map.on('style.load', () => {
        map.addLayer({
            id: 'custom-threebox-model',
            type: 'custom',
            renderingMode: '3d',
            onAdd: function () {
                // Creative Commons License attribution:  Metlife Building model by https://sketchfab.com/NanoRay
                // https://sketchfab.com/3d-models/metlife-building-32d3a4a1810a4d64abb9547bb661f7f3
                const scale = 3.2;
                const options = {
                    obj: 'https://docs.mapbox.com/mapbox-gl-js/assets/metlife-building.gltf',
                    type: 'gltf',
                    scale: { x: scale, y: scale, z: 2.7 },
                    units: 'meters',
                    rotation: { x: 90, y: -90, z: 0 }
                };

                tb.loadObj(options, (model) => {
                    model.setCoords([-73.976799, 40.754145]);
                    model.setRotation({ x: 0, y: 0, z: 241 });
                    tb.add(model);
                });
            },

            render: function () {
                tb.update();
            }
        });
    });

threebox是一个第三方工具库,它会帮助你完成矩阵转换和坐标转换,是你能在mapbox中使用更多的three功能。关于其版本,原作者不再维护,jscastro76fork仓库继续维护(近期更新也很少了),在球形地图之前,他的矩阵转换是正常的,球形地图版本后,在地图开始展现球形时,矩阵转换失效,非球形时,仍然有效。

代码比较简洁,首先初始化threebox,通过loadObj方法加载模型,用setCoords转换坐标,随后添加进场景。

threebox相对与第二种方法,他的矩阵转换是完善的,可以让相机相关功能正常工作。

总结

以上是三种官方案例中的加载模型,随着版本的更新,mapbox自身也会更加支持这些功能,不过学习一下threebox的实现原理,对我们理解mapbox的代码是有帮助的。