vue3+three.js中国3D地图

代码如下

china.json文件放在public文件夹里面
china.json文件在https://datav.aliyun.com/portal/school/atlas/area_selector这里面下载GeoJSON
javascript 复制代码
<template>
  <div>
    <div id="provinceInfo" ref="provinceInfo"></div>
  </div>
</template>

<script setup lang="ts">
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { onMounted, ref, onUnmounted } from 'vue'
import * as d3 from 'd3-geo'

// 浮层 DOM 引用
const provinceInfo = ref<HTMLDivElement | null>(null)

// 全局变量
let renderer: THREE.WebGLRenderer
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let controller: OrbitControls
let raycaster: THREE.Raycaster
let mouse: THREE.Vector2
let map: THREE.Object3D
let activeIntersect: any[] = []
let eventOffset = { x: 0, y: 0 }

onMounted(() => {
  init()
})

onUnmounted(() => {
  window.removeEventListener('resize', onWindowResize)
})

function init() {
  // 渲染器
  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(window.innerWidth, window.innerHeight)
  document.body.appendChild(renderer.domElement)

  // 场景
  scene = new THREE.Scene()
  // 背景颜色
  scene.background = new THREE.Color(0x072761)

  // 相机
  camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  )
  camera.position.set(0, -70, 150)
  camera.lookAt(0, 0, 0)

  setController()
  setLight()
  setRaycaster()
  loadMapData()
  animate()
  window.addEventListener('resize', onWindowResize)
}

// 窗口缩放
function onWindowResize() {
  renderer.setSize(window.innerWidth, window.innerHeight)
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
}

// 加载地图数据
function loadMapData() {
  const loader = new THREE.FileLoader()
  loader.load('/china.json', (data: string) => {
    const jsonData = JSON.parse(data)
    initMap(jsonData)
  })
}

// 初始化地图
function initMap(chinaJson: any) {
  map = new THREE.Object3D()

  const projection = d3.geoMercator()
    .center([104.0, 37.5])
    .scale(80)
    .translate([0, 0])

  chinaJson.features.forEach((elem: any) => {
    const province = new THREE.Object3D()
    const coordinates = elem.geometry.coordinates

    coordinates.forEach((multiPolygon: any) => {
      multiPolygon.forEach((polygon: any) => {
        const shape = new THREE.Shape()
        const points: THREE.Vector3[] = []

        for (let i = 0; i < polygon.length; i++) {
          const [x, y] = projection(polygon[i])
          const px = x
          const py = -y

          if (i === 0) shape.moveTo(px, py)
          shape.lineTo(px, py)
          points.push(new THREE.Vector3(px, py, 4.01))
        }

        const extrudeSettings = { depth: 4, bevelEnabled: false }
        const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings)

        const material = new THREE.MeshBasicMaterial({
          color: '#1478d2',
          transparent: true,
          opacity: 0.8
        })
        const material1 = new THREE.MeshBasicMaterial({
          color: '#41c7fd',
          transparent: true,
          opacity: 0.8
        })

        const mesh = new THREE.Mesh(geometry, [material, material1])

        // 边框(修复新版 three.js 废弃 Geometry)
        const lineGeometry = new THREE.BufferGeometry().setFromPoints(points)
        const lineMaterial = new THREE.LineBasicMaterial({ color: '#41c7fd' })
        const line = new THREE.Line(lineGeometry, lineMaterial)

        province.add(mesh)
        province.add(line)
      })
    })

    // 省份信息
    province.userData = elem.properties
    if (elem.properties.contorid) {
      const [x, y] = projection(elem.properties.contorid)
      province.userData._centroid = [x, y]
    }

    map.add(province)
  })

  scene.add(map)
}

// 射线拾取(鼠标检测)
function setRaycaster() {
  raycaster = new THREE.Raycaster()
  mouse = new THREE.Vector2()

  window.addEventListener('mousemove', (event) => {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
    eventOffset.x = event.clientX
    eventOffset.y = event.clientY

    if (provinceInfo.value) {
      provinceInfo.value.style.left = eventOffset.x + 2 + 'px'
      provinceInfo.value.style.top = eventOffset.y + 2 + 'px'
    }
  })
}

// 灯光
function setLight() {
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
  scene.add(ambientLight)
}

// 控制器
function setController() {
  controller = new OrbitControls(camera, renderer.domElement)
}

// 动画循环
function animate() {
  requestAnimationFrame(animate)
  controller.update()

  raycaster.setFromCamera(mouse, camera)
  const intersects = raycaster.intersectObjects(scene.children, true)

  // 恢复上一次颜色
  if (activeIntersect.length > 0) {
    activeIntersect.forEach(item => {
      item.object.material[0].color.set('#1478d2')
      item.object.material[1].color.set('#41c7fd')
    })
  }

  activeIntersect = []

  // 选中变色
  for (let i = 0; i < intersects.length; i++) {
    const obj = intersects[i].object
    if (obj.material && obj.material.length === 2) {
      activeIntersect.push(intersects[i])
      obj.material[0].color.set(0x41c7fd)
      obj.material[1].color.set(0x41c7fd)
      break
    }
  }

  createProvinceInfo()
  renderer.render(scene, camera)
}

// 显示省份浮层
function createProvinceInfo() {
  if (!provinceInfo.value) return

  if (activeIntersect.length !== 0 && activeIntersect[0].object.parent.userData?.name) {
    const info = activeIntersect[0].object.parent.userData
    provinceInfo.value.textContent = info.name
    provinceInfo.value.style.visibility = 'visible'
  } else {
    provinceInfo.value.style.visibility = 'hidden'
  }
}
</script>

<style>
/* 必须全局样式,不能 scoped */
html,
body {
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

#provinceInfo {
  position: absolute;
  z-index: 2;
  background: white;
  padding: 10px;
  visibility: hidden;
  pointer-events: none;
}
</style>
相关推荐
An_s34 分钟前
rust开发wasm与浏览器(js)交互
javascript·rust·wasm
山河木马1 小时前
GPU自动处理专题4-填充覆盖区域(光栅化)
javascript·webgl·计算机图形学
从零开始的代码生活_1 小时前
C++ 内存管理:从内存分区到 new/delete 底层原理
开发语言·c++·后端
悦儿遥遥雨11 小时前
PXE + Kickstart 无人值守批量部署系统
linux·javascript·nginx
wabs6661 小时前
关于单调栈【力扣739.每日温度的思考】
java·开发语言
AOwhisky1 小时前
Python 学习笔记(第三期)——流程控制核心知识点自测与详解
开发语言·笔记·python·学习·云原生·运维开发·流程控制
清秋2 小时前
全网最全 ECMAScript 攻略( 更新至 ES2026)
前端·javascript·编程语言
Hilaku2 小时前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
逆向编程2 小时前
Socket异常连接自动清理方案
开发语言
va学弟3 小时前
Java 网络通信编程(10):Channel 和 Selector
java·开发语言