Cesium学习笔记 3 —— 地形相关

1,651 阅读7分钟

前言

最近开始学习 Cesium,首先我在b站找了一个教程 Cesium快速上手(2020/02)_哔哩哔哩_bilibili,发现这个教程并没有讲的特别清楚。《 Cesium 学习笔记 》系列文章,将按照这个教程的顺序,结合cesium源码,对教程内容做一个学习总结和扩展。

文章中出现的 Demo链接 和环境都基于 Cesium教程中 的官方案例,安装过程可以参考 Cesium快速上手(2020/02)_哔哩哔哩_bilibili 的第一集。

本篇文章《 Cesium学习笔记 3 —— 地形相关 》主要是对 Cesium快速上手(2020/02)_哔哩哔哩_bilibili第四集的前半部分进行一个总结和扩展。也就是 cesium 地形相关的知识。

image.png


TerrainProviders

new Cesium.TerrainProvider() 描述的是一个接口,并不会返回一个实例 为椭圆体的表面提供地形或其他几何图形。根据 TilingScheme 将表面几何结构组织成 a pyramid of tiles。

TilingScheme 是椭圆体表面上的几何图形或图像的平铺方案。在 level 为零,即最粗略、最不详细的级别,图块的数量是可配置的。在细节级别一,每个零级 tiles 有四个孩子,每个方向两个。在细节级别二,每个级别 1 tiles 有四个子级,每个方向两个。持续到几何或图像源中存在的多个级别。

createTerrainProvider

该接口提供了全球在线地形数据,不过不稳定,经常获取不到数据。如果获取失败,则会导致场景无法渲染

Cesium World Terrain 将多个数据源融合到一个单一的量化网格地形图块集中,针对 3D 地图可视化和高效流式传输到 CesiumJS、Cesium for Unreal 和其他 3D 引擎进行了优化。 Cesium World Terrain 被 curated 和 tiled 以实现高效的 3D 可视化。Leaf tiles 包含该位置的最高分辨率源数据的预生成 meshes;non-leaf tiles 的细节层次是使用特定于地形的简化算法创建的,该算法为运行时细节层次控制计算几何误差。

// createWorldTerrain 返回的是 createTerrainProvider
const worldTerrain = Cesium.createWorldTerrain({
  requestWaterMask: true,     // 是否应从服务器请求每片水面罩(如果有)。
  requestVertexNormals: true, // 是否应从服务器请求其他照明信息(如果有)的标志。
});

// new Cesium.Viewer (container, options ) : 用于构建应用程序的基本小部件,其添加功能可用于多种应用进行扩展。
const viewer = new Cesium.Viewer("cesiumContainer", {
  terrainProvider: worldTerrain,   // 要使用的地形提供商
});

image.png

关于 requestWaterMask

STK 世界地形图块现在支持 Watermask 功能,以前只能在小地形中使用。下图是温哥华的视图,requestWaterMask 设置为 true,就可以看到地形照明与水效果相结合。

image.png

关于 requestVertexNormals

顶点法线和光照计算密切相关

我们拿 立方体添加点光源 作为例子

  • 添加平行光是直接定义光线照射物体的方向,点光源的光线是发散的,无法直接定义它的光线方向,不过只要定义好点光源的位置坐标,然后与某个顶点的位置坐标进行减法运算,计算结果就是光源射到该顶点的方向。
  • 把该向量和顶点法向量作为dot()点积函数的参数,可以计算出光线入射角余弦值然后代入漫反射光照模型公式可以得到新的顶点颜色,渲染管线利用新的顶点颜色进行插值计算可以得到立方体表面每一个像素的值

EllipsoidTerrainProvider

个人理解是,只使用地形的二维贴图,没有 3D 效果。

该接口实际上是全部为零的数据。它提供了一个全球范围内高度为0的地形,不需要额外的地形文件,就可以实时的自己来构建这个高度为 0的Mesh。

对那些没有网络环境,或网络不理想,或不需要地形的应用,EllipsoidTerrainProvider提供了最简单的,无需额外负担的地形数据来构网。

const ellipsoidProvider = new Cesium.EllipsoidTerrainProvider();

image.png

VRTheWorldTerrainProvider

一个 TerrainProvider,它通过对从 VT MÄK VR-TheWorld server. 服务器检索到的高度图进行细分来生成地形几何。

const vrTheWorldProvider = new Cesium.VRTheWorldTerrainProvider({
  url: "http://www.vr-theworld.com/vr-theworld/tiles1.0.0/73/",
  credit: "Terrain data courtesy VT MÄK",
});

image.png

GoogleEarthEnterpriseTerrainProvider 和 ArcGISTiledElevationTerrainProvider 类似,不再赘述。

CustomHeightmapTerrainProvider

Cesium 1.83 新增的 API CustomHeightmapTerrainProvider。自定义高度图地形提供器

从一个回调函数里获取高程数据。从下列例子中可看出,只需指定能 返回高度类型数组 的回调函数、宽度、高度三个属性即可。

CustomHeightmapTerrainProvider 无需创建 TerrainProvider 的子类。存在一些限制,例如没有 water mask,没有顶点法线和没有可用性,因此成熟的TerrainProvider子类更适合这些更复杂的用例。

const customHeightmapWidth = 32;
const customHeightmapHeight = 32;

const customHeightmapProvider = new Cesium.CustomHeightmapTerrainProvider(
  {
    width: customHeightmapWidth,
    height: customHeightmapHeight,
    
    callback: function (x, y, level) {  // 回调函数里获取高程数据
      const width = customHeightmapWidth;
      const height = customHeightmapHeight;
      const buffer = new Float32Array(width * height);
      
      for (let yy = 0; yy < height; yy++) {
        for (let xx = 0; xx < width; xx++) {
          const u = (x + xx / (width - 1)) / Math.pow(2, level);
          const v = (y + yy / (height - 1)) / Math.pow(2, level);
          const heightValue = 4000 * (Math.sin(8000 * v) * 0.5 + 0.5);
          const index = yy * width + xx;
          buffer[index] = heightValue;
        }
      }
      return buffer;
    },
  }
);

使得 2D 的地形贴图可以自定义高程,显示出自定义的 3d 地形

image.png


在地形表面添加高度信息标识

代码和展示效果

const terrainSamplePositions = createGrid(0.0005);
Promise.resolve(
  Cesium.sampleTerrainMostDetailed(
    viewer.terrainProvider,
    terrainSamplePositions
  )
).then(sampleTerrainSuccess);


// 返回在地形中采样的位置信息的数组
function createGrid(rectangleHalfSize) {
  const gridWidth = 41;
  const gridHeight = 41;
  // 因为是在地球表面,所以只需要两个被转化为弧度的坐标即可确定在地球表面的位置,不需要笛卡尔3维坐标
  const everestLatitude = Cesium.Math.toRadians(27.988257);
  const everestLongitude = Cesium.Math.toRadians(86.925145);
  // 指定为经度和纬度坐标的二维区域
  const e = new Cesium.Rectangle(
    everestLongitude - rectangleHalfSize,
    everestLatitude - rectangleHalfSize,
    everestLongitude + rectangleHalfSize,
    everestLatitude + rectangleHalfSize
  );
  const terrainSamplePositions = [];
  for (let y = 0; y < gridHeight; ++y) {
    for (let x = 0; x < gridWidth; ++x) {
      const longitude = Cesium.Math.lerp(
        e.west,
        e.east,
        x / (gridWidth - 1)
      );
      const latitude = Cesium.Math.lerp(
        e.south,
        e.north,
        y / (gridHeight - 1)
      );
      const position = new Cesium.Cartographic(longitude, latitude); // 第三个参数 height 默认是 0
      terrainSamplePositions.push(position);
    }
  }
  return terrainSamplePositions;
}

function sampleTerrainSuccess(terrainSamplePositions) {
  const ellipsoid = Cesium.Ellipsoid.WGS84;
 //默认情况下,cesium 不会破坏几何图形。在地形后面,设置此标志将启用此功能。
  viewer.scene.globe.depthTestAgainstTerrain = true;

  viewer.entities.suspendEvents();
  viewer.entities.removeAll();

  for (let i = 0; i < terrainSamplePositions.length; ++i) {
    const position = terrainSamplePositions[i];
// cartographicToCartesian can converts the provided cartographic to Cartesian representation.
    viewer.entities.add({
      name: position.height.toFixed(1),
      position: ellipsoid.cartographicToCartesian(position),
      billboard: {
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        scale: 0.7,
        image: "../images/facility.gif",
      },
      label: {
        text: position.height.toFixed(1),
        font: "10pt monospace",
        horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
        pixelOffset: new Cesium.Cartesian2(0, -14),
        fillColor: Cesium.Color.BLACK,
        outlineColor: Cesium.Color.BLACK,
        showBackground: true,
        backgroundColor: new Cesium.Color(0.9, 0.9, 0.9, 0.7),
        backgroundPadding: new Cesium.Cartesian2(4, 3),
      },
    });
  }
  viewer.entities.resumeEvents();
}

自定义函数 createGrid

1. new Cesium.Cartographic( longitude, latitude )

用弧度表示的经纬度坐标,经纬度其实就是角度。弧度即角度对应弧长是半径的倍数。

image.png

借此,我们来扩展一下怎么把 Cartesian3 坐标和 经纬度坐标(WGS84)转换成 Cartographic 坐标

1. Cartesian3 → Cartographic
Cesium.Cartographic.fromCartesian(cartesian, ellipsoid, result) → Cartographic

2. 经纬度坐标(WGS84)→ Cartographic
Cesium.Cartographic.fromDegrees(longitude, latitude, height, result) → Cartographic   

3. 经纬度坐标和弧度坐标也可以通过Cesium.Math来转换
Cesium.CesiumMath.toDegrees(radians) → Number
Cesium.CesiumMath.toRadians(degrees) → Number   

2. 参数和返回值

  1. 参数 rectangleHalfSize 控制采样的密度

  2. 返回值 terrainSamplePositions

  • 返回在地形中采样的位置信息的数组,数组中的每一个项都是一个坐标,包含经纬度,但是高度为 0

image.png

sampleTerrainMostDetailed 方法

Promise.resolve(
  Cesium.sampleTerrainMostDetailed( // 在地形数据集的最大可用切片级别启动 sampleTerrain() 请求。返回的是一个 promise
    viewer.terrainProvider,  // terrainProvider
    terrainSamplePositions   // position
  )
).then(sampleTerrainSuccess); //  .then()块中的执行程序函数将包含promise的返回值。

sampleTerrainMostDetailed 对 Cartographic 位置数组进行地形高度查询向地形提供者请求图块,采样和插值。插值与用于在指定级别渲染地形的三角形匹配。查询异步发生,因此此函数返回一个promise,当查询完成。每个点的高度都已修改。如果高度不能确定是因为该位置的指定级别没有可用的地形数据,或发生另一个错误,高度设置为不确定。正如典型的 Cartographic 类型,提供的高度是参考椭球上方的高度(例如 Ellipsoid.WGS84 ),而不是高于平均海平面的高度。其他换句话说,如果在海洋中采样,则不一定为0.0。此功能需要如果需要精确获取地​​形的高度,请输入地形的详细程度作为输入尽可能(即具有最高的详细程度)使用 sampleTerrainMostDetailed 

换句话说,执行 sampleTerrainSuccess 时传入的函数参数是对 terrainSamplePositions 中的点进行求高程后的补充高程的点。

此时,我们可以发现 terrainSamplePositions 数组中的项除了每个点的经纬度,该点的高度信息也被补充上去了。

image.png

自定义函数 sampleTerrainSuccess

1. Ellipsoid

const ellipsoid = Cesium.Ellipsoid.WGS84;

Ellipsoid是由等式 (x / a)^2 + (y / b)^2 + (z / c)^2 = 1 在笛卡尔坐标中定义的二次曲面

const ellipsoid = Cesium.Ellipsoid.WGS84执行后,ellipsoid 是初始化为 WGS84 标准的 Ellipsoid 实例。

2. depthTestAgainstTerrain

viewer.scene.globe.depthTestAgainstTerrain = true

如果 billboards, polylines, labels 等图元应针对地形表面进行深度测试,则为 true;如果此类图元应始终绘制在地形顶部,除非它们位于地球的另一侧,则为 false。针对地形进行深度测试图元的缺点是,轻微的数值噪声或地形细节级别的切换有时会使应该在表面上的图元在其下方消失。