版權沒有,請尊重翻譯成果,有翻譯錯誤請指出,規范性轉載。@秋意正寒
本文通過解讀 Scene.render 方法,觀察 WebGL 在 Cesium 1.9 中如何渲染一幀。讀者可以在 Scene.render 方法處打斷點進入調試。
由於 Cesium 專注於可視化地理空間內容,因此多光源的場景並不擅長、不多見,Cesium 使用的是傳統的前向陰影流水線。Cesium 的流水線之所以獨特,是因為它使用了多個視錐體來支持大范圍的視距,而不需要對z軸進行扭曲變化(這句翻譯得不是很好)。
起步
Cesium 把每一幀的生命周期相關的數據存儲在一個叫 FrameState
(參考 FrameState.js) 的對象中。在幀最開始時,初始化相機參數、時間之類的東西。幀的狀態可用於其他的對象,例如 Primitive 對象可以調用當前幀的狀態數據。
UniformState(參考 UniformState.js)是 FrameState 的一部分,它具有共有的、預先計算好的 uniforms。在幀開始時,它計算視圖矩陣、太陽向量等 uniforms。
更新
Cesium 的動畫、更新、渲染流水線是很經典的,動畫的步驟可能是對 WebGL 無交互地對 primitive 的移動、改變其材質屬性、添加刪除 primitive 等。這並不是 Scene.render 的一部分,這些動畫可能會在渲染一幀之前通過代碼顯式指定,或者使用 Entity API 的 Property 在后面默默改變。
經典的 動畫-更新-渲染 流水線。
Scene.update 這個方法最主要的第一步是更新所有在 Scene 中的 primitives。(參考Scene.js > function updatePrimitives(scene) {})
在這一步,每一個 primitive 將會:
- 創建/更新其對應的 WebGL 資源,即編譯、鏈接着色器,加載紋理,刷新頂點緩沖區等;Cesium 永遠不會在 Scene.render 方法外調用 WebGL,因為這樣會浪費 requestAnimationFrame() 這個函數的時間,並使其與其他的 WebGL 引擎集成變得困難。
- 返回一列
DrawCommand
對象,這些對象代表的是 primitive 們創建的 drawcall 和 WebGL 資源。像 polyline、billboard集合可能會返回一個 DrawCommand,Globe 對象或 Model 等則可能會返回數百個 DrawCommands。大多數幀會包含幾百到幾千個 DrawCommand。
譯者注:下面代碼選自 1.9,在 1.75 已經找不到這個函數了,commandList 也找不到了。不過,這個 updatePrimitives 函數是在 function render 函數中調用的,render 函數(不是Scene.prototype.render 方法)在1.75版本中卻還在,_primitives.update() 這一步也移動到了 scene.updateAndExecuteCommands() 方法中的 executeCommandsInViewport() 函數里了,commandList 也被拆開了。有興趣的讀者可以對比研究研究。
// Scene.js 中的 updatePrimitives() 函數 -- v1.9
function updatePrimitives(scene) {
var context = scene.context;
var frameState = scene._frameState;
var commandList = scene._commandList;
if (scene._globe) {
scene._globe.update(context, frameState, commandList);
}
scene._primitives.update(context, frameState, commandList);
if (defined(scene.moon)) {
scene.moon.update(context, frameState, commandList);
}
}
// 調用
function render(scene, time) {
// ...
updatePrimitives(scene);
// ...
}
Cesium 中的地球對象:Globe,地形和衛星影像瓦片的引擎,一樣是一個 “primitive”。它的更新功能指揮着瓦片的層次調度、剔除,以及負責管理加載地形瓦片和影像瓦片的內存。
潛在的可見數據集
剔除,是圖形引擎對看不見的物體進行消除的優化方法,這樣流水線就不必處理那些看不到的對象了。通過了可見性測試的物體,被稱作“潛在可見性數據集”,將隨着流水線傳遞下去。為了提高速度,可見性測試使用了不精確的測試方法,所有這些 “潛在可見性數據集” 可能最終是可見的,也可能是不可見的。
對於獨立的繪制命令,Cesium 支持使用命令的的 boundingVolume (世界坐標空間下)進行視錐體和地平線的自動剔除。(這句話翻譯得不太好,不太懂表達了什么)對於能自我剔除的 primitive,例如 Globe 對象,可以關閉這個功能。
傳統的圖形引擎檢查每一個繪制命令,進行可見性測試,從而找到潛在的可見數據集。Cesium 的 createPotentiallyVisibleSet 函數(譯者注:現在移動到 Scene.view 屬性內了)先走了第一步,它將繪制命令動態地分為多個視錐體(通常是三個),這些視錐體把所有的繪制命令綁定在一起,並保持一定的遠近比例以避免z值沖突。每個視錐體的截頭體的張角和寬高比是一樣的,只有近平面和遠平面的舉例不同。
這個函數做了優化。它利用時間上的連續,如果前后幀的繪制命令條件合適,那么已經計算好的視錐體及其截頭體將會被重用,以減少計算量。
上圖左邊:多個視錐體(紫橙綠);右邊:一個視錐體的截頭體的繪制命令
譯者注
這段文章啃得生硬,不知道講了什么東西,應該是 Cesium 的多視錐體機制能更好地優化剔除吧,源碼要去了解 createPotentiallyVisibleSet 是怎么做的。注意版本。
渲染
每個視錐體都有自己的繪制命令列表,現在就可以觸發 WebGL 的 drawElements 和 drawArrays 了。
Cesium 的渲染流水線核心是 executeCommand
函數,你能在 Scene.js 中找到。
首先,清除顏色緩存。如果使用了與順序無關的透明度、快速近似抗鋸齒(FXAA),則它們的緩存也被清除。
然后,使用整個視錐體(不是上面分開的那三個)繪制一些特殊的 primitive:
- 天空盒。老式的優化方法是跳過清除顏色緩存,先渲染天空盒。實際上這很損耗性能,因為清除顏色緩存有助於壓縮GPU(與清除深度緩存類似)最佳的實踐是,先渲染天空盒。Cesium 必須這么做,因為繪制完視錐后深度緩存會被清除(這里翻譯不太懂)
- 大氣層。
- 太陽。如果太陽被設為可見,則渲染太陽。如果還啟用了輝光濾鏡,則剔除太陽,然后對顏色緩存進行采樣、變亮、模糊等操作,然后混合成輝光效果。
接下來,從最遠的視錐體開始,按以下步驟執行每個視錐體中的繪制命令:
- 賦予當前視錐體的 uniform,僅為視錐體的近距離和遠距離;
- 清除深度緩存
- 執行不透明圖元的繪制命令。執行一個繪制命令會設置一些 WebGL 的狀態,例如渲染狀態(深度、混合模式等)、頂點數組、紋理、着色器程序、uniforms等,然后觸發 drawcall。
- 接下來執行半透明的繪制命令。如果沒有浮點數紋理導致 OIT 不被支持,則將繪制命令從頭到尾排序並執行命令。(這句話又不是很懂了)否則,OIT 將用於提高重疊的半透明對象的顯示質量,避免排序時 CPU 的開銷。繪制命令的着色器對 OIT 進行了修正並緩存,如果支持 MRT,則執行一次 OIT 並進行渲染,或者作為回調進行兩次渲染(還是不懂說什么)。見
OIT.js
中的 executeCommands() 函數。
使用多個視錐體會導致一些有趣的情況,例如一個繪制命令跨了兩個視錐,那么命令就會被執行兩次。
至此,每個視錐體的截頭體的所有繪制命令已經執行,如果使用 OIT,則執行最后的 OIT 復合遍歷將被執行。如果 FXAA (快速抗鋸齒)啟用了,那么還會執行全屏傳遞以進行抗鋸齒。
與 Heads-Up Display 類似,最后執行的是 overlay pass 繪制命令。(這句話還是不懂)
Cesium 在 1.9 版本時的渲染流水線。
排序並合批
在每個視錐體中,由 primitive 傳過來的繪制命令是按順序執行的。例如,Globe 對其繪制命令進行了從前到后的排序,以利用 GPU early-z 的優勢(z優先?不懂,需要查資料學習)。
繪制命令的數量決定了性能如何,因此 primitive 通過把多個對象的組合在一起,僅發出一條繪制命令來提高性能。例如,BillboardCollection 在一個頂點緩存中存儲盡可能多的 Billboard,並使用同一個着色器進行渲染。
拾取
Cesium 的拾取功能利用了顏色緩存。每一個可拾取的對象都有一個唯一的 id(即顏色)。
給定視窗坐標系的 (x,y) 坐標,為了確定拾取了什么,則需要將幀渲染到屏幕之外的幀緩存,這個寫在外面的幀緩存記錄的顏色值即為拾取的 id。隨后,使用 WebGL 的 readPixels 函數,讀取顏色,拿到id,然后就能返回拾取的對象了。
Scene.pick 方法的流水線和 Scene.render 方法很類似,不過拾取的東西並不需要包括天空盒、太陽、大氣層,所以能簡化一些。
之后要做的
下列計划將提升幀渲染的性能。
地面遍歷(原文 Ground Pass 不知道怎么翻譯好)
上面關於在 Scene.render 方法中的遍歷順序( opaque不透明,半透明translucent,overlay覆蓋)其實在普通的圖形引擎中很常見。實際上,不透明還要分開成 globe 和 opaque(不太懂是什么意思,應該說的是不透明的東西還能繼續分解為地球對象和其他不透明對象)。
可以這么說,分開后的不透明物體順序是:基本的 globe對象、貼地的矢量數據和一般的不透明對象。
陰影
陰影將通過陰影映射實現。場景從可產生陰影的光線觸發,進行渲染,每個能投射的物體均作用於深度緩存,或者陰影貼圖(模型到光源的距離?)。然后,在主色通道中,每個能接收陰影的對象檢查每個燈光的陰影貼圖中的距離值,檢查是否在陰影內。
這個實現非常復雜,需要解決混疊偽影、柔和陰影、多視錐截頭體以及地形引擎等因素。
深度紋理
陰影子集添加了對深度紋理的支持。例如,深度紋理能用來對 billboards 在地形上的深度測試,並根據深度值重新構造它在世界空間中的位置。
WebVR
添加陰影后,提供了對不同角度進行渲染的能力。WebVR基於此。
每個眼睛使用一個視錐體進行渲染即可。
立方體貼圖
陰影的另一個擴展能力是對立方體進行六面貼圖,以進行環境渲染,將環境貼到盒子的六個面上,以顯示盒子位於場景中的何處。這個功能會非常消耗計算資源(作者猜測),所以可能並不會用在實時計算上。
后處理效果
Scene.render 具有一些后處理效果,例如輝光、FXAA和OIT合成等。
官方計划創建一個通用的后處理框架,將紋理作為輸入,通過一或多個后處理階段來處理它們。這些后處理操作基本上是在視圖窗口上的幀運行片元着色器,然后輸出。
與其用硬算的方式制造輝光,可以用后處理的方式更好地完成,還能做景深、SSAO、發光、運動模糊等效果。
Compute pass
Cesium 使用舊式的 GPGPU 進行 GPU 加速來計算圖像重投影。過程中,離屏渲染一個與屏幕(就是canvas)對齊的幀,然后推到着色器中。(不知道說了什么)
未來的渲染流水線
譯者注
渲染流水線這個過程是相當的長,而且這個“未來的渲染流水線” 不一定適用了,不過大體框架的思路已經闡明,希望各位讀者能得到一些啟發,有些地方翻譯得並不是很好。
致謝
作者和 Dan Bagnell 編寫了大多數的渲染器。要獲得細節可以參考 Cesium Wiki。作者還在念高中時,Ed Mackey 在90年代就在 AGI 進行了原生的多視錐體實現。
參考
[Bagnell13] Dan Bagnell. Weighted Blended Order-Independent Transparency. 2013
[Cozzi13] Patrick Cozzi. Using Multiple Frustums for Massive Worlds. In Rendering Massive Virtual Worlds Course. SIGGRAPH 2013.
[McGuire13] McGuire and Bavoil, Weighted Blended Order-Independent Transparency, Journal of Computer Graphics Techniques (JCGT), vol. 2, no. 2, 122–141, 2013
[ONeil05] Sean O’Neil. Accurate Atmospheric Scattering. In GPU Gems. Edited by Matt Pharr and Randima Fernando. 2005.
[Ring13a] Kevin Ring. Horizon Culling. 2013.
[Ring13b] Kevin Ring. Computing the horizon occlusion point. 2013.