之前的最长的一帧系列,我们主要集中在地形和影像服务方面。简单说,之前我们都集中在地球是怎么造出来的,从这一系列开始,我们的目光从GLOBE上解放出来,看看球面上的地物是如何渲染的。本篇也是先开一个头,讲一下涉及到的类结构和整体的流程,有一个系统的,概括的理解。
我们先看看Cesium的渲染队列:
var Pass = {
// 环境,比如大气层,月亮,天空盒等
ENVIRONMENT : 0,
//之前介绍的ComputeEngine,比如影像服务里面的投影涉及的计算
COMPUTE : 1,
// 地球切片
GLOBE : 2,
// 贴地的Geometry
GROUND : 3,
// 不透明的Geometry
OPAQUE : 4,
// (半)透明的Geometry
TRANSLUCENT : 5,
OVERLAY : 6,
NUMBER_OF_PASSES : 7
};
如上是Cesium的渲染队列,也是Cesium渲染的优先级,非常的清晰。就像画家作画一样,我们先渲染地球,这样就有了一个落脚地(同时也有了深度纹理和模版缓冲区等),然后再画贴地的地物,然后是那些不贴地的且不透明的地物,接着是哪些透明度的地物,这时之前渲染地物的颜色和深度缓冲区,可以方便的进行深度计算和颜色的叠加,最上面的则是Overlay,比如一些公告板。这样一层层的渲染,建立起整个场景的层次感和立体感。
Geometry的难点在于种类繁多,材质类型也不少,技术上如何高效的渲染,根据是否贴地,是否透明等属性来设定渲染的优先级,根据材质和Geometry的类型来打组和实例化。同时Geometry的Type也是多样的,如何调度,并且将参数化的Geometry高效的分解为VBO三角面片,里面的水确实很深。万事开头难,我们先从简单的说起。
var entities = viewer.entities; entities.add({
rectangle : {
coordinates : Cesium.Rectangle.fromDegrees(-92.0, 20.0, -86.0, 27.0),
outline : true,
outlineColor : Cesium.Color.WHITE,
outlineWidth : 4,
stRotation : Cesium.Math.toRadians(45),
}
});
这是Cesium提供的添加Geometry的示例代码。So easy,不是吗。Viewer中提供了entities,是一个EntityCollection类,用户创建的不同类型的Geometry,都可以通过add方法添加,其他的就交给Cesium,最终实现屏幕渲染。顺藤摸瓜,我们先列举出相关的类,以及它们之间关系草图:
Viewer
DataSourceDisplay
DataSourceCollection
CustomDataSource
EntityCollection
Entity
先不关注具体的细节,不难理解,当我们调用EntityCollection.add方法时,根据参数会new一个Entity对象,此时EntityCollection充当了一个容器的作用。接着,Cesium内部通过事件的机制,在DataSourceDisplay中根据Entity类型安排具体模块,最终该模块完成对应Entity的解析工作。这里你就会发现,在这个通讯过程中,EntityCollection和DataSourceDisplay是直接进行消息传递的,直接绕开了CustomDataSource,那DataSource模块又有什么鸟用,岂不是站着坑不干活。下面通过一个例子让大家有一个形象了解。
首先,作为一名热爱编码的程序员而言,是否曾经仰天长叹,为什么那个谁谁谁天天不干活,工资还比我高,我不明白。想想自家公司那些占坑不干活的人都是什么角色——管理人员。CustomDataSource并没有参与实际的工作中,当状态变化时,维护一下EntityCollection和DataSourceDisplay两者的关联,保证他们之间消息的畅通。比如初始化时,CustomDataSource把EntityCollection带到DataSourceDisplay处,抛下一句话,以后有什么事情就找他,多多联系,然后立马就闪人了。这时,一脸懵逼的EntityCollection说,我得赶紧接客户去了,要不留个联系方式,有什么事情我好通知你。DataSourceDisplay也是管理层,自然不肯交出手机号,立马找到助理(Visualizer),抛下一句话“以后你俩直接联系”。这样,EntityCollection和Visualizer的通讯渠道就建立起来了。Visualizer我们后面会介绍,这里略过。
DataSourceCollection的作用也大致如此,但不同于CustomDataSource主要是接散户,消极怠慢。DataSourceCollection主要针对大客户或集团客户,比如GeoJson,KML,CZML等,它就不敢怠慢了,根据每一种情况做好功课,熟悉每一个集团的特点和特殊需要,提供专业高效的服务。通过上面的例子我们大概明白为什么要增加DataSource这一层,来方面管理,通过分层来优化,消化异常情况,尽量保证EntityCollection和DataSourceDisplay之间的交流是标准化,才能保证它们的通畅。针对这一方面,在后面的章节中在详细讨论,我们还是把注意力放在在整个流程上。
如上主要是介绍了人事关系,现在我们再来看看业务流程。大家都有去银行办理业务的经历,这里DataSourceDisplay相当于这个银行大厅的大厅经理,主要维持好秩序,而真正工作的则是大厅经理的手下——营业员,也就是我们刚刚提到的Visualizer。每天早上开工前(初始化阶段),大厅经理(DataSourceDisplay)把今天上班的营业员(Visualizer)都叫到一起,指着客户经理(EntityCollection)说,这位就是客户经理,所有的客户都是他来负责,你们认识一下,一定要和客户经理好好配合,服务好每一位客户啊。代码如下,这里又出来了一个Updater模块,实际上内部是Updater处理每一个客户的具体业务(将Entity转化为最终的Primitive),我们不妨认为Updater就是每一个营业员手中的电脑吧:
DataSourceDisplay.defaultVisualizersCallback = function(scene, entityCluster, dataSource) {
var entities = dataSource.entities;
return [new BillboardVisualizer(entityCluster, entities),
new GeometryVisualizer(BoxGeometryUpdater, scene, entities),
new GeometryVisualizer(CylinderGeometryUpdater, scene, entities),
new GeometryVisualizer(CorridorGeometryUpdater, scene, entities),
new GeometryVisualizer(EllipseGeometryUpdater, scene, entities),
new GeometryVisualizer(EllipsoidGeometryUpdater, scene, entities),
new GeometryVisualizer(PolygonGeometryUpdater, scene, entities),
new GeometryVisualizer(PolylineGeometryUpdater, scene, entities),
new GeometryVisualizer(PolylineVolumeGeometryUpdater, scene, entities),
new GeometryVisualizer(RectangleGeometryUpdater, scene, entities),
new GeometryVisualizer(WallGeometryUpdater, scene, entities),
new LabelVisualizer(entityCluster, entities),
new ModelVisualizer(scene, entities),
new PointVisualizer(entityCluster, entities),
new PathVisualizer(scene, entities)];
};
这里,客户经理(entities)在Visualizer的构造函数阶段耍了一个小花招,对每一个营业人员前面贴了一个标签,指明每一个营业员的业务范畴,跟每个营业员交代了一番,白纸黑字写的清清楚楚,到时候我把他们的材料审一边,然后让他们直接找你了。做管理也是要有两把刷子的,起码要知人善用,人尽其才。这时他走到其中一个营业员GeometryVisualizer身边仔细嘱咐了具体工作,我们来看看GeometryVisualizer的构造函数,看看人家的套路:
function GeometryVisualizer(type, scene, entityCollection) {
// Key 1
this._type = type; // Key 2
for (var i = 0; i < numberOfShadowModes; ++i) {
this._outlineBatches[i] = new StaticOutlineGeometryBatch(primitives, scene, i);
this._closedColorBatches[i] = new StaticGeometryColorBatch(primitives, type.perInstanceColorAppearanceType, true, i);
this._closedMaterialBatches[i] = new StaticGeometryPerMaterialBatch(primitives, type.materialAppearanceType, true, i);
this._openColorBatches[i] = new StaticGeometryColorBatch(primitives, type.perInstanceColorAppearanceType, false, i);
this._openMaterialBatches[i] = new StaticGeometryPerMaterialBatch(primitives, type.materialAppearanceType, false, i);
} // Key 3
entityCollection.collectionChanged.addEventListener(GeometryVisualizer.prototype._onCollectionChanged, this)
}
假设给他的安排(参数)如下:new GeometryVisualizer(RectangleGeometryUpdater, scene, entities)。 我们省去多余的客套话,直接看里面的三个要点:
- Key 1
你今天就专门负责Rectangle身份的客户 - Key 2
Rectangle属于几何对象,这类几何身份的客户,可能会有轮廓线,你得把他们的轮廓线信息放到_outlineBatches这一栏,它们有可能是闭合的,或者非闭合状态,也需要分别把他们的颜色和材质都筛选到对应栏 - Key 3
好了,我就简单的说道这里,一会Rectangle类的客户来了,我让它直接找你。浅层意思就是没事别找我,有事自己搞
细数下来,一共有六类客户,分别是:
- BillboardVisualizer
- GeometryVisualizer
- LabelVisualizer
- ModelVisualizer
- PointVisualizer
- PathVisualizer
其中,GeometryVisualizer是最常见的,就是我们最常见的屌丝,因为屌丝众多,屌丝和屌丝之间也是各有千秋,我们根据它们的外形(Geometry)分成了十类:
- BoxGeometry
- CylinderGeometry
- CorridorGeometry
- EllipseGeometry
- EllipsoidGeometry
- PolygonGeometry
- PolylineGeometry
- PolylineVolumeGeometry
- RectangleGeometry
- WallGeometryUpdater
EntityCollection分别就这六大类详细说明,有针对Geometry这类的十种情况额外强调一番。就到了营业时间。各就各位,准备接客。
EntityCollection.prototype.add
我们再次回到本文开头的代码片段:
entities.add({
rectangle : {
coordinates : Cesium.Rectangle.fromDegrees(-92.0, 20.0, -86.0, 27.0),
outline : true,
outlineColor : Cesium.Color.WHITE,
outlineWidth : 4,
stRotation : Cesium.Math.toRadians(45),
}
});
经常上面的介绍,我们比较清楚了DataSourceDisplay->DataSource->EntityCollection->Entity之间的层级关联,自然,专注的重点转到了业务流程。该客户类型为rectangle,有位置,有外边框,且宽度为4,颜色是白色,沿中心旋转45度。
EntityCollection.prototype.add = function(entity) {
if (!(entity instanceof Entity)) {
entity = new Entity(entity);
} var id = entity.id;
if (!this._removedEntities.remove(id)) {
this._addedEntities.set(id, entity);
} fireChangedEvent(this);
return entity;
};
EntityCollection看到这位客户,熟练的做了三个事情:1把它手里的材料整理好;2给它一张排队的小票;3通知对应的Visualizer接客。我们把这三个过程一一道来。
1.new Entity
function Entity(options) {
// 获取一个唯一id
// 可以指定,也可以系统自动创建一个
var id = options.id;
if (!defined(id)) {
id = createGuid();
} // 解析参数,把对应的属性赋给该entity
this.merge(options);
} Entity.prototype.merge = function(source) {
// 获取属性名,根据代码输入的参数,可知
// 调用的是Object.keys(source)
// 返回值是一个数组:0: "rectangle"
var sourcePropertyNames = defined(source._propertyNames) ? source._propertyNames : Object.keys(source);
var propertyNamesLength = sourcePropertyNames.length;
for (var i = 0; i < propertyNamesLength; i++) {
var name = sourcePropertyNames[i];
// 获取参数中"rectangle"对应的属性
var sourceProperty = source[name]; if (defined(sourceProperty)) {
// 将sourceProperty赋予该entity对应的rectangle属性
this[name] = sourceProperty;
}
}
};
这段代码看上去平淡无奇,很自然的通过用户输入的Object封装成一个新的Entity。实际上,这得益于Cesium强大的属性封装。在Cesium中,简单的属性可以通过defineProperties的形式,内部是采用Object.defineProperties的方式来提供set和get方法,但Entity中,不同的Geometry对应的属性并不完全相同,比如矩形是有左上角和右下角两个点就可以明确其position属性,而圆则需要圆心+半径,风格上也有一定差异;同时还有很多相同的属性,比如颜色等;另外一个特点是Entity对应的类型是可枚举的,所以Cesium在提供了createPropertyDescriptor类进行了封装,提高重用度。
defineProperties(Entity.prototype, {
rectangle : createPropertyTypeDescriptor('rectangle', RectangleGraphics)
};
如上通过createPropertyTypeDescriptor实现了Entity中rectangle属性,正因为如此,才保证我们能够做到this[“rectangle”] = sourceProperty。另外,这里出现了一个新类RectangleGraphics,实际上是它针对rectangle的json参数进行的封装,此时Entity对应的rectangle属性,已经有初始化时的createPropertyDescriptor返回的function变成了该RectangleGraphics。此时的RectangleGraphics内部保存的是参数化的数据内容,并不参与几何数据处理的过程。稍后会提到。
2.EntityCollection._addedEntities
当Entity创建成功后,就好比客户经理检查完你的材料,确认无误后,给你一张小条,上面写着你的id,然后就是排队了:
EntityCollection.prototype.add = function(entity) {
if (!this._removedEntities.remove(id)) {
this._addedEntities.set(id, entity);
}
return entity;
};
这里想说的是,EntityCollection总共有三个更新队列:_addedEntities,_removedEntities,_changedEntities。由此可以得出,Entity的调度和Globe在设计上也是如出一辙,都是基于状态的。
3.fireChangedEvent
当把该Entity放入添加队列后,EntityCollection则通过ChangedEvent事件,通知所有绑定该事件的Visualizer,有新的Entity进来了。还记得Visualizer初始化时绑定的_onCollectionChanged事件:
GeometryVisualizer.prototype._onCollectionChanged = function(entityCollection, added, removed) {
var addedObjects = this._addedObjects;
var removedObjects = this._removedObjects;
var changedObjects = this._changedObjects; var i;
var id;
var entity; for (i = added.length - 1; i > -1; i--) {
entity = added[i];
id = entity.id;
if (removedObjects.remove(id)) {
changedObjects.set(id, entity);
} else {
addedObjects.set(id, entity);
}
}
};
针对EntityCollection的每一种队列,Visualizer分别提供了_addedObjects,_removedObjects,_changedObjects三个队列一一对接,负责的营业员一个不落的把客户从客户经理手中转交到自己的柜台前。这里,个人觉得Cesium还是有一个优化的空间,当然价值大不大就不清楚了。因为每一个Visualizer都会收到消息,说有一个新来的Entity,需要接待一下。这时,大家并不知道该Entity的类型,所以无法判断具体应该是哪一个Visualizer去接待。所以大家每个人都把该entity加到自己的添加队列了,这个在如上的代码可以看到。,最终是每一个Visualizer对应的Updater来解析该Entity,看看是不是自己的菜。其实我觉得,_onCollectionChanged事件中,可以很简单的通过Type来判断是否是当前Updater是否可解析该Entity,就好比看看菜单就能知道是否是自己的菜,没必要非要自己吃一口。
如上,我们在类的层次关系以及事件驱动两个维度介绍了AddEntity涉及到的具体内容。下篇介绍如何处理这些数据,最终通过DrawCommand使其可渲染。