0. 前置約定
- 對類的使用,不添加
Cesium
命名空間前綴,例如對於Viewer
,不會寫Cesium.Viewer
,默認使用ESM
格式解構導入類; - JavaScript 代碼使用最簡格式(源碼除外),不加分號,不用雙引號,少注釋,雙空格縮進
本系列說明
佛系連載,想到什么寫什么。
2022 年,寫原理類的文顯得非常“蠢”,大家都想吃快餐,看效果。法克雞絲老哥的系列博客思路跳躍很快,單步說明之間的信息量很大,需要消化很長時間才能啃完一篇文章,遂決定另開一個風格,提綱挈領地把主要關鍵邏輯大白話說說 —— 可不是真的“大白話”,還是要有一些功底的。
我寫這個,只是為了從 CesiumJS 的渲染架構中汲取一些營養,希望對自己的程序設計能力有提高,希望能從其它繪圖 API 的角度看看能不能優化和實現。
1. 開始
很多人寫 CesiumJS 程序是從 Viewer 開始的
new Viewer('container') // div id
你若只需要一個最干凈的場景(此場景非 Scene
類),不需要時間條、時間控制器、右上角一堆的按鈕,只需要
new CesiumWidget('container') // div id
CesiumJS 內置了大量的默認值,以至於簡單到你可以只傳遞 DOM 的 id 或本身即可創建場景。
1.1. CesiumWidget 類是控制場景對象觸發渲染的調度器
Scene
類是一個三維空間對象的容器,它在原型鏈上有一個 render
方法,寥寥百行,控制了三維場景中若干物體的更新、渲染。
Scene.prototype.render
方法調用一次,只更新並渲染一幀。
眾所周知,WebGL 一般會和 requestAnimationFrame, rAF
這個 API 循環調用渲染函數。而讓 canvas 中場景能連續多幀循環往復運行的調度者,是 CesiumWidget
類。
CesiumWidget
類有一個使用 Object.defineProperties()
方法定義的 setter
:
useDefaultRenderLoop: {
get: function () {
return this._useDefaultRenderLoop;
},
set: function (value) {
if (this._useDefaultRenderLoop !== value) {
this._useDefaultRenderLoop = value;
if (value && !this._renderLoopRunning) {
startRenderLoop(this);
}
}
},
}
在實例化 CesiumWidget
時,它會使用傳入的值,若沒有,則是 true
:
this._useDefaultRenderLoop = undefined;
this.useDefaultRenderLoop = defaultValue(
options.useDefaultRenderLoop,
true
);
一旦賦值,就開始了 CesiumJS 的渲染循環,是一個在 模塊內 的函數 startRenderLoop
負責控制的。
function startRenderLoop(widget) {
// ... 節約篇幅,此處非源碼,省略大量代碼層級,有興趣自己看源碼
function render(frameTime) {
// ...
widget.render()
requestAnimationFrame(render)
// ...
}
requestAnimationFrame(render)
}
傳入的 widget
是 CesiumWidget
實例,通過 requestAnimationFrame
的調用,則不斷地在調用這個函數內的局部函數 render
。
render
函數內調用 widget
的 render
方法,再往下就是調用 widget
所擁有的 scene 的 render 方法了。
1.2. Scene 對象
接上文說。
於全局,CesiumWidget
負責控制 DOM 的變化情況,例如窗口尺寸變化導致 DIV 的變化等,並負責起 渲染循環 的調度。
於單幀,Scene
類則需要使用自己原型鏈上的 render
方法完成自我狀態、數據對象的更新,以及 Scene.js
模塊內的 render
函數觸發 WebGL 繪制。
Scene
類是一個場景對象容器,其 render
方法負責:
- 生命周期事件(preUpdate、preRender、postUpdate、postRender)回調觸發;
- 更新幀狀態和幀號
- 更新 Scene 中的 Primitive
- 移交渲染權給模塊內的
render
函數觸發 WebGL 繪制
2. 三維地球哪來的?
CesiumJS 的三維地球,實際上分兩大部分:
- 地球橢球體與表面的 GIS 影像服務
- 場景中的三維物體
我說過了,CesiumJS 內置了大量的默認值,包括地球橢球體以及影像服務(默認用的必應瓦片服務,要 token)。但是,實際上可以不需要地球橢球體和底圖的:
if (defined(scene.globe)) {
scene.globe.beginFrame(frameState);
}
上述代碼片段是 Scene.js
模塊內的 render
函數的一小段,也就是說,若沒有定義 globe
,那就不繪制橢球上的幀。
3. 本篇總結
綜上 1、2 節,我認為 CesiumJS 的渲染循環,到本文 1.2 小節末尾提及的 Scene.js
模塊內 render
函數的調用,觸發 WebGL 繪制,就算一幀的邏輯結束,沒有必要再向下探究 Primitive、DataSource、Globe 等數據實體的更新和渲染,也沒有必要深究 WebGL 在 CesiumJS 中如何調度 —— 那都不是渲染循環的主要內容。
Scene
原型鏈上的 render
函數並沒有更新橢球體,沒有請求地形四叉樹瓦片,而是等待更重要的 Primitive 等三維物體的更新后,才判斷 globe 是否存在,從而決定要不要畫地球(的皮膚),最終才更新並執行 Command
,也就是 scene.updateAndExecuteCommands(passState, backgroundColor);
一句代碼。