CesiumJS 2022^ 原理[2] 渲染架構之三維物體 - 創建並執行指令


回顧

書接上文,Scene.js 模塊內的 render 函數會將控制權交給 WebGL,執行 CesiumJS 自己封裝的指令對象,畫出每一幀來。

模塊內的 render 函數首先會更新一批狀態信息,譬如幀狀態、霧效、Uniform 值、通道狀態、三維場景中的環境信息等,然後就開始更新並執行指令,調用的是 Scene 原型鏈上的 updateAndExecuteCommands 方法。

整個過程大致的調用鏈是這樣的(function 關鍵字簡寫爲 fn):

[Module Scene.js]
- fn render()
  - Scene.prototype.updateAndExecuteCommands()
    - fn executeCommandsInViewport()
      - fn updateAndRenderPrimitives()
          [Module Primitive.js]
          - fn createCommands()
          - fn updateAndQueueCommands()
      - fn executeCommands()
     	- fn executeCommand()

本篇講解的是從 Scene 原型鏈上的 updateAndExcuteCommands() 方法開始,期間 Scene 中的 Primitives 是如何創建指令,又最終如何被 WebGL 執行的。

這個過程涉及非常多細節代碼,但是爲了快速聚焦整個過程,本篇先介紹兩個 CesiumJS 封裝的概念:指令和通道。

預備知識:指令

WebGL 是一種依賴於“全局狀態”的繪圖 API,面向對象特徵比較弱,爲了修改全局狀態上的頂點數據、着色器程序、幀緩衝、紋理等“資源”,必須通過 gl.XXX 函數調用觸發全局狀態的改變。

而圖形編程基礎提出的渲染管線、通道等概念偏向於面向對象,顯然 WebGL 這種偏過程的風格需要被 JavaScript 運行時引擎封裝。

CesiumJS 將 WebGL 的繪製過程,也就是行爲,封裝成了“指令”,不同的指令對象有不同的用途。指令對象保存的行爲,具體就是指由 Primitive 對象(不一定全是 Primitive)生成的 WebGL 所需的數據資源(緩衝、紋理、唯一值等),以及着色器對象。數據資源和着色器對象仍然是 CesiumJS 封裝的對象,而不是 WebGL 原生的對象,這是爲了更好地與 CesiumJS 各種對象結合去繪圖。

CesiumJS 有三類指令:

  • DrawCommand 繪圖指令
  • ClearCommand 清屏指令
  • ComputeCommand 計算指令

繪圖指令最終會把控制權交給 Context 對象,根據自身的着色器對象,繪製自己身上的數據資源。

清屏指令比較簡單,目的就是調用 WebGL 的清屏函數,清空各類緩衝區並填充清空後的顏色值,依舊會把控制權傳遞給 Context 對象。

計算指令則藉助 WebGL 並行計算的特點,將指令對象上的數據在着色器中編碼、計算、解碼,最後寫入到輸出緩衝(通常是幀緩衝的紋理上),同樣控制權會給到 Context 對象。

預備知識:通道

一幀是由多個通道順序繪製構成的,在 CesiumJS 中,通道英文單詞是 Pass

既然通道的繪製是有順序的,其順序保存在 Renderer/Pass.js 模塊導出的凍結對象中,目前(1.92版本)有 10 個優先順序等級:

const Pass = {
  ENVIRONMENT: 0,
  COMPUTE: 1,
  GLOBE: 2,
  TERRAIN_CLASSIFICATION: 3,
  CESIUM_3D_TILE: 4,
  CESIUM_3D_TILE_CLASSIFICATION: 5,
  CESIUM_3D_TILE_CLASSIFICATION_IGNORE_SHOW: 6,
  OPAQUE: 7,
  TRANSLUCENT: 8,
  OVERLAY: 9,
  NUMBER_OF_PASSES: 10,
}

以上爲例,第一優先被繪製的指令,是環境(ENVIRONMENT: 0)方面的對象、物體。而不透明(OPAQUE: 7)的三維對象的繪製指令,則要先於透明(TRANSLUCENT: 8)物體被執行。

CesiumJS 會在每一幀即將開始繪製前,對所有已經收集好的指令根據通道進行排序,實現順序繪製,下文會細談。

1. 生成並執行指令

原型鏈上的 updateAndExecuteCommands 方法會做模式判斷,我們一般使用的是三維模式(SceneMode.SCENE3D),所以只需要看 else if 分支即可,也就是

executeCommandsInViewport(true, this, passState, backgroundColor);

此處,this 就是 Scene 自己。

executeCommandsInViewport() 是一個 Scene.js 模塊內的函數,這個函數比較短,對於當前我們關心的東西,只需要看它調用的 updateAndRenderPrimitives() 和最後的 executeCommands() 函數調用即可。

1.1. Primitive 生成指令

[Module Scene.js]
- fn updateAndRenderPrimitives()
  [Module Primitive.js]
  - fn createCommands()
  - fn updateAndQueueCommands()

Scene.js 模塊內的函數 updateAndRenderPrimitives() 負責更新 Scene 上所有的 Primitive。

期間,控制權會通過 PrimitiveCollection 轉移到 Primitive 類(或者有類似結構的類,譬如 Cesium3DTileset 等)上,令其更新本身的數據資源,根據情況創建新的着色器,並隨之創建 繪圖指令,最終在 Primitive.js 模塊內的 updateAndQueueCommands() 函數排序、並推入幀狀態對象的指令列表上。

1.2. Context 對象負責執行 WebGL 底層代碼

[Module Scene.js]
- fn executeCommands()
- fn executeCommand() // 收到 Command 和 Context
  [Module Context.js]
  - Context.prototype.draw()

另一個模塊內的函數 executeCommands() 則負責執行這些指令(中間還有一些小插曲,下文再提)。

此時,上文的“通道”再次起作用,此函數內會根據 Pass 的優先順序依次更新唯一值狀態(UniformState),然後下發給 executeCommand() 函數(注意少了個s)以具體的某個指令對象以及 Context 對象。

除了 executeCommand() 函數外,Scene.js 模塊內仍還有其它類似的函數,例如 executeIdCommand() 負責執行繪製 ID 信息紋理的指令,executeTranslucentCommandsBackToFront() / executeTranslucentCommandsFrontToBack() 函數負責透明物體的指令等。甚至在 Scene 對象自己的屬性中,就有清屏指令字段,會在 executeCommands() 函數中直接執行,不經過上述幾個執行具體指令的函數。

Context 對象是對 WebGL(2)RenderingContext 等 WebGL 原生接口、參數的封裝,所有指令對象最終都會由其進行拆包裝、組裝 WebGL 函數調用並執行繪圖、計算、清屏(見上文介紹的預備知識:指令)。

2. 多段視錐體技術

先介紹一個概念,WebGL 中的深度。

簡單的說,屏幕朝裏,三維物體的前後順序就是深度。CesiumJS 的深度非常大,即使不考慮遠太空,只考慮地球表面附近的範圍,WebGL 的數值範圍也不太夠用。聰明的前輩們想到了使用對數函數壓縮深度的值域,因爲一個非常大的數字只需取其對數,很快就能小下來。

Scene 對象上有一個可讀可寫訪問器 logarithmicDepthBuffer,它指示是否啓用對數深度。

現在,CesiumJS 通常使用的就是對數深度。

對數深度解決的不僅僅只是普通深度值值域不夠的問題,還解決了不支持對數深度技術之前使用的多段視錐技術問題。

再次簡單的說,多段視錐技術將視錐體由遠及近切成多個段,保證了相機近段的指令足夠多以保證效果,遠段儘量少滿足性能。

你在 Scene.js 模塊中的 executeCommands() 函數的最後能找到一個循環體:

// Execute commands in each frustum in back to front order
let j;
for (let i = 0; i < numFrustums; ++i) {
  // ...
}

打開調試工具,很容易擊中這個斷點,查看 numFrustums 變量的值,有啓用對數深度的 CesiumJS 程序,一般 numFrustums 都是 1。

3. 指令對象的轉移

在本文第 1 節中,詳細說明了指令對象的生成與被執行。

上述其實忽略了很多細節,現在撿起來說。

指令對象在 Primitive(或類似的類)生成後,由 模塊內的 updateAndQueueCommands() 函數排序並推入幀狀態對象的指令列表上:

// updateAndQueueCommands 函數中,函數接收來自 Scene 逐級傳入的幀狀態對象 -- frameState
const commandList = frameState.commandList;
const passes = frameState.passes;
if (passes.render || passes.pick) {
  // ... 省略部分代碼
  commandList.push(colorCommand);
}

frameState.commandList 就是個普通的數組。

而在執行時,卻是從 View 對象上的 frustumCommandsList 上取的指令:

// Scene.js 模塊的 executeCommands 函數中

const frustumCommandsList = view.frustumCommandsList;
const numFrustums = frustumCommandsList.length;

let j;
for (let i = 0; i < numFrustums; ++i) {
  const index = numFrustums - i - 1;
  const frustumCommands = frustumCommandsList[index];
  
  // ...
  
  // 截取不透明物體的通道指令執行代碼片段
  us.updatePass(Pass.OPAQUE);
  commands = frustumCommands.commands[Pass.OPAQUE];
  length = frustumCommands.indices[Pass.OPAQUE];
  for (j = 0; j < length; ++j) {
    executeCommand(commands[j], scene, context, passState);
  }
  
  // ...
}

其中,下發給 executeCommand() 函數的 commands[j] 參數,就是具體的某個指令對象。

所以這兩個過程之間,是怎麼做指令對象傳遞的?

答案就在 View 原型鏈上的 createPotentiallyVisibleSet 方法中。

篩選可見集

View 對象是 Scene、Camera 之間的紐帶,負責“眼睛”與“世界”之間信息的處理,即視圖。

View 原型鏈上的 createPotentiallyVisibleSet 方法的作用,就是把 Scene 上的計算指令、覆蓋物指令,幀狀態上的指令列表,根據 View 的可見範圍做一次篩選,減少要執行指令個數提升性能。

具體來說,就是把分散在各處的指令,篩選後綁至 View 對象的 frustumCommandsList 列表中,藉助 View.js 模塊內的 insertIntoBin() 函數完成:

// View.js 模塊內

function insertIntoBin(view, scene, command, commandNear, commandFar) {
  // ...
  
  const frustumCommandsList = view.frustumCommandsList;
  const length = frustumCommandsList.length;

  for (let i = 0; i < length; ++i) {
    // ...
    
    frustumCommands.commands[pass][index] = command;
   
    // ...
  }
  
  // ...
}   

這個函數內做了對指令的篩選判斷。

4. 本篇總結

本篇調查清楚了 Scene 對象上各種三維物體是如何繪製的,即藉助 指令 對象保存待繪製的信息,最後交由 Context 對象完成 WebGL 代碼的執行。

期間,發生了指令的分類和可見集的篩選;篇幅原因,CesiumJS 在這漫長的渲染過程中還做了很多細節的事情。

不過,Cesium 的三維物體的渲染架構就算講完了。

接下來,則是另兩個比較頭痛的話題:

  • 地球的渲染架構(瓦片四叉樹)
  • 具備創建指令的各路數據源(Entity、DataSource、Model、Cesium3DTileset等)

指令和通道的概念仍然會繼續使用,敬請期待。

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