前言
最近在做一个arcgis
项目的时候遇到了一个需求,要实现类似下面这个效果。
调研了一些方案之后,最终决定用threejs
去实现这个效果。
相关版本如下:
arcgis
版本:4.31
threejs
版版:r149
vue
版本:3.5.13
esri-loader
版本:3.7.0
最终效果
实现步骤
文件介绍
index.ts
:arcgis
载入three
场景roadData.js
:模拟数据RoadStramers.js
:具体逻辑
引入js包
首先你得下载threejs
相关资源,我这边用的是149版本
,直接引入three.js
的three.min.js
文件到全局,你也可以选择直接pnpm add threejs@xxx
的方式。
找到three.js
的github
库
找到149
版本
在最底下找到压缩包并下载
解压之后,在build
包里面找到three.min.js
把它放到你的项目里面引入,我这边是 vite+vue3
项目,所以我直接放到public
下面,并在index.html
里面引入。
创建RoadStreamers.js
我们需要通过arcgis
提供的externalRenderers
这个模块去加载我们的threejs
场景。
这个模块需要我们提供一个对象里面得包含setup
和render
两个方法,具体的描述可以查看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中
github
:leixq1024/vue3-gis-example: vue3的gis项目,里面有arcgis cesium openlayer supermap等相关gis示例,目前正在绝赞更新中...