threejs创建3d地图

文章目录

  • 前言
  • 基本思路


前言

基于react-hooks创建的三维地图,只实现了基本的交互展示,可根据个人喜好增加各种交互和展示效果,效果如下。


基本思路

使用threejs创建3d地图注意的组要是以下几点。

  • GeoJson数据规范,尤其是面状Feature的数据结构特点,可参考官网:https://geojson.org/。
  • 地图生成和交互主要是使用THREE.ExtrudeBufferGeometry和THREE.Raycaster()方法。
  • 中国政区GeoJSON数据可从阿里云数据平台下载。将经纬度坐标转移到屏幕上使用d3.js的geoMercator()方法d3官网。

完整代码如下,附注释。

import { useRef, useEffect, useCallback, useState } from 'react'
import * as THREE from 'three'
import * as D3 from 'd3'
import OrbitControls from 'three-orbitcontrols';
import './index.scss'

const View = () => {
  const page = useRef(); // useRef不会导致重新渲染

  /**
   * 场景、相机、渲染器作为threejs的基本结构,需要在页面进入时渲染完毕
   */
  const scence = useRef(new THREE.Scene()).current; //场景
  const camera = useRef(new THREE.PerspectiveCamera()).current; //摄像机(透视投影)
  const render = useRef(new THREE.WebGLRenderer()).current; //渲染器
  const controls = new OrbitControls(camera, render.domElement);//创建控件对象
  const timer = useRef(null) // 定义定时器

  const handleProj = D3.geoMercator().center([109, 34.5]).scale(80).translate([0, 0]) // d3投影转换函数
  const mapContainer = useRef(new THREE.Object3D()).current; // 存储地图Object3D对象
  const lastPickedProvince = useRef(null) // 上次选中的省份

  useEffect(() => {
    page.current.appendChild(render.domElement);
    initScene();
    initLight();
    initGeom();
    renderScene();
  }, [])

  useEffect(() => {
    bindEvent();
  }, [])

  const bindEvent = () => {
    window.addEventListener('mousemove', (event) => {
      const pointer = new THREE.Vector2();
      // 像素坐标=>规范化设备坐标系 [-1,1]
      pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
      pointer.y = - (event.clientY / window.innerHeight) * 2 + 1;

      // 获取鼠标点击的位置生成射线
      const raycaster = new THREE.Raycaster();
      raycaster.setFromCamera(pointer, camera);
      // 获取射线相交的物体集合
      // debugger
      const intersects = raycaster.intersectObjects(mapContainer.children, true);
      if (intersects.length) {
        const pcickedProvice = intersects[0].object;
        // 选中了新的省份
        if (lastPickedProvince.current?.properties !== pcickedProvice.properties) {
          // 上次选中的恢复半透明
          if (lastPickedProvince.current) {
            lastPickedProvince.current.material.opacity = 0.5
          }
          pcickedProvice.material.opacity = 1;  // 新选中的设置为不透明
          lastPickedProvince.current = pcickedProvice;
        }
      } else { // 鼠标移开地图,之前选中的省份回复半透明
        if (lastPickedProvince.current) {
          lastPickedProvince.current.material.opacity = 0.5
        }
        lastPickedProvince.current = null;
      }
    }, false)
  }

  // 初始化场景
  const initScene = useCallback(() => {
    render.setSize(page.current.offsetWidth, page.current.offsetHeight); // 渲染器设置尺寸
    // 设置背景颜色
    render.setClearColor(new THREE.Color(0x000000)); // 设置背景颜色和透明度
    render.shadowMap.enabled = true; // 渲染器允许渲染阴影⭐

    /**
     * 设置摄像机的属性
     */
    camera.aspect = (page.current.offsetWidth / page.current.offsetHeight) // 摄像机设置屏幕宽高比
    camera.fov = 45; // 摄像机的视角
    camera.near = 0.01; // 近面距离
    camera.far = 1001; // 远面距离
    camera.position.set(2, 2, 200) // 设置摄像机在threejs坐标系中的位置
    camera.lookAt(0, 0, 0) // 摄像机的指向
    camera.updateProjectionMatrix(); // 更新摄像机投影矩阵,在任何参数被改变以后必须被调用

  }, [render, scence])

  // 初始化环境光
  const initLight = () => {
    const ambLight = new THREE.AmbientLight('#ffffff', 0.3) // 基本光源
    /**
     * 设置聚光灯相关的的属性,详情见P54
     */
    const spotLight = new THREE.SpotLight(0xFFFFFF); // 聚光灯
    spotLight.position.set(40, 200, 10);
    spotLight.castShadow = true; // 只有该属性为true时,该点光源允许产生阴影,并且下列属性可用
    scence.add(ambLight, spotLight); // 向场景中添加光源
  }

  // 初始化地理数据集
  const initGeom = () => {
    // 加载中国地区的geoJson数据集
    const fileLoader = new THREE.FileLoader();
    fileLoader.load(
      'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json',
      (data) => {
        const chinaJson = JSON.parse(data)
        handleData(chinaJson)
      }
    )
  }

  // 处理GeoJson data
  const handleData = (jsonData) => {
    const feaureList = jsonData.features;
    feaureList.forEach((feature) => { // 每个feature都代表一个省份
      const province = new THREE.Object3D;
      province.properties = feature.properties.name // 省份名称
      province.name = feature.properties.name // 省份名称
      const coordinates = feature.geometry.coordinates // 省份坐标信息
      if (feature.geometry.type === 'MultiPolygon') {
        coordinates.forEach((coord) => {
          coord.forEach((coordinate) => {
            const extrudeMesh = creatDepthPolygon(coordinate)
            extrudeMesh.properties = feature.properties.name
            province.add(extrudeMesh)
          })
        })
      }
      if (feature.geometry.type === 'Polygon') {
        coordinates.forEach((coordinate) => {
          const extrudeMesh = creatDepthPolygon(coordinate)
          extrudeMesh.properties = feature.properties.name
          province.add(extrudeMesh)
        })
      }
      mapContainer.add(province)
    })
    scence.add(mapContainer)
  }

  // 创建三维多边形
  const creatDepthPolygon = (coordinate) => {
    const shape = new THREE.Shape();

    coordinate.forEach((item, index) => { // 每一个item都是MultiPolygon中的一个polygon
      const [x_XYZ, y_XYZ] = handleProj(item)
      if (index === 0) {
        shape.moveTo(x_XYZ, -y_XYZ)
      } else {
        shape.lineTo(x_XYZ, -y_XYZ)
      }
    })

    const geometry = new THREE.ExtrudeBufferGeometry(shape, { depth: 1, bevelEnabled: false, })
    const material = new THREE.MeshBasicMaterial({
      color: new THREE.Color(Math.random() * 0xffffff), // 每个省随机赋色
      transparent: true,
      opacity: 0.5
    })
    return new THREE.Mesh(geometry, material)
  }

  // 渲染器执行渲染
  const renderScene = useCallback(() => {
    timer.current = window.requestAnimationFrame(() => renderScene())
    controls.update();
    render.render(scence, camera);
  }, [render])

  return (
    <>
      <div className='page' ref={page} >
        <div style={{ position: 'fixed', top: '0', right: '0' }}>
          <button onClick={() => { console.log(scence) }} style={{ marginRight: '10px' }}>打印场景</button>
        </div>
      </div>
    </>
  )
};

export default View

你可能感兴趣的:(threejs,3d,react.js,javascript)