使用three.js渲染瓦片地球-第一篇

近期有個需求是想使用自己的3d引擎去渲染地球邏輯,如果從頭實現一遍會比較耗費時間,而且後續還要實現傾斜攝影等等,所以打算使用cesium來幫我們做調度然後用自己的3d引擎渲染。
在github上找到了個cesium-three發現該項目的策略是three.js去渲染three到物體,cesium去渲染cesium物體,這樣實現起來比較容易,但是問題是當一個複雜場景出現的時候,排序怎麼辦,當然還有更多問題根本是無法解決的。正所謂一山不容二虎,當二虎有分歧時聽誰的那?
看了下cesium代碼發現cesium的邏輯和渲染分離的比較好,每幀的渲染就是執行最終的drawCommand,本來以爲一天就能搞定,沒想到還是花了幾天時間,裏面還是涉及到一些細節問題,現在分享給大家思路和方法(目前cesium地球調度還有一兩個小點沒有完全弄清楚,不過我這邊選擇了跳過,不影響瓦片邏輯,如果有知道的大神歡迎評論和指導,目前自己的引擎我暫且使用three.js來表示吧)

渲染順序如下:

  1. frameState的更新-----主要是傳入Cesium需要的framestate需要的各種參數,攝影機,viewPort,視錐等
    這其中我們需要禁掉一些cesium不必要的開銷,如陰影等等(因爲陰影我們還是要用自己的引擎實現),當然由於攝影機操作也需要自己實現一套,攝影機操作和更新肯定是自己的引擎做的事情,同時cesium攝影機也並不太符合自己的操作習慣,攝影機操作其實也不是太簡單,後續有時間也可以分享一下

  2. Cesium調度------主要是地塊數據的創建與更新 + image的請求與下載,這一塊基本上是最複雜的,需要完全理清楚cesium的邏輯,其中需要把一些不必要的邏輯停掉,儘量最大化性能(比如pick肯定要停掉,因爲pick肯定是自己的引擎去實現,一定要明確一個思路,只用cesium地球調度,所以無關緊要的全部停掉,使性能達到最優)。當然中間還需要停掉cesium很多邏輯,如attribute的創建和上傳(cesium直接創建buffer了這可不行,浪費內存啊),貼圖sample對象的創建等等

  3. 渲染-----這時候我們拿到當前幀要渲染的地塊tile(上面有imagery, data等各種信息),然後就去創建mesh掛到我們自己的scene下面執行渲染。每幀先清空地球節點下面的所有mesh,然後這幀cesium告訴我們渲染哪些就去掛上去(這樣做完全是模擬cesium的drawCommand邏輯)。當然這裏要做好mesh的緩存,material的緩存,texture的緩存以及各種uniform對象的緩存(其實這裏還有個比較核心的就是地球的shader,這個其實還是比較複雜的,後續再花時間補上吧)

哎,說個小插曲,剛開始做的時候我理清楚了這個邏輯,但是由於我們這邊渲染引擎的渲染比較複雜,導致我cesium的更新沒有在引擎渲染之前(我一直以爲是執行的上述邏輯,哎),然後一直出現縮小兩邊閃白塊的問題(其實白塊就是因爲地球沒有渲染,那一塊白色的其實是大氣。我還曾經一度懷疑是cesium將前後幾幀的圖全渲到targetBuffer然後混合出的結果那,但是實驗了一下發現不是。最後又找了自己的原因,原來是先執行渲染了,相當於比cesium快了一幀)

下面詳細說一下這三步如何實現的:
1.第一步 frameState的更新:
查看一下frameState這個類,看看裏面都需要傳入哪些信息,這一步應該比較容易,當然pick,depth,postProcess,creditDisplay(證書),shadow這些邏輯我們不需要(如果需要當然是用自己的引擎實現,不需要額外的開銷)。fog的話其實可以使用cesium的邏輯,計算出fog的濃度,但是我這邊自己實現了fog,所以也禁掉了fog的邏輯。
第一步核心就是攝影機和視錐,具體信息如下
frameState.camera = {
positionWC:frameState.positionWC,
positionCartographic: frameState.positionCartographic,
directionWC:frameState.directionWC,
frustum:frameState.perspectiveOffCenterFrustum,
}
frameState.cullingVolume = frameState.perspectiveOffCenterFrustum.computeCullingVolume(frameState.positionWC, cameraDirectionWC, cameraUpWC);
(很多參數具體追一下cesium代碼就知道如何計算的,不過要注意webgl引擎座標系和cesium座標系的轉換)

2.第二步 cesium調度
首先要先理清cesium的地塊渲染邏輯(這裏由於篇幅原因不詳細講了,如果不清楚,cesium最長的一幀可以參考一下)
我們目前只是使用地塊最核心的部分自己去手動調接口進行渲染調度,
(1)首先創建QuadtreePrimitive
(2)beginFrame: 接下來就可以可以開始調用它的接口手動更新了
直接調用:this._surface.beginFrame(this._frameState),這裏主要是釋放和創新創建0級的tile,不需要做更改
(3)render: 這塊是比較核心的地方:直接調用this._surface.render(this._frameState);
在這裏插入圖片描述
這裏把endUpdate幹掉,其實我們的draw基本就是和endUpdate做一樣的事情(生成drawComand,使用var tilesToRenderByTextureCount = this._tilesToRenderByTextureCount;最終的這個tilesToRenderByTextureCount數據,從裏面取我們的tile,imagery和terrian),當然和pick有關的邏輯一定要幹掉,接下來就是調用selectTilesForRendering來獲取當前需要創建的網格(不過這些可能需要對一些參數進行相應的更改,畢竟像context這種webgl的上下文我們不可能傳入framestate中,當然render之後你如果需要使用cesium的截流或者別的功能也可以手動去更新)
(4)endFrame:接下來就是最核心的部分:直接調用this._surface.endFrame(this._frameState);在processTileLoadQueue中,遍歷更新隊列中所有的GlobeSurfaceTile,解析tile數據,下載圖片
在這裏插入圖片描述
測試發現updateHeighs在持續飛行很長時間會掉幀很厲害,這塊我先註釋掉了,沒有細看,如果有知道的可以留言,謝謝
接下來就是 GlobeSurfaceTile.processStateMachine,這裏會遍歷所有tile如果狀態是START就去prepareNewTile,如果是LOADING就去processTerrainStateMachine,這裏處理地形的下載以及地塊網格的生成邏輯,水面的生成。這部分cesium解析成功之後就直接去生成attribute了,我們要做的就是跳過attribute的邏輯,手動去標記地塊(地形)網格ready的狀態。
接下來就是圖片的下載,這部分處理起來最複雜,我剛開始是用自己的引擎去下載圖片資源,雖然沒問題,但是就無法使用cesium的截流(可以設置每幀最大請求數,多餘請求可以攔截下來,這樣再持續飛行的時候能夠解決內存泄露的問題)。所以建議還是使用cesium的下圖邏輯,但是攔截掉sampler的創建就行了,不過注意需要綁定texture對象,因爲cesium在沒有下載到新圖的時候回遞歸查找最近的圖,這一點很重要,具體細節可以看TileImagery.prototype.processStateMachine的邏輯。
在這裏插入圖片描述reprojectTexture我們可以不需要,將其與createTexture合併在一起,然後重點修改createTexture的代碼,將創建sampler的代碼改成自己引擎生成texture的代碼即可。(好了,cesium代碼基本改造完了,對了,還有卸載,在cesium卸載tile和image的時候注意卸載你自己的資源就行了)
3最後一步:自己引擎的渲染邏輯,由於是大尺度下的渲染,所以引擎首先要實現logDepth。然後開始準備地球的shader(就是把cesium的shader看一遍,理解了就行了,不過注意一些座標轉換,當然這部分也有一定難度,後續可以分享一下)。然後就可以開始渲染邏輯了。專門爲地球創建一個節點,然後每幀先清空,然後遍歷this._surface.tileProvider._tilesToRenderByTextureCount去執行渲染,注意緩存tile和material,也要注意緩存uniform,這一塊實現之後一個流暢的地球已經出現了,沒想到完美實現一個hello world並不是那麼容易
在這裏插入圖片描述
當然我們可以加入各種地圖濾鏡效果:
在這裏插入圖片描述

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