想要升值加薪麻???分享”three.js“实现一个3D版的地图!

131 阅读4分钟

首先Three.js (想要升值加薪麻??那就卷他!)

思路:通过各地区的JSON信息,画出不规则的点,连成线。 在通过线成面,在通过拉伸实现地图的3D效果。

HTML部分

<template>
  <canvas id="c2d" :width="width" :height="height" class="canvas"></canvas>
</template>

JS部分👻

import * as THREE from 'three'
import china from '@/assets/map/china.json' // 这个是地图的JSON
import { geoMercator } from 'd3-geo'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'


const width = (document.documentElement.clientWidth / 7) * 3 + 100
const height = document.documentElement.clientHeight - 300

// 墨卡尔投影转换
const projection = geoMercator().center([120.498, 29.0918]).translate([0, 0]) // 设置中心坐标
const historyIntersects = ref([])

const url = new URL('@/assets/map/china.json', import.meta.url).href // 把地图转换成地址不然后续本地地图加载可能会失败        

const rundNum = ref(0) // 渲染次数

const sceneMolud = ref()  //  THREE.Scene() 

核心代码🙄 这里没写好 可以把init里面的代码很多拆分出来。 这样就不会显着臃肿了

function init() {
const map = new THREE.Object3D()
  // 场景
  const scene = new THREE.Scene()
  sceneMolud.value = scene
  const canvas = document.querySelector('#c2d') as HTMLElement
  // 渲染器
  const renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true,
    alpha: true,
  })

  const fov = 28 // 视野范围
  const aspect = 1 // 相机默认值 画布的宽高比
  const near = 0.1 // 近平面
  const far = 10000 // 远平面
  // 透视投影相机
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far)

  camera.position.set(180, 0, 300)
  camera.lookAt(0, 0, 0)

  // 控制相机
  const controls = new OrbitControls(camera, canvas)
  controls.update()

  {
    const color = 0xffffff
    const intensity = 1
    // 环境光
    const light = new THREE.AmbientLight(color, intensity)
    // 加入场景
    scene.add(light)
  }
  
  
  //---------------------------分割-------------------
  
  
  /**
  * 渲染函数 
 */
  function render() {
    renderer.render(scene, camera)


    requestAnimationFrame(render)
  }
  requestAnimationFrame(render)
  
 //---------------------------分割------------------- 
  
  // 这里加载地图信息
const loader = new THREE.FileLoader()
  loader.load(url, (data: any) => {
    const jsondata = JSON.parse(data)
    operationData(jsondata)
  }) 
  
 
//---------------------------分割------------------- 
function operationData(jsondata: { features: any }) {
    // 全国信息
    const features = jsondata.features

    features.forEach(
      (feature: {
        properties: { name: string }
        geometry: { coordinates: any; type: string }
      }) => {
        // 单个省份 对象
        const province = new THREE.Object3D()
      


        const coordinates = feature.geometry.coordinates


        const color = 'rgb(55, 143, 240, 0.2)'

        if (feature.geometry.type === 'MultiPolygon') {
          // 多个,多边形
          coordinates.forEach((coordinate: any[]) => {
            // coordinate 多边形数据
            coordinate.forEach(rows => {
              const mesh = drawExtrudeMesh(rows, color, feature.properties.name)
              const line = lineDraw(rows, 'rgb(145, 180, 185)')

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

        if (feature.geometry.type === 'Polygon') {
          // 多边形
          coordinates.forEach((coordinate: any) => {
            const mesh = drawExtrudeMesh(
              coordinate,
              color,
              feature.properties.name,
            )
            const line = lineDraw(coordinate, 'rgb(145, 180, 185 )')

            province.add(line)
            province.add(mesh)
          })
        }
        map.add(province)
      },
    )
    scene.add(map)
  }


//---------------------------分割-------------------

  /**
   * 立体几何图形
   * @param polygon 多边形 点数组
   * @param color 材质颜色
   * */
  function drawExtrudeMesh(polygon: any[], color: string, name: string) {
 

    const shape = new THREE.Shape()
    // 更具数组画多边形材料
    polygon.forEach((row, i) => {
      const [x, y] = projection(row)!
      if (i === 0) {
        shape.moveTo(x, -y)
      }
      shape.lineTo(x, -y)
    })


    const dt = new URL('@/assets/map/img_bg_map.jpg', import.meta.url).href

    const texture = new THREE.TextureLoader().load(dt) // 这里是贴图   
   


    texture.center.set(0.5, 0.5) // 设置贴图居中
    texture.repeat.set(0.01, 0.015)

    texture.wrapS = THREE.RepeatWrapping
    texture.wrapT = THREE.RepeatWrapping

    const geometry = new THREE.ExtrudeGeometry(shape, {
      depth: 10,
      bevelEnabled: false,
    })
    // MeshBasicMaterial
    const material = new THREE.MeshBasicMaterial({
      color: color,
      transparent: true,
      opacity: 0.8,
      map: texture,
      // FrontSide   DoubleSide
      side: THREE.DoubleSide,
    })
    const mesh = new THREE.Mesh(geometry, material)
    mesh.name = name
    return mesh
  }
  
  
  //---------------------------分割-------------------
    /**
   * 边框 图形绘制
   * @param polygon 多边形 点数组
   * @param color 材质颜色
   * */
  function lineDraw(polygon: any[], color: string) {
    const lineGeometry = new THREE.BufferGeometry()
    const pointsArray = new Array()
    polygon.forEach((row: any) => {
      const [x, y] = projection(row)!
      // 创建三维点
      pointsArray.push(new THREE.Vector3(x, -y, 10))
    })
    // 放入多个点
    lineGeometry.setFromPoints(pointsArray)

    const lineMaterial = new THREE.LineBasicMaterial({
      color: color,
      transparent: true,
      linewidth: 20,
      fog: true,

      // opacity: 1,
    })
    return new THREE.Line(lineGeometry, lineMaterial)
  }

//---------------------------分割-------------------

  /**
   * 文字渲染
   * */
  async function addTownNameMarker() {
    rundNum.value++
    const markers = new THREE.Group()
    markers.position.y = 0.8
    const ky = new URL('@assets/images/login_img_logo.png', import.meta.url)
      .href
    const points = china.features.map(item => {
      return {
        name: item.properties.name,
        lnglat: item.properties.center as [number, number],
      }
    })

    for (let i = 0; i < points.length - 1; i++) {
      let [x, y] = projection(points[i].lnglat)!

      let canvas = document.createElement('canvas')
      canvas.width = 200
      canvas.height = 100

      const ctx = canvas.getContext('2d')!

      ctx.fillStyle = '#6ff3c5'
      ctx.font = '22px 微软雅黑'
      ctx.textAlign = 'center'
      ctx.textBaseline = 'middle'
      ctx.fillText(points[i].name, 100, 70, 200)

      // if (points[i].name == '浙江') {
      //   const images = new Image()
      //   images.src = ky

      //   ctx.drawImage(images, 92, 65, 30, 30)
      // }

      switch (points[i].name) {
        case '青海':
          x = x - 10
          y = y + 1
          break
        case '浙江':
          x = x + 1
          y = y + 5
          break

        default:
          break
      }

      const textTexture = new THREE.Texture(canvas)
      textTexture.magFilter = THREE.NearestFilter
      textTexture.minFilter = THREE.NearestFilter

      const textMaterial = new THREE.SpriteMaterial({ color: 0xffffff })
      textMaterial.map = textTexture
      textMaterial.transparent = true
      textMaterial.depthWrite = false
      textMaterial.sizeAttenuation = false

      const textSprite = new THREE.Sprite(textMaterial)

      textSprite.position.set(x, -y + 3, 15) // 字体位置
      textSprite.scale.set(0.08, 0.04, 1) // 缩放
      textTexture.needsUpdate = true
      textSprite.name = points[i].name
      markers.add(textSprite)

      canvas = null as any
    }

    scene.add(markers)
  }

//---------------------------分割-------------------
  // 模型居中
  function calcMeshCenter(group: any) {
    const box3 = new THREE.Box3()

    box3.expandByObject(group)

    const center = new THREE.Vector3(0, 0, 0)

    box3.getCenter(center)

    group.position.x = group.position.x - center.x

    group.position.y = group.position.y - center.y

    group.position.z = group.position.z - center.z
  }

  // addFloor()
  addTownNameMarker()
  calcMeshCenter(sceneMolud.value) // 模型居中
  // 点击文案效果
  renderer.domElement.addEventListener('dblclick', function (event) {
    // const rect = renderer.domElement.getBoundingClientRect()
    // .offsetY、.offsetX以canvas画布左上角为坐标原点,单位px

    const px = event.offsetX
    const py = event.offsetY
    //屏幕坐标px、py转WebGL标准设备坐标x、y
    //width、height表示canvas画布宽高度
    const x = (px / width) * 2 - 1
    const y = -(py / height) * 2 + 1

    //创建一个射线投射器`Raycaster`
    const raycaster = new THREE.Raycaster()
    //.setFromCamera()计算射线投射器`Raycaster`的射线属性.ray
    // 形象点说就是在点击位置创建一条射线,射线穿过的模型代表选中
    raycaster.setFromCamera(new THREE.Vector2(x, y), camera)
    // 第二个参数 是越靠近的 排序越前
    const intersects = raycaster.intersectObjects(scene.children, true) as any

    if (intersects[0] === undefined) return
    // 操作历史记录
    for (let i = 0; i < historyIntersects.value.length; i++) {
      const targe = historyIntersects.value[i]
      if (targe.object.type === 'Mesh') {
        // targe.object.material.color.set({
        //   b: 240,
        //   g: 143,
        //   isColor: true,
        //   r: 55,
        //   c: 0.2,
        // })
        targe.object.material.color.set('rgb(55, 143, 240, 0.2)')
        // rgb(55, 143, 240, 0.2)
        targe.object.position.set(1, 1, 1)
        break
      }
    }

    historyIntersects.value = intersects // 赋值历史记录

    for (let i = 0; i < intersects.length; i++) {
      const element = intersects[i]

      if (element.object.type === 'Mesh') {
        element.object.material.color.set('rgb(0, 140, 255)')

        element.object.position.set(1, 1, 2)
        // Message({ text: element.object.name, type: 'warn' })

        break
      }
    }
  })


}

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