Cesium 中官方提供 Entity api 在创建图元时给开发者提供了一种便捷的方式,避免了直接与 Primitive 直接接触。这样能够让刚开始接触这类webgl框架的开发者能够快速上手。这一切的便利都建立在 Cesium 复杂的 Entity 机制中,本篇文章将深入浅出的刨析这背后的运作原理,帮助大家更好的理解。
ps: 本文仅介绍关于 Entity 相关知识,关于如何渲染请参考 Primitive 篇。
const entity = viewer.entities.add(
new Cesium.Entity({
name: item.id,
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArray(points),
extrudedHeight: 100,
material: Cesium.Color.WHITE
}
})
)
以上是我们通过 Entity API 创建一个多边形的代码
首先我们提出几点疑问,带着疑问去看源码能够更加深入的帮助我们去理解
- 在创建阶段
Cesium
为我们做了哪些工作 -
Cesium
是如何将 Entity 转换成Primitive
的 -
Cesium
是如何做到当我们修改entity.polygon
属性时自动更新的
在了解这些问题之前,我们先需要对 DataSource
机制进行一个大致的认识:
Cesium.DataSource
作为一个抽象类,并不能直接被初始化,但是他的一些派生类是我们经常使用到的,例如 GeoJsonDataSource
、KmlDataSources
。他们可以简单的被理解成是一个图形的集合,下面存放了各种的 Entity
,例如我们所熟悉的 viewer.entities
就是 viewer.dataSourceDisplay.defaultDataSource.entities
的一个快捷访问属性,后者是 Cesium.CustomDataSource
的一个实例。
所有的 DataSource
被管理在 viewer.datasources
中,它是 DataSourceCollection
的一个实例,它仅负责管理各个 DataSource
,可以理解成一个容器,提供add
、remove
、contain
等方法,具体负责对它们进行更新的则是 viewer.dataSourceDisplay
,这个类在初始化Viewer
的时候会被创建,在这个过程中主要做了以下工作
- 创建
PrimitiveCollection
容器,负责存放所有entity
被转换后的primitive
,同时将他们合并到scene.primitives
中,参与到渲染
const primitives = new PrimitiveCollection();
const groundPrimitives = new PrimitiveCollection();
if (dataSourceCollection.length > 0) {
scene.primitives.add(primitives);
scene.groundPrimitives.add(groundPrimitives);
primitivesAdded = true;
}
- 注册一个事件,用于监听当有新的
DataSource
被添加到viewer.datasources
时,给这些新添加的DataSource
创建子容器PrimitiveCollection
并将它们挂在到DataSource._primitives
。于此同时更加重要的是给DataSource
创建一堆Visualizer
这才是背后真正发挥作用的
代码太长就不粘贴了,具体参考 DataSourceDisplay.prototype._onDataSourceAdded、DataSourceDisplay.defaultVisualizersCallback
- 创建
CustomDatasource
const defaultDataSource = new CustomDataSource();
this._onDataSourceAdded(undefined, defaultDataSource);
this._defaultDataSource = defaultDataSource;
完成了对 DataSource
机制的一个介绍,接下来正式介绍创建 Entity
的过程,当调用 datasources.entities(EntityCollection)
的 add
方法时会触发以下事件,如下
function fireChangedEvent(collection) {
...省略
collection._collectionChanged.raiseEvent(
collection,
addedArray, //本次变动新增的entity
removedArray, //本次变动移除的entity
changedArray //本次变动修改的entity
);
...省略
}
而_collectionChanged
事件早在创建各个Visualizer
就已经被监听,例如在GeometryVisualizer.constructor
的最后
entityCollection.collectionChanged.addEventListener(
GeometryVisualizer.prototype._onCollectionChanged,
this
);
因此这一动作就被各个Visualizer
所捕获,继而它们内部对新增/移除的Entity
进行处理,具体代码参考Visualizer.prototype._onCollectionChanged
,简单一句话介绍就是将这些 entity 放入各自的缓存中①,等待下一轮的 update
至此我们解答了第一个问题,着重需要额外提到的是在各个
Visualizer
的初始化阶段,创建了一系列的 Batch 容器,它们将帮助这些 entity 实现合批渲染
接下来我们需要着重介绍一下Cesium
是怎么将Entity
转换到Primitive
的这一过程
从源码中我们可以得知,这一行为发生在update阶段。viewer
在创建阶段注册了 _onTick
事件,每次的更新会触发该事件,继而触发DataSourceDisplay
的update方法。在该update方法中会遍历所有DataSource
,触发他们的Visualizer
,重点来了:
在这些 Visualizer
( 这里以GeometryVisualizer举例 ) 的update方法中会对缓存中的entity
①进行处理:
- 新增
- 修改
- 移除
这里以新增为例:
for (i = added.length - 1; i > -1; i--) {
entity = added[i];
id = entity.id;
updaterSet = new GeometryUpdaterSet(entity, this._scene); //创建Updater
this._updaterSets.set(id, updaterSet);
updaterSet.forEach(function (updater) {
that._insertUpdaterIntoBatch(time, updater); //分批
});
this._subscriptions.set(
id,
updaterSet.geometryChanged.addEventListener(
GeometryVisualizer._onGeometryChanged,
this
)
);
}
这里有几个重要的操作:
- 创建 UpdaterSet,这里面包含了各种的
Updater
,不管在 entity 中是否声明了对应的 geometry 都会创建,这也是常说Entity
性能会略差的原因,包括之前创建的各种Visualizer
。不过 Entity 强大之处在于极大的简化了开发者的工作量,包括后面的合批渲染。
Updater 在创建阶段会根据 entity 中声明对应的属性进行填充,例如:声明entity.box属性,那么在BoxGeometryUpdater 中会自动从我们声明的内容中提取,没有声明的则取默认值。每个 updater 提供一个
createFillGeometryInstance
方法,负责创建 geometryInstance
- 创建好 Updater 以后,接下来就是根据以下状态将他们放入不同的批次(Batch):
-
material
属性 -
closeTop
/closeBottom
属性 -
outline
属性
这次批次被保存在一系列的批次容器中,这类批次容器有几大类:
StaticOutlineGeometryBatch
StaticGeometryColorBatch
StaticGeometryPerMaterialBatch
在初始化GeometryVisualizer
阶段时就已经对这些容器进行创建
this._outlineBatches = new Array(numberOfShadowModes * 2);
this._closedColorBatches = new Array(numberOfShadowModes * 2);
this._closedMaterialBatches = new Array(numberOfShadowModes * 2);
this._openColorBatches = new Array(numberOfShadowModes * 2);
this._openMaterialBatches = new Array(numberOfShadowModes * 2);
省略后面的一系列new
操作
被放入同一批次的
entity
会被合并到一个Primitve
中
- 监听 Updater 的
geometryChanged
事件,当他们被触发时意味着这个entity发生了改动,因此需要将他们放入到changedObjects
队列,等待下一次update
时将他们重新分批