之前的最長的一幀系列,我們主要集中在地形和影像服務方面。簡單說,之前我們都集中在地球是怎么造出來的,從這一系列開始,我們的目光從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使其可渲染。