前言

数字孪生作为当前 WebGIS 和智慧城市建设中的热点应用之一,被广泛应用于交通、安防、能源等多个领域。它通过虚拟与现实的联动,将传感器采集到的各类信息映射到三维场景中,实现数据的空间化可视、事件的实时化响应、系统的智能化管理。Cesium 作为一个高性能的三维地球引擎,具备良好的可视化能力和开放扩展性,成为构建三维数字孪生场景的主流技术选择。

通过 Cesium,我们可以将车辆、人员、设备等实体以三维模型的形式呈现在场景中,并结合实时数据动态驱动这些模型运动,直观地反映真实世界的变化过程。本文将介绍如何使用 Cesium 实现车辆模型的位置与轨迹可视化,以及如何动态更新模型的位置和轨迹。

效果预览

在这里插入图片描述

主要流程

  • 添加一个模型实体和一个线实体;
  • 每获取到一个点,即更新模型实体和新线实体的位置;

代码实现

这里我们选择封装一个类(ModelAnnimateTool)用于实现模型动画工具,方便后续扩展。
首先是构造函数,接收一个对象参数,包含 Cesium 的 Viewer 实例、模型 URL、模型参数、默认朝向、额外偏移角、是否显示轨迹等属性。

  • defaultHeading:模型默认朝向(弧度),即接收到第一个点时,模型的朝向;
  • headingOffset:额外偏移角(弧度),因为后续计算模型朝向时,都默认建模时模型朝向东方,以此为基准计算,如果模型不朝向东方,需要设置偏移量;
class ModelAnnimateTool {
  constructor(options) {
    this.viewer = options.viewer;
    this.id = options.id || Math.random().toString(36).substr(2); // 生成一个随机id
    this.modelUrl = options.modelUrl; // 模型的uri
    this.modelOptions = options.modelOptions || {}; // 模型的参数
    // 模型默认朝向(弧度)
    this.defaultHeading = options.defaultHeading || -Math.PI / 2;
    // 额外偏移角(弧度),可用于细调
    this.headingOffset = options.headingOffset || 0;
    this.showTrail = options.showTrail || false;

    this._entity = null;
    this._polylineEntity = null;
    this._isPlaying = false;
    this._currentIndex = 0;
    this._positions = [];  // 保存历史轨迹点
    this.updatePosition = this.updatePosition.bind(this); // 可选


    this._onTick = options.onTick || function () { };

    this._createModelEntity();
    if (this.showTrail) {
      this._createTrailEntity();
    }

  }
  // ... 方法
}

第一个方法,创建模型实体,即添加一个模型实体,用于显示模型。这里两个重要的参数是:

  • position:模型的位置,这里使用了一个回调函数,每次渲染场景时都会调用这个函数,返回 this._positions 数组中的最新位置(最后一个元素);
  • orientation:模型的方向,这里也使用了一个回调函数,每次渲染场景时都会调用这个函数,返回 this.updateOrientation() 的结果,即模型的朝向;
  // 创建模型
  _createModelEntity() {
    if (!this.modelUrl) { throw new Error('modelUrl不能为空') };
    //  初始化模型,即取position中的第一个点作为模型的初始点
    this._entity = this.viewer.entities.add({
      id: this.id,
      //通过回调函数动态计算属性值,每当 Cesium 渲染场景时都会调用这个函数,使实体的位置始终保持在 this._positions 数组中的最新位置(最后一个元素)
      position: new Cesium.CallbackProperty(() => {
        return this._positions.length > 0 ? this._positions[this._positions.length - 1] : null;
      }, false),
      model: {
        uri: this.modelUrl,
        scale: this.modelOptions.scale || 1.0,
        minimumPixelSize: this.modelOptions.minimumPixelSize || 64,
        maximumScale: this.modelOptions.maximumScale || 200,
        show: this.modelOptions.show || true,
      },
      //orientation: 一个属性,用于指定相对于地球固定地球中心 (ECEF) 的实体方向。如果未定义,则在实体位置使用 east-north-up。
      orientation: new Cesium.CallbackProperty(() => this.updateOrientation(), false)

    })

  }

第二个函数,创建轨迹实体,即添加一个线实体,用于显示模型的运动轨迹。这里使用了一个回调函数,每次渲染场景时都会调用这个函数,返回 this._positions 数组中的所有位置,即模型的运动轨迹;

  // 创建轨迹
  _createTrailEntity() {
    this._polylineEntity = this.viewer.entities.add({
      name: `${this.id}-trail`,
      polyline: {
        positions: new Cesium.CallbackProperty(() => this._positions, false),
        width: 2,
        material: Cesium.Color.RED.withAlpha(0.5),
      }
    });
  }

第三个函数,更新模型朝向,即计算模型的方向。这里使用了 Cesium 的 Cesium.Cartographic.fromCartesian() 方法,将笛卡尔坐标转换为地理坐标,然后计算两个点之间的经纬度差,再使用 Math.atan2() 方法计算航向角(heading),最后使用 Cesium.Transforms.headingPitchRollQuaternion() 方法将航向角转换为四元数,即模型的方向;

  // 更新模型朝向
  updateOrientation() {
    if (this._positions.length < 2) {
      const offsetHeading = this.defaultHeading - Cesium.Math.PI_OVER_TWO;
      const orientation = Cesium.Transforms.headingPitchRollQuaternion(
        this._positions[0],
        new Cesium.HeadingPitchRoll(offsetHeading, 0, 0)
      );
      return orientation
    }
    // 两个及以上点时,计算当前运动方向的朝向
    const p1 = this._positions[this._positions.length - 2];
    const p2 = this._positions[this._positions.length - 1];
    // 计算航向角(heading),pitch和roll保持0(水平)
    const carto1 = Cesium.Cartographic.fromCartesian(p1);
    const carto2 = Cesium.Cartographic.fromCartesian(p2);

    const deltaLon = carto2.longitude - carto1.longitude;
    const deltaLat = carto2.latitude - carto1.latitude;

    const heading = Math.atan2(deltaLon, deltaLat);
    // 如果模型默认朝东,需要调整偏移角度
    const offsetHeading = heading - Cesium.Math.PI_OVER_TWO;
    const orientation = Cesium.Transforms.headingPitchRollQuaternion(
      p2,
      new Cesium.HeadingPitchRoll(offsetHeading, 0, 0)
    );
    return orientation;
  }

第四个函数,更新运动轨迹,即添加一个新的位置到 this._positions 数组中,并更新模型朝向;

  // 更新运动轨迹
  updatePosition = (position) => {

    if (!position) {
      throw new Error('position为空');
    }
    if (!position instanceof Cesium.Cartesian3) {
      position = Cesium.Cartesian3.fromDegrees(position[0], position[1], position[2]); // 将经纬度转换为笛卡尔坐标
    } else {
      position = Cesium.Cartesian3.clone(position);
    }
    this._positions.push(position);
  }

其他的一些方法,如获取模型、获取路径、销毁漫游实例等;

  // 获取模型
  // 用于用户自定义模型的样式属性
  getEntity() {
    return this._entity;
  }
  // 获取路径
  // 用于用户自定义路径的样式属性
  getTrailEntity() {
    return this._polylineEntity;
  }
  // 销毁漫游实例
  destroy() {
    if (this._entity) this.viewer.entities.remove(this._entity);
    if (this._polylineEntity) this.viewer.entities.remove(this._polylineEntity);
  }

注意事项

tips1:如何判断模型是否朝向东方

绘制一条朝东的线(中国位于东半球,往东经度增加,因此经度增加,纬度相同的两个点,就是东西朝向),并在线的起点加一个模型,看模型的朝向即可。

const start = Cesium.Cartesian3.fromDegrees(114.408, 30.470);
    const end = Cesium.Cartesian3.fromDegrees(114.518, 30.470);

    // 添加线实体
    viewer.entities.add({
      polyline: {
        positions: [start, end],
        width: 3,
        material: Cesium.Color.BLUE,
      },
    });
    const car = viewer.entities.add({
      name: 'car1',
      position: start,
      model: {
        uri: './public/police_car.gltf',
        minimumPixelSize: 128,
        maximumScale: 20000,
      },
    })
    viewer.flyTo(car);
Logo

永洪科技,致力于打造全球领先的数据技术厂商,具备从数据应用方案咨询、BI、AIGC智能分析、数字孪生、数据资产、数据治理、数据实施的端到端大数据价值服务能力。

更多推荐