当前位置:   article > 正文

three.js实现3d地图,包含散点,地图背景图片(vue)_three.js 3d地图

three.js 3d地图

效果图

图片资源:

1、下载three.js及d3依赖,我这里使用的是three r153版本

  1. npm i three
  2. npm i d3

2、使用d3将地理坐标转换成坐标轴xy值,在地图区域及边界线加载到场景后,通过包围盒计算完整地图的max和min坐标,再根据max和min的值重新计算每个地图区域的uv坐标,如果不重新计算uv坐标会导致贴图显示异常

  1. this.projection = d3
  2. .geoMercator()
  3. .center(this.centerCoordinate)
  4. .translate([0, 0]);
  5. // 加载地图背景
  6. const backgroundTexture = new THREE.TextureLoader().load(
  7. require("@/assets/images/map.png")
  8. );
  9. // 加载地图
  10. let fileLoader = new THREE.FileLoader();
  11. fileLoader.load("/anhui.json", (data) => {
  12. // 添加地图及边界线
  13. this.addMapGeometry(data);
  14. // 重新计算地图uv坐标
  15. let arr = [];
  16. let box = new THREE.Box3();
  17. for (let v of this.map.children) {
  18. for (let v2 of v.children) {
  19. // 判断是否为ExtrudeGeometry
  20. if (v2.geometry instanceof THREE.ExtrudeGeometry) {
  21. arr.push(v2);
  22. let itemBox = new THREE.Box3().setFromObject(v);
  23. box.union(itemBox);
  24. }
  25. }
  26. }
  27. var bboxMin = box.min;
  28. var bboxMax = box.max;
  29. // 计算UV的缩放比例
  30. var uvScale = new THREE.Vector2(
  31. 1 / (bboxMax.x - bboxMin.x),
  32. 1 / (bboxMax.y - bboxMin.y)
  33. );
  34. for (let v of arr) {
  35. let uvAttribute = v.geometry.getAttribute("uv");
  36. for (let i = 0; i < uvAttribute.count; i++) {
  37. let u = uvAttribute.getX(i);
  38. let v = uvAttribute.getY(i);
  39. // 将UV坐标进行归一化
  40. let normalizedU = (u - bboxMin.x) * uvScale.x;
  41. let normalizedV = (v - bboxMin.y) * uvScale.y;
  42. // 更新UV坐标
  43. uvAttribute.setXY(i, normalizedU, normalizedV);
  44. }
  45. // 更新几何体的UV属性
  46. v.geometry.setAttribute("uv", uvAttribute);
  47. v.material.map = backgroundTexture;
  48. v.material.needsUpdate = true;
  49. }
  50. });
  51. addMapGeometry(jsondata) {
  52. // 初始化一个地图对象
  53. this.map = new THREE.Object3D();
  54. jsondata = JSON.parse(jsondata);
  55. jsondata.features.forEach((elem) => {
  56. // 定一个省份3D对象
  57. const province = new THREE.Object3D();
  58. // 每个的 坐标 数组
  59. const coordinates = elem.geometry.coordinates;
  60. if (elem.geometry.type === "MultiPolygon") {
  61. // 循环坐标数组
  62. coordinates.forEach((multiPolygon) => {
  63. multiPolygon.forEach((polygon) => {
  64. this.drawItem(elem, polygon, province);
  65. });
  66. });
  67. this.map.add(province);
  68. } else if (elem.geometry.type === "Polygon") {
  69. // 循环坐标数组
  70. coordinates.forEach((polygon) => {
  71. this.drawItem(elem, polygon, province);
  72. });
  73. this.map.add(province);
  74. }
  75. });
  76. this.scene.add(this.map);
  77. },
  78. drawItem(elem, polygon, province) {
  79. const shape = new THREE.Shape();
  80. const pointsArray = new Array();
  81. for (let i = 0; i < polygon.length; i++) {
  82. const [x, y] = this.projection(polygon[i]);
  83. if (i === 0) {
  84. shape.moveTo(x, -y);
  85. }
  86. shape.lineTo(x, -y);
  87. pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep));
  88. }
  89. let curve = new THREE.CatmullRomCurve3(pointsArray);
  90. // 这里使用TubeGeometry没有使用line,主要考虑到line的宽度无法设置,也可以使用其他第三方依赖
  91. var tubeGeometry = new THREE.TubeGeometry(
  92. curve,
  93. Math.floor(pointsArray.length),
  94. 0.02,
  95. 10
  96. );
  97. const extrudeSettings = {
  98. depth: this.mapConfig.deep,
  99. bevelEnabled: false, // 对挤出的形状应用是否斜角
  100. };
  101. const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
  102. geometry.computeBoundingBox();
  103. // 创建地图区域材质
  104. let meshMaterial = new THREE.MeshStandardMaterial({
  105. color: "#ffffff",
  106. transparent: true,
  107. opacity: 1,
  108. });
  109. // 创建地图边界线材质
  110. let lineMaterial = new THREE.MeshBasicMaterial({
  111. color: "#ceebf7",
  112. });
  113. const mesh = new THREE.Mesh(geometry, meshMaterial);
  114. const line = new THREE.Mesh(tubeGeometry, lineMaterial);
  115. // 将省份的属性 加进来
  116. province.properties = elem.properties;
  117. province.add(mesh);
  118. this.boundaryLineArr.push(line);
  119. province.add(line);
  120. },

3、设置后期处理

  1. //设置光晕
  2. this.composer = new EffectComposer(this.renderer); //效果组合器
  3. //创建通道
  4. let renderScene = new RenderPass(this.scene, this.camera);
  5. this.composer.addPass(renderScene);
  6. let outlinePass = new OutlinePass(
  7. new THREE.Vector2(window.innerWidth, window.innerHeight),
  8. this.scene,
  9. this.camera,
  10. this.boundaryLineArr // 边界线数组
  11. );
  12. outlinePass.renderToScreen = true;
  13. outlinePass.edgeGlow = 2; // 光晕效果
  14. outlinePass.usePatternTexture = false;
  15. outlinePass.edgeThickness = 10; // 边框宽度
  16. outlinePass.edgeStrength = 1.5; // 光晕效果
  17. outlinePass.pulsePeriod = 0; // 光晕闪烁的速度
  18. outlinePass.visibleEdgeColor.set("#1acdec");
  19. outlinePass.hiddenEdgeColor.set("#1acdec");
  20. this.composer.addPass(outlinePass);

完整代码

注:我这边使用的是安徽的地图,需要替换自己的地图json以及地图中心地理坐标

  1. <template>
  2. <div class="page" id="page" ref="page">
  3. <div class="tooltip" ref="tooltip" v-show="show">
  4. {{ selectedPointData.name }}
  5. </div>
  6. </div>
  7. </template>
  8. <script>
  9. import * as THREE from "three";
  10. import * as d3 from "d3";
  11. import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
  12. import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
  13. import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
  14. import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
  15. import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
  16. export default {
  17. data() {
  18. return {
  19. scene: null,
  20. camera: null,
  21. renderer: null,
  22. controls: null,
  23. centerCoordinate: [117.13, 31.89], // 地图中心地理坐标
  24. projection: null, // Mercator 投影
  25. mapConfig: {
  26. deep: 0.2, // 挤出的深度
  27. },
  28. boundaryLineArr: [], // 边界线
  29. composer: "", // 后期处理
  30. pointData: [
  31. {
  32. coordinates: [117.33, 31.79],
  33. type: 1,
  34. name: "合肥",
  35. value: 100,
  36. },
  37. {
  38. coordinates: [118.502, 31.684],
  39. type: 1,
  40. name: "马鞍山",
  41. value: 100,
  42. },
  43. ],
  44. pointInstanceArr: [], // 坐标点实例
  45. show: false, // 是否显示tooltip
  46. selectedPointData: {}, // 选中的坐标点数据
  47. };
  48. },
  49. mounted() {
  50. this.init();
  51. window.addEventListener("resize", () => {
  52. this.renderer.setSize(window.innerWidth, window.innerHeight);
  53. this.camera.aspect = window.innerWidth / window.innerHeight;
  54. this.camera.updateProjectionMatrix();
  55. });
  56. },
  57. methods: {
  58. init() {
  59. this.renderer = new THREE.WebGLRenderer();
  60. this.renderer.setSize(window.innerWidth, window.innerHeight);
  61. this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
  62. document.querySelector("#page").appendChild(this.renderer.domElement);
  63. this.scene = new THREE.Scene();
  64. this.camera = new THREE.PerspectiveCamera(
  65. 45,
  66. window.innerWidth / window.innerHeight,
  67. 0.1,
  68. 1000
  69. );
  70. this.camera.position.set(5, 5, 26);
  71. this.camera.lookAt(0, 0, 0);
  72. // let axesHelp = new THREE.AxesHelper(5);
  73. // this.scene.add(axesHelp);
  74. this.controls = new OrbitControls(this.camera, this.renderer.domElement);
  75. // 墨卡托投影转换
  76. this.projection = d3
  77. .geoMercator()
  78. .center(this.centerCoordinate)
  79. .translate([0, 0]); // 根据地球贴图做轻微调整
  80. // 添加地图
  81. this.addMap();
  82. // 给地图边界线添加outline效果
  83. this.setLineOutline();
  84. // 添加灯光
  85. let ambientLight = new THREE.AmbientLight(0xffffff, 1);
  86. this.scene.add(ambientLight);
  87. // 添加散点
  88. this.setPoint();
  89. // 设置光线投射
  90. this.setRaycaster();
  91. this.render();
  92. },
  93. render() {
  94. this.renderer.render(this.scene, this.camera);
  95. this.controls.update();
  96. if (this.composer) this.composer.render();
  97. requestAnimationFrame(this.render);
  98. },
  99. // 添加地图
  100. addMap() {
  101. // 加载地图背景
  102. const backgroundTexture = new THREE.TextureLoader().load(
  103. require("@/assets/images/map.png")
  104. );
  105. // 加载地图
  106. let fileLoader = new THREE.FileLoader();
  107. fileLoader.load("/anhui.json", (data) => {
  108. // 添加地图及边界线
  109. this.addMapGeometry(data);
  110. // 重新计算地图uv坐标
  111. let arr = [];
  112. let box = new THREE.Box3();
  113. for (let v of this.map.children) {
  114. for (let v2 of v.children) {
  115. // 判断是否为ExtrudeGeometry,只计算所有地图区域总和的包围盒大小
  116. if (v2.geometry instanceof THREE.ExtrudeGeometry) {
  117. arr.push(v2);
  118. let itemBox = new THREE.Box3().setFromObject(v2);
  119. box.union(itemBox);
  120. }
  121. }
  122. }
  123. var bboxMin = box.min;
  124. var bboxMax = box.max;
  125. // 计算UV的缩放比例
  126. var uvScale = new THREE.Vector2(
  127. 1 / (bboxMax.x - bboxMin.x),
  128. 1 / (bboxMax.y - bboxMin.y)
  129. );
  130. for (let v of arr) {
  131. let uvAttribute = v.geometry.getAttribute("uv");
  132. for (let i = 0; i < uvAttribute.count; i++) {
  133. let u = uvAttribute.getX(i);
  134. let v = uvAttribute.getY(i);
  135. // 将UV坐标进行归一化
  136. let normalizedU = (u - bboxMin.x) * uvScale.x;
  137. let normalizedV = (v - bboxMin.y) * uvScale.y;
  138. // 更新UV坐标
  139. uvAttribute.setXY(i, normalizedU, normalizedV);
  140. }
  141. // 更新几何体的UV属性
  142. v.geometry.setAttribute("uv", uvAttribute);
  143. v.material.map = backgroundTexture;
  144. v.material.needsUpdate = true;
  145. }
  146. });
  147. },
  148. addMapGeometry(jsondata) {
  149. // 初始化一个地图对象
  150. this.map = new THREE.Object3D();
  151. jsondata = JSON.parse(jsondata);
  152. jsondata.features.forEach((elem) => {
  153. // 定一个省份3D对象
  154. const province = new THREE.Object3D();
  155. // 每个的 坐标 数组
  156. const coordinates = elem.geometry.coordinates;
  157. if (elem.geometry.type === "MultiPolygon") {
  158. // 循环坐标数组
  159. coordinates.forEach((multiPolygon) => {
  160. multiPolygon.forEach((polygon) => {
  161. this.drawItem(elem, polygon, province);
  162. });
  163. });
  164. this.map.add(province);
  165. } else if (elem.geometry.type === "Polygon") {
  166. // 循环坐标数组
  167. coordinates.forEach((polygon) => {
  168. this.drawItem(elem, polygon, province);
  169. });
  170. this.map.add(province);
  171. }
  172. });
  173. this.scene.add(this.map);
  174. },
  175. drawItem(elem, polygon, province) {
  176. const shape = new THREE.Shape();
  177. const pointsArray = new Array();
  178. for (let i = 0; i < polygon.length; i++) {
  179. const [x, y] = this.projection(polygon[i]);
  180. if (i === 0) {
  181. shape.moveTo(x, -y);
  182. }
  183. shape.lineTo(x, -y);
  184. pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep));
  185. }
  186. let curve = new THREE.CatmullRomCurve3(pointsArray);
  187. // 这里使用TubeGeometry没有使用line,主要考虑到line的宽度无法设置,也可以使用其他第三方依赖去做
  188. var tubeGeometry = new THREE.TubeGeometry(
  189. curve,
  190. Math.floor(pointsArray.length),
  191. 0.02,
  192. 10
  193. );
  194. const extrudeSettings = {
  195. depth: this.mapConfig.deep,
  196. bevelEnabled: false, // 对挤出的形状应用是否斜角
  197. };
  198. const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
  199. geometry.computeBoundingBox();
  200. // 创建地图区域材质
  201. let meshMaterial = new THREE.MeshStandardMaterial({
  202. color: "#ffffff",
  203. transparent: true,
  204. opacity: 1,
  205. });
  206. // 创建地图边界线材质
  207. let lineMaterial = new THREE.MeshBasicMaterial({
  208. color: "#ceebf7",
  209. });
  210. const mesh = new THREE.Mesh(geometry, meshMaterial);
  211. const line = new THREE.Mesh(tubeGeometry, lineMaterial);
  212. // 将省份的属性 加进来
  213. province.properties = elem.properties;
  214. province.add(mesh);
  215. this.boundaryLineArr.push(line);
  216. province.add(line);
  217. },
  218. // 给地图边界线添加outline效果
  219. setLineOutline() {
  220. //设置光晕
  221. this.composer = new EffectComposer(this.renderer); //效果组合器
  222. //创建通道
  223. let renderScene = new RenderPass(this.scene, this.camera);
  224. this.composer.addPass(renderScene);
  225. let outlinePass = new OutlinePass(
  226. new THREE.Vector2(window.innerWidth, window.innerHeight),
  227. this.scene,
  228. this.camera,
  229. this.boundaryLineArr
  230. );
  231. outlinePass.renderToScreen = true;
  232. outlinePass.edgeGlow = 2; // 光晕效果
  233. outlinePass.usePatternTexture = false;
  234. outlinePass.edgeThickness = 10; // 边框宽度
  235. outlinePass.edgeStrength = 1.5; // 光晕效果
  236. outlinePass.pulsePeriod = 0; // 光晕闪烁的速度
  237. outlinePass.visibleEdgeColor.set("#1acdec");
  238. outlinePass.hiddenEdgeColor.set("#1acdec");
  239. this.composer.addPass(outlinePass);
  240. },
  241. // 添加散点
  242. setPoint() {
  243. let pointTexture = new THREE.TextureLoader().load(
  244. require("@/assets/images/point.png")
  245. );
  246. for (let v of this.pointData) {
  247. let [x, y] = this.projection(v.coordinates);
  248. const sprite = new THREE.Sprite(
  249. new THREE.SpriteMaterial({
  250. map: pointTexture,
  251. })
  252. );
  253. sprite.scale.set(0.7, 0.7, 1);
  254. sprite.position.set(x, -y, this.mapConfig.deep + 0.5);
  255. sprite.properties = v;
  256. this.pointInstanceArr.push(sprite);
  257. this.scene.add(sprite);
  258. }
  259. },
  260. // 光线投射
  261. setRaycaster() {
  262. const raycaster = new THREE.Raycaster();
  263. const pointer = new THREE.Vector2();
  264. this.$refs.page.addEventListener("click", (event) => {
  265. pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
  266. pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
  267. raycaster.setFromCamera(pointer, this.camera);
  268. const intersects = raycaster.intersectObjects(this.pointInstanceArr);
  269. if (intersects && intersects.length > 0) {
  270. let tooltip = this.$refs.tooltip;
  271. tooltip.style.left = event.pageX + "px";
  272. tooltip.style.top = event.pageY + "px";
  273. this.selectedPointData = intersects[0].object.properties;
  274. this.show = true;
  275. } else {
  276. this.selectedPointData = {};
  277. this.show = false;
  278. }
  279. });
  280. },
  281. },
  282. };
  283. </script>
  284. <style scoped lang="scss">
  285. .page {
  286. height: 100vh;
  287. .tooltip {
  288. position: absolute;
  289. background-color: #fff;
  290. padding: 10px;
  291. border-radius: 8px;
  292. }
  293. }
  294. </style>

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/665428
推荐阅读
相关标签
  

闽ICP备14008679号