介绍
本文承接《低成本创建数字孪生场景-数据篇》,获取到了数据之后就是愉快的开发环节,我们用到了开源项目CesiumJS做为GIS技术框架。
CesiumJS项目从创建至今已经有12年历史,提供了创建3D地球和2D地图的强大功能,它支持多种地图数据源,可以创建复杂的3D地形和城市模型。CesiumJS的功能强大,但入门难度比较高,需要提前了解很多概念和设计理念,为方便理解本案例仅仅使用其提供的一些基础功能。
需求说明
为了搭建1个简易的山区小乡镇场景,我们首先梳理下需求,把任务分解好。
- 在底图上叠加各种图层
- 支持叠加地形图层、3DTiles图层、数据图层
- 支持多种方式分发图层数据
- 鼠标与图层元素的交互
- 鼠标移动时,使用屏幕事件处理器监听事件,获取当前屏幕坐标
- 如果已经有高亮的元素,将其恢复为正常状态
- 以当前屏幕坐标为起点发送射线,获取射线命中的元素,如果有命中的元素就高亮它
- 鼠标点击时使用屏幕事件处理器获取命中元素,如果命中了,就判断元素是否描边状态,有则取消描边,没有则增加描边轮廓
- 加载Gltf等其他模型
- 模型与其他图层元素一样,可以被光标拾取
- 模型支持播放自带动画
准备工作
数据分发服务
当前案例涉及的图层数据以文件类为主,为方便多处使用,需要将图层的服务独立部署,这里有两个方案:
-
自行搭建静态文件服务器,网上搜一个常用的node.js静态服务脚本即可
-
把文件放到cesium ion上,如果你要使用cesium ion资产,需要注意配好defaultAccessToken,具体调用方式看下文的代码实现
安装依赖
以下为本案例的前端工程使用的核心框架版本
依赖 | 版本 |
---|---|
vue | ^3.2.37 |
vite | ^2.9.14 |
Cesium | ^1.112.0 |
代码实现
-
地图基本场景,本示例使用vite+vue3开发,html非常简单,只需要一个
标签即可,在cesiumjs中以Viewer为起点调用其他控件,因此我们实例化一个Cesium.viewer, 这里面有非常多配置参数,详细请看开发文档
jsximport * as Cesium from 'cesium' import 'cesium/Build/Cesium/Widgets/widgets.css' Cesium.Ion.defaultAccessToken = '可以把一些GIS资产放到Cesium ION上托管,Tokenw为调用凭证' // 地图中心 const center = [1150, 29] // cesium实例 let viewer = null // 容器 const cesiumContainer = ref(null) onMounted(async () => { await init() }) async function init() { viewer = new Cesium.Viewer(cesiumContainer.value, { timeline: true, //显示时间轴 animation: true, //开启动画 sceneModePicker: true, //场景内容可点击 baseLayerPicker: true, //图层可点击 infoBox: false, // 自动信息弹窗 shouldAnimate: true // 允许播放动画 }) // 初始化镜头视角 restoreCameraView() // 开启地形深度检测 viewer.scene.globe.depthTestAgainstTerrain = true // 开启全局光照 viewer.scene.globe.enableLighting = true // 开启阴影 viewer.shadows = true }) // 设置初始镜头 function restoreCameraView(){ viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(center[0], center[1], 0), orientation: { heading: Cesium.Math.toRadians(0), // 相机的方向 pitch: Cesium.Math.toRadians(-90), // 相机的俯仰角度 roll: 0 // 相机的滚动角度 } }) } // 加载地形图层 async function initTerrainLayer() { const tileset = await Cesium.CesiumTerrainProvider.fromUrl( 'http://localhost:9003/terrain/c8Wcm59W/', { requestWaterMask: true, requestVertexNormals: false } ) viewer.terrainProvider = tileset }
-
在地图上叠加地形图层,图层数据可以自行部署
jsx// 方法1: 加载本地地形图层 async function initTerrainLayer() { const tileset = await Cesium.CesiumTerrainProvider.fromUrl( 'http://localhost:9003/terrain/c8Wcm59W/', { requestWaterMask: true, requestVertexNormals: false } ) viewer.terrainProvider = tileset } // 方法2: 加载Ion地形图层 async function initTerrainLayer() { const tileset = await Cesium.CesiumTerrainProvider.fromIonAssetId(1,{ requestVertexNormals: true } ) viewer.terrainProvider = tileset }
-
加载3DTiles图层,与地形图层类似,换成了Cesium3DTileset类。需要注意使用url加载需要自行解决跨域问题
jsxconst tileset = await Cesium.Cesium3DTileset.fromUrl( 'http://localhost:9003/model/tHuVnsJXZ/tileset.json', {} ) // 将图层加入到场景 viewer.scene.primitives.add(tileset) // 适当调整图层位置 const translation = getTransformMatrix(tileset, { x: 0, y: 0, z: 86 }) tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation) // 获取变化矩阵 function getTransformMatrix (tileset, { x, y, z }) { // 高度偏差,正数为向上偏,负数为向下偏,根据真实的模型位置不断进行调整 const heightOffset = z // 计算tileset的绑定范围 const boundingSphere = tileset.boundingSphere // 计算中心点位置 const cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center) // 计算中心点位置坐标 const surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0) // 偏移后的三维坐标 const offset = Cesium.Cartesian3.fromRadians(cartographic.longitude + x, cartographic.latitude + y, heightOffset) return Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3()) }
-
鼠标事件交互,鼠标悬浮,在改变选中元素的状态之前,需要将它的当前状态保存下来以便下次可以恢复。
jsx// 缓存高亮状态 const highlighted = { feature: undefined, originalColor: new Cesium.Color() } // 鼠标与物体交互事件 function initMouseInteract () { // 事件处理器 const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas) // 鼠标悬浮选中 handler.setInputAction((event) => { // 将原有高亮对象恢复 if (Cesium.defined(highlighted.feature)) { highlighted.feature.color = highlighted.originalColor highlighted.feature = undefined } // 获取选中对象 const pickedFeature = viewer.scene.pick(event.endPosition) if (Cesium.defined(pickedFeature)) { // 高亮选中对象 if (pickedFeature !== moveSelected.feature) { highlighted.feature = pickedFeature Cesium.Color.clone(pickedFeature.color, highlighted.originalColor) pickedFeature.color = Cesium.Color.YELLOW } } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)
-
鼠标事件,鼠标点击,描边轮廓使用了Cesium自带的后期效果处理器,不需要自行编写着色器等操作,因此实现起来很便捷。只需要将选中的元素放到 效果的selected对象数组内就行了。
jsx// 缓存后期效果 let edgeEffect = null function initMouseInteract(){ // 鼠标点击选中 handler.setInputAction((event) => { // 获取选中对象 const pickedFeature = viewer.scene.pick(event.position) if (!Cesium.defined(pickedFeature)) { return null } else { // 描边效果:兼容GLTF和3DTiles setEdgeEffect(pickedFeature.primitive || pickedFeature) // 如果拾取的要素包含属性信息,则打印出来 if (Cesium.defined(pickedFeature.getPropertyIds)) { const propertyNames = pickedFeature.getPropertyIds() const props = propertyNames.map(key => { return { name: key, value: pickedFeature.getProperty(key) } }) console.info(props) } } }, Cesium.ScreenSpaceEventType.LEFT_CLICK) } // 选中描边 function setEdgeEffect (feature) { if (edgeEffect == null) { // 后期效果 const postProcessStages = viewer.scene.postProcessStages // 增加轮廓线 const stage = Cesium.PostProcessStageLibrary.createEdgeDetectionStage() stage.uniforms.color = Cesium.Color.LIME //描边颜色 stage.uniforms.length = 0.05 // 产生描边的阀值 stage.selected = [] // 用于放置对元素 // 将描边效果放到场景后期效果中 const silhouette = Cesium.PostProcessStageLibrary.createSilhouetteStage([stage]) postProcessStages.add(silhouette) edgeEffect = stage } // 选多个元素进行描边 const matchIndex = edgeEffect.selected.findIndex(v => v._batchId === feature._batchId) if (matchIndex > -1) { edgeEffect.selected.splice(matchIndex, 1) } else { edgeEffect.selected.push(feature) } }
-
加载gltf模型, gltf加载后需要进行一次矩阵变换modelMatrix, 加载后启动指定索引的动画进行播放。
jsx// 加载模型 async function loadGLTF () { let animations = null let modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame( Cesium.Cartesian3.fromDegrees(lng,lat,altitude) ) const model = await Cesium.Model.fromGltfAsync({ url: './static/gltf/windmill.glb', modelMatrix: modelMatrix, scale: 30, // minimumPixelSize: 128, // 设定模型最小显示尺寸 gltfCallback: (gltf) => { animations = gltf.animations } }) model.readyEvent.addEventListener(() => { const ani = model.activeAnimations.add({ index: animations.length - 1, // 播放第几个动画 loop: Cesium.ModelAnimationLoop.REPEAT, //循环播放 multiplier: 1.0 //播放速度 }) ani.start.addEventListener(function (model, animation) { console.log(`动画开始: ${animation.name}`) }) }) viewer.scene.primitives.add(model) }
部署说明
- 场景演示包括前端工程、GIS数据分发服务、服务端接口几个部分
- 前端工程使用vue3开发,其中CesiumJs通过NPM依赖包引入
- 场景中相关图层均为静态文件,可放入主工程静态目录中,也可以独立部署(需解决跨域访问),或者使用cesiumlab3分发服务便于管理
- web端场景对终端设备和浏览器有一定要求,具体配置需要进一步测试
总结
在本文中并没有涉及到服务端数据的接入,数据接入进来后,我们可以利用Cesium在GIS开发领域强大功能,与three.js的webGL开发优势,两者相互融合创建更多数据可视化效果。那么关于Cesium和three.js的融合开发还在初步探索阶段,希望下一次有精彩内容分享给大家。