CesiumJS 2022^ 原理[3] 渲染原理之從 Entity 看 DataSource 架構 - 生成 Primitive 的過程
theme: arknights highlight: night-owl
API 用法回顧
只需傳入引數物件,就可以簡單地建立三維幾何體或者三維模型。
js
const modelEntity = viewer.entites.add({
id: 'some-entitiy',
name: 'some-name',
position: Cartesian3.fromDegrees(112.5, 22.3, 0),
model: {
uri: 'path/to/model.glb'
}
})
Entity API
通常會被拿來與 Primitive API
比較,無外乎:
- 前者使用 Property API 使得動態效果簡單化,後者需要自己編寫著色器;
- 個體數量較多時,前者的效能不如後者;
- 後者支援較底層的用法,可以自己控制材質著色器、幾何資料並批優化;
- ...
本篇感興趣的是 Entity API
是如何從引數化物件到 WebGL 渲染的。
首先,上結論:Entity 最終也會變成 Primitive。
從上面簡單的示例程式碼可以看出,使用 Entity API
的入口是 Viewer
,它不像 Primitive API
是從 Scene
訪問的。
這正是關於 Entity API
原始碼和設計架構的第一個知識,Entity API 必須依賴 Viewer 容器。
前提是隻用公開出來的 API
1. 為什麼要從 Viewer 訪問 Entity API
Viewer
其實是 CesiumJS 長期維護的一個成果,它在大多數時候扮演的是 Web3D GIS 地球的總入口物件。今天的主角是它暴露出來的 Entity API
,不過在介紹它之前,還要再提一提 Scene
暴露出來的 Primitive API
Scene
暴露出來的 Primitive API
是一種比較接近 WebGL 資料介面的 API,面對接近業務層的資料格式,譬如 GeoJSON、KML、GPX 等,Primitive API
就略顯吃力了。
雖然可以做一些轉換介面,不過 Cesium 團隊結合自己研發的資料標記語言 -- CZML,配上內建的時鐘,封裝出了更高級別的架構。
CesiumJS 使用 DataSource API
和 Entity API
這套組合實現了複雜、動態空間地理資料格式的接入。
1.1. 高層資料模型的封裝 - DataSource API
這個 API 其實是 Entity API
的基礎設施,在原始碼資料夾下就有一個 DataSources/
資料夾專門收納 Entity API
和 DataSource API
的原始碼,可見重要程度之高。
首先,分別看定義在 Viewer
原型鏈上的兩個屬性 entities
、dataSourceDisplay
:
js
Object.defineProperties(Viewer.prototype, {
// ...
dataSourceDisplay: {
get: function () {
return this._dataSourceDisplay;
},
},
entities: {
get: function () {
return this._dataSourceDisplay.defaultDataSource.entities;
},
},
// ...
}
從上面兩個 getter 看,EntityCollection
似乎是被 DataSourceDisplay
物件的 defaultDataSource
管轄的;defaultDataSource
是 CustomDataSource
型別的。
Viewer
擁有一個 DataSourceDisplay
成員,它負責所有 DataSource
的更新。接下來先介紹這個“顯示管理器”類。
1.2. 顯示管理器 DataSourceDisplay 與預設資料來源 CustomDataSource
它隨 Viewer
建立而建立,而且優先順序相當高,僅次於 CesiumWidget
;它自己則建立預設的 DataSource,也就是 CustomDataSource
:
js
// DataSourceDisplay.js
function DataSourceDisplay(options) {
// ...
const defaultDataSource = new CustomDataSource();
this._onDataSourceAdded(undefined, defaultDataSource);
this._defaultDataSource = defaultDataSource;
// ...
}
在這個 CustomDataSource
的建構函式裡,就能找到 Viewer
暴露出去的 EntityCollection
:
``` js // CustomDataSource.js function CustomDataSource(name) { // ... this._entityCollection = new EntityCollection(this); // ... }
Object.defineProperties(CustomDataSource.prototype, { // ... entities: { get: function () { return this._entityCollection; }, }, // ... } ```
所以,包含關係就說清楚了:
Viewer
┖ DataSourceDisplay
┖ CustomDataSource
┖ EntityCollection
DataSourceDisplay
除了管著CustomDataSource
這個服務於 Entity API 的預設資料來源外,還管著其它的 DataSource,其它的都會裝入DataSourceDisplay
的DataSourceCollection
容器下,譬如GeoJsonDataSource
、CzmlDataSource
等,在文件中搜 DataSource 關鍵字基本能找齊。
1.3. 預設的資料來源 - CustomDataSource
預設的資料來源的作用,就是給 Entity API
提供土壤。
但是不要輕易認為 CustomDataSource
只能給 Entity API
使用,在官方沙盒中可以找到直接使用 CustomDataSource
的例子的。本文
1.4. DataSource API 與 Scene 之間的橋樑
文章一開頭就說了,Entity
最終是會轉換成 Primitive
的。
目前為止,CesiumJS 有更新 Primitive
權力的物件,只有 Scene
上那個 PrimitiveCollection
才能更新 Primitive
,進而建立 DrawCommand
。
DataSource API
的管家是 DataSourceDisplay
物件,它擁有一個私有的 PrimitiveCollection
成員:
``` js function DataSourceDisplay(options) { // ... const scene = options.scene; const dataSourceCollection = options.dataSourceCollection; // ...
let primitivesAdded = false; const primitives = new PrimitiveCollection(); const groundPrimitives = new PrimitiveCollection();
if (dataSourceCollection.length > 0) { scene.primitives.add(primitives); scene.groundPrimitives.add(groundPrimitives); primitivesAdded = true; }
this._primitives = primitives; this._groundPrimitives = groundPrimitives;
// ...
if (!primitivesAdded) { // 對於 dataSourceCollection.length 是 0 的情況 // 使用事件機制把私有的 PrimitiveCollection 新增到 scene.primitives 中 } } ```
看得到,這個私有的 PrimitiveCollection
建立完成後,就把它新增到 Scene
的 PrimitiveCollection
中了,伴隨著 CesiumWidget
排程的渲染迴圈進行幀渲染。
而這個私有的 PrimitiveCollection
通過層層傳遞,會傳遞到最終負責建立 Primitive 的方法中(負責 Entity 當前時刻的 Primitive 的 API 在最後一小節會提及,別急)
PrimitiveCollection
支援巢狀新增,也就是 Collection 可以新增到 Collection 中,update 時也會樹狀逐級向下更新。
2. 負責 DataSource API 視覺化的一線員工 - Visualizer
2.1. 為 CustomDataSource 建立 Visualizer
注意到 DataSourceDisplay
建立 defaultDataSource 時,它會主動呼叫 _onDataSourceAdded
方法:
js
// function DataSourceDisplay() 中
const defaultDataSource = new CustomDataSource();
this._onDataSourceAdded(undefined, defaultDataSource);
this._defaultDataSource = defaultDataSource;
這個方法會給 defaultDataSource 再建立一個私有的 PrimitiveCollection
,塞入 DataSourceDisplay
的 PrimitiveCollection
中(好傢伙,套娃是吧);但是這不是重點,重點是在 _onDataSourceAdded
方法中會緊接著呼叫 _visualizersCallback
方法建立 視覺化器(Visualizer):
js
// DataSourceDisplay.prototype._onDataSourceAdded 中
dataSource._visualizers = this._visualizersCallback(
scene,
entityCluster,
dataSource
);
_visualizersCallback
方法是 DataSourceDisplay
的一個私有原型鏈上的方法,可以在建立時自定義。簡單起見,就當預設情況討論吧,預設情況用的是 DataSourceDisplay
類的靜態方法:
``` js function DataSourceDisplay(options) { // ... this._visualizersCallback = defaultValue( options.visualizersCallback, DataSourceDisplay.defaultVisualizersCallback ); // ... }
DataSourceDisplay.defaultVisualizersCallback = function ( scene, entityCluster, dataSource ) { const entities = dataSource.entities; return [ new BillboardVisualizer(entityCluster, entities), new GeometryVisualizer( scene, entities, dataSource._primitives, dataSource._groundPrimitives ), new LabelVisualizer(entityCluster, entities), new ModelVisualizer(scene, entities), new Cesium3DTilesetVisualizer(scene, entities), new PointVisualizer(entityCluster, entities), new PathVisualizer(scene, entities), new PolylineVisualizer( scene, entities, dataSource._primitives, dataSource._groundPrimitives ), ]; }; ```
靜態方法是 ES6 Class 的說法,CesiumJS 作為一套 ES5 時代的原始碼,大家意會即可。這個方法會返回一個數組,陣列內是一堆 Visualizer
物件。
每個 Visualizer 就負責一類 Entity 的具體視覺化工作,譬如 ModelVisualizer
負責 glTF 模型型別的 Entity
的視覺化工作,Cesium3DTilesetVisualizer
負責 3DTiles 資料集型別的 Entity
的視覺化。
幾何型別有幾個比較特殊的,被單獨拎出來作為視覺化器,就是 PointVisualizer
、PathVisualizer
和 PolylineVisualizer
;其它的都被收入到 GeometryVisualizer
去了。
我就以 GeometryVisualizer
為例,解釋視覺化器究竟是如何轉換 Entity
成 Primitive
的。
2.2. EntityCollection 與 Visualizer 之間的通訊 - 事件機制
實際上,CustomDataSource
只是“擁有”EntityCollection
,它讓它管轄的 EntityCollection
在 DataSourceDisplay
這個管家中合理地作為一個數據源存在,並不負責監控 Entity
的變化(增刪改)。
真正監聽 Entity
變化的是通過 EntityCollection
的事件機制完成的,EntityCollection
無論發生什麼變化,都會傳遞給 Visualizer,圖解如下:
DataSourceDisplay
┖ CustomDataSource
┠ EntityCollection
┃ ↑
┃ 事件機制監聽變化
┃ |
┖ [Visualizers]
接下來看看程式碼中的實現。EntityCollection
原型鏈上的 add/removeById/removeAll
方法會執行一個模組內的函式 fireChangedEvent()
,它最核心的作用,就是把增加、刪除、修改的 Entity
通過事件觸發通知給 Visualizer:
``` js // function fireChangedEvent() 中 const addedArray = added.values.slice(0); const removedArray = removed.values.slice(0); const changedArray = changed.values.slice(0);
added.removeAll(); removed.removeAll(); changed.removeAll(); collection._collectionChanged.raiseEvent( collection, addedArray, removedArray, changedArray ); ```
其中,added/removed/changed
是 Entity
增刪改時的臨時儲存容器,每次執行 fireChangedEvent
函式時都會把這三個容器清除。
在上面這段程式碼中,觸發事件的還是 EntityCollection
本身,fireChangedEvent
只是把變動的、最新那個 Entity
取出並通知註冊的回撥。
Visualizer 在建立的時候,就給 EntityCollection
註冊了事件:
js
// 在 GeometryVisualizer 的建構函式中
entityCollection.collectionChanged.addEventListener(
GeometryVisualizer.prototype._onCollectionChanged,
this
);
這就是說,每當 EntityCollection
有增刪改變化時,GeometryVisualizer
的 _onCollectionChanged
就會收到變化的 Entity
,並繼續執行後續動作。
Entity
的屬性修改是藉助 Property API
完成的,它新增到 EntityCollection
時(add
方法),容器就會為該 Entity 註冊屬性變動事件的回撥:
js
// EntityCollection.prototype.add 中
entity.definitionChanged.addEventListener(
EntityCollection.prototype._onEntityDefinitionChanged,
this
);
_onEntityDefinitionChanged
在 Entity 的 definitionChanged
事件觸發後執行,即也是執行 fireChangedEvent
函式。
3. 時鐘 - 如何讓 Viewer 參與 CesiumWidget 的渲染迴圈
在前兩篇文章中,詳細解析了 CesiumWidget
是如何排程 Scene
的幀渲染的。
CesiumWidget
擁有一個時鐘成員:
js
// CesiumWidget 建構函式中
this._clock = defined(options.clock) ? options.clock : new Clock();
預設的時鐘會在每一幀渲染排程函式中 跳動:
js
CesiumWidget.prototype.render = function () {
if (this._canRender) {
this._scene.initializeFrame();
const currentTime = this._clock.tick();
this._scene.render(currentTime);
} else {
this._clock.tick();
}
};
無論是否渲染,都會呼叫 Clock.prototype.tick()
方法跳動一次時鐘,這個方法會觸發 onTick
事件:
js
Clock.prototype.tick = function () {
// ...
this.onTick.raiseEvent(this);
// ...
}
也就是這個重要的時鐘,讓 Viewer
通過事件機制參與了 CesiumWidget
排程的渲染迴圈。
Viewer
在建構函式中,先建立了 CesiumWidget
,隨後就為時鐘註冊了 onTick
的回撥函式:
``` js function Viewer(container, options) { // ... // eventHelper 是一個事件助手物件,此處為 clock 註冊事件用 eventHelper.add(clock.onTick, Viewer.prototype._onTick, this); // ... }
Viewer.prototype._onTick = function (clock) { const time = clock.currentTime;
const isUpdated = this._dataSourceDisplay.update(time); // ... } ```
在 _onTick
方法中,第一件做的事情就是執行 DataSourceDisplay
的更新:
``` js DataSourceDisplay.prototype.update = function (time) { // ... let result = true;
let visualizers; let vLength;
visualizers = this._defaultDataSource._visualizers; vLength = visualizers.length; for (x = 0; x < vLength; x++) { result = visualizers[x].update(time) && result; }
// ... } ```
這個更新方法其實就是 進一步更新 DataSourceDisplay
中所有的資料來源(無論是資料來源容器中的還是預設的 CustomDataSource
的)的 視覺化器(Visualizer),視覺化器在上一節已經介紹過它的建立和如何與 EntityCollection 繫結的了。
待介紹完各個層級的資料容器建立、事件的繫結後,終於可以把目光聚焦在渲染上了。
CesiumWidget
負責排程 Scene
的幀渲染,同時會跳動時鐘物件,時鐘物件的跳動又進而通知 Viewer
更新 DataSourceDisplay
下轄的所有 DataSource。
到這裡,各個資料來源物件的 Visualizer 才開始了建立 Primitive
之路。
4. Visualizer 的更新之路
4.1. 更新方法中的三個迴圈
仍以 GeometryVisualizer
為例。接續第 3 節的內容,Viewer
伴隨著時鐘物件的回撥,會一路更新資料來源物件的 Visualizer。
看看 GeometryVisualizer
的更新方法:
``` js GeometryVisualizer.prototype.update = function (time) { // ... const addedObjects = this._addedObjects; const added = addedObjects.values; const removedObjects = this._removedObjects; const removed = removedObjects.values; const changedObjects = this._changedObjects; const changed = changedObjects.values;
let i; let entity; let id; let updaterSet; const that = this;
for (i = changed.length - 1; i > -1; i--) { / ... / } for (i = removed.length - 1; i > -1; i--) { / ... / } for (i = added.length - 1; i > -1; i--) { / ... / }
addedObjects.removeAll(); removedObjects.removeAll(); changedObjects.removeAll();
// ...
}
```
更新方法會取三類 Entity
(_addedObjects/_removedObjects/_changedObjects
)進行逆序遍歷,這三個容器在 2.2 小節中會通過 EntityCollection
的事件機制傳遞給 Visualizer。
遍歷這些 Entity
是打算做什麼呢?Entity
這個時候仍然是引數物件,還不能直接拿去建立 Primitive
。在討論為什麼之前,先介紹兩個東西,見 4.1 和 4.2:
4.1. Visualizer 的資料轉換工具 - Updater
我們知道,Entity
使用 Property API
去修改實體的形狀、外觀,而這些動態值每一幀必須變成靜態值傳遞給 WebGL,Entity
中的幾何型別不少,CesiumJS 分別給這些幾何型別的動態轉靜態的過程做了封裝 —— 也就是叫做 Updater 的東西,來輔助幾何型別的 Entity 的幾何資料更新。
在 GeometryVisualizer.js
檔案靠前的位置,你可以找到一個數組:
js
const geometryUpdaters = [
BoxGeometryUpdater,
CylinderGeometryUpdater,
CorridorGeometryUpdater,
EllipseGeometryUpdater,
EllipsoidGeometryUpdater,
PlaneGeometryUpdater,
PolygonGeometryUpdater,
PolylineVolumeGeometryUpdater,
RectangleGeometryUpdater,
WallGeometryUpdater,
];
這些就是對應的幾何更新器。
你可以在這些幾何更新器類中找到 createXXXGeometryInstance
的原型鏈上的方法,例如 EllipsoidGeometryUpdater.prototype.createFillGeometryInstance
方法。
這些方法就是最後建立 Primitive
時所需的 GeometryInstance
的建立者,它們依賴於時間,返回該時間的靜態幾何值。
4.2. Updater 的集合 - GeometryUpdaterSet
回到 GeometryVisualizer
的 update
方法,很容易發現那三個逆序迴圈在訪問 GeometryUpdaterSet
型別的容器,這個容器是 GeometryVisualizer.js
模組內的私有類。
只有在遍歷 _addedObjects
時才會建立 GeometryUpdaterSet
,此時新來的 Entity
會傳給這個集合。這個集合的左右也比較簡單:
- 為新來
Entity
建立所有的幾何更新器(這就是效能可能會出現問題的原因之一了) - 為所有的幾何更新器註冊
geometryChanged
事件的響應函式
這個幾何更新器集合建立完後,會儲存到 GeometryVisualizer
中,並與 Entity
的 id
作繫結(方便其它兩個逆序迴圈查詢)。
4.3. 效能的提升 - Updater 的分批
之所以在 GeometryVisualizer
的 update
方法中還不能建立 Primitive
,儘管 CesiumJS 已經把建立靜態幾何值的行為封裝在 4.1 和 4.2 中提到的幾何更新器中了,是因為涉及一個性能問題:幾何並批。
WebGL 的特點就是,單幀內繪製的次數越少,就越流暢。GeometryVisualizer
如果不為這些接受來的 Entity 分類歸併批次,而是粗暴地把每個 Entity 直接生成靜態幾何、外觀資料就建立 Primitive 的話,有多少 Entity 就會有多少 Primitive,也就有多少 DrawCommand
,效能可見會非常糟糕。
CesiumJS 在 GeometryVisualizer
中設計了一個分批的過程,也就是原型鏈上的 _insertUpdaterIntoBatch
方法。
在 GeometryVisualizer
更新時,三個列表迴圈中的兩個(新增列表和更改列表)都會呼叫 _insertUpdaterIntoBatch
方法,把由於新增或修改 Entity
而創建出來的新的 Updater 做分批。
``` js GeometryVisualizer.prototype.update = function (time) { // ... for (i = changed.length - 1; i > -1; i--) { // ... that._insertUpdaterIntoBatch(time, updater); }
// ...
for (i = added.length - 1; i > -1; i--) { // ... that._insertUpdaterIntoBatch(time, updater); // ... }
// ... } ```
而在 _insertUpdaterIntoBatch
方法中,能看到非常多的分支判斷以及 add
操作,這就是將 Updater 根據不同的條件甩到 Visualizer 上不同的批次容器中的過程了。
關於批次容器,會在第 5 節講解。
4.4. Visualizer 更新的最後一步 - 批次容器更新
待 Visuailzer 更新方法的三個迴圈結束後,也就意味著完成了 Updater 的分批。
Updater 分批完成後,自然就是更新這些批次容器,進而創建出當前時刻的 Primitive
,讓他們等待 Scene
的渲染了:
``` js GeometryVisualizer.prototype.update = function (time) { // ...
let isUpdated = true; const batches = this._batches; const length = batches.length; for (i = 0; i < length; i++) { isUpdated = batches[i].update(time) && isUpdated; }
return isUpdated; } ```
直到這時,Primitive
所需的 Appearance
和 GeometryInstance
仍然沒有建立,它將延續到本文的第 5 節中完成。
5. 批次容器完成資料合併 - Primitive 建立
在臨門一腳之前,我還是想介紹完批次容器。
5.1. 批次容器的型別與建立
CesiumJS 目前版本提供了若干種批次容器:
DynamicGeometryBatch
:_dynamicBatchStaticOutlineGeometryBatch
:_outlineBatchesStaticGroundGeometryColorBatch
:_groundColorBatchesStaticGroundGeometryPerMaterialBatch
:_groundMaterialBatchesStaticGeometryColorBatch
:_closedColorBatches、_openColorBatchesStaticGeometryPerMaterialBatch
:_closedMaterialBatches、_openMaterialBatches
上面列出的,前者是型別,冒號後面的是 Visualizer 的成員欄位(也就是具體批次容器物件),從名稱不難看出它們的不同之處,大部分是用材質或顏色來作為分類依據。
上述批次容器可以在 DataSources/
資料夾中找到對應的模組以及匯出的類。
你可以在 GeometryVisualizer
的建構函式中找到建立這些成員欄位的程式碼(其實建構函式裡大部分程式碼也是在建立批次容器)。它們最終會合併到 _batches
陣列中方便遍歷:
js
this._batches = this._outlineBatches.concat(
this._closedColorBatches,
this._closedMaterialBatches,
this._openColorBatches,
this._openMaterialBatches,
this._groundColorBatches,
this._groundMaterialBatches,
this._dynamicBatch
);
5.2. 內部批次容器
沒想到吧?上面列舉的,名字上使用材質或顏色來區分的批次容器,還只是一個代理人。真正起儲存作用的,還得看這些批次容器模組檔案中內部的 Batch
類。
以最簡單的靜態批次容器 StaticGeometryColorBatch
為例,它在 Updater 通過 add
方法新增進來時,就會建立內部 Batch
,同時建立這個時刻的 GeometryInstance
:
``` js // StaticGeometryColorBatch.js
function Batch( primitives, translucent, appearanceType, depthFailAppearanceType, depthFailMaterialProperty, closed, shadows ) { // ... }
StaticGeometryColorBatch.prototype.add = function (time, updater) { // ... const instance = updater.createFillGeometryInstance(time); // ...
const batch = new Batch(/ ... /); batch.add(updater, instance); items.push(batch); } ```
這個內部 Batch
存放著外觀資訊和 GeometryInstance
物件。
5.3. 建立 Primitive
在 Visualizer 的更新方法中,最後就是對所有批次容器進行更新。仍以 StaticGeometryColorBatch
為例,它的更新方法會呼叫一個模組內的 updateItems
函式,這個函式對傳入的某部分內部 Batch
執行更新:
``` js // StaticGeometryColorBatch.js 中
function updateItems(batch, items, time, isUpdated) { // ... for (i = 0; i < length; ++i) { isUpdated = items[i].update(time) && isUpdated; } // ... }
StaticGeometryColorBatch.prototype.update = function (time) { // ... if (solidsMoved || translucentsMoved) { isUpdated = updateItems(this, this._solidItems, time, isUpdated) && isUpdated; isUpdated = updateItems(this, this._translucentItems, time, isUpdated) && isUpdated; } // ... } ```
StaticGeometryColorBatch
上的 _solidItems
和 _translucentItems
都是普通的陣列,儲存的是模組內部定義 Batch
型別的物件。
而這些內部 Batch
的更新函式,最終就會根據手上的資料,完成 Primitive
的建立:
``` js // StaticGeometryColorBatch.js 中
// ... 這個方法很長,節約篇幅 Batch.prototype.update = function (time) { let isUpdated = true; let removedCount = 0; let primitive = this.primitive; const primitives = this.primitives; let i;
if (this.createPrimitive) { const geometries = this.geometry.values; const geometriesLength = geometries.length; if (geometriesLength > 0) { // ... primitive = new Primitive({ / ... / }) primitives.add(primitive); } // else ... } // else ... } ```
而這個內建 Batch
上的 PrimitiveCollection
(this.primitives
),則是由 CustomDataSource
~ GeometryVisualizer
~ StaticGeometryColorBatch
一路傳下來的,它早已在本文 1.4 小節中提及。
至此,Entity
終於穿過九曲十八彎,完成了靜態 Primitive
的建立,終於可以把事情交給 Scene
繼續做了,等待 Scene
在幀渲染流程中更新 PrimitiveCollection
進而創建出 DrawCommand
,等待 WebGL 繪製。
最後,補個關係圖:
Viewer
┖ DataSourceDisplay
┖ CustomDataSource
┠ EntityCollection
┃ ↑
┃ 事件機制監聽變化
┃ |
┖ GeometryVisualizer
┠ GeometryUpdaterSet
┃ ┖ [Updaters]
┃ ┃
┃ ┎─┸─ 建立→ Primitive
┃ ┃
┖ [Batches]s
本篇小結
我本來是想寫 Entity API
的設計架構的,但是為了弄清楚這個比渲染迴圈複雜得多的架構(主要是事件回撥機制到處穿插,顯得複雜),我做了很多細碎的文章片段,最後收攏在一起的時候,才挖出 CesiumJS 中 DataSource
這套高層級的資料模型的架構設計。
雖然 Entity API
從引數化 JavaScript 物件到 Scene + Primitive API
這一層的路線比較長,但是易用性提高卻是事實。
Scene + Primitive API
作為基底,本身是比較高效率的,也留下了自定義的入口。Viewer + DataSource/Entity API
更進一步,使得 CesiumJS 更易於簡單業務的實現。
我覺得寫完幾何型別的 Entity
渲染架構,就算點到為止了(其它型別的 Entity
有專屬的 Visualizer,請讀者帶著幾何型別的 Entity
的思路類比),CesiumJS 中的三維物體渲染架構設計就算解讀完成。
渲染的細節、三維物體的建立行為、渲染排程優化仍然值得細細挖掘、學習,不過我認為都要基於渲染架構的基礎之上。
之後要寫的就是三維地球的骨架和面板了,就是旋轉橢球體和瓦片四叉樹設計架構。