three.js绘制中国地理数据

threejs渲染地图数据

一年合同到期公司不续约了,闲来无事,我写一下个人技术博客吧。之前也一直在掘金上学习threejs怎么渲染地图数据的,今天我把自己学到的给大家来个总结。

1.准备地理数据

网址:DataV.GeoAtlas地理小工具系列,这个是阿里的下载地图数据的网站,你可以去下载全中国或者省市县的数据。

2.用到的three.js的绘制地图的api

three.js是一个3d渲染引擎,它能够绘制3d的物体,也有丰富的api实现很多很炫很酷的效果。 渲染地图的数据就要依赖ExtrudeGeometry(shapes : Array, options : Object) (挤压缓冲几何体)。接受两个参数,shapes是形状,这个shapes就用到地理数据,划重点;options是几何体的一些配置。

ExtrudeGeometry(shapes : Array, options : Object)

shapes --- 形状或者一个包含形状的数组。

options --- 一个包含有下列参数的对象:

  • curveSegments --- int,曲线上点的数量,默认值是12。
  • steps --- int,用于沿着挤出样条的深度细分的点的数量,默认值为1。
  • depth --- float,挤出的形状的深度,默认值为1。
  • bevelEnabled --- bool,对挤出的形状应用是否斜角,默认值为true。
  • bevelThickness --- float,设置原始形状上斜角的厚度。默认值为0.2。
  • bevelSize --- float。斜角与原始形状轮廓之间的延伸距离,默认值为bevelThickness-0.1。
  • bevelOffset --- float. Distance from the shape outline that the bevel starts. Default is 0.
  • bevelSegments --- int。斜角的分段层数,默认值为3。
  • extrudePath --- THREE.Curve对象。一条沿着被挤出形状的三维样条线。Bevels not supported for path extrusion.
  • UVGenerator --- Object。提供了UV生成器函数的对象。

shapes要用到另一个api THREE.Shape (使用路径以及可选的孔洞来定义一个二维形状平面),需要用到两个它的方法。

3.坐标系的转换

因为我们是基于threejs渲染的,而threejs的坐标系肯定和经纬度的地球坐标系不同,这就要借助一个工具,把地球经纬度数据转变成一个threejs的数据了。这个工具是d3-geo这个库里的geoMercator 圆柱投影 | D3 中文网。球面墨卡托投影。将世界投影到一个正方形中。

4.接下来直接放代码吧,这段代码实现了展示全国的各个省份的区域、边界和名称,悬浮移动到每个省有悬浮效果。

js 复制代码
<template>
  <div id="map" ref="mapRef"></div>
</template>

<script setup>
import { ref, nextTick, onMounted } from 'vue'
import geoData from './data/geo.json'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { geoMercator } from 'd3-geo'
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
import * as turf from '@turf/turf'
import { throttle } from 'lodash-es'
const mapRef = ref(null)
const mapInfo = {
  maxLng: -Infinity,
  minLng: Infinity,
  maxLat: -Infinity,
  minLat: Infinity,
  centerPos: []
}
let projection, renderer, camera, scene, labelRenderer;
let texture;
const createMap = () => {
  const map = new THREE.Object3D()
  geoData.features.forEach(item => {
    const { coordinates, type } = item.geometry
    coordinates.forEach(coord => {
      if (type === 'MultiPolygon') {
        coord.forEach(coordItem => {
          const mesh = createMesh(coordItem)
        })
      } else {
        const mesh = createMesh(coord)
      }
    })

  })
  mapInfo.centerPos = [(mapInfo.maxLng + mapInfo.minLng) / 2, (mapInfo.maxLat + mapInfo.minLat) / 2]
  console.log(mapInfo);
  projection = geoMercator().center(mapInfo.centerPos).translate([0, 0])
  console.log(projection([(mapInfo.maxLng + mapInfo.minLng) / 2, (mapInfo.maxLat + mapInfo.minLat) / 2]));

  // 墨卡托投影
  geoData.features.forEach(item => {
    const unit = new THREE.Object3D()
    const unit2 = new THREE.Object3D()
    const { coordinates, type } = item.geometry
    console.log(item);
    const depth = Math.random() * 6 + 0.3;
    if (item.properties.center) {
      let center;
      if (type === 'MultiPolygon') {
        center = turf.centroid(turf.polygon(coordinates[0]))
      } else {
        center = turf.centroid(turf.polygon(coordinates))
      }
      const label = createLabel(item.properties.name, item.properties.center, depth)
      const icon = createIcon(item.properties.center, depth)
      unit2.add(label, icon)
    }
    coordinates.forEach(coord => {
      // console.log(coordinates);
      if (type === 'MultiPolygon') {
        coord.forEach(coordItem => {
          const mesh = createMesh2(coordItem, depth,item.properties.name)
          const line = createLine(coordItem, depth)
          unit.add(mesh, ...line)
        })
      } else {
        const mesh = createMesh2(coord, depth,item.properties.name)
        const line = createLine(coord, depth)
        unit.add(mesh, ...line)
      }
    })

    map.add(unit,unit2)
  })
  return map
}
const createMesh = (data, depth, color) => {
  data.forEach((item, idx) => {
    const [x, y] = item
    if (x > mapInfo.maxLng) {
      mapInfo.maxLng = x
    }
    if (x < mapInfo.minLng) {
      mapInfo.minLng = x
    }
    if (y > mapInfo.maxLat) {
      mapInfo.maxLat = y
    }
    if (y < mapInfo.minLat) {
      mapInfo.minLat = y
    }
  })
}
function randomHexColor() {
  const color = new THREE.Color(`hsl(
      ${233},
      ${Math.random() * 30 + 55}%,
      ${Math.random() * 30 + 55}%)`).getHex()

  return color;
}
const createMesh2 = (data, depth, name) => {
  const shape = new THREE.Shape()
  data.forEach((item, idx) => {
    // 墨卡托投影导致y值变化,所以y值要取反
    const [x, y] = projection(item)

    if (idx == 0) {
      shape.moveTo(x, -y)

    } else {
      shape.lineTo(x, -y)
    }
  })
  // 平面没有厚度
  // const shapeGeometry = new THREE.ShapeGeometry(shape)
  // 厚度
  const shapeGeometry = new THREE.ExtrudeGeometry(
    shape,
    {
      depth,
      // bevelThickness: 5, //倒角尺寸:拉伸方向
      // bevelSize: 5, //倒角尺寸:垂直拉伸方向
      // bevelSegments: 20, //倒圆角:倒角细分精度,默认3
    }
  );
  const shapeMat = new THREE.MeshBasicMaterial({
    color: randomHexColor(),
    side: THREE.DoubleSide,
    transparent: true,
    opacity: 0.95
  })
  let mesh=new THREE.Mesh(shapeGeometry, shapeMat)
  mesh.name=name
  return mesh
}
const createLine = (data, depth) => {
  const points = []
  data.forEach(item => {
    const [x, y] = projection(item)
    points.push(new THREE.Vector3(x, -y, 0))
  })
  // BufferGeometry 是 Three.js 中一种高效的几何体表示方式,它通过使用 WebGL 的缓冲区(如顶点缓冲区)来存储几何数据
  const lineGeometry = new THREE.BufferGeometry().setFromPoints(points)
  const uplineMat = new THREE.LineBasicMaterial({
    color: 0xffffff,
    side: THREE.DoubleSide
  })
  const downlineMat = new THREE.LineBasicMaterial({
    color: 0xffffff,
    side: THREE.DoubleSide
  })
  const upline = new THREE.Line(lineGeometry, uplineMat)
  const downline = new THREE.Line(lineGeometry, downlineMat)
  upline.position.set(0, 0, depth + 0.5)
  downline.position.set(0, 0, -0.5)
  return [upline, downline]
}
const createLabel = (text, point, depth) => {
  const element = document.createElement('div')
  element.className = 'label'
  element.textContent = text
  element.style.cssText = `
    color: #fff;
    font-size: 10px;
    text-shadow: 0 0 2px #000;
  `
  const label = new CSS2DObject(element)
  const [x, y] = projection(point)
  label.position.set(x, -y, depth + 1)
  return label
}
const initCSS2DRenderer = () => {
  labelRenderer = new CSS2DRenderer()
  labelRenderer.domElement.style.position = 'absolute'
  labelRenderer.domElement.style.top = '0px'
  labelRenderer.domElement.style.left = '0px'
  labelRenderer.domElement.style.pointerEvents = 'none'
  labelRenderer.setSize(window.innerWidth, window.innerHeight)
  mapRef.value?.appendChild(labelRenderer.domElement)
}
const createIcon = (point, depth) => {
  const iconUrl = new URL('./img/pos.png', import.meta.url).href
  const map = new THREE.TextureLoader().load(iconUrl)
  const material = new THREE.SpriteMaterial({
    map,
    transparent: true,
  })
  const sprite = new THREE.Sprite(material)
  const [x, y] = projection(point)
  sprite.position.set(x, -y + 1, depth + 1)
  // sprite.renderOrder = 1;
  sprite.scale.set(2, 2, 2);
  console.log(sprite);
  return sprite
}
const hoverMap = () => {
  function changeOpacity(obj,opacity) {

    obj.children.forEach((item) => {
      if (item.type === "Mesh") {
        item.material.opacity = opacity;
      }else{
        if(item.children&&item.children.length){
          changeOpacity(item,opacity)
        }
      }
    });
  }
  // 实现鼠标的交互
  mapRef.value.addEventListener('mousemove', throttle((e) => {
    const mouse = new THREE.Vector2()
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1
    const raycaster = new THREE.Raycaster()
    raycaster.setFromCamera(mouse, camera)
    const intersects = raycaster.intersectObjects(scene.children).filter(item=>item.object.type!='line')
    if (intersects.length > 0) {
      let intersect = intersects[0].object.parent.parent
      console.log(intersect);
      changeOpacity(intersect,1)
      let intersect2=intersects[0].object.parent
      changeOpacity(intersect2,.6)
    }
  },200))
}
const initMap = (fn) => {
  scene = new THREE.Scene()
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
  camera.position.set(-10, -20, 40)
  camera.lookAt(0, 0, 0)
  renderer = new THREE.WebGLRenderer()
  renderer.setSize(window.innerWidth, window.innerHeight)
  renderer.render(scene, camera)
  mapRef.value?.appendChild(renderer.domElement)
  const gridHelper = new THREE.GridHelper(100, 10)
  gridHelper.position.set(0, -100, 0)
  scene.add(gridHelper)
  const axisHelper = new THREE.AxesHelper(500)
  scene.add(axisHelper)
  const controls = new OrbitControls(camera, renderer.domElement)
  const map = createMap()
  scene.add(map)
  initCSS2DRenderer()
  hoverMap()
  fn?.()
}
onMounted(() => {
  initMap(() => {
    function animate() {
      requestAnimationFrame(animate)
      renderer.render(scene, camera)
      labelRenderer.render(scene, camera);
    }
    animate()
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight
      camera.updateProjectionMatrix()
      renderer.setSize(window.innerWidth, window.innerHeight)
    })
  })
})

</script>
<style scoped lang="scss">
#map {
  width: 100%;
  height: 100%;
}
</style>
相关推荐
烛阴29 分钟前
WebSocket实时通信入门到实践
前端·javascript
草巾冒小子32 分钟前
vue3实战:.ts文件中的interface定义与抛出、其他文件的调用方式
前端·javascript·vue.js
DoraBigHead1 小时前
你写前端按钮,他们扛服务器压力:搞懂后端那些“黑话”!
前端·javascript·架构
Xiaouuuuua2 小时前
一个简单的脚本,让pdf开启夜间模式
java·前端·pdf
@Dream_Chaser2 小时前
uniapp ruoyi-app 中使用checkbox 无法选中问题
前端·javascript·uni-app
深耕AI2 小时前
【教程】在ubuntu安装Edge浏览器
前端·edge
倔强青铜三2 小时前
苦练Python第4天:Python变量与数据类型入门
前端·后端·python
倔强青铜三2 小时前
苦练Python第3天:Hello, World! + input()
前端·后端·python
上单带刀不带妹2 小时前
JavaScript中的Request详解:掌握Fetch API与XMLHttpRequest
开发语言·前端·javascript·ecmascript
倔强青铜三3 小时前
苦练Python第2天:安装 Python 与设置环境
前端·后端·python