注册
web

低成本创建数字孪生场景-开发篇

介绍


本文承接《低成本创建数字孪生场景-数据篇》,获取到了数据之后就是愉快的开发环节,我们用到了开源项目CesiumJS做为GIS技术框架。


CesiumJS项目从创建至今已经有12年历史,提供了创建3D地球和2D地图的强大功能,它支持多种地图数据源,可以创建复杂的3D地形和城市模型。CesiumJS的功能强大,但入门难度比较高,需要提前了解很多概念和设计理念,为方便理解本案例仅仅使用其提供的一些基础功能。


Guanlianx_5.gif


需求说明


为了搭建1个简易的山区小乡镇场景,我们首先梳理下需求,把任务分解好。



  1. 在底图上叠加各种图层

    • 支持叠加地形图层、3DTiles图层、数据图层
    • 支持多种方式分发图层数据


  2. 鼠标与图层元素的交互

    • 鼠标移动时,使用屏幕事件处理器监听事件,获取当前屏幕坐标
    • 如果已经有高亮的元素,将其恢复为正常状态
    • 以当前屏幕坐标为起点发送射线,获取射线命中的元素,如果有命中的元素就高亮它
    • 鼠标点击时使用屏幕事件处理器获取命中元素,如果命中了,就判断元素是否描边状态,有则取消描边,没有则增加描边轮廓


  3. 加载Gltf等其他模型

    • 模型与其他图层元素一样,可以被光标拾取
    • 模型支持播放自带动画



准备工作


数据分发服务


当前案例涉及的图层数据以文件类为主,为方便多处使用,需要将图层的服务独立部署,这里有两个方案:



  1. 自行搭建静态文件服务器,网上搜一个常用的node.js静态服务脚本即可
  2. 把文件放到cesium ion上,如果你要使用cesium ion资产,需要注意配好defaultAccessToken,具体调用方式看下文的代码实现

安装依赖


以下为本案例的前端工程使用的核心框架版本


依赖版本
vue^3.2.37
vite^2.9.14
Cesium^1.112.0

代码实现



  1. 地图基本场景,本示例使用vite+vue3开发,html非常简单,只需要一个
    标签即可,在cesiumjs中以Viewer为起点调用其他控件,因此我们实例化一个Cesium.viewer, 这里面有非常多配置参数,详细请看开发文档


    import * 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
    }


  2. 在地图上叠加地形图层,图层数据可以自行部署
    // 方法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
    }


  3. 加载3DTiles图层,与地形图层类似,换成了Cesium3DTileset类。需要注意使用url加载需要自行解决跨域问题
    const 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())
    }


  4. 鼠标事件交互,鼠标悬浮,在改变选中元素的状态之前,需要将它的当前状态保存下来以便下次可以恢复。
    // 缓存高亮状态
    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)


  5. 鼠标事件,鼠标点击,描边轮廓使用了Cesium自带的后期效果处理器,不需要自行编写着色器等操作,因此实现起来很便捷。只需要将选中的元素放到 效果的selected对象数组内就行了。
    // 缓存后期效果
    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)
    }

    }


  6. 加载gltf模型, gltf加载后需要进行一次矩阵变换modelMatrix, 加载后启动指定索引的动画进行播放。
    // 加载模型
    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)
    }



部署说明



  1. 场景演示包括前端工程、GIS数据分发服务、服务端接口几个部分
  2. 前端工程使用vue3开发,其中CesiumJs通过NPM依赖包引入
  3. 场景中相关图层均为静态文件,可放入主工程静态目录中,也可以独立部署(需解决跨域访问),或者使用cesiumlab3分发服务便于管理
  4. web端场景对终端设备和浏览器有一定要求,具体配置需要进一步测试

总结


在本文中并没有涉及到服务端数据的接入,数据接入进来后,我们可以利用Cesium在GIS开发领域强大功能,与three.js的webGL开发优势,两者相互融合创建更多数据可视化效果。那么关于Cesium和three.js的融合开发还在初步探索阶段,希望下一次有精彩内容分享给大家。


Hengjiang3.gif


相关链接


最新版cesium集成threejs


Cesium和Three.js结合的5个方案


Cesium实现更实用的3D描边效果


作者:gyratesky
来源:juejin.cn/post/7331626882552872986

0 个评论

要回复文章请先登录注册