探索 CesiumJS 中的 Geometry 与 Appearances

812 阅读11分钟

原文地址:Custom Geometry & Appearances – Cesium

这篇教程将指导您如何使用 CesiumJS 的 Primitive API 来实现复杂的几何图形和外观。这是一个高级主题,专为那些希望通过创建自定义网格、形状、体积和外观,以扩展 CesiumJS 功能的用户设计,不适用于一般的 Cesium 用户。

如果你有兴趣了解如何在地球上绘制各种平面和立体图形,请参阅 Creating Entities – Cesium / CesiumJS 实体(Entity)创建指南 - 掘金 (juejin.cn)教程。

概述

在 CesiumJS 中,可以使用 Entity API 创建不同类型的几何图形,比如多边形和椭圆。例如,将以下内容复制并粘贴到 Hello World Sandcastle 示例 中,可以在地球上创建带有条纹图案的矩形。

const viewer = new Cesium.Viewer("cesiumContainer");
viewer.entities.add({
  rectangle: {
    coordinates: Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
    material: new Cesium.StripeMaterialProperty({
      evenColor: Cesium.Color.WHITE,
      oddColor: Cesium.Color.BLUE,
      repeat: 5,
    }),
  },
});

在本教程中,我们将深入了解 GeometryAppearance 。Geometry 定义了 Primitive(基元)的结构,比如组成 Primitive 的三角形、线条或点等。Appearance 决定了 Primitive 的着色方式,包括完整的 GLSL 顶点和片段着色器,以及用于控制渲染过程的各种状态设置,如深度测试和混合模式等。

使用 Geometry 和 Appearance 的优点

  • 性能:当绘制大量静态的 Primitive(例如美国每个邮政编码区的多边形)时,直接使用 Geometry 而非更高级的 Entity 可以有效提升性能。通过将多个静态 Primitive 组合成一个几何体,可以减少 CPU 开销并更好地利用 GPU。优化过程在 Web Worker 中进行,不会影响用户界面的响应速度。
  • 灵活性:Primitive 对象结合了 Geometry 和 Appearance 两部分。通过解耦,可以分别修改 Geometry 和 Appearance,而不影响另外一个。这样,系统可以更加灵活:您可以轻松地为各种不同外观添加新几何体,或者为现有几何体应用不同的外观。
  • 底层访问:Appearance 提供了接近底层的渲染访问,却无需关注直接使用渲染器的底层细节。Appearance 使得编写 GLSL 着色器和使用自定义渲染状态变得容易。

使用 Geometry 和 Appearance 的缺点

  • 直接使用 Geometry 和 Appearance 需要编写更多代码,并且需要对图形学有更深入的理解。相比之下,使用 Entity API 更加的简单,更适合一般的地图应用程序。Geometry 和 Appearance 提供了更低级别的控制,这种抽象层次更类似于传统的 3D 引擎,更适合需要细粒度控制的应用。
  • 组合几何体的方法在处理静态数据(即几何体的位置和形状不会随时间变化的数据)时非常有效。但是,这种方法不一定适用于动态数据(即几何体的位置和形状会随时间改变的数据)。

让我们使用 Geometry 和 Appearance 重新编写初始代码示例:

const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;
// 原始代码:
// viewer.entities.add({
//     rectangle : {
//         coordinates: Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
//         material: new Cesium.StripeMaterialProperty({
//             evenColor: Cesium.Color.WHITE,
//             oddColor: Cesium.Color.BLUE,
//             repeat: 5
//         })
//     }
// });

const instance = new Cesium.GeometryInstance({
  geometry: new Cesium.RectangleGeometry({
    rectangle: Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
    vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
  }),
});

scene.primitives.add(
  new Cesium.Primitive({
    geometryInstances: instance,
    appearance: new Cesium.EllipsoidSurfaceAppearance({
      material: Cesium.Material.fromType("Stripe"),
    }),
  })
);

我们没有使用矩形实体,而是使用了 Primitive 将几何体(Geometry)和外观(Appearance)结合在一起。目前不用区分 GeometryGeometryInstance,只需要将 GeometryInstance 视为一个装有几何体的容器即可。为了在特定地理区域内创建一个符合地球曲率的矩形几何体,我们使用了 RectangleGeometry 类,它的作用是生成一个矩形,覆盖特定的经纬度范围,并将其分割成多个三角形,以便更好地匹配地球的椭球形状。在渲染时,这个矩形几何体会自动适应地球的曲率,从而在 Cesium 的 3D 地球视图中正确显示。

由于它在地球表面,我们可以使用 EllipsoidSurfaceAppearance。通过假设几何体直接位于地球表面,或者在一个固定的高度上方,可以减少复杂的高度计算和数据存储,从而节省内存和提高性能。

几何图形类型

CesiumJS 支持以下几何图形,点击查看示例

GeometryOutlineDescription
BoxGeometryBoxOutlineGeometryA box (盒子)
CircleGeometryCircleOutlineGeometryA circle or extruded circle (圆或拉伸的圆)
CorridorGeometryCorridorOutlineGeometryA polyline normal to the surface with a width in meters and optional extruded height (一条与表面垂直的折线,宽度以米为单位,可以有可选的拉伸高度)
CylinderGeometryCylinderOutlineGeometryA cylinder, cone, or truncated cone (圆柱体、锥体或截锥体)
EllipseGeometryEllipseOutlineGeometryAn ellipse or extruded ellipse (椭圆或拉伸的椭圆)
EllipsoidGeometryEllipsoidOutlineGeometryAn ellipsoid (椭球体)
RectangleGeometryRectangleOutlineGeometryA rectangle or extruded rectangle (矩形或拉伸的矩形)
PolygonGeometryPolygonOutlineGeometryA polygon with optional holes or extruded polygon (一个多边形,可以有可选的孔或拉伸多边形)
PolylineGeometrySimplePolylineGeometryA collection of line segments with a width in pixels (一组线段,宽度以像素为单位)
PolylineVolumeGeometryPolylineVolumeOutlineGeometryA 2D shape extruded along a polyline (沿着折线拉伸的二维形状)
SphereGeometrySphereOutlineGeometryA sphere (球体)
WallGeometryWallOutlineGeometryA wall perpendicular to the globe (垂直于地球的墙)

组合几何图形

通过将多个静态几何体组合到一个 Primitive 中,可以显著提升渲染性能。例如,在一个 Primitive 中绘制两个矩形:

const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;

const instance = new Cesium.GeometryInstance({
  geometry: new Cesium.RectangleGeometry({
    rectangle: Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
    vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
  }),
});

const anotherInstance = new Cesium.GeometryInstance({
  geometry: new Cesium.RectangleGeometry({
    rectangle: Cesium.Rectangle.fromDegrees(-85.0, 20.0, -75.0, 30.0),
    vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
  }),
});

scene.primitives.add(
  new Cesium.Primitive({
    geometryInstances: [instance, anotherInstance],
    appearance: new Cesium.EllipsoidSurfaceAppearance({
      material: Cesium.Material.fromType("Stripe"),
    }),
  })
);

在上述示例代码中,我们创建了另一个矩形实例,并将这两个实例一起赋值给 Primitive,同时,这两个实例使用相同的外观进行绘制。

有些外观允许每个实例提供独特的属性。例如,我们可以使用 PerInstanceColorAppearance 为每个实例设置不同的颜色:

const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;

const instance = new Cesium.GeometryInstance({
  geometry: new Cesium.RectangleGeometry({
    rectangle: Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
    vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
  }),
  attributes: {
    color: new Cesium.ColorGeometryInstanceAttribute(0.0, 0.0, 1.0, 0.8),
  },
});

const anotherInstance = new Cesium.GeometryInstance({
  geometry: new Cesium.RectangleGeometry({
    rectangle: Cesium.Rectangle.fromDegrees(-85.0, 20.0, -75.0, 30.0),
    vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
  }),
  attributes: {
    color: new Cesium.ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 0.8),
  },
});

scene.primitives.add(
  new Cesium.Primitive({
    geometryInstances: [instance, anotherInstance],
    appearance: new Cesium.PerInstanceColorAppearance(),
  })
);

当 Primitive 使用 PerInstanceColorAppearance 进行渲染时,会根据每个实例的颜色属性来确定着色。

组合几何体可以让 CesiumJS 高效地绘制大量几何体。下面的示例绘制了 2,592 个不同颜色的矩形:

const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;
const instances = [];

for (let lon = -180.0; lon < 180.0; lon += 5.0) {
  for (let lat = -85.0; lat < 85.0; lat += 5.0) {
    instances.push(
      new Cesium.GeometryInstance({
        geometry: new Cesium.RectangleGeometry({
          rectangle: Cesium.Rectangle.fromDegrees(
            lon, lat, lon + 5.0, lat + 5.0
          ),
          vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
        }),
        attributes: {
          color: Cesium.ColorGeometryInstanceAttribute.fromColor(
            Cesium.Color.fromRandom({ alpha: 0.5 })
          ),
        },
      })
    );
  }
}

scene.primitives.add(
  new Cesium.Primitive({
    geometryInstances: instances,
    appearance: new Cesium.PerInstanceColorAppearance(),
  })
);

拾取

当实例(GeometryInstance)被组合在一起后,它们仍然是可以被独立访问的。通过为每个实例分配一个 ID,可以在使用 Scene.pick 进行拾取时,借助该 ID 来识别具体的实例。

以下示例展示了如何创建带有 ID 的实例,并在实例被点击时向控制台输出一条消息:

const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;

const instance = new Cesium.GeometryInstance({
  geometry: new Cesium.RectangleGeometry({
    rectangle: Cesium.Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0),
    vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
  }),
  id: "my rectangle",
  attributes: {
    color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.RED),
  },
});

scene.primitives.add(
  new Cesium.Primitive({
    geometryInstances: instance,
    appearance: new Cesium.PerInstanceColorAppearance(),
  })
);

const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
handler.setInputAction((movement) => {
  const pick = scene.pick(movement.position);
  if (Cesium.defined(pick) && pick.id === "my rectangle") {
    console.log("Mouse clicked rectangle.");
  }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

使用 ID 可以避免在 Primitive 被创建后需要将整个几何体实例保留在内存中的问题,从而更加高效地管理内存。

几何体实例(GeometryInstance)

几何体实例(GeometryInstance)允许在场景的不同位置、以不同变换方式(缩放、旋转)展示相同的几何体(Geometry)。这是因为多个实例可以共享同一个几何体定义,但每个实例可以应用不同的 modelMatrix 来实现各自独特的变换。

下面的示例创建了一个 EllipsoidGeometry 和两个实例。每个实例引用相同的椭圆体几何图形,但使用不同的 modelMatrix 进行放置,最终效果是一个椭圆体位于另一个上方。

const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;
const ellipsoidGeometry = new Cesium.EllipsoidGeometry({
  vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
  radii: new Cesium.Cartesian3(300000.0, 200000.0, 150000.0),
});

const cyanEllipsoidInstance = new Cesium.GeometryInstance({
  geometry: ellipsoidGeometry,
  modelMatrix: Cesium.Matrix4.multiplyByTranslation(
    Cesium.Transforms.eastNorthUpToFixedFrame(
      Cesium.Cartesian3.fromDegrees(-100.0, 40.0)
    ),
    new Cesium.Cartesian3(0.0, 0.0, 150000.0),
    new Cesium.Matrix4()
  ),
  attributes: {
    color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.CYAN),
  },
});

const orangeEllipsoidInstance = new Cesium.GeometryInstance({
  geometry: ellipsoidGeometry,
  modelMatrix: Cesium.Matrix4.multiplyByTranslation(
    Cesium.Transforms.eastNorthUpToFixedFrame(
      Cesium.Cartesian3.fromDegrees(-100.0, 40.0)
    ),
    new Cesium.Cartesian3(0.0, 0.0, 450000.0),
    new Cesium.Matrix4()
  ),
  attributes: {
    color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.ORANGE),
  },
});

scene.primitives.add(
  new Cesium.Primitive({
    geometryInstances: [cyanEllipsoidInstance, orangeEllipsoidInstance],
    appearance: new Cesium.PerInstanceColorAppearance({
      translucent: false,
      closed: true,
    }),
  })
);

更新实例属性

在将几何体添加到 Primitive 后,仍然可以动态地修改几何体实例的属性,以改变它们在场景中的外观,例如颜色和可见性。实例属性包括:

下面的示例展示了如何更改几何体实例的颜色:

const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;

const circleInstance = new Cesium.GeometryInstance({
  geometry: new Cesium.CircleGeometry({
    center: Cesium.Cartesian3.fromDegrees(-95.0, 43.0),
    radius: 250000.0,
    vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
  }),
  attributes: {
    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
      new Cesium.Color(1.0, 0.0, 0.0, 0.5)
    ),
  },
  id: "circle",
});

const primitive = new Cesium.Primitive({
  geometryInstances: circleInstance,
  appearance: new Cesium.PerInstanceColorAppearance({
    translucent: false,
    closed: true,
  }),
});
scene.primitives.add(primitive);

setInterval(() => {
  const attributes = primitive.getGeometryInstanceAttributes("circle");
  attributes.color = Cesium.ColorGeometryInstanceAttribute.toValue(
    Cesium.Color.fromRandom({ alpha: 1.0 })
  );
}, 2000);

在这个示例中,我们首先创建了一个几何体实例(circleInstance)表示一个圆形几何体,并设置颜色、ID 等属性。然后创建了一个 Primitive,将几何体实例添加到该 Primitive 中,并设置其外观为 PerInstanceColorAppearance

通过使用 setInterval 函数,每两秒钟随机更改一次几何体实例的颜色。更改颜色时,通过调用 primitive.getGeometryInstanceAttributes("circle") 检索到实例的属性,并直接更改 attributes.color 的值。

1

外观

Geometry 定义了物体的结构,而另一个关键属性 Appearance 则定义了 Primitive 的着色方式,即如何对每个像素进行渲染。虽然一个 Primitive 可以有许多几何体实例,但它只能拥有一种外观。当你为一个 Primitive 选择外观时,这种特定类型的外观将使用一个 材质 来对几何体进行着色和渲染。例如,MaterialAppearance 可以使用自定义材质来渲染复杂的纹理和光照效果;而 PerInstanceColorAppearance 则允许为每个几何体实例设置不同的颜色。

Geometry and appearances highleveldesign

CesiumJS 提供了以下外观:

外观描述
MaterialAppearance一种适用于所有几何类型的外观,支持使用材料来定义着色。
EllipsoidSurfaceAppearanceMaterialAppearance 的一个版本,假设几何体平行于地球表面(例如多边形),并利用这一假设通过过程计算许多顶点属性来节省内存。
PerInstanceColorAppearance使用每个实例的颜色属性来进行着色。
PolylineMaterialAppearance支持使用材料来对折线进行着色。
PolylineColorAppearance使用每顶点或每段的颜色来对折线进行着色。

外观不仅定义了在 GPU 上执行的完整 GLSL 顶点和片段着色器,还决定了绘制 Primitive 时 GPU 的渲染状态。你可以直接定义渲染状态,或者使用更高级别的属性(使用高级别属性简化了操作,使得在不深入了解底层渲染细节的情况下,也能有效地设置渲染行为),如 closedtranslucent,这些属性将由 Appearance 转换为具体的渲染状态。

// 定义一个具有高级别属性的 Appearance
// translucent: 这个属性用于指示几何体是否为透明的。如果设置为 true,意味着渲染时需要启用混合(Blending),确保透明效果正确显示。
// closed:这个属性用于指示几何体是否为封闭的。如果设置为 true,意味着背面剔除(Cull Face)将被启用,以优化渲染性能。
const appearance = new Cesium.PerInstanceColorAppearance({
  translucent: false,
  closed: true,
});

// 这个外观与上面的相同,通过直接定义渲染状态实现
// depthTest.enabled: true:启用深度测试。
// cull.enabled: true 和 cull.face: Cesium.CullFace.BACK:启用并设置背面剔除,这意味着只渲染面朝摄像机的一侧。
const anotherAppearance = new Cesium.PerInstanceColorAppearance({
  renderState: {
    depthTest: {
      enabled: true,
    },
    cull: {
      enabled: true,
      face: Cesium.CullFace.BACK,
    },
  },
});

一旦创建了 Appearance,将不能更改它的 renderState 属性,但可以修改它的 material 属性。当然,也可以动态地修改 Primitive 的 appearance 属性,以完全改变几何体的外观。

大多数外观都有 flatfaceForward 这两个属性,它们通过间接控制 GLSL 着色器来影响渲染结果。

  • flat
    • 功能:控制是否采用平面着色。
    • 作用:当 flat 设置为 true 时,渲染过程中不会考虑光照效果。这意味着物体表面的颜色是均匀的,不会受到光线和阴影的影响。平面着色(flat shading)通常用于更简洁和艺术风格的渲染。
    • 应用场景:适用于需要消除光照影响,保持一致颜色的场景。
  • faceForward
    • 功能:控制光照处理中法线的翻转。
    • 作用:当 faceForward 设置为 true 时,在进行光照计算时会翻转背面法线,使它们始终朝向观察者。这可以避免背面区域(例如墙壁的内侧)出现完全黑色的情况,因为翻转后的法线将正确反射光线。
    • 应用场景:适用于有背面光照需求的场景,确保物体的背面也能正确显示光照效果。

几何体与外观的兼容性

并非所有外观都适用于所有的几何体。例如,EllipsoidSurfaceAppearance 不适用于 WallGeometry,因为墙体不位于地球表面。

为了确保外观与几何体兼容,它们必须具有匹配的顶点格式,也就是说几何体必须提供外观所期望的输入数据。创建几何体时,可以通过 VertexFormat 来指定这些数据。

几何体的 vertexFormat 决定了它是否可以与其他几何体组合。尽管这两个几何体不需要是相同类型,但它们必须有匹配的顶点格式。

为了方便起见,外观要么有一个 vertexFormat 属性,要么有一个 VERTEX_FORMAT 的静态常量,可以作为选项传递给几何体。

const geometry = new Cesium.RectangleGeometry({
  vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
  // ...
});

const geometry2 = new Cesium.RectangleGeometry({
  vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
  // ...
});

const appearance = new Cesium.MaterialAppearance(/* ... */);

const geometry3 = new Cesium.RectangleGeometry({
  vertexFormat: appearance.vertexFormat,
  // ...
});

资源

如需进一步了解,请参考以下文档:

想了解更多有关材质的信息,请参阅 Fabric

关于未来的发展计划,请参阅 Geometry and Appearances Roadmap