CesiumJS 2022^ 原理[3] 渲染原理之從 Entity 看 DataSource 架構 - 生成 Primitive 的過程

四季留歌發表於2022-04-17


API 用法回顧

只需傳入引數物件,就可以簡單地建立三維幾何體或者三維模型。

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 APIEntity API 這套組合實現了複雜、動態空間地理資料格式的接入。

1.1. 高層資料模型的封裝 - DataSource API

這個 API 其實是 Entity API 的基礎設施,在原始碼資料夾下就有一個 DataSources/ 資料夾專門收納 Entity APIDataSource API 的原始碼,可見重要程度之高。

首先,分別看定義在 Viewer 原型鏈上的兩個屬性 entitiesdataSourceDisplay

Object.defineProperties(Viewer.prototype, {
  // ...
  dataSourceDisplay: {
    get: function () {
      return this._dataSourceDisplay;
    },
  },
  entities: {
    get: function () {
      return this._dataSourceDisplay.defaultDataSource.entities;
    },
  },
  // ...
}

從上面兩個 getter 看,EntityCollection 似乎是被 DataSourceDisplay 物件的 defaultDataSource 管轄的;defaultDataSourceCustomDataSource 型別的。

Viewer 擁有一個 DataSourceDisplay 成員,它負責所有 DataSource 的更新。接下來先介紹這個“顯示管理器”類。

1.2. 顯示管理器 DataSourceDisplay 與預設資料來源 CustomDataSource

它隨 Viewer 建立而建立,而且優先順序相當高,僅次於 CesiumWidget;它自己則建立預設的 DataSource,也就是 CustomDataSource

// DataSourceDisplay.js
function DataSourceDisplay(options) {
  // ...
  const defaultDataSource = new CustomDataSource();
  this._onDataSourceAdded(undefined, defaultDataSource);
  this._defaultDataSource = defaultDataSource;
  // ...
}

在這個 CustomDataSource 的建構函式裡,就能找到 Viewer 暴露出去的 EntityCollection

// 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,其它的都會裝入 DataSourceDisplayDataSourceCollection 容器下,譬如 GeoJsonDataSourceCzmlDataSource 等,在文件中搜 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 成員:

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 建立完成後,就把它新增到 ScenePrimitiveCollection 中了,伴隨著 CesiumWidget 排程的渲染迴圈進行幀渲染。

而這個私有的 PrimitiveCollection 通過層層傳遞,會傳遞到最終負責建立 Primitive 的方法中(負責 Entity 當前時刻的 Primitive 的 API 在最後一小節會提及,別急)

PrimitiveCollection 支援巢狀新增,也就是 Collection 可以新增到 Collection 中,update 時也會樹狀逐級向下更新。

2. 負責 DataSource API 視覺化的一線員工 - Visualizer

2.1. 為 CustomDataSource 建立 Visualizer

注意到 DataSourceDisplay 建立 defaultDataSource 時,它會主動呼叫 _onDataSourceAdded 方法:

// function DataSourceDisplay() 中
const defaultDataSource = new CustomDataSource();
this._onDataSourceAdded(undefined, defaultDataSource);
this._defaultDataSource = defaultDataSource;

這個方法會給 defaultDataSource 再建立一個私有的 PrimitiveCollection,塞入 DataSourceDisplayPrimitiveCollection 中(好傢伙,套娃是吧);但是這不是重點,重點是在 _onDataSourceAdded 方法中會緊接著呼叫 _visualizersCallback 方法建立 視覺化器(Visualizer)

// DataSourceDisplay.prototype._onDataSourceAdded 中
dataSource._visualizers = this._visualizersCallback(
  scene,
  entityCluster,
  dataSource
);

_visualizersCallback 方法是 DataSourceDisplay 的一個私有原型鏈上的方法,可以在建立時自定義。簡單起見,就當預設情況討論吧,預設情況用的是 DataSourceDisplay 類的靜態方法:

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 的視覺化。

幾何型別有幾個比較特殊的,被單獨拎出來作為視覺化器,就是 PointVisualizerPathVisualizerPolylineVisualizer;其它的都被收入到 GeometryVisualizer 去了。

我就以 GeometryVisualizer 為例,解釋視覺化器究竟是如何轉換 EntityPrimitive 的。

2.2. EntityCollection 與 Visualizer 之間的通訊 - 事件機制

實際上,CustomDataSource 只是“擁有”EntityCollection,它讓它管轄的 EntityCollectionDataSourceDisplay 這個管家中合理地作為一個資料來源存在,並不負責監控 Entity 的變化(增刪改)。

真正監聽 Entity 變化的是通過 EntityCollection 的事件機制完成的,EntityCollection 無論發生什麼變化,都會傳遞給 Visualizer,圖解如下:

DataSourceDisplay
┖ CustomDataSource
  ┠ EntityCollection
  ┃      ↑
  ┃  事件機制監聽變化
  ┃      |
  ┖ [Visualizers]

接下來看看程式碼中的實現。EntityCollection 原型鏈上的 add/removeById/removeAll 方法會執行一個模組內的函式 fireChangedEvent(),它最核心的作用,就是把增加、刪除、修改的 Entity 通過事件觸發通知給 Visualizer:

// 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/changedEntity 增刪改時的臨時儲存容器,每次執行 fireChangedEvent 函式時都會把這三個容器清除。

在上面這段程式碼中,觸發事件的還是 EntityCollection 本身,fireChangedEvent 只是把變動的、最新那個 Entity 取出並通知註冊的回撥。

Visualizer 在建立的時候,就給 EntityCollection 註冊了事件:

// 在 GeometryVisualizer 的建構函式中
entityCollection.collectionChanged.addEventListener(
  GeometryVisualizer.prototype._onCollectionChanged,
  this
);

這就是說,每當 EntityCollection 有增刪改變化時,GeometryVisualizer_onCollectionChanged 就會收到變化的 Entity,並繼續執行後續動作。

Entity 的屬性修改是藉助 Property API 完成的,它新增到 EntityCollection 時(add 方法),容器就會為該 Entity 註冊屬性變動事件的回撥:

// EntityCollection.prototype.add 中
entity.definitionChanged.addEventListener(
  EntityCollection.prototype._onEntityDefinitionChanged,
  this
);

_onEntityDefinitionChanged 在 Entity 的 definitionChanged 事件觸發後執行,即也是執行 fireChangedEvent 函式。

3. 時鐘 - 如何讓 Viewer 參與 CesiumWidget 的渲染迴圈

在前兩篇文章中,詳細解析了 CesiumWidget 是如何排程 Scene 的幀渲染的。

CesiumWidget 擁有一個時鐘成員:

// CesiumWidget 建構函式中
this._clock = defined(options.clock) ? options.clock : new Clock();

預設的時鐘會在每一幀渲染排程函式中 跳動

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 事件:

Clock.prototype.tick = function () {
  // ...
  this.onTick.raiseEvent(this);
  // ...
}

也就是這個重要的時鐘,讓 Viewer 通過事件機制參與了 CesiumWidget 排程的渲染迴圈。

Viewer 在建構函式中,先建立了 CesiumWidget,隨後就為時鐘註冊了 onTick 的回撥函式:

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 的更新:

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 的更新方法:

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 檔案靠前的位置,你可以找到一個陣列:

const geometryUpdaters = [
  BoxGeometryUpdater,
  CylinderGeometryUpdater,
  CorridorGeometryUpdater,
  EllipseGeometryUpdater,
  EllipsoidGeometryUpdater,
  PlaneGeometryUpdater,
  PolygonGeometryUpdater,
  PolylineVolumeGeometryUpdater,
  RectangleGeometryUpdater,
  WallGeometryUpdater,
];

這些就是對應的幾何更新器。

你可以在這些幾何更新器類中找到 createXXXGeometryInstance 的原型鏈上的方法,例如 EllipsoidGeometryUpdater.prototype.createFillGeometryInstance 方法。

這些方法就是最後建立 Primitive 時所需的 GeometryInstance 的建立者,它們依賴於時間,返回該時間的靜態幾何值。

4.2. Updater 的集合 - GeometryUpdaterSet

回到 GeometryVisualizerupdate 方法,很容易發現那三個逆序迴圈在訪問 GeometryUpdaterSet 型別的容器,這個容器是 GeometryVisualizer.js 模組內的私有類。

只有在遍歷 _addedObjects 時才會建立 GeometryUpdaterSet,此時新來的 Entity 會傳給這個集合。這個集合的左右也比較簡單:

  • 為新來 Entity 建立所有的幾何更新器(這就是效能可能會出現問題的原因之一了)
  • 為所有的幾何更新器註冊 geometryChanged 事件的響應函式

這個幾何更新器集合建立完後,會儲存到 GeometryVisualizer 中,並與 Entityid 作繫結(方便其它兩個逆序迴圈查詢)。

4.3. 效能的提升 - Updater 的分批

之所以在 GeometryVisualizerupdate 方法中還不能建立 Primitive,儘管 CesiumJS 已經把建立靜態幾何值的行為封裝在 4.1 和 4.2 中提到的幾何更新器中了,是因為涉及一個效能問題:幾何並批。

WebGL 的特點就是,單幀內繪製的次數越少,就越流暢。GeometryVisualizer 如果不為這些接受來的 Entity 分類歸併批次,而是粗暴地把每個 Entity 直接生成靜態幾何、外觀資料就建立 Primitive 的話,有多少 Entity 就會有多少 Primitive,也就有多少 DrawCommand,效能可見會非常糟糕。

CesiumJS 在 GeometryVisualizer 中設計了一個分批的過程,也就是原型鏈上的 _insertUpdaterIntoBatch 方法。

GeometryVisualizer 更新時,三個列表迴圈中的兩個(新增列表和更改列表)都會呼叫 _insertUpdaterIntoBatch 方法,把由於新增或修改 Entity 而建立出來的新的 Updater 做分批。

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 的渲染了:

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 所需的 AppearanceGeometryInstance 仍然沒有建立,它將延續到本文的第 5 節中完成。

5. 批次容器完成資料合併 - Primitive 建立

在臨門一腳之前,我還是想介紹完批次容器。

5.1. 批次容器的型別與建立

CesiumJS 目前版本提供了若干種批次容器:

  • DynamicGeometryBatch:_dynamicBatch
  • StaticOutlineGeometryBatch:_outlineBatches
  • StaticGroundGeometryColorBatch:_groundColorBatches
  • StaticGroundGeometryPerMaterialBatch:_groundMaterialBatches
  • StaticGeometryColorBatch:_closedColorBatches、_openColorBatches
  • StaticGeometryPerMaterialBatch:_closedMaterialBatches、_openMaterialBatches

上面列出的,前者是型別,冒號後面的是 Visualizer 的成員欄位(也就是具體批次容器物件),從名稱不難看出它們的不同之處,大部分是用材質或顏色來作為分類依據。

上述批次容器可以在 DataSources/ 資料夾中找到對應的模組以及匯出的類。

你可以在 GeometryVisualizer 的建構函式中找到建立這些成員欄位的程式碼(其實建構函式裡大部分程式碼也是在建立批次容器)。它們最終會合併到 _batches 陣列中方便遍歷:

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

// 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 執行更新:

// 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 的建立:

// 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 上的 PrimitiveCollectionthis.primitives),則是由 CustomDataSource ~ GeometryVisualizer ~ StaticGeometryColorBatch 一路傳下來的,它早已在本文 1.4 小節中提及。

至此,Entity 終於穿過九曲十八彎,完成了靜態 Primitive 的建立,終於可以把事情交給 Scene 繼續做了,等待 Scene 在幀渲染流程中更新 PrimitiveCollection 進而建立出 DrawCommand,等待 WebGL 繪製。

最後,補個關係圖:

Viewer
┖ DataSourceDisplay
  ┖ CustomDataSource
    ┠ EntityCollection
    ┃      ↑
    ┃  事件機制監聽變化
    ┃      |
    ┖ GeometryVisualizer
      ┠ GeometryUpdaterSet
      ┃ ┖ [Updaters]
      ┃      ┃
      ┃    ┎─┸─ 建立→ Primitive
      ┃    ┃
      ┖ [Batches]

本篇小結

我本來是想寫 Entity API 的設計架構的,但是為了弄清楚這個比渲染迴圈複雜得多的架構(主要是事件回撥機制到處穿插,顯得複雜),我做了很多細碎的文章片段,最後收攏在一起的時候,才挖出 CesiumJS 中 DataSource 這套高層級的資料模型的架構設計。

雖然 Entity API 從引數化 JavaScript 物件到 Scene + Primitive API 這一層的路線比較長,但是易用性提高卻是事實。

Scene + Primitive API 作為基底,本身是比較高效率的,也留下了自定義的入口。Viewer + DataSource/Entity API 更進一步,使得 CesiumJS 更易於簡單業務的實現。

我覺得寫完幾何型別的 Entity 渲染架構,就算點到為止了(其它型別的 Entity 有專屬的 Visualizer,請讀者帶著幾何型別的 Entity 的思路類比),CesiumJS 中的三維物體渲染架構設計就算解讀完成。

渲染的細節、三維物體的建立行為、渲染排程優化仍然值得細細挖掘、學習,不過我認為都要基於渲染架構的基礎之上。

之後要寫的就是三維地球的骨架和皮膚了,就是旋轉橢球體和瓦片四叉樹設計架構。

相關文章