threejs 在3d地图中绘制css2d地名

threejs yekong

vue 数据可视化大屏 项目开发中,经常需要渲染一些3d地图效果,今天整理一下3d地图并在3d地图中标注地名代码,希望能够让代码快速应用到项目中。

CSS2D标签面向摄像机,场景缩放时,缩小放大都一样大,不被模型遮挡,通过DOM事件点击

threejs 在3d地图中绘制css2d地名

css2d标签渲染效果视频

演示实例

threejs 在vue3项目中绘制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

相关文件下载地址
此资源需支付 ¥2 后下载
支付宝购买扫右侧红包码购买更优惠,如无法下载请联系微信:17331886870
喜欢
threejs 在3d地图中绘制css2d地名