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


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 中的三維物體渲染架構設計就算解讀完成。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章