three.js实现3D地图( 四 )


three.js实现3D地图

文章插图
label样式
three.js实现3D地图

文章插图
3D中国地图
此时的完整代码:
import * as THREE from 'three' import * as d3 from 'd3-geo' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' import { CSS2DObject, CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' const MATERIAL_COLOR1 = "#2887ee"; const MATERIAL_COLOR2 = "#2887d9"; class Map3D { constructor() { this.scene = undefined // 场景 this.camera = undefined // 相机 this.renderer = undefined // 渲染器 this.css2dRenderer = undefined // html渲染器 this.geojson = undefined // 地图json数据 this.init() } init() { // 创建场景 this.scene = new THREE.Scene() // 创建相机 this.setCamera() // 创建渲染器 this.setRender() // 创建控制器 this.setControls() // 光线投射 this.setRaycaster() // 加载数据 this.loadData() // 渲染函数 this.render() } /** * 创建相机 */ setCamera() { // PerspectiveCamera(角度 , 长宽比 , 近端面 , 远端面) —— 透视相机 this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) // 设置相机位置 this.camera.position.set(0, 0, 120) // 把相机添加到场景中 this.scene.add(this.camera) } /** * 创建渲染器 */ setRender() { this.renderer = new THREE.WebGLRenderer() // 渲染器尺寸 this.renderer.setSize(window.innerWidth, window.innerHeight) //设置背景颜色 this.renderer.setClearColor(0x000000) // 将渲染器追加到dom中 document.body.appendChild(this.renderer.domElement) // CSS2DRenderer 创建的是html的div元素 // 这里将div设置成绝对定位 , 盖住canvas画布 this.css2dRenderer = new CSS2DRenderer(); this.css2dRenderer.setSize(window.innerWidth, window.innerHeight); this.css2dRenderer.domElement.style.position = "absolute"; this.css2dRenderer.domElement.style.top = "0px"; this.css2dRenderer.domElement.style.pointerEvents = "none"; document.body.appendChild(this.css2dRenderer.domElement); } setRaycaster() { this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); const onMouse = (event) => { // 将鼠标位置归一化为设备坐标 。x 和 y 方向的取值范围是 (-1 to +1) // threejs的三维坐标系是中间为原点 , 鼠标事件的获得的坐标是左上角为原点 。因此需要在这里转换 this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1 this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1 }; window.addEventListener("mousemove", onMouse, false); } showTip () { if (!this.dom) { this.dom = document.createElement("div"); this.tip = new CSS2DObject(this.dom); } if (this.lastPick) { const { x, y, z } = this.lastPick.point; const properties = this.lastPick.object.parent.properties; this.dom.className = "label"; this.dom.innerText = properties.name this.tip.position.set(x + 10, y + 10, z); this.map && this.map.add(this.tip); } } render() { this.raycaster.setFromCamera(this.mouse, this.camera) const intersects = this.raycaster.intersectObjects( this.scene.children, true ) // 如果this.lastPick存在 , 将材质颜色还原 if (this.lastPick) { this.lastPick.object.material[0].color.set(MATERIAL_COLOR1); this.lastPick.object.material[1].color.set(MATERIAL_COLOR2); } // 置空 this.lastPick = null; // 查询当前鼠标移动所产生的射线与物体的焦点 // 有两个material的就是我们要找的对象 this.lastPick = intersects.find( (item) => item.object.material && item.object.material.length === 2 ); // 找到后把颜色换成一个鲜艳的绿色 if (this.lastPick) { this.lastPick.object.material[0].color.set("aquamarine"); this.lastPick.object.material[1].color.set("aquamarine"); } this.showTip() this.renderer.render(this.scene, this.camera) this.css2dRenderer.render(this.scene, this.camera) requestAnimationFrame(this.render.bind(this)) } setControls() { this.controls = new OrbitControls(this.camera, this.renderer.domElement) // 太灵活了 , 来个阻尼 this.controls.enableDamping = true; this.controls.dampingFactor = 0.1; } getGeoJson (adcode = '100000') { return fetch(`https://geo.datav.aliyun.com/areas_v3/bound/${adcode}_full.json`) .then(res => res.json()) } async loadData(adcode) { // 获取geojson数据 this.geojson = await this.getGeoJson(adcode) // 创建墨卡托投影 this.projection = d3 .geoMercator() .center([104.0, 37.5]) .translate([0, 0]) // Object3D是Three.js中大部分对象的基类 , 提供了一系列的属性和方法来对三维空间中的物体进行操纵 。// 初始化一个地图 this.map = new THREE.Object3D(); this.geojson.features.forEach(elem => { const area = new THREE.Object3D() // 坐标系数组(为什么是数组 , 因为有地区不止一个几何体 , 比如河北被北京分开了 , 比如舟山群岛) const coordinates = elem.geometry.coordinates const type = elem.geometry.type // 定义一个画几何体的方法 const drawPolygon = (polygon) => { // Shape(形状) 。使用路径以及可选的孔洞来定义一个二维形状平面 。它可以和ExtrudeGeometry、ShapeGeometry一起使用 , 获取点 , 或者获取三角面 。const shape = new THREE.Shape() // 存放的点位 , 最后需要用THREE.Line将点位构成一条线 , 也就是地图上区域间的边界线 // 为什么两个数组 , 因为需要三维地图的两面都画线 , 且它们的z坐标不同 let points1 = []; let points2 = []; for (let i = 0; i < polygon.length; i++) { // 将经纬度通过墨卡托投影转换成threejs中的坐标 const [x, y] = this.projection(polygon[i]); // 画二维形状 if (i === 0) { shape.moveTo(x, -y); } shape.lineTo(x, -y); points1.push(new THREE.Vector3(x, -y, 10)); points2.push(new THREE.Vector3(x, -y, 0)); } /** * ExtrudeGeometry (挤压缓冲几何体) * 文档链接:https://threejs.org/docs/index.html?q=ExtrudeGeometry#api/zh/geometries/ExtrudeGeometry */ const geometry = new THREE.ExtrudeGeometry(shape, { depth: 10, bevelEnabled: false, }); /** * 基础材质 */ // 正反两面的材质 const material1 = new THREE.MeshBasicMaterial({ color: MATERIAL_COLOR1, }); // 侧边材质 const material2 = new THREE.MeshBasicMaterial({ color: MATERIAL_COLOR2, }); // 生成一个几何物体(如果是中国地图 , 那么每一个mesh就是一个省份几何体) const mesh = new THREE.Mesh(geometry, [material1, material2]); area.add(mesh); /** * 画线 * link: https://threejs.org/docs/index.html?q=Line#api/zh/objects/Line */ const lineGeometry1 = new THREE.BufferGeometry().setFromPoints(points1); const lineGeometry2 = new THREE.BufferGeometry().setFromPoints(points2); const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff }); const line1 = new THREE.Line(lineGeometry1, lineMaterial); const line2 = new THREE.Line(lineGeometry2, lineMaterial); area.add(line1); area.add(line2); // 把地区属性存到area对象中 area.properties = elem.properties } // type可能是MultiPolygon 也可能是Polygon if (type === "MultiPolygon") { coordinates.forEach((multiPolygon) => { multiPolygon.forEach((polygon) => { drawPolygon(polygon); }); }); } else { coordinates.forEach((polygon) => { drawPolygon(polygon); }); } // 把区域添加到地图中 this.map.add(area); }) // 把地图添加到场景中 this.scene.add(this.map) } } const map = new Map3D()


推荐阅读