这篇教程将指导您如何使用 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,
}),
},
});
在本教程中,我们将深入了解 Geometry 和 Appearance 。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)结合在一起。目前不用区分 Geometry 和 GeometryInstance,只需要将 GeometryInstance 视为一个装有几何体的容器即可。为了在特定地理区域内创建一个符合地球曲率的矩形几何体,我们使用了 RectangleGeometry 类,它的作用是生成一个矩形,覆盖特定的经纬度范围,并将其分割成多个三角形,以便更好地匹配地球的椭球形状。在渲染时,这个矩形几何体会自动适应地球的曲率,从而在 Cesium 的 3D 地球视图中正确显示。
由于它在地球表面,我们可以使用 EllipsoidSurfaceAppearance。通过假设几何体直接位于地球表面,或者在一个固定的高度上方,可以减少复杂的高度计算和数据存储,从而节省内存和提高性能。
几何图形类型
CesiumJS 支持以下几何图形,点击查看示例:
| Geometry | Outline | Description |
|---|---|---|
BoxGeometry | BoxOutlineGeometry | A box (盒子) |
CircleGeometry | CircleOutlineGeometry | A circle or extruded circle (圆或拉伸的圆) |
CorridorGeometry | CorridorOutlineGeometry | A polyline normal to the surface with a width in meters and optional extruded height (一条与表面垂直的折线,宽度以米为单位,可以有可选的拉伸高度) |
CylinderGeometry | CylinderOutlineGeometry | A cylinder, cone, or truncated cone (圆柱体、锥体或截锥体) |
EllipseGeometry | EllipseOutlineGeometry | An ellipse or extruded ellipse (椭圆或拉伸的椭圆) |
EllipsoidGeometry | EllipsoidOutlineGeometry | An ellipsoid (椭球体) |
RectangleGeometry | RectangleOutlineGeometry | A rectangle or extruded rectangle (矩形或拉伸的矩形) |
PolygonGeometry | PolygonOutlineGeometry | A polygon with optional holes or extruded polygon (一个多边形,可以有可选的孔或拉伸多边形) |
PolylineGeometry | SimplePolylineGeometry | A collection of line segments with a width in pixels (一组线段,宽度以像素为单位) |
PolylineVolumeGeometry | PolylineVolumeOutlineGeometry | A 2D shape extruded along a polyline (沿着折线拉伸的二维形状) |
SphereGeometry | SphereOutlineGeometry | A sphere (球体) |
WallGeometry | WallOutlineGeometry | A 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 后,仍然可以动态地修改几何体实例的属性,以改变它们在场景中的外观,例如颜色和可见性。实例属性包括:
- 颜色(color):ColorGeometryInstanceAttribute 决定了实例的颜色。Primitive 必须具有 PerInstanceColorAppearance 才能使用该属性。
- 可见性(show):决定实例的可见性。所有实例都可以使用该属性。
下面的示例展示了如何更改几何体实例的颜色:
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 的值。
外观
Geometry 定义了物体的结构,而另一个关键属性 Appearance 则定义了 Primitive 的着色方式,即如何对每个像素进行渲染。虽然一个 Primitive 可以有许多几何体实例,但它只能拥有一种外观。当你为一个 Primitive 选择外观时,这种特定类型的外观将使用一个 材质 来对几何体进行着色和渲染。例如,MaterialAppearance 可以使用自定义材质来渲染复杂的纹理和光照效果;而 PerInstanceColorAppearance 则允许为每个几何体实例设置不同的颜色。
CesiumJS 提供了以下外观:
| 外观 | 描述 |
|---|---|
| MaterialAppearance | 一种适用于所有几何类型的外观,支持使用材料来定义着色。 |
| EllipsoidSurfaceAppearance | MaterialAppearance 的一个版本,假设几何体平行于地球表面(例如多边形),并利用这一假设通过过程计算许多顶点属性来节省内存。 |
| PerInstanceColorAppearance | 使用每个实例的颜色属性来进行着色。 |
| PolylineMaterialAppearance | 支持使用材料来对折线进行着色。 |
| PolylineColorAppearance | 使用每顶点或每段的颜色来对折线进行着色。 |
外观不仅定义了在 GPU 上执行的完整 GLSL 顶点和片段着色器,还决定了绘制 Primitive 时 GPU 的渲染状态。你可以直接定义渲染状态,或者使用更高级别的属性(使用高级别属性简化了操作,使得在不深入了解底层渲染细节的情况下,也能有效地设置渲染行为),如 closed 和 translucent,这些属性将由 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 属性,以完全改变几何体的外观。
大多数外观都有 flat 和 faceForward 这两个属性,它们通过间接控制 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。