1.2 Cesium渲染流程


  

從前有座山,山里有座廟,廟里有個......”我們喜歡這樣講故事,有頭有尾,一個調用接一個,特別因為JS本身的一些特點,往往我們會發現,半路殺出個“程咬金”,一些對象變量臨場出現讓人迷糊,這里面弄清楚整個流程顯得尤為重要,搞清楚這個引擎流水線,我們才能把控這里面的機制。Cesium實時刷新,就是說每一幀都在更新,這就像最原始的動畫制作一樣,一頁翻完翻另一個,只是刷新間隔快了,我們眼皮沒發覺(但這也是一般狀態下,如果場景完全靜悄悄也可請求渲染模式,這時就不是每一幀都更新了,這個我們后面再說,先認為是動起來的)。 

 

我們在初始化一個球的時候,比較常用的個方式是創建一個Viewer容器,如下:

var viewer = new Cesium.Viewer('cesiumContainer', {
    shadows : true
});

然后可以在里面初始化一堆參數,很多人就誤以為這個球的開口就是Viewer,其實不然;Viewer只是一個簡單的啟動器,幫忙攜帶了一些球啟動的參數,但歸根結底它仍然是一個二次封裝的產物。Cesium球的大門在CesiumWidget里。在Viewer里我們可以看到,經過一通傳遞和組合,用戶傳入的參數,最終還是構建給了CesiumWidget:

         var cesiumWidget = new CesiumWidget(cesiumWidgetContainer, {
            imageryProvider: createBaseLayerPicker || defined(options.imageryProvider) ? false : undefined,           
            skyBox : options.skyBox,            
            scene3DOnly : scene3DOnly,            
            shadows : options.shadows,            
            mapMode2D : options.mapMode2D,
            requestRenderMode : options.requestRenderMode

...... });

options就是在外圍Viewer傳入進來的,包括最基礎的影像、陰影設置等,一個widget包含一個三維場景。我們轉入來看CesiumWidget,這里面也洋洋散散為自己也為外圍調用封裝了一堆東西,可謂是細致入微,但我們最終要看的是startRenderLoop函數

function render(frameTime) {  if (widget._useDefaultRenderLoop) {
        try {
          ......
                requestAnimationFrame(render);
            }
        } catch (error) {
           ......
        }
    } 
}
requestAnimationFrame(render);

看到這對WebGL稍熟悉便大徹大悟,requestAnimationFrame()是專門為腳本式的動畫而生的,通過requestAnimationFrame()函數,跟傳送帶一樣,高速運轉一幀一幀,一個畫面又一個畫面的輸送,不同的三維場景映入眼簾。開篇我們說Cesium是動起來的,就是在這運作的,當然,光到這我們只能說我們看清楚了JS的動畫機制,還不能說看清楚了Cesium的動畫機制,因為截止目前為止,Cesium還沒有創建任何能看到的球上東西。

再接下來,同樣是在CesiumWidget里,場景的渲染在一個自動調用函數里

 CesiumWidget.prototype.render = function() {
        if (this._canRender) {
            this._scene.initializeFrame();
            var currentTime = this._clock.tick();
            this._scene.render(currentTime);
        } else {
            this._clock.tick();
        }
    };

這里調用scene的render(time)函數,才是至關重要,細微化會調用scene里面的私有函數render(scene),它就是這個WebGL三維場景的渲染調度和繪制命令的組織者。但特別說一下,Cesium利用顏色緩沖區來實現拾取,在scene.pick函數里面,將ID當作顏色寫入到一個離屏緩沖區,對象與ID唯一對應,然后根窗口坐標(x,y)拾取內容,readPixels讀取顏色,並返回拾取的對象,看源碼會發現scene.pick與scene.render很相似,但太陽、大氣和天空盒沒必要做拾取做了不處理。

  function render(scene) {
var frameState = scene._frameState; var context = scene.context; var us = context.uniformState;

//Cesium最近幾個版本在scene的基礎之上,有多加了一層view,方便數據調用和抽象共有層 var view = scene._defaultView; scene._view = view; updateFrameState(scene); frameState.passes.render = true; frameState.passes.postProcess = scene.postProcessStages.hasSelected; frameState.tilesetPassState = renderTilesetPassState; var backgroundColor = defaultValue(scene.backgroundColor, Color.BLACK); if (scene._hdr) { backgroundColor = Color.clone(backgroundColor, scratchBackgroundColor); backgroundColor.red = Math.pow(backgroundColor.red, scene.gamma); backgroundColor.green = Math.pow(backgroundColor.green, scene.gamma); backgroundColor.blue = Math.pow(backgroundColor.blue, scene.gamma); } frameState.backgroundColor = backgroundColor; frameState.creditDisplay.beginFrame(); scene.fog.update(frameState); us.update(frameState); var shadowMap = scene.shadowMap; if (defined(shadowMap) && shadowMap.enabled) { // Update the sun's direction Cartesian3.negate(us.sunDirectionWC, scene._sunCamera.direction); frameState.shadowMaps.push(shadowMap); } scene._computeCommandList.length = 0; scene._overlayCommandList.length = 0; var viewport = view.viewport; viewport.x = 0; viewport.y = 0; viewport.width = context.drawingBufferWidth; viewport.height = context.drawingBufferHeight; var passState = view.passState; passState.framebuffer = undefined; passState.blendingEnabled = undefined; passState.scissorTest = undefined; passState.viewport = BoundingRectangle.clone(viewport, passState.viewport); if (defined(scene.globe)) { scene.globe.beginFrame(frameState); } updateEnvironment(scene); updateAndExecuteCommands(scene, passState, backgroundColor); resolveFramebuffers(scene, passState); passState.framebuffer = undefined; executeOverlayCommands(scene, passState); if (defined(scene.globe)) { scene.globe.endFrame(frameState); if (!scene.globe.tilesLoaded) { scene._renderRequested = true; } } frameState.creditDisplay.endFrame(); context.endFrame(); }

這里完整地將整個函數列出來,表面不算太長,但設計了整個渲染周期前期准備、完整繪制、后期效果方方面面,可謂保羅萬象,有必要仔細跟一下函數里面的細節,Cesium必由之路,不管是自建功能還是調整源碼,都會不斷地與這段代碼打交道。下面我們就來仔細過一下這段代碼。

首先一開始做了一些渲染前的參數傳遞與准備,frameState.passes.render = true是默認是渲染狀態,有人可能問,這個重點函數不就是負責渲染的嗎,為什么還有具體的默認狀態?其實Cesium由於主要還是基於WebGL1.0來實現,很多的包括深度信息、最終合成幀等都是一幀一幀重新渲染場景的得到的;正常情況下就是很普通的將場景最終的樣子推送到繪制緩沖區就OK了,但有些時候,我們並不需要它繪制出來,僅僅是為了獲取深度圖或者點選對象的ID列表,因此嚴謹Cesium對渲染不同的需求又做了分流如下幾種:

passes = {
render : 最常見的渲染,
pick : 圖元拾取渲染,
depth : 深度渲染,
postProcess : 后處理階段渲染,
offscreen : 離屏渲染

......
}

 這下就明了了,比如僅僅為了拿到深度圖,frameState.passes.depth= true設為即可,自然往下執行繪制命令,這張深度圖就出來了。

var pickDepth = getPickDepth(this, 0);
var context = frameState.context;
var windowCoordinate = SceneTransforms.wgs84ToWindowCoordinates(this._scene,targetPosition,new Cartesian2());
var drawingBufferPosition = SceneTransforms.transformWindowToDrawingBuffer(this._scene, windowCoordinate, new Cartesian2());
drawingBufferPosition.y = this._scene.drawingBufferHeight - drawingBufferPosition.y;
var depth = pickDepth.getDepth(context, drawingBufferPosition.x, drawingBufferPosition.y);

具體屏幕坐標對應的深度值獲取操作就順理成章了,繪制結束后,直接獲取深度圖,計算繪制緩沖的坐標,y軸反轉,就得到深度值了,封裝很到位,看源碼其實也就是WebGL坐標反算那套,這點很有用,很多用到深度對比的空間分析手法都可以插入,當然這是最簡單粗暴的獲取方式,因為中間Cesium又會根據各個繪制命令再次計算分支命令,其實有相當一部分是不需要的......關於這些繪制命令我們下節再討論,不然本章不知道要寫到猴年馬月了。總有人問,Cesium深度圖如何獲取,是的,就是如此簡單啊!

接下來就是一桶更新,每一幀都是不一樣的世界。

視口指定和backgroundColor這些就沒必要過多去討論了。直接來看如下三個更新:

       1)  updateFrameState(scene);

        2) context.uniformState.update(frameState)

        3)  scene.fog.update(frameState)

第一個函數主要任務是更新幀狀態,而FrameState是Cesium里很重要的一個渲染變量,還有context、PassState等,這里面是一些環境基礎更新,包塊地球mode、相機、投影方式、太陽顏色、對數深度設置等等,后面的計算需要基於這些設置生成;第二函數主要復雜US狀態機的更新,對太陽月亮信息做了進一步計算;第三個函數就是對霧的專門更新,包括density、sse、brightness。

好了,總算是更新完了!

好比一場大考,前面該裝備的裝備好了,該上戰場了吧,下面三個函數我們習慣性放一起看。

updateEnvironment(scene);
updateAndExecuteCommands(scene, passState, backgroundColor);
resolveFramebuffers(scene, passState);

這三個方法可謂是整個渲染過程的中流砥柱!重要繪制在updateAndExecuteCommands()函數里一步一步完成實現的,其實地形要特殊一些,它的完成是在scene.globe.endFrame(frameState)基於四叉樹逐步分瓦片請求。在updateAndExecuteCommands()函數里,一開始對不同的視圖做了分別計算

 if (useWebVR) {
            executeWebVRCommands(scene, passState, backgroundColor);
        } else if (mode !== SceneMode.SCENE2D || scene._mapMode2D === MapMode2D.ROTATE) {
            executeCommandsInViewport(true, scene, passState, backgroundColor);
        } else {
            updateAndClearFramebuffers(scene, passState, backgroundColor);
            execute2DViewportCommands(scene, passState);
        }

有沒有很熟悉,針對VR做VR命令計算,針對3D在視口下計算,針對2D也有對應的計算。我們常見的自然是三維模式走的 executeCommandsInViewport(true, scene, passState, backgroundColor)方法。參數早在前面的的更新中早就准備好了,這個方法也很講究。

一開始是對所有的圖元進行更新,這就包括我們在上層使用的一堆堆primitives、陰影圖的更新,Cesium的機制是這樣的,有什么三維數據類型 就有對象的集合類, 有add  就有對應的remove,對象渲染每個都有對應的update()函數,這個函數負責自己圖元的渲染調度,這種設計很巧妙,真正意義做到了分而治之,而每個圖元的update()就是這里遍歷調用的,就像排隊面試一樣,HR只負責叫你伙計輪到你了,至於你要向我傾訴什么那也是你自己來說。

if (!renderTranslucentDepthForPick) {
            updateAndRenderPrimitives(scene);
        }

接下來該推Call了吧,但Cesium不急不忙,基於上面的圖元來計算視錐體,這里就有點“多此一舉”的感覺,其實不然。首先我們要明白,並不是每一幀都必須運用相同的視錐體,這是一個前提,然后,圖形學里,我們知道當場景拉得很大很長的時候,近裁剪面與遠裁剪面不得不拉開很大,這個時候有些問題就暴露出來了,最難看的便是精度問題。關於解決精度問題我們也可以將近裁剪面再拉遠一點,或者遠裁剪面在拉近一些,但這里面的開銷以及效果只能說差強人意;多視錐體就此誕生了,它將當前場景分成了多個視錐體,通常2個左右,有效避免 z-fighting,每個frustum都有相同的視野范圍和縱橫比,只有近裁剪面和遠裁剪面的距離不同。

view.createPotentiallyVisibleSet(scene);

最后,執行繪制命令,大功告成。

executeCommands(scene, passState);
resolveFramebuffers(scene, passState)函數主要是將前一個效果作為輸入,計算后期渲染效果,再輸出,作為下一個效果的輸入,其中就包括我們知道的 


WebGL如此炫麗,綻放真實世界!

一陣清雨,躲進滿是書香的小樓

愛技術,愛交流 685834990


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM