🌿 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示例,目前正在绝赞更新中...

相关推荐
从文处安16 分钟前
Vue3 + Vite 项目部署后刷新白屏问题解决方案
vue.js
LanceJiang21 分钟前
Element-Plus 二次封装 el-table LeTable组件
前端·vue.js
一个爱挣扎的旺崽28 分钟前
【步骤条轮子】迎合业务的步骤条
前端·vue.js
前端摸鱼杭小哥2 小时前
三分钟手写JSBridge让Vue3 + TS 项目拥有调用鸿蒙原生能力
前端·vue.js·harmonyos
Watermelo6173 小时前
前端实战:基于Vue3与免费满血版DeepSeek实现无限滚动+懒加载+瀑布流模块及优化策略
前端·vue.js·人工智能·语言模型·自然语言处理·人机交互·deepseek
前端切图仔0014 小时前
Vue 3 项目实现国际化指南 i18n
前端·javascript·vue.js
阿珊和她的猫9 小时前
Vue.js的ref:轻松获取DOM元素的魔法
前端·javascript·vue.js
没资格抱怨10 小时前
vue3中如何缓存路由组件
vue.js·spring·缓存
萌萌哒草头将军11 小时前
⚡⚡⚡Vite 被发现存在安全漏洞🕷,请及时升级到安全版本
前端·javascript·vue.js
会功夫的李白11 小时前
Electron + Vite + Vue 桌面应用模板
javascript·vue.js·electron·vite·模版