效果图
图片资源:
1、下载three.js及d3依赖,我这里使用的是three r153版本
npm i three npm i d3
复制
2、使用d3将地理坐标转换成坐标轴xy值,在地图区域及边界线加载到场景后,通过包围盒计算完整地图的max和min坐标,再根据max和min的值重新计算每个地图区域的uv坐标,如果不重新计算uv坐标会导致贴图显示异常
this.projection = d3 .geoMercator() .center(this.centerCoordinate) .translate([0, 0]); // 加载地图背景 const backgroundTexture = new THREE.TextureLoader().load( require("@/assets/images/map.png") ); // 加载地图 let fileLoader = new THREE.FileLoader(); fileLoader.load("/anhui.json", (data) => { // 添加地图及边界线 this.addMapGeometry(data); // 重新计算地图uv坐标 let arr = []; let box = new THREE.Box3(); for (let v of this.map.children) { for (let v2 of v.children) { // 判断是否为ExtrudeGeometry if (v2.geometry instanceof THREE.ExtrudeGeometry) { arr.push(v2); let itemBox = new THREE.Box3().setFromObject(v); box.union(itemBox); } } } var bboxMin = box.min; var bboxMax = box.max; // 计算UV的缩放比例 var uvScale = new THREE.Vector2( 1 / (bboxMax.x - bboxMin.x), 1 / (bboxMax.y - bboxMin.y) ); for (let v of arr) { let uvAttribute = v.geometry.getAttribute("uv"); for (let i = 0; i < uvAttribute.count; i++) { let u = uvAttribute.getX(i); let v = uvAttribute.getY(i); // 将UV坐标进行归一化 let normalizedU = (u - bboxMin.x) * uvScale.x; let normalizedV = (v - bboxMin.y) * uvScale.y; // 更新UV坐标 uvAttribute.setXY(i, normalizedU, normalizedV); } // 更新几何体的UV属性 v.geometry.setAttribute("uv", uvAttribute); v.material.map = backgroundTexture; v.material.needsUpdate = true; } }); addMapGeometry(jsondata) { // 初始化一个地图对象 this.map = new THREE.Object3D(); jsondata = JSON.parse(jsondata); jsondata.features.forEach((elem) => { // 定一个省份3D对象 const province = new THREE.Object3D(); // 每个的 坐标 数组 const coordinates = elem.geometry.coordinates; if (elem.geometry.type === "MultiPolygon") { // 循环坐标数组 coordinates.forEach((multiPolygon) => { multiPolygon.forEach((polygon) => { this.drawItem(elem, polygon, province); }); }); this.map.add(province); } else if (elem.geometry.type === "Polygon") { // 循环坐标数组 coordinates.forEach((polygon) => { this.drawItem(elem, polygon, province); }); this.map.add(province); } }); this.scene.add(this.map); }, drawItem(elem, polygon, province) { const shape = new THREE.Shape(); const pointsArray = new Array(); for (let i = 0; i < polygon.length; i++) { const [x, y] = this.projection(polygon[i]); if (i === 0) { shape.moveTo(x, -y); } shape.lineTo(x, -y); pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep)); } let curve = new THREE.CatmullRomCurve3(pointsArray); // 这里使用TubeGeometry没有使用line,主要考虑到line的宽度无法设置,也可以使用其他第三方依赖 var tubeGeometry = new THREE.TubeGeometry( curve, Math.floor(pointsArray.length), 0.02, 10 ); const extrudeSettings = { depth: this.mapConfig.deep, bevelEnabled: false, // 对挤出的形状应用是否斜角 }; const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); geometry.computeBoundingBox(); // 创建地图区域材质 let meshMaterial = new THREE.MeshStandardMaterial({ color: "#ffffff", transparent: true, opacity: 1, }); // 创建地图边界线材质 let lineMaterial = new THREE.MeshBasicMaterial({ color: "#ceebf7", }); const mesh = new THREE.Mesh(geometry, meshMaterial); const line = new THREE.Mesh(tubeGeometry, lineMaterial); // 将省份的属性 加进来 province.properties = elem.properties; province.add(mesh); this.boundaryLineArr.push(line); province.add(line); },
复制
3、设置后期处理
//设置光晕 this.composer = new EffectComposer(this.renderer); //效果组合器 //创建通道 let renderScene = new RenderPass(this.scene, this.camera); this.composer.addPass(renderScene); let outlinePass = new OutlinePass( new THREE.Vector2(window.innerWidth, window.innerHeight), this.scene, this.camera, this.boundaryLineArr // 边界线数组 ); outlinePass.renderToScreen = true; outlinePass.edgeGlow = 2; // 光晕效果 outlinePass.usePatternTexture = false; outlinePass.edgeThickness = 10; // 边框宽度 outlinePass.edgeStrength = 1.5; // 光晕效果 outlinePass.pulsePeriod = 0; // 光晕闪烁的速度 outlinePass.visibleEdgeColor.set("#1acdec"); outlinePass.hiddenEdgeColor.set("#1acdec"); this.composer.addPass(outlinePass);
复制
完整代码
注:我这边使用的是安徽的地图,需要替换自己的地图json以及地图中心地理坐标
<template> <div class="page" id="page" ref="page"> <div class="tooltip" ref="tooltip" v-show="show"> {{ selectedPointData.name }} </div> </div> </template> <script> import * as THREE from "three"; import * as d3 from "d3"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js"; import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js"; import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js"; import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"; export default { data() { return { scene: null, camera: null, renderer: null, controls: null, centerCoordinate: [117.13, 31.89], // 地图中心地理坐标 projection: null, // Mercator 投影 mapConfig: { deep: 0.2, // 挤出的深度 }, boundaryLineArr: [], // 边界线 composer: "", // 后期处理 pointData: [ { coordinates: [117.33, 31.79], type: 1, name: "合肥", value: 100, }, { coordinates: [118.502, 31.684], type: 1, name: "马鞍山", value: 100, }, ], pointInstanceArr: [], // 坐标点实例 show: false, // 是否显示tooltip selectedPointData: {}, // 选中的坐标点数据 }; }, mounted() { this.init(); window.addEventListener("resize", () => { this.renderer.setSize(window.innerWidth, window.innerHeight); this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); }); }, methods: { init() { this.renderer = new THREE.WebGLRenderer(); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace; document.querySelector("#page").appendChild(this.renderer.domElement); this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 1000 ); this.camera.position.set(5, 5, 26); this.camera.lookAt(0, 0, 0); // let axesHelp = new THREE.AxesHelper(5); // this.scene.add(axesHelp); this.controls = new OrbitControls(this.camera, this.renderer.domElement); // 墨卡托投影转换 this.projection = d3 .geoMercator() .center(this.centerCoordinate) .translate([0, 0]); // 根据地球贴图做轻微调整 // 添加地图 this.addMap(); // 给地图边界线添加outline效果 this.setLineOutline(); // 添加灯光 let ambientLight = new THREE.AmbientLight(0xffffff, 1); this.scene.add(ambientLight); // 添加散点 this.setPoint(); // 设置光线投射 this.setRaycaster(); this.render(); }, render() { this.renderer.render(this.scene, this.camera); this.controls.update(); if (this.composer) this.composer.render(); requestAnimationFrame(this.render); }, // 添加地图 addMap() { // 加载地图背景 const backgroundTexture = new THREE.TextureLoader().load( require("@/assets/images/map.png") ); // 加载地图 let fileLoader = new THREE.FileLoader(); fileLoader.load("/anhui.json", (data) => { // 添加地图及边界线 this.addMapGeometry(data); // 重新计算地图uv坐标 let arr = []; let box = new THREE.Box3(); for (let v of this.map.children) { for (let v2 of v.children) { // 判断是否为ExtrudeGeometry,只计算所有地图区域总和的包围盒大小 if (v2.geometry instanceof THREE.ExtrudeGeometry) { arr.push(v2); let itemBox = new THREE.Box3().setFromObject(v2); box.union(itemBox); } } } var bboxMin = box.min; var bboxMax = box.max; // 计算UV的缩放比例 var uvScale = new THREE.Vector2( 1 / (bboxMax.x - bboxMin.x), 1 / (bboxMax.y - bboxMin.y) ); for (let v of arr) { let uvAttribute = v.geometry.getAttribute("uv"); for (let i = 0; i < uvAttribute.count; i++) { let u = uvAttribute.getX(i); let v = uvAttribute.getY(i); // 将UV坐标进行归一化 let normalizedU = (u - bboxMin.x) * uvScale.x; let normalizedV = (v - bboxMin.y) * uvScale.y; // 更新UV坐标 uvAttribute.setXY(i, normalizedU, normalizedV); } // 更新几何体的UV属性 v.geometry.setAttribute("uv", uvAttribute); v.material.map = backgroundTexture; v.material.needsUpdate = true; } }); }, addMapGeometry(jsondata) { // 初始化一个地图对象 this.map = new THREE.Object3D(); jsondata = JSON.parse(jsondata); jsondata.features.forEach((elem) => { // 定一个省份3D对象 const province = new THREE.Object3D(); // 每个的 坐标 数组 const coordinates = elem.geometry.coordinates; if (elem.geometry.type === "MultiPolygon") { // 循环坐标数组 coordinates.forEach((multiPolygon) => { multiPolygon.forEach((polygon) => { this.drawItem(elem, polygon, province); }); }); this.map.add(province); } else if (elem.geometry.type === "Polygon") { // 循环坐标数组 coordinates.forEach((polygon) => { this.drawItem(elem, polygon, province); }); this.map.add(province); } }); this.scene.add(this.map); }, drawItem(elem, polygon, province) { const shape = new THREE.Shape(); const pointsArray = new Array(); for (let i = 0; i < polygon.length; i++) { const [x, y] = this.projection(polygon[i]); if (i === 0) { shape.moveTo(x, -y); } shape.lineTo(x, -y); pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep)); } let curve = new THREE.CatmullRomCurve3(pointsArray); // 这里使用TubeGeometry没有使用line,主要考虑到line的宽度无法设置,也可以使用其他第三方依赖去做 var tubeGeometry = new THREE.TubeGeometry( curve, Math.floor(pointsArray.length), 0.02, 10 ); const extrudeSettings = { depth: this.mapConfig.deep, bevelEnabled: false, // 对挤出的形状应用是否斜角 }; const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); geometry.computeBoundingBox(); // 创建地图区域材质 let meshMaterial = new THREE.MeshStandardMaterial({ color: "#ffffff", transparent: true, opacity: 1, }); // 创建地图边界线材质 let lineMaterial = new THREE.MeshBasicMaterial({ color: "#ceebf7", }); const mesh = new THREE.Mesh(geometry, meshMaterial); const line = new THREE.Mesh(tubeGeometry, lineMaterial); // 将省份的属性 加进来 province.properties = elem.properties; province.add(mesh); this.boundaryLineArr.push(line); province.add(line); }, // 给地图边界线添加outline效果 setLineOutline() { //设置光晕 this.composer = new EffectComposer(this.renderer); //效果组合器 //创建通道 let renderScene = new RenderPass(this.scene, this.camera); this.composer.addPass(renderScene); let outlinePass = new OutlinePass( new THREE.Vector2(window.innerWidth, window.innerHeight), this.scene, this.camera, this.boundaryLineArr ); outlinePass.renderToScreen = true; outlinePass.edgeGlow = 2; // 光晕效果 outlinePass.usePatternTexture = false; outlinePass.edgeThickness = 10; // 边框宽度 outlinePass.edgeStrength = 1.5; // 光晕效果 outlinePass.pulsePeriod = 0; // 光晕闪烁的速度 outlinePass.visibleEdgeColor.set("#1acdec"); outlinePass.hiddenEdgeColor.set("#1acdec"); this.composer.addPass(outlinePass); }, // 添加散点 setPoint() { let pointTexture = new THREE.TextureLoader().load( require("@/assets/images/point.png") ); for (let v of this.pointData) { let [x, y] = this.projection(v.coordinates); const sprite = new THREE.Sprite( new THREE.SpriteMaterial({ map: pointTexture, }) ); sprite.scale.set(0.7, 0.7, 1); sprite.position.set(x, -y, this.mapConfig.deep + 0.5); sprite.properties = v; this.pointInstanceArr.push(sprite); this.scene.add(sprite); } }, // 光线投射 setRaycaster() { const raycaster = new THREE.Raycaster(); const pointer = new THREE.Vector2(); this.$refs.page.addEventListener("click", (event) => { pointer.x = (event.clientX / window.innerWidth) * 2 - 1; pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(pointer, this.camera); const intersects = raycaster.intersectObjects(this.pointInstanceArr); if (intersects && intersects.length > 0) { let tooltip = this.$refs.tooltip; tooltip.style.left = event.pageX + "px"; tooltip.style.top = event.pageY + "px"; this.selectedPointData = intersects[0].object.properties; this.show = true; } else { this.selectedPointData = {}; this.show = false; } }); }, }, }; </script> <style scoped lang="scss"> .page { height: 100vh; .tooltip { position: absolute; background-color: #fff; padding: 10px; border-radius: 8px; } } </style>
复制