vue 数据可视化大屏 项目开发中,经常需要渲染一些3d地图效果,今天整理一下3d地图并在3d地图中标注地名代码,希望能够让代码快速应用到项目中。
CSS2D标签面向摄像机,场景缩放时,缩小放大都一样大,不被模型遮挡,通过DOM事件点击
css2d标签渲染效果视频
演示实例
安装依赖
3d地图是使用three实现的,所以首先需要安装一下依赖,threejs不同版本是有差异的,为了避免出现因为版本差异造成的问题,这里我们指定一下版本
pnpm i three@0.123.0
html
这里我们创建两个div,一个外层父div,一个内层three渲染使用的div.之所以创建两层,是因为我们除了three渲染外,还可能会在three图层上面再增加一些其他内容,让其都在同一个父div下方便管理。
<div class="map3D" ref="Map3D">
<div ref='map3DMain' class="map3DMain">
</div>
</div>
引入three
引入three,因为我们可能会有拖动地图的操作,所以我们事先也把OrbitControls
控制器引入。
import * as THREE from 'three';
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
创建窗口监听
一般three会应用到数据可视化大屏 项目中,为了让threejs能够跟随窗口进行大小变化,我们需要监听窗口。当我们拖动窗口的时候会触发窗口监听,然后重新获取div的宽高重置three大小,但是实际项目开发中,我们拖动窗口后立刻获取的窗口大小是不准确的,所以我们要设置一个延迟,这样获取的大小才相对准确,这里我们设置的是300毫秒的延迟。
mounted() {
var that = this;
const viewElem = document.body;
that.drawMap()
// 监听窗口变化,重绘地图
const resizeObserver = new ResizeObserver(() => {
setTimeout(() => {
that.getReSize();
}, 300)
});
resizeObserver.observe(viewElem);
},
three获取窗口
这里我们获取three索要渲染的div的宽高,然后判断renderer是否存在,如果存在就重新设置threejs视图窗口的大小。因为地图中要添加css2d标签,所以我们需要再添加一个labelRenderers
渲染器
getReSize() {
var that = this;
that.width = this.$refs.map3DMain.offsetWidth
that.height = this.$refs.map3DMain.offsetHeight
if (that.renderer) {
that.renderer.setSize(that.width, that.height);
that.labelRenderers.setSize(that.width, that.height);
}
},
three创建地图
窗口大小获取
创建地图前,我们需要先获取div的大小,用来进行视图窗口的渲染。
that.width = this.$refs.map3DMain.offsetWidth
that.height = this.$refs.map3DMain.offsetHeight
初始化空间
初始化一个空间,所有的东西都会放到这个空间里。
var scene = new THREE.Scene();
添加光源
空间创建完成后,我们需要加入光源,没有光源的话是看不到东西的,这里我们创建了3个光源。
// 增加光源
var directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight.position.set(400, 200, 300);
scene.add(directionalLight);
var directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight2.position.set(-400, -200, -300);
scene.add(directionalLight2);
var ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
// 光源结束
添加相机
有了光源,要看到东西的话,还需要眼睛,在three是相机
// 添加相机
var k = that.width / that.height
var s = 0;
s = (getBoxSize(mapGroup).x + getBoxSize(mapGroup).y) / 4
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(104, -105, 200);
camera.lookAt(scene.position);
添加渲染器
渲染器,渲染器除了渲染我们要显示的内容外,还会渲染背景色,这里我们不需要背景色,所以需要设置一下alpha让渲染器可以背景透明。
// 添加渲染器
var renderer = that.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
// 设置背景颜色 第一个值是颜色,第二个值是透明度,需要先将alpha设置为true透明度才会生效。
renderer.setClearColor(0x000000, 0)
renderer.setSize(that.width, that.height);
执行渲染
因为需要添加css2d标签,所以需要再添加一个labelRenderers
function render() {
labelRenderers.render(scene, camera);
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
}
render();
添加控制器
因为我们需要鼠标拖动地图后,地图可以变化,所以添加控制器。
var controls = new OrbitControls(camera, renderer.domElement);
引入地图数据
要渲染地图,需要引入geoJson格式的地图数据
import data from './data/data.json'
然后我们将地图渲染出来,这里地图分为两部分一部分是地图上的边线,一部分是地图本体,需要遍历,获取地名以及中心点,用来渲染地图标签。
// 地图绘制
var mapGroup = new THREE.Group();
scene.add(mapGroup);
var lineGroup = new THREE.Group();
mapGroup.add(lineGroup);
var meshGroup = new THREE.Group();
mapGroup.add(meshGroup);
data.features.forEach(function (area) {
if (area.geometry.type === "Polygon") {
area.geometry.coordinates = [area.geometry.coordinates];
}
// 需要遍历,获取地名以及中心点
// 遍历地图中的数据 以tag2d的方式渲染
var listItem = {
title: area.properties.name,
center: area.properties.center,
}
that.tagList.push(listItem)
lineGroup.add(borderLine(area.geometry.coordinates));
var mesh = ExtrudeMesh(area.geometry.coordinates, TensileHeight)
meshGroup.add(mesh);
});
获取到地名以后我们需要将地名和中心点渲染到地图中
// 添加tag标签
this.tagList.forEach((type) => {
scene.add(tag(type.title, type.center[0], type.center[1], TensileHeight + 0.021));
});
地图轮廓绘制
地图绘制的时候,需要在上面绘制轮廓,也就是一条条闭合的线,这里我们使用LineLoop将一个个经纬度的点连接成一个闭合的几何图形。就形成了地图上的一条条轮廓的线了。
import * as THREE from 'three';
function borderLine(pointsArrs) {
var group = new THREE.Group();
pointsArrs.forEach(polygon => {
var pointArr = [];
polygon[0].forEach(elem => {
pointArr.push(elem[0], elem[1], 0);
});
group.add(closedContourLine(pointArr));
});
return group;
}
function closedContourLine(pointArr) {
/**
* 通过BufferGeometry构建一个几何体,传入顶点数据
* 通过Line模型渲染几何体,连点成线
* LineLoop和Line功能一样,区别在于首尾顶点相连,轮廓闭合
*/
var geometry = new THREE.BufferGeometry();
var vertices = new Float32Array(pointArr);
// 创建属性缓冲区对象
var attribue = new THREE.BufferAttribute(vertices, 3);
geometry.attributes.position = attribue;
var material = new THREE.LineBasicMaterial({
color: 0x00cccc
});
var line = new THREE.LineLoop(geometry, material);
return line;
}
export { borderLine };
绘制地图
除了绘制地图上面的线条区域外,我们还需要绘制立体的地图。将一组组经纬度数据转为一个个网格模型。最终成为立体的地图。
import * as THREE from 'three';
function ExtrudeMesh(pointsArrs, height) {
var shapeArr = [];
pointsArrs.forEach(pointsArr => {
var vector2Arr = [];
pointsArr[0].forEach(elem => {
vector2Arr.push(new THREE.Vector2(elem[0], elem[1]))
});
var shape = new THREE.Shape(vector2Arr);
shapeArr.push(shape);
});
var material = new THREE.MeshLambertMaterial({
color: 0x004444,
}); //材质对象
var sidebarMaterial = new THREE.MeshLambertMaterial({
color: 0x043736,
}); //材质对象
var geometry = new THREE.ExtrudeBufferGeometry( //拉伸造型
shapeArr,
{
depth: height,
bevelEnabled: false
}
);
var mesh = new THREE.Mesh(geometry, [material, sidebarMaterial]); //网格模型对象
return mesh;
}
export {ExtrudeMesh};
添加标签
这里我们需要创建一个css2d标签,用来渲染内容,这里渲染器在创建的时候,需要接收外部传入的宽和高。因为需要将dom插入到指定div内,并且需要跟随窗口变化而调整大小,所以我们需要将创建的dom返回,以便于调整视图大小。
import {CSS2DRenderer, CSS2DObject} from 'three/examples/jsm/renderers/CSS2DRenderer.js';
// 创建一个HTML标签
function tag(name, x, y, z) {
// 创建div元素(作为标签)
var div = document.createElement('div');
div.innerHTML = name;
div.style.padding = '5px 10px';
div.style.color = '#fff';
div.style.fontSize = '16px';
div.style.position = 'absolute';
div.style.backgroundColor = 'rgba(25,25,25,0.5)';
div.style.borderRadius = '5px';
//div元素包装为CSS2模型对象CSS2DObject
var label = new CSS2DObject(div);
div.style.pointerEvents = 'none';//避免HTML标签遮挡三维场景的鼠标事件
// 设置HTML元素标签在three.js世界坐标中位置
label.position.set(x, y, z);
return label;//返回CSS2模型标签
}
// 创建一个CSS2渲染器CSS2DRenderer
function labelRenderer(width, height) {
var labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(width, height);
labelRenderer.domElement.style.position = 'absolute';
// // 避免renderer.domElement影响HTMl标签定位,设置top为0px
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.left = '0px';
// //设置.pointerEvents=none,以免模型标签HTML元素遮挡鼠标选择场景模型
labelRenderer.domElement.style.pointerEvents = 'none';
return labelRenderer
}
export {tag, labelRenderer}
设置中心点
地图绘制出来后,并不一定会处在视图的中心,但是我们需要让地图处在中心,这需要我们获取网格模型的中心点,然后通过设置控制器来让地图出现在视图中心。
var controls = new OrbitControls(camera, renderer.domElement);
// // 获取中心点
var getBoxCenterData = getBoxCenter(mapGroup)
// // 设置中心
controls.target.set(getBoxCenterData.x, getBoxCenterData.y, 0);
controls.update();
获取中心
threejs有一个包围盒方法,我们可以通过包围盒的getCenter
方法来获取模型的中心点
var box3 = new THREE.Box3(); //创建一个包围盒
box3.expandByObject(mapGroup); // .expandByObject()方法:计算层级模型group包围盒
var center = new THREE.Vector3(); //scaleV3表示包围盒的几何体中心
box3.getCenter(center); // .getCenter()计算一个层级模型对应包围盒的几何体中心
return center
插入div
最后将渲染生成的内容插入到我们的div中。这里需要插入两个dom。
that.$refs.map3DMain.appendChild(renderer.domElement)
that.$refs.map3DMain.appendChild(labelRenderers.domElement)
css2d效果渲染完成了,那么接下来我们渲染一下css3d标签的效果threejs 在3d地图中绘制css3d标签标注省份,来观察他们效果的不同。
实例代码下载
项目运行环境 vue3 vite js nodejs 14