回顧
書接上文,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等)
指令和通道的概念仍然會繼續使用,敬請期待。