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>
未解决问题
- 地图坐标和3维坐标的转换
- 地图中添加的3维模型点击事件问题
- 第一视角漫游未实现