🌿 10分钟学会用arcgis+threejs实现动态地铁路线图

前言

​ 最近在做一个arcgis项目的时候遇到了一个需求,要实现类似下面这个效果。

调研了一些方案之后,最终决定用threejs去实现这个效果。

相关版本如下:

arcgis版本:4.31

threejs版版:r149

vue版本:3.5.13

esri-loader版本:3.7.0

最终效果

实现步骤

文件介绍

  • index.tsarcgis载入three场景
  • roadData.js:模拟数据
  • RoadStramers.js:具体逻辑

引入js包

首先你得下载threejs相关资源,我这边用的是149版本,直接引入three.jsthree.min.js文件到全局,你也可以选择直接pnpm add threejs@xxx的方式。

找到three.jsgithub

找到149版本

在最底下找到压缩包并下载

解压之后,在build包里面找到three.min.js

把它放到你的项目里面引入,我这边是 vite+vue3项目,所以我直接放到public下面,并在index.html里面引入。

创建RoadStreamers.js

​ 我们需要通过arcgis提供的externalRenderers这个模块去加载我们的threejs场景。

这个模块需要我们提供一个对象里面得包含setuprender两个方法,具体的描述可以查看arcgis的官网,这里不做过多介绍。

实现代码如下:

js 复制代码
import { geoJSON } from './roadData.js'
// 定义外部渲染器类,用于在ArcGIS地图中集成Three.js三维渲染
export default class RoadStreamers {
  constructor(options) {
    this.opts = {
      externalRenderers: null
    }
    this.opts.externalRenderers = options.externalRenderers
    this.view = options.view
    this.mesh = null
    this.textures = []
  }

  // 初始化Three.js渲染环境
  async setup(context) {
    const THREE = window.THREE
    this.horseMixer = THREE.AnimationMixer // 动画混合器类引用
    this.clock = new THREE.Clock() // 通用时钟
    this.horseClock = new THREE.Clock() // 专用动画时钟
    this.mixer = THREE.AnimationMixer // 动画混合器类引用

    // 创建Three.js WebGL渲染器
    this.renderer = new THREE.WebGLRenderer({
      context: context.gl, // 共享ArcGIS的WebGL上下文
      premultipliedAlpha: false // 禁用预乘透明度通道
    })
    // 配置渲染器参数
    this.renderer.setPixelRatio(window.devicePixelRatio) // 设置设备像素比
    this.renderer.setViewport(0, 0, this.view.width, this.view.height) // 视口尺寸匹配地图视图

    // 配置自动清除策略
    this.renderer.autoClear = false // 禁用自动清除缓冲区
    this.renderer.autoClearDepth = false // 禁用自动清除深度缓冲
    this.renderer.autoClearColor = false // 禁用自动清除颜色缓冲
    const originalSetRenderTarget = this.renderer.setRenderTarget.bind(this.renderer)
    this.renderer.setRenderTarget = function (target) {
      originalSetRenderTarget(target)
      if (target == null) {
        context.bindRenderTarget() // 当目标为空时重新绑定ArcGIS渲染目标
      }
    }
    // 初始化Three.js场景
    this.scene = new THREE.Scene()
    // 配置Three.js相机参数匹配ArcGIS相机
    const cam = context.camera
    this.camera = new THREE.PerspectiveCamera(
      cam.fovY, // 垂直视野
      cam.aspect, // 宽高比
      cam.near, // 近裁剪面
      cam.far // 远裁剪面
    )
    // 创建空几何体
    geoJSON['features'].forEach(item => {
      const points = []
      item['geometry']['coordinates'].forEach(element => {
        const cenP = []
        this.opts.externalRenderers.toRenderCoordinates(
          this.view,
          [element[0], element[1], 1000],
          0,
          this.view.spatialReference,
          cenP,
          0,
          1
        )
        points.push(new THREE.Vector3(cenP[0], cenP[1], cenP[2]))
      })
      let lineTexture = new THREE.TextureLoader().load('/img/line.png')
      lineTexture.wrapS = lineTexture.wrapT = THREE.RepeatWrapping //每个都重复
      lineTexture.repeat.set(1, 1)
      lineTexture.needsUpdate = true

      let material = new THREE.MeshBasicMaterial({
        map: lineTexture,
        side: THREE.DoubleSide,
        transparent: true
      })
      const curvePath = new THREE.CatmullRomCurve3(points)
      let geometry = new THREE.TubeGeometry(curvePath, 64, 300, 8, true)
      let mesh = new THREE.Mesh(geometry, material)
      this.scene.add(mesh)
      this.textures.push(lineTexture)
    })

    context.resetWebGLState()
  }
  /**
   * 每帧渲染方法
   * @param {Object} context - ArcGIS提供的渲染上下文
   */
  render(context) {
    const THREE = window.THREE
    const cam = context.camera
    if (this.camera) {
      // 同步ArcGIS相机参数到Three.js相机
      this.camera.position.set(cam.eye[0], cam.eye[1], cam.eye[2]) // 相机位置
      this.camera.up.set(cam.up[0], cam.up[1], cam.up[2]) // 相机上方向
      this.camera.lookAt(new THREE.Vector3(cam.center[0], cam.center[1], cam.center[2])) // 相机注视点
      this.camera.projectionMatrix.fromArray(cam.projectionMatrix) // 投影矩阵
    }
    if (this.textures.length > 0) {
      this.textures.forEach(texture => {
        if (texture) texture.offset.x -= 0.005
      })
      this.opts.externalRenderers.requestRender(this.view)
    }
    // 请求下一帧渲染
    if (this.renderer) {
      context.resetWebGLState()
      this.renderer.state.reset()
      context.bindRenderTarget()
      this.renderer.render(this.scene, this.camera)
    }
  }
  destroy() {
    this.stopAnimationLoop()
  }
}

roadData.js :我的mock数据,里面是一个geoJson格式的数据

核心实现代码如下

js 复制代码
 // 初始化Three.js场景
    this.scene = new THREE.Scene()
    // 配置Three.js相机参数匹配ArcGIS相机
    const cam = context.camera
    this.camera = new THREE.PerspectiveCamera(
      cam.fovY, // 垂直视野
      cam.aspect, // 宽高比
      cam.near, // 近裁剪面
      cam.far // 远裁剪面
    )
    // 创建空几何体
    geoJSON['features'].forEach(item => {
      const points = []
      item['geometry']['coordinates'].forEach(element => {
        const cenP = []
        this.opts.externalRenderers.toRenderCoordinates(
          this.view,
          [element[0], element[1], 1000],
          0,
          this.view.spatialReference,
          cenP,
          0,
          1
        )
        points.push(new THREE.Vector3(cenP[0], cenP[1], cenP[2]))
      })
      let lineTexture = new THREE.TextureLoader().load('/img/line.png')
      lineTexture.wrapS = lineTexture.wrapT = THREE.RepeatWrapping //每个都重复
      lineTexture.repeat.set(1, 1)
      lineTexture.needsUpdate = true

      let material = new THREE.MeshBasicMaterial({
        map: lineTexture,
        side: THREE.DoubleSide,
        transparent: true
      })
      const curvePath = new THREE.CatmullRomCurve3(points)
      let geometry = new THREE.TubeGeometry(curvePath, 64, 300, 8, true)
      let mesh = new THREE.Mesh(geometry, material)
      this.scene.add(mesh)
      this.textures.push(lineTexture)
  • CatmullRomCurve3+TubeGeometry:生成平滑3D管状几何体
  • MeshBasicMaterial+Texture:带透明贴图的管线材质

贴图如上

整个管道的背景颜色,流光都是通过渲染这个贴图材质来实现,所以你想要改变管道颜色或者 修改流光样式的时候,应该去直接用ps把图片改了,代码就不用改。

js 复制代码
this.opts.externalRenderers.toRenderCoordinates()

这句代码非常重要,主要是调用arcgis的坐标转换代码,将经纬度坐标,转化为threejs可以使用的渲染坐标。

加载到arcgis

ts 复制代码
loadRoadStreamers = async () => {
  const [externalRenderers] = await getModules([
    'esri/views/3d/externalRenderers' // 外部渲染器(用于集成Three.js)
  ])
  const sceneView = config.sceneView
  // 如果已经加载了道路流光就将其关闭
  if (threeRendererStatus) {
    externalRenderers.remove(sceneView, threeRendererStatus)
    threeRendererStatus = null
    return
  }

  //  加载第三方渲染器
  const threeRenderer = new RoadStreamers({
    externalRenderers,
    view: sceneView
  })
  externalRenderers.add(sceneView, threeRenderer)
  threeRendererStatus = threeRenderer
  // 定位到路线位置
  sceneView.goTo({
    target: [113.2024691, 22.92555768],
    zoom: 9
  })
}

通过externalRenderers.add方法将其添加到arcgis场景中。

如果要删除就用externalRenderers.remove方法

类型报错

因为我这边是用js去写的实现逻辑,在ts文件中引入会报类型问题,这时候你需要创建一个.d.ts文件

RoadStreamers.d.ts

ts 复制代码
declare module '*.js' {
  // 定义 GeoJSON 数据结构
  interface GeoJSONFeature {
    geometry: {
      coordinates: [number, number, number][][]
    }
    [key: string]: any
  }

  interface GeoJSONData {
    features: GeoJSONFeature[]
  }

  // 声明导入的 roadData.js 模块
  declare module './roadData.js' {
    const geoJSON: GeoJSONData
    export { geoJSON }
  }

  // 渲染上下文接口
  interface RenderContext {
    gl: WebGLRenderingContext
    camera: {
      fovY: number
      aspect: number
      near: number
      far: number
      eye: [number, number, number]
      up: [number, number, number]
      center: [number, number, number]
      projectionMatrix: number[]
    }
    resetWebGLState: () => void
    bindRenderTarget: () => void
  }

  // 类选项接口
  interface RoadStreamersOptions {
    externalRenderers: {
      requestRender: (view: SceneView) => void
      toRenderCoordinates: (
        view: SceneView,
        input: [number, number, number],
        srcSpatialReference: number,
        destSpatialReference: number,
        output: number[],
        offset: number,
        count: number
      ) => void
    }
    view: SceneView
  }

  // 主类声明
  declare class RoadStreamers {
    constructor(options: RoadStreamersOptions)

    opts: {
      externalRenderers: RoadStreamersOptions['externalRenderers']
    }
    view: SceneView
    scene: THREE.Scene
    camera: THREE.PerspectiveCamera
    renderer: THREE.WebGLRenderer
    textures: THREE.Texture[]
    mesh: THREE.Mesh | null
    clock: THREE.Clock
    horseClock: THREE.Clock
    mixer: typeof THREE.AnimationMixer
    horseMixer: typeof THREE.AnimationMixer

    setup(context: RenderContext): Promise<void>
    render(context: RenderContext): void
    destroy(): void
  }

  export default RoadStreamers
}

如果没有效果,请检查一下你的tsconfig.app.json有没有设置好.d.ts文件路径,是不是识别不到。

ts 复制代码
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["src/**/*.ts", "src/types/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

项目地址

上面的示例的完整代码已经上传到我的github中

githubleixq1024/vue3-gis-example: vue3的gis项目,里面有arcgis cesium openlayer supermap等相关gis示例,目前正在绝赞更新中...

相关推荐
一枚小小程序员哈2 小时前
基于Vue的个人博客网站的设计与实现/基于node.js的博客系统的设计与实现#express框架、vscode
vue.js·node.js·express
定栓2 小时前
vue3入门-v-model、ref和reactive讲解
前端·javascript·vue.js
LIUENG3 小时前
Vue3 响应式原理
前端·vue.js
wycode4 小时前
Vue2实践(3)之用component做一个动态表单(二)
前端·javascript·vue.js
wycode5 小时前
Vue2实践(2)之用component做一个动态表单(一)
前端·javascript·vue.js
第七种黄昏5 小时前
Vue3 中的 ref、模板引用和 defineExpose 详解
前端·javascript·vue.js
pepedd8646 小时前
还在开发vue2老项目吗?本文带你梳理vue版本区别
前端·vue.js·trae
前端缘梦6 小时前
深入理解 Vue 中的虚拟 DOM:原理与实战价值
前端·vue.js·面试
HWL56796 小时前
pnpm(Performant npm)的安装
前端·vue.js·npm·node.js
柯南95277 小时前
Vue 3 reactive.ts 源码理解
vue.js