mapboxgl+threejs

2,178 阅读1分钟

mapboxgl+threejs的结合使用

安装插件

pnpm i mapbox-gl three -S

创建mapbox地图

<script setup>
// mapboxgl地图
import mapboxgl from 'mapbox-gl';

// 初始化生命周期
onMounted(() => {
    init();
});

function init() {
    mapboxgl.accessToken = 'you.accessToken';
    map = new mapboxgl.Map({
        container: 'container', // 渲染dom
        style: 'mapbox://styles/mapbox/streets-v11', 
        center: [80, 80], // 地图中心点
        zoom: 1,
        pitch: 0,
        bearing: 0,
        projection: 'globe', // 为 3D 地球
        antialias: false, //抗锯齿,通过false关闭提升性能
    });
}
</script>

创建three

<template>
    <div class="w100 h100" style="position: relative">
        <div ref="basicMapbox" style="position: relative" class="w100 h100"></div>
    </div>
</template>

<script setup>
// mapboxgl地图
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'; 
// mapboxgl汉化
import MapboxLanguage from '@mapbox/mapbox-gl-language';
// THREE
import * as THREE from 'three';
// 加载器
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
// label标签
import { CSS2DObject, CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer';

let map, // 地图
    renderer, //  WebGLRenderer渲染器
    scene, // 场景
    camera; // 相机

// 初始化生命周期
onMounted(() => {
    init();
});
onBeforeUnmount(() => {
    cancelAnimationFrame(requestAnimationFrameIndex);
    // 释放显存
    renderer?.dispose();
    scene = null;
    map = null;
});

const basicMapbox = ref(null),
    start = {
        center: [80, 80],
        zoom: 1,
        pitch: 0,
        bearing: 0,
    },
    end = {
        center: [118.72791630249077, 32.00910104313064],
        zoom: 17,
        bearing: 60, //目标方位角
        pitch: 75,
    };

// 初始化函数
function init() {
    mapboxgl.accessToken = 'pk.eyJ1IjoicGxheS1pc2FhYyIsImEiOiJjazU0cDkzbWowamd2M2dtemd4bW9mbzRhIn0.cxD4Fw3ZPB_taMkyUSFENA';
    map = new mapboxgl.Map({
        container: basicMapbox.value,
        style: 'mapbox://styles/mapbox-map-design/ckhqrf2tz0dt119ny6azh975y',
        ...start,
        projection: 'globe', // 为 3D 地球
        antialias: true, //抗锯齿,通过false关闭提升性能
    });
    map.addControl(new MapboxLanguage({ defaultLanguage: 'zh-Hans' }));
    map.addControl(new mapboxgl.NavigationControl(), 'top-left');
    // 添加threejs
    addThree();
}

// 添加threejs
function addThree() {
    // 确保模型在地图上正确地理参照的参数
    const modelOrigin = [118.72791630249077, 32.00910104313064],
        modelAltitude = 0,
        modelRotate = [Math.PI / 2, 0, 0],
        modelScale = 5.41843220338983e-8;

    // 用于在地图上定位、旋转和缩放三维模型的变换参数
    const modelTransform = {
        translateX: mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude).x,
        translateY: mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude).y,
        translateZ: mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude).z,
        rotateX: modelRotate[0],
        rotateY: modelRotate[1],
        rotateZ: modelRotate[2],
        /*由于我们的3D模型是以真实世界的米为单位的,因此需要进行比例变换
         *应用,因为CustomLayerInterface需要墨卡托坐标中的单位。
         */
        // scale: mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude).meterInMercatorCoordinateUnits(),
        scale: modelScale,
    };

    // 根据CustomLayerInterface为三维模型配置自定义层
    const customLayer = {
        id: '3dmodel',
        type: 'custom',
        renderingMode: '3d',
        onAdd: function (map, gl) {
            // 场景
            scene = new THREE.Scene();
            scene.fog = new THREE.Fog('#ffffff', 20, 500);
            scene.add(Object3D);
            // 相机
            camera = new THREE.PerspectiveCamera(45, basicMapbox.value.offsetWidth / basicMapbox.value.offsetHeight, 1, 2000);
            
            // 使用Mapbox GL JS地图画布,添加 `THREE.WebGLRenderer`
            renderer = new THREE.WebGLRenderer({
                canvas: map.getCanvas(),
                context: gl,
                alpha: true,
                antialias: true,
            });
            // renderer.shadowMap.enabled = true;
            // 定义渲染器是否在渲染每一帧之前自动清除其输出。
            renderer.autoClear = false;
            // 自然光
            scene.add(new THREE.AmbientLight('#ffffff', 1));
            // 加载器
            loaderFn();
        },
        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);

            camera.projectionMatrix.elements = matrix;
            camera.projectionMatrix = m.multiply(l);
            renderer?.render(scene, camera);
            
            // 必须调用该函数更新视图
            map?.triggerRepaint();
        },
    };

    map.on('style.load', function () {
        map.setFog({});
        map.flyTo({
            ...end,
            duration: 10000,
            essential: true,
        });
        map.addLayer(customLayer);
    });
}
</script>

<style>
.mapboxgl-ctrl-bottom-left,
.mapboxgl-ctrl-bottom-right {
    display: none;
}
.mapboxgl-ctrl-icon {
    box-sizing: border-box;
}
</style>

three场景中场景模型,几何体等

注意:

  • three模型默认朝向问题调整,使用 new THREE.Object3D() 创建组,进而操作组 Object3D.rotateY(-Math.PI / 5.3); Object3D.scale.set(0.8, 0.8, 0.8);

  • 操作组之后按照路径移动的小车模型朝向问题混乱,还是指向未调整之前的坐标,使用如下方法处理:

 // 转向
 const data = new THREE.Vector3(x2, y2, z2).applyAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 5.3);
 modelCar.lookAt(data.x * 0.8, data.y * 0.8, data.z * 0.8);
<template>
    <div class="w100 h100" style="position: relative">
        <div style="position: absolute; right: 10px; top: 10px; z-index: 100">
            <el-button type="primary" @click="crateCar()">{{ inspect ? '停止' : '开始' }}巡检</el-button>
        </div>
        <div ref="basicMapbox" style="position: relative" class="w100 h100"></div>
    </div>
</template>

<script setup>
// mapboxgl地图
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css'; 
// mapboxgl汉化
import MapboxLanguage from '@mapbox/mapbox-gl-language';
// THREE
import * as THREE from 'three';
// 加载器
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
// label标签
import { CSS2DObject, CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer';

let map, // 地图
    renderer, //  WebGLRenderer渲染器
    labelRenderer, //  CSS2DRenderer渲染器
    scene, // 场景
    camera, // 相机
    Object3D = new THREE.Object3D(); // 组

// 初始化生命周期
onMounted(() => {
    init();
});
onBeforeUnmount(() => {
    cancelAnimationFrame(requestAnimationFrameIndex);
    // 释放显存
    renderer?.dispose();
    scene = null;
    map = null;
});

const basicMapbox = ref(null),
    start = {
        center: [80, 80],
        zoom: 1,
        pitch: 0,
        bearing: 0,
    },
    end = {
        center: [118.72791630249077, 32.00910104313064],
        zoom: 17,
        bearing: 60, //目标方位角
        pitch: 75,
    };

// 初始化函数
function init() {
    mapboxgl.accessToken = 'pk.eyJ1IjoicGxheS1pc2FhYyIsImEiOiJjazU0cDkzbWowamd2M2dtemd4bW9mbzRhIn0.cxD4Fw3ZPB_taMkyUSFENA';
    map = new mapboxgl.Map({
        container: basicMapbox.value,
        style: 'mapbox://styles/mapbox-map-design/ckhqrf2tz0dt119ny6azh975y',
        ...start,
        projection: 'globe', // 为 3D 地球
        antialias: true, //抗锯齿,通过false关闭提升性能
    });
    map.addControl(new MapboxLanguage({ defaultLanguage: 'zh-Hans' }));
    map.addControl(new mapboxgl.NavigationControl(), 'top-left');
    //当鼠标滚动时,触发事件,获取当前地图缩放级别
    map.on('wheel', function () {
        const range = map.getZoom();
        if (range <= 15) {
            labelRenderer.domElement.style.display = 'none';
        } else {
            labelRenderer.domElement.style.display = 'block';
        }
    });
    // 添加threejs
    addThree();
}

// 添加threejs
function addThree() {
    // 确保模型在地图上正确地理参照的参数
    const modelOrigin = [118.72791630249077, 32.00910104313064],
        modelAltitude = 0,
        modelRotate = [Math.PI / 2, 0, 0],
        modelScale = 5.41843220338983e-8;

    // 用于在地图上定位、旋转和缩放三维模型的变换参数
    const modelTransform = {
        translateX: mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude).x,
        translateY: mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude).y,
        translateZ: mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude).z,
        rotateX: modelRotate[0],
        rotateY: modelRotate[1],
        rotateZ: modelRotate[2],
        /*由于我们的3D模型是以真实世界的米为单位的,因此需要进行比例变换
         *应用,因为CustomLayerInterface需要墨卡托坐标中的单位。
         */
        // scale: mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude).meterInMercatorCoordinateUnits(),
        scale: modelScale,
    };

    // 根据CustomLayerInterface为三维模型配置自定义层
    const customLayer = {
        id: '3dmodel',
        type: 'custom',
        renderingMode: '3d',
        onAdd: function (map, gl) {
            // 场景
            scene = new THREE.Scene();
            scene.fog = new THREE.Fog('#ffffff', 20, 500);
            scene.add(Object3D);
            Object3D.rotateY(-Math.PI / 5.3);
            Object3D.scale.set(0.8, 0.8, 0.8);
            // 相机
            camera = new THREE.PerspectiveCamera(45, basicMapbox.value.offsetWidth / basicMapbox.value.offsetHeight, 1, 2000);
            // 使用Mapbox GL JS地图画布
            renderer = new THREE.WebGLRenderer({
                canvas: map.getCanvas(),
                context: gl,
                alpha: true,
                antialias: true,
            });
            // renderer.shadowMap.enabled = true;
            // 定义渲染器是否在渲染每一帧之前自动清除其输出。
            renderer.autoClear = false;

            // 添加label场景
            labelRenderer = new CSS2DRenderer(); //新建CSS2DRenderer
            labelRenderer.setSize(basicMapbox.value.offsetWidth, basicMapbox.value.offsetHeight);
            labelRenderer.domElement.style.position = 'absolute';
            labelRenderer.domElement.style.top = 0;
            labelRenderer.domElement.style.pointerEvents = 'none';
            basicMapbox.value.appendChild(labelRenderer.domElement);
            // 自然光
            scene.add(new THREE.AmbientLight('#ffffff', 1));
            // 加载器
            loaderFn();
        },
        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);

            camera.projectionMatrix.elements = matrix;
            camera.projectionMatrix = m.multiply(l);
            render();
            //渲染
            map?.triggerRepaint();
        },
    };

    map.on('style.load', function () {
        map.setFog({});
        map.flyTo({
            ...end,
            duration: 10000,
            essential: true,
        });
        map.addLayer(customLayer);
    });
}

// 模型加载
let objects = [];
function loaderFn() {
    // 将平面添加到场景中
    createPlaneGeometryBasicMaterial();
    // 立方体
    createBoxGeometryBasicMaterial();
    creatRoadSurface();
    createRoundGeometryBasicMaterialMax();
    createRoundGeometryBasicMaterialMin();
    crateWall();
}

// 创建圆仓大public\textures
let objLoader = new OBJLoader(),
    textureLoader = new THREE.TextureLoader(); // 贴图加载器
function createRoundGeometryBasicMaterialMax() {
    objLoader.load('/models/demo7/textures/gong001.obj', function (obj) {
        var mesh = obj.children[0];
        mesh.material = new THREE.MeshBasicMaterial({
            map: textureLoader.load('/models/demo7/textures/d001.png'),
            transparent: true,
            side: THREE.DoubleSide,
            clipIntersection: true,
        });
        mesh.rotateZ(Math.PI);
        mesh.position.set(-40, 36, -105);

        for (let i = 0; i < 2; i++) {
            for (let j = 0; j < 3; j++) {
                var mc = mesh.clone();
                mc.translateX(i * 28);
                mc.translateZ(j * 20);
                mc.name = '大存储罐-G' + (i + 1) * (j + 1);
                Object3D.add(mc);
                objects.push(mc);
            }
        }
    });
}

// 新建标签
function createLableObj(text, vector) {
    let laberDiv = document.createElement('div'); //创建div容器
    laberDiv.className = 'laber_name';
    laberDiv.textContent = text + '\n' + '余量:123';
    let pointLabel = new CSS2DObject(laberDiv);
    pointLabel.position.set(vector.x, vector.y, vector.z);
    return pointLabel;
}

// 创建圆仓小
function createRoundGeometryBasicMaterialMin() {
    objLoader.load('/models/demo7/textures/002.obj', function (obj) {
        var mesh = obj.children[0];
        mesh.material = new THREE.MeshBasicMaterial({
            map: textureLoader.load('/models/demo7/textures/002.png'),
            transparent: true,
            side: THREE.DoubleSide,
            clipIntersection: true,
        });
        mesh.rotateZ(Math.PI);
        mesh.position.set(-40, 20, -19);

        for (let i = 0; i < 2; i++) {
            for (let j = 0; j < 6; j++) {
                var mc = mesh.clone();
                mc.translateX(i * 28);
                mc.translateZ(j * 24);
                mc.name = '小存储罐-G' + (i + 1) * (j + 1);
                Object3D.add(mc);
                objects.push(mc);
            }
        }
    });
}

// 创建围栏
function crateWall() {
    objLoader.load('/models/demo7/textures/wall.obj', function (obj) {
        obj.scale.set(0.98, 0.6, 1);
        const texLan = textureLoader.load('/models/demo7/textures/lan2.png');
        // 纹理重复
        texLan.wrapS = texLan.wrapT = THREE.RepeatWrapping;
        texLan.repeat.set(40, 1);
        obj.children[0].material = new THREE.MeshBasicMaterial({
            side: THREE.DoubleSide,
            map: texLan,
            transparent: true,
        });
        obj.children[1].material = new THREE.MeshBasicMaterial({
            map: textureLoader.load('/models/demo7/textures/door.png'),
            side: THREE.DoubleSide,
            transparent: true,
        });
        Object3D.add(obj);
        objects.push(...obj.children);
    });
}

// 创建房屋
function createBoxGeometryBasicMaterial() {
    objLoader.load('/models/demo7/textures/003.obj', function (obj) {
        var mesh = obj.children[0];
        mesh.material = new THREE.MeshBasicMaterial({
            map: textureLoader.load('/models/demo7/textures/003.png'),
        });
        mesh.scale.set(1.3, 1.4, 1.5);
        mesh.position.set(11, 0, -85);

        for (let i = 0; i < 2; i++) {
            for (let j = 0; j < 3; j++) {
                var mc = mesh.clone();
                mc.translateX(i * 52);
                mc.translateZ(j * 83);
                mc.name = '工作厂房-W' + (i + 1) * (j + 1);
                Object3D.add(mc);
                objects.push(mc);
                let text = '工作厂房-W' + (i + 1) * (j + 1);
                let vector = new THREE.Vector3(11 + i * 52, 18, -85 + j * 83);
                let pointLabel = createLableObj(text, vector);
                setTimeout(() => {
                    Object3D.add(pointLabel);
                }, 8000);
            }
        }
    });
}

// 加载车辆模型
let modelCar;
function crateCar() {
    if (!modelCar) {
        map.flyTo({
            curve: 1,
            zoom: 17.25,
            bearing: -35, //目标方位角
            duration: 1000, //飞行总时长,单位ms
            essential: true, //动画
            easing: (t) => {
                //飞行时间进度
                if (t == 1) {
                    const dracoLoader = new DRACOLoader();
                    dracoLoader.setDecoderPath('/models/demo1/gltf/');
                    const loader = new GLTFLoader();
                    loader.setDRACOLoader(dracoLoader);
                    loader.load(`/models/demo7/叉车.glb`, function (object) {
                        modelCar = object.scene;
                        modelCar.scale.set(4, 4, 4);
                        modelCar.position.set(88, 0, 115);
                        modelCar.rotateY(Math.PI);
                        Object3D.add(modelCar);
                        crateCarPath();
                    });
                }
                return t;
            },
        });
    }
    inspect = !inspect;
}

// 车辆模型按照固定路线运动
let curve,
    inspect = $ref(false);
function crateCarPath() {
    curve = new THREE.CatmullRomCurve3(
        [
            new THREE.Vector3(86, 0, 115),
            new THREE.Vector3(86, 0, -115),
            new THREE.Vector3(37, 0, -115),
            new THREE.Vector3(37, 0, 115),
            new THREE.Vector3(-15, 0, 115),
            new THREE.Vector3(-15, 0, -115),
            new THREE.Vector3(-86, 0, -115),
            new THREE.Vector3(-86, 0, 115),
            new THREE.Vector3(-15, 0, 115),
            new THREE.Vector3(-15, 0, -115),
            new THREE.Vector3(37, 0, -115),
            new THREE.Vector3(37, 0, 115),
        ],
        true,
        'catmullrom',
        0.15
    );
    // 绘制轨迹线
    let geometry = new THREE.BufferGeometry();
    geometry.setFromPoints(curve.getPoints(10000));
    let material = new THREE.LineDashedMaterial({ color: 0x4488ff });
    Object3D.add(new THREE.Line(geometry, material));
}

/**
 * 创建地面并添加材质
 * wrapS属性定义的是纹理沿x轴方向的行为,而warpT属性定义的是纹理沿y轴方向的行为。
 * Three.js为这些属性提供了如下两个选项:
 * ·THREE.RepeatWrapping允许纹理重复自己。
 * ·THREE.ClampToEdgeWrapping是属性的默认值。
 * 属性值为THREE.ClampToEdgeWrapping时,那么纹理的整体不会重复,只会重复纹理边缘的像素来填满剩下的空间。
 */
function createPlaneGeometryBasicMaterial() {
    const cubeMaterial = new THREE.MeshStandardMaterial({
        map: textureLoader.load('/models/demo7/textures/floor3.png'),
        transparent: true,
        side: THREE.DoubleSide,
    });
    // 创建地平面并设置大小
    let planeGeometry = new THREE.PlaneGeometry(190, 260);
    const plane = new THREE.Mesh(planeGeometry, cubeMaterial);
    // 设置平面位置并旋转
    plane.rotateX(-Math.PI / 2);
    Object3D.add(plane);
}

// 创建路面
function creatRoadSurface() {
    const geometry = new THREE.PlaneGeometry(24, 190);
    const texture = textureLoader.load('/models/demo7/textures/road2.png');
    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
    texture.repeat.set(1, 10);
    const material = new THREE.MeshBasicMaterial({
        map: texture,
        side: THREE.DoubleSide,
    });
    const mesh = new THREE.Mesh(geometry, material);
    Object3D.add(mesh);
    mesh.rotateX(-Math.PI / 2);
    mesh.rotateZ(-Math.PI / 2);
    mesh.position.z = 142;
}

// 渲染逻辑
let progress = 0,
    requestAnimationFrameIndex;
function render() {
    // 巡检车辆移动
    if (curve && inspect) {
        progress += 0.0002;
        if (progress > 1) progress = 0;
        //取路径上当前点的坐标
        const { x, y, z } = curve.getPointAt(progress);
        const { x: x2, y: y2, z: z2 } = curve.getPointAt(progress + 0.0001);
        //设置车模型坐标为在相机路径上当前点的位置
        modelCar?.position?.set(x, y, z);
        // 转向
        const data = new THREE.Vector3(x2, y2, z2).applyAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 5.3);
        modelCar.lookAt(data.x * 0.8, data.y * 0.8, data.z * 0.8);
    }
    // label标签渲染
    labelRenderer?.render(scene, camera);
    renderer?.render(scene, camera);
}
</script>

<style>
.mapboxgl-ctrl-bottom-left,
.mapboxgl-ctrl-bottom-right {
    display: none;
}
.mapboxgl-ctrl-icon {
    box-sizing: border-box;
}
.laber_name {
    color: rgb(0, 255, 255);
    width: 100px;
    height: 30px;
    font-size: 14px;
    padding: 15px;
    background: url('/models/demo7/textures/msg-bg.png') no-repeat;
    background-size: cover<img src=";" alt="" width="100%" />
    cursor: pointer;
    pointer-events: auto;
}
</style>

GIF 2022-9-8 18-18-56.gif

未解决问题

  • 地图坐标和3维坐标的转换
  • 地图中添加的3维模型点击事件问题
  • 第一视角漫游未实现