Cesium(九)加载三维模型

1,593 阅读5分钟

1. 前言

三维模型可以提供更真实的视觉效果,cesium将三维模型与其他地理数据(如地形、影像和矢量数据)结合,提供综合的可视化平台,用户能够直观理解地理信息和对象的空间关系。八青妹将通过cesium/Clamp to 3D Model这个例子,循序渐进,讲解下是如何实现的。

我们使用的模型可以在github中下载: github.com/.../GroundV…。下载后将文件放到项目的public目录下。

2.加载三维模型

方法一

很多的教程上是用以下方式举例如何加载模型的:

var model = Cesium.Model.fromGltf({
    url : '/GroundVehicle.glb',
    modelMatrix : modelMatrix,
    scale : 200.0
});
scene.primitives.add(model);

这种写法已经废弃了。现在的写法是用异步的方式Cesium.Model.fromGltfAsync()进行调用,上述的案例需要改为:

const model =await Cesium.Model.fromGltfAsync({
    url : '/GroundVehicle.glb',
    modelMatrix : modelMatrix,
    scale : 200.0
});
scene.primitives.add(model);

方法二

或者将模型添加到实体当中。

  const entity = viewer.entities.add({
    position: Cesium.Cartesian3.fromRadians(longitude, latitude),
    model: {
      uri: "/GroundVehicle.glb",
    },
  });

添加到实体之后,需要自动调整相机的位置和方向。使用viewer.trackedEntity = entity,相机便会自动调整视角,将焦点保持在该实体上。

  const viewer = new Cesium.Viewer("cesiumContainer", {
    infoBox: false,
  });
  const scene = viewer.scene;

  const longitude = -2.1480545852753163;
  const latitude = 0.7688240036937101;

  const entity = viewer.entities.add({
    position: Cesium.Cartesian3.fromRadians(longitude, latitude),
    model: {
      uri: "/GroundVehicle.glb",
    },
  });

  viewer.trackedEntity = entity;

效果如下所示,是一个静态的车。

image.png

这里有一点需要注意的地方,在Viewer的属性中,要允许动画效果的运行。即

 const viewer = new Cesium.Viewer("cesiumContainer", {
    infoBox: false,
    shouldAnimate: true//启用动画
  });

启用动画后,模型中的车轮开始原地转动了。下面将在方法二的基础上讲解。

3. 创建动态的实体

在八青妹cesium(七)和cesium(八)系列中讲解了一些简单的交互,主要是通过变更实体的label或者动态给已有实体增加属性。这里我们将尝试下创建动态的实体。

3.1 画一个点

从最最简易的说起,简易到只是画一个点😄。点的位置在原来实体放置的经纬度基础上增加一个高度。设置下点的大小和颜色。

  // 创建一个动态颜色的实体
  const point = viewer.entities.add({
    position: Cesium.Cartesian3.fromRadians(longitude, latitude, 4),
    point: {
      pixelSize: 10,
      color: Cesium.Color.YELLOW,
    },
  });

看下效果: image.png

3.2 动态更改点的颜色

在这之前,了解下new Cesium.CallbackProperty()是cesium中用于创建动态属性的构造函数,该函数允许定义一个回调函数,该函数在每次场景更新时被调用,从而返回当前的属性值。

Cesium.JulianDate.now()函数用于获取当前时间,返回一个表示当前时刻的JulianDate对象,JulianDate 是 Cesium 中用来处理时间的类,在涉及动画、时间序列数据或需要时间计算的场景中常用。将当前时间在一天中的秒数取模,得到当前秒数在周期 duration 内的相对位置,再除以 duration,可以将结果标准化到 0 到 1 的范围。用代码来解释会更清楚:

 const duration=60;//定义一个周期
 const time = Cesium.JulianDate.now();
 const offset=(time.secondsOfDay % duration) / duration;//对当前时间取模和归一化

这个偏移量的计算很常见,划个重点。

根据色调、饱和度和亮度创建颜色实例是Cesium.Color.fromHsl(hue,saturation,lightness, alpha, result),其中入参hue, saturation, lightness, alpha的值的范围均为0~1,将上述offset作为色调hue入参。

综合看下这段的代码:

  const duration = 60;
  // 创建一个动态颜色的实体
  const point = viewer.entities.add({
    position: Cesium.Cartesian3.fromRadians(longitude, latitude, 4),
    point: {
      pixelSize: 10,
      color: new Cesium.CallbackProperty(() => {
        const time = Cesium.JulianDate.now();
        const cycleTime = (time.secondsOfDay % duration) / duration;
        return Cesium.Color.fromHsl((time.secondsOfDay % duration) / duration, 1.0, 0.5);
      }, false),
    },
  });

刷新页面后,会看到原点开始不停地在平缓的改变颜色。如果想让颜色改变的快一些,就缩短动画周期,将duration的值减小。

3.3 动态展示实体各个部分的高度

我们加载的模型是一辆车,现在要实现的功能是:从车后方到车前测量车中间位置的高度。

首先,要使用 sampleHeight 方法获取高度,这个方法需要的参数为笛卡尔坐标的经纬度。 由于sampleHeight 方法依赖于 Cesium 加载的地形数据。如果场景中没有加载地形数据,可能无法获取高度。这里先加个延迟3秒获取,举例:

  const cartographic = new Cesium.Cartographic(longitude, latitude);
  setTimeout(() => {
    if (scene.sampleHeightSupported) {
      const height = scene.sampleHeight(cartographic);
      console.log(height);//结果是:2.5083911982150893
    }
  }, 3000);

这个测量的高度,也将包括后面添加实体的高度,所以需要将后面添加的实体放到objectsToExclude

//排除点的高度
const objectsToExclude = [point];
const height = scene.sampleHeight(cartographic,objectsToExclude);

将上述代码片段都结合起来,编写函数updatePosition(),返回位置信息,添加到动态的实体当中。

现在,再来阅读下面的代码,应该会比较轻松了吧☺️。

// 动画的范围
  const range = 0.000001;

  // 周期的时间
  const duration = 8;

  const point = viewer.entities.add({
    position: new Cesium.CallbackProperty(updatePosition, false),
    point: {
      pixelSize: 10, // 点的大小
      color: Cesium.Color.YELLOW, // 点的颜色
      disableDepthTestDistance: Number.POSITIVE_INFINITY, // 禁用深度测试
    },
    label: {
      show: false,
      showBackground: true,
      font: "14px monospace",
      horizontalOrigin: Cesium.HorizontalOrigin.LEFT, // 水平对齐方式
      verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 垂直对齐方式
      pixelOffset: new Cesium.Cartesian2(5, 5), // 标签的偏移量
      disableDepthTestDistance: Number.POSITIVE_INFINITY, // 禁用深度测试
    },
  });
  // 排除点的高度
  const objectsToExclude = [point];
  // 笛卡尔坐标
  const cartographic = new Cesium.Cartographic();
  // 动画的更新函数
  function updatePosition(time, result) {
    // 偏移量
    const offset = (time.secondsOfDay % duration) / duration;
    // 计算动画的经度和纬度
    cartographic.longitude = longitude - range + offset * range * 2.0;
    cartographic.latitude = latitude;
    // 计算动画的高度
    let height;
    if (scene.sampleHeightSupported) {
      height = scene.sampleHeight(cartographic, objectsToExclude);
    }

    if (Cesium.defined(height)) {
      cartographic.height = height;
      point.label.text = `${Math.abs(height).toFixed(2).toString()} m`;
      point.label.show = true;
    } else {
      cartographic.height = 0.0;
      point.label.show = false;
    }
    // 计算动画的位置
    return Cesium.Cartographic.toCartesian(cartographic, Cesium.Ellipsoid.WGS84, result);
  }

如果想从车的右侧测量到车的左侧,而不是从后到前测量高度的话,可以尝试更改以下代码:

//偏移范围缩小
const range = 0.0000003;
//经度不变,更改纬度
cartographic.longitude = longitude;
cartographic.latitude = latitude - range + offset * range * 2.0;

总结

细心的你也许会发现,当将相机的视角增加高度,会发现这个点的大小和label标签并不会越来越小,它在画布中始终保持10px,因为动态增加的实体并不是添加到模型实体当中,是独立出来的。本节内容从加载模型的方式,到动态添加点,再到让这个点根据实体的高度进行位置变更。是个比较基础的案例。越是基础,越是重要。