Cesium学习笔记 2 —— Billboard, Label, Polylines 图元使用讲解

2,746 阅读8分钟

前言

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

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

本篇文章《 Billboard, Label, Polylines 图元使用讲解 》主要是对 Cesium快速上手(2020/02)_哔哩哔哩_bilibili第三集进行一个总结和扩展。也就是 Primitives 中的 Billboard, Label, PointPrimitives

image.png


Billboard

1. Billboard & Cesium.BillboardCollection

代码与展示效果

1. 代码

function addBillboard() {
  Sandcastle.declare(addBillboard);
  const billboards = scene.primitives.add(
    new Cesium.BillboardCollection()
  );
  billboards.add({
    image: "../images/Cesium_Logo_overlay.png",
    position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883, 3000),
  });
  billboards.add({
    image: "../images/Cesium_Logo_overlay.png",
    position: Cesium.Cartesian3.fromDegrees(-75.69777, 40.13883, 9000),
  });
  console.log(billboards)
  console.log(billboards.length)
}

2. log输出和图片

image.png

image.png

image.png

Billboard的特性

1. 面朝屏幕的图片

创建的 billboard,无论是否转动 camera ,始终面朝屏幕

2. BillboardCollection 对象

const billboards = scene.primitives.add( new Cesium.BillboardCollection() );

billboards 变量是一个 BillboardCollection 对象, 创建一个 billboard,并通过调用 BillboardCollection#add 设置其初始属性。比如后续执行了n次 billboards.add({options}), BillboardCollection 对象的 length 属性就为 n。

其他属性设置可参考以下代码:

function setBillboardProperties() {
  Sandcastle.declare(setBillboardProperties);

  const billboards = scene.primitives.add(
    new Cesium.BillboardCollection()
  );
  billboards.add({
    image: "../images/Cesium_Logo_overlay.png", // default: undefined
    show: true, // default
    position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
    pixelOffset: new Cesium.Cartesian2(0, -50), // default: (0, 0)
    eyeOffset: new Cesium.Cartesian3(0.0, 0.0, 0.0), // default
    horizontalOrigin: Cesium.HorizontalOrigin.CENTER, // default
    verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // default: CENTER
    scale: 2.0, // default: 1.0
    color: Cesium.Color.LIME, // default: WHITE
    rotation: Cesium.Math.PI_OVER_FOUR, // default: 0.0
    alignedAxis: Cesium.Cartesian3.ZERO, // default
    width: 100, // default: undefined
    height: 25, // default: undefined
    sizeInMeters: false, // default
  });
}

image.png

3. BillboardCollection 的更新

调用 BillboardCollection#update 时会导致 CPU 到 GPU 有大量的数据流量。无论更新了多少属性,每个 billboard 的流量都是相同的。如果集合中的大多数 billboard 需要更新,使用 BillboardCollection#removeAll 清除集合并添加新的 billboard 而不是修改每个 billboard 可能会更有效。


2. scaleByDistance 和 translucencyByDistance

有时候,当地球作为背景进行缩放时,billboard 的像素值并不会发生改变,所以可能会导致以下图片的情况发生。

image.png

我们可以使用 scaleByDistance 设置屏幕像素缩放比.150米的时候 billboard 放大一倍,150000米的时候,billboard 缩放到0.5,用代码表示也就是 scaleByDistance: new Cesium.NearFarScalar(1.5e2, 2.0, 1.5e7, 0.5)。此时,对整个地球进行缩放,billboard 的像素值大小是较为合理的。

translucencyByDistance 进行设置可以提升观感,也就是当 camera 往高空移动时(也就是对地球进行缩小时),billboard 可以降低透明度以减少视觉干扰

还有一个叫做 pixelOffsetScaleByDistance 的属性在以下代码中没有体现,但是在 官方 demo 中是有的。当地球表面有两个不同 billboard时,我们需要 设置一个合适的pixelOffsetScaleByDistance来保持在地球缩放过程中,两个 billboard 一个正确的相对位置。

pixelOffsetScaleByDistance: new Cesium.NearFarScalar(
  1.0e3,
  1.0,
  1.5e6,
  0.0
),
function scaleByDistance() {
  Sandcastle.declare(scaleByDistance);

  const billboards = scene.primitives.add(
    new Cesium.BillboardCollection()
  );
  billboards.add({
    image: "../images/facility.gif",
    position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
    // Cesium.NearFarScalar四个值,最近的距离,最远的距离,缩放比例范围
    scaleByDistance: new Cesium.NearFarScalar(1.5e2, 2.0, 1.5e7, 0.5),
    translucencyByDistance : new Cesium.NearFarScalar(1.5e2, 1.0, 1.5e7, 0.2)
  });
}

3. 使用canvas 添加点的 billboard

代码与展示效果

function addPointBillboards() {
  Sandcastle.declare(addPointBillboards);
 
 // canvas 的操作部分可见 MDN 文档,这里的主要操作就是画一个圆的 canvas
  const canvas = document.createElement("canvas");
  canvas.width = 16;
  canvas.height = 16;
  const context2D = canvas.getContext("2d");
  context2D.beginPath();
  context2D.arc(8, 8, 8, 0, Cesium.Math.TWO_PI, true);
  context2D.closePath();
  context2D.fillStyle = "rgb(255, 255, 255)";
  context2D.fill();

  const billboards = scene.primitives.add(
    new Cesium.BillboardCollection()
  );
  billboards.add({
    imageId: "custom canvas point",
    image: canvas,  // 这里不再是图片的 url
    position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
    color: Cesium.Color.RED, // billboard 的纹理,每个 billboard 应用不同的颜色
    scale: 0.5,   // 缩放以更改点的外观。
  });
  billboards.add({
    imageId: "custom canvas point",
    image: canvas,
    position: Cesium.Cartesian3.fromDegrees(-80.5, 35.14),
    color: Cesium.Color.BLUE,
  });
  billboards.add({
    imageId: "custom canvas point",
    image: canvas,
    position: Cesium.Cartesian3.fromDegrees(-80.12, 25.46),
    color: Cesium.Color.LIME,
    scale: 2,
  });
}

image.png

4. 使用图片切片做 billboard

代码与展示效果

function addMarkerBillboards() {
  Sandcastle.declare(addMarkerBillboards);

  const billboards = scene.primitives.add(
    new Cesium.BillboardCollection()
  );

  billboards.add({
    image: "../images/whiteShapes.png",
    imageSubRegion: new Cesium.BoundingRectangle(49, 43, 18, 18),
    position: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
    color: Cesium.Color.LIME,
  });
  billboards.add({
    image: "../images/whiteShapes.png",
    imageSubRegion: new Cesium.BoundingRectangle(61, 23, 18, 18),
    position: Cesium.Cartesian3.fromDegrees(-84.0, 39.0),
    color: new Cesium.Color(0, 0.5, 1.0, 1.0),
  });
  billboards.add({
    image: "../images/whiteShapes.png",
    imageSubRegion: new Cesium.BoundingRectangle(67, 80, 14, 14),
    position: Cesium.Cartesian3.fromDegrees(-70.0, 41.0),
    color: new Cesium.Color(0.5, 0.9, 1.0, 1.0),
  });
  billboards.add({
    image: "../images/whiteShapes.png",
    imageSubRegion: new Cesium.BoundingRectangle(27, 103, 22, 22),
    position: Cesium.Cartesian3.fromDegrees(-73.0, 37.0),
    color: Cesium.Color.RED,
  });
  billboards.add({
    image: "../images/whiteShapes.png",
    imageSubRegion: new Cesium.BoundingRectangle(105, 105, 18, 18),
    position: Cesium.Cartesian3.fromDegrees(-79.0, 35.0),
    color: Cesium.Color.YELLOW,
  });
}

"../images/whiteShapes.png"引用的图片是

whiteShapes.png

展现的效果如图

image.png

billboard 的 imageSubRegion 属性

我并没有在 billboard 的文档中找到 imageSubRegion 属性的解释。

但是我在源码发现 imageSubRegion 属性 实际上是借用 BoundingRectangle 来实现图片的切割的。该属性定义用于 billboard 图像的子区域,而不是整个图像,从左下角开始以像素为单位测量。

我的理解就是使用矩形切割 image ,形成的子图像作为 marker 实现 billboard 的效果。


Label

面朝屏幕的文字,其实这部分我觉得是比较简单的,主要是熟悉 options 配置,在此就不给实例图片了

注意创建的是集群对象 Cesium.LabelCollection(),Label对象只能用在LabelCollection当中

Labels

1. 字体和 Properties 配置

function setFont() {
  Sandcastle.declare(setFont);
  scene.primitives.removeAll();
  const labels = scene.primitives.add(new Cesium.LabelCollection());
  labels.add({
    position: Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),
    text: "Philadelphia",
    font: "24px Helvetica",  // 设置字体
    fillColor: new Cesium.Color(0.6, 0.9, 1.0), // 设置字体颜色
    outlineColor: Cesium.Color.BLACK, // 设置外轮廓颜色
    outlineWidth: 2, // 设置外轮廓宽度
    style: Cesium.LabelStyle.FILL_AND_OUTLINE,  
  });
}

function setProperties() {
  Sandcastle.declare(setProperties);
  scene.primitives.removeAll();
  const labels = scene.primitives.add(new Cesium.LabelCollection());
  const l = labels.add({ // l 是一个 Label 对象
    position: Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222), 
    text: "Philadelphia",
  });

  l.position = Cesium.Cartesian3.fromDegrees(
    -75.1641667,
    39.9522222,
    300000.0
  );
  l.scale = 2.0; // 设置放大倍数
}

2. offsetByDistance 和 fadeByDistance 的设置

function offsetByDistance() {
  Sandcastle.declare(offsetByDistance);
  scene.primitives.removeAll();
  const image = new Image();
  image.onload = function () {
    const billboards = scene.primitives.add(
      new Cesium.BillboardCollection()
    );
    billboards.add({
      position: Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),
      scaleByDistance: new Cesium.NearFarScalar(1.5e2, 5.0, 1.5e7, 0.5),
      image: image,
    });

    const labels = scene.primitives.add(new Cesium.LabelCollection());
    labels.add({
      position: Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),
      text: "Label on top of scaling billboard",
      font: "20px sans-serif",
      showBackground: true,
      horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
      pixelOffset: new Cesium.Cartesian2(0.0, -image.height),
      pixelOffsetScaleByDistance: new Cesium.NearFarScalar(
        1.5e2,
        3.0,
        1.5e7,
        0.5
      ),
    });
  };
  image.src = "../images/facility.gif";
}

function fadeByDistance() {
  Sandcastle.declare(fadeByDistance);
  scene.primitives.removeAll();
  const labels = scene.primitives.add(new Cesium.LabelCollection());
  labels.add({
    position: Cesium.Cartesian3.fromDegrees(-73.94, 40.67),
    text: "New York",
    translucencyByDistance: new Cesium.NearFarScalar(
      1.5e2,
      1.0,
      1.5e8,
      0.0
    ),
  });
  labels.add({
    position: Cesium.Cartesian3.fromDegrees(-84.39, 33.75),
    text: "Atlanta",
    translucencyByDistance: new Cesium.NearFarScalar(
      1.5e5,
      1.0,
      1.5e7,
      0.0
    ),
  });
}

Polylines

1. 使用折线在地球表面画一个三角形

代码和展示效果

const polylines = scene.primitives.add(
new Cesium.PolylineCollection()
);

// A simple polyline with two points.
const polyline = polylines.add({
positions: Cesium.PolylinePipeline.generateCartesianArc({
    positions: Cesium.Cartesian3.fromDegreesArray([
      0.0, 0.0,
      10.0, 0.0,
      0.0, 20.0,
      0.0, 0.0,
    ]),
}),
material: Cesium.Material.fromType("Color", {
  color: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
}),
});

image.png

关于 generateCartesianArc

在表现上来看,使用了 generateCartesianArc ,折线就会“贴合”在地球表面,也就是直线变成曲线贴合在地球表面,观感会好很多 image.png

我们可以看一下上述代码中分解一下

const polylines = scene.primitives.add(
  new Cesium.PolylineCollection()
);

const fromDegreesArray = Cesium.Cartesian3.fromDegreesArray([
  0.0, 0.0,
  10.0, 0.0,
  0.0, 20.0,
  0.0, 0.0,
])

const generateCartesianArc = Cesium.PolylinePipeline.generateCartesianArc({
  positions: fromDegreesArray,
})

const polyline = polylines.add({
  positions: generateCartesianArc,
  material: Cesium.Material.fromType("Color", {
    color: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
  }),
});

变量 fromDegreesArray 是一个包含4个 Cartesian3 坐标的数组,这四个坐标就是画这个三角形经过的四个点的坐标。

image.png

Cesium.PolylinePipeline.generateCartesianArc 接收了变量 fromDegreesArray 作为 position 属性,然后返回了一个包含了54个 Cartesian3 坐标的数组。个人理解 generateCartesianArc 应该是一种类似于“微分”的操作,使得原有的折线“微分”成一个近似曲线,从而可以贴近地球平面。

image.png

如果不使用 generateCartesianArc ,因为地球的表面是圆的,所以当距离较大时,折线的中间部分可能会出现在地底下

image.png

2. 多段折线的轮廓材质

代码和展示效果

const widePolyline = polylines.add({
  positions: Cesium.PolylinePipeline.generateCartesianArc({
    positions: Cesium.Cartesian3.fromDegreesArray([
      -105.0,
      40.0,
      -100.0,
      38.0,
      -105.0,
      35.0,
    ]),
  }),
  
  material: Cesium.Material.fromType(
    Cesium.Material.PolylineOutlineType,
    {
      outlineColor:  new Cesium.Color(0.0, 1.0, 0.0, 1.0),
      outlineWidth: 10.0,
    }
  ),
  width: 15.0,
});

image.png

关于 fromType

Cesium 提供了23种现成的 Material 类型,可通过 Material.fromType 方法和 Fabric 两种方式去获取并设置几何对象材质。

Cesium.Material.PolylineOutlineType 就是一种现成的 PolylineOutlineType 材质预设,下面一段代码是 PolylineOutlineType 的材质预设。

Material.PolylineOutlineType = "PolylineOutline";
Material._materialCache.addMaterial(Material.PolylineOutlineType, {
  fabric: {
    type: Material.PolylineOutlineType,
    uniforms: {
      color: new Color(1.0, 1.0, 1.0, 1.0),
      outlineColor: new Color(1.0, 0.0, 0.0, 1.0),
      outlineWidth: 1.0,
    },
    source: PolylineOutlineMaterial,
  },
  translucent: function (material) {
    const uniforms = material.uniforms;
    return uniforms.color.alpha < 1.0 || uniforms.outlineColor.alpha < 1.0;
  },
});

在原本的预设中,该折线的边缘轮廓线的颜色应该是透明度为1的红色,其宽度为1。但是我通过 outlineColor: new Cesium.Color(0.0, 1.0, 0.0, 1.0), outlineWidth: 10.0,将原本预设中的 outlineColor 属性和 outlineWidth 属性进行了一个覆盖。因此,在呈现的效果中,折线的边缘轮廓线是透明度为1,宽度为10的绿色轮廓线。

材质相关的内容会在后续系列文章中详细阐述。

3.画出一个有方向指向的箭头

代码和展示效果

const localPolylines = scene.primitives.add(
  new Cesium.PolylineCollection()
);
const center = Cesium.Cartesian3.fromDegrees(-80.0, 35.0, 200000);
localPolylines.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(
  center
);

const localPolyline = localPolylines.add({
  positions: [
    new Cesium.Cartesian3(0.0, 0.0, 0.0),
    new Cesium.Cartesian3(1000000.0, 0.0, 0.0),
  ],
  width: 10.0,
  material: Cesium.Material.fromType(
    Cesium.Material.PolylineArrowType
  ),
});

image.png

image.png

以不同的坐标系作为参考进行折线绘制

与画普通 polyline 不同的是,箭头是具有方向的。

此前的例子都是基于地球坐标系,使用转化为 Cartesian3 坐标的两端进行画线,但是在这个例子中,参考坐标系发生了改变

在这个例子中,首先使用 eastNorthUpToFixedFrame 来确定该箭头直线的原点和自身方向坐标系,x轴指向本地的东向、y轴指向本地的北向、z轴指向穿过该位置的椭球曲面法线方向。 localPolylines.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame ( center )

此时再进行 position的设置,,就不再是以地球坐标系来画线了,而是以使用 eastNorthUpToFixedFrame 确立的该直线自身的坐标系来进行画线操作。 positions: [ new Cesium.Cartesian3(0.0, 0.0, 0.0), new Cesium.Cartesian3(1000000.0, 0.0, 0.0), ]

也就是说 画笔会从 西经80、北纬35、高度200000 的点,沿着地球的切线,向东画出长度为1000000的一条线。

然后再使用 Cesium.Material.PolylineArrowType 画出箭头样式

4.含有渐变的折线

这个应该都能通过代码理解,就不再赘述了

const fadingPolyline = polylines.add({
  positions: Cesium.PolylinePipeline.generateCartesianArc({
    positions: Cesium.Cartesian3.fromDegreesArrayHeights([
      -75,
      43,
      500000,
      -125,
      43,
      500000,
    ]),
  }),
  width: 5,
  material: Cesium.Material.fromType(Cesium.Material.FadeType, {
    repeat: true,
    fadeInColor: Cesium.Color.CYAN,
    fadeOutColor: Cesium.Color.CYAN.withAlpha(0),
    time: new Cesium.Cartesian2(0.0, 0.0),
    fadeDirection: {
      x: true,
      y: false,
    },
  }),
});

5. generateCartesianRhumbArc 和 generateCartesianArc

代码和展示效果

const rhumbLine = polylines.add({
  positions: Cesium.PolylinePipeline.generateCartesianRhumbArc({
    positions: Cesium.Cartesian3.fromDegreesArray([
      -130.0,
      30.0,
      -75.0,
      30.0,
    ]),
  }),
  width: 5,
  material: Cesium.Material.fromType("Color", {
    color: new Cesium.Color(0.0, 1.0, 0.0, 1.0),  // 绿色
  }),
});

const polyline = polylines.add({
  positions: Cesium.PolylinePipeline.generateCartesianArc({
    positions: Cesium.Cartesian3.fromDegreesArray([
      -130.0,
      30.0,
      -75.0,
      30.0,
    ]),
  }),
  width: 5,
  material: Cesium.Material.fromType("Color", {
    color: new Cesium.Color(1.0, 0.0, 0.0, 1.0),  // 红色
  }),
});

image.png

generateCartesianRhumbArc 创造的是 Rhumb同向线,弧线切线方向都是一致的;若拿着罗盘针的话,航线都是一致的。

在这个例子中,可以看到,generateCartesianRhumbArc 和纬度线是重合的