Cesium原理篇:GroundPrimitive【轉】


  今天來看看GroundPrimitive,選擇GroundPrimitive有三個目的:1 了解GroundPrimitive和Primitive的區別和關系 2 createGeometry的特殊處理 3 如何通過陰影體的方式實現貼地效果。

GroundPrimitive.prototype.update

       可以認為GroundPrimitive是Primitive的擴展,通過Update我們可以很好的理解這個過程:

復制代碼
GroundPrimitive.prototype.update = function(frameState) { if (!defined(this._primitive)) { // Key 1 setMinMaxTerrainHeights(this, rectangle, frameState.mapProjection.ellipsoid); // Key 2 for (i = 0; i < length; ++i) { instance = instances[i]; geometry = instance.geometry; instanceType = geometry.constructor; groundInstances[i] = new GeometryInstance({ geometry : instanceType.createShadowVolume(geometry, getComputeMinimumHeightFunction(this), getComputeMaximumHeightFunction(this)), attributes : instance.attributes, id : instance.id, pickPrimitive : this
 }); } // Key 3 var that = this; primitiveOptions._createBoundingVolumeFunction = function(frameState, geometry) { createBoundingVolume(that, frameState, geometry); }; primitiveOptions._createRenderStatesFunction = function(primitive, context, appearance, twoPasses) { createRenderStates(that, context); }; primitiveOptions._createShaderProgramFunction = function(primitive, frameState, appearance) { createShaderProgram(that, frameState); }; primitiveOptions._createCommandsFunction = function(primitive, appearance, material, translucent, twoPasses, colorCommands, pickCommands) { createCommands(that, undefined, undefined, true, false, colorCommands, pickCommands); }; primitiveOptions._updateAndQueueCommandsFunction = function(primitive, frameState, colorCommands, pickCommands, modelMatrix, cull, debugShowBoundingVolume, twoPasses) { updateAndQueueCommands(that, frameState, colorCommands, pickCommands, modelMatrix, cull, debugShowBoundingVolume, twoPasses); }; this._primitive = new Primitive(primitiveOptions); } // Key 4 this._primitive.update(frameState); }
復制代碼

       Key 1是計算該Primitive所在GlobeTile對應地形的最高點和最低點,這里只是一個粗略的計算,假如該Primitive的范圍覆蓋了多個Tile則取最后一個Tile高度的最大最小值,另外Cesium有一個json文件,里面保存了全球前6層的地球切片,里面的信息是該Tile對應高度的極值。因為GroundPrimitive對應的幾何對象要貼地,所以這個信息會在構建geometry的時候用到。

       Key 2是構建適合的Geometry,因為需要實現貼地效果,所以該geometry做了一個類似拉伸的效果,從一個面拉伸為一個體,這也是為什么要獲取該Entity對應地形的極值,因為要根據極值拉伸到對應的高度。這個后面詳細介紹。

       key 3是構建了primitiveOptions,重載了里面的Render模塊實現方式,Key2在數據層面上為貼地做准備,那Key3則在渲染邏輯上根據對應的geometry實現陰影體,從渲染角度實現了貼地

       Key4,一切准備就緒,最后還是構造的Primitive,通過update完成最后的執行過程,這個過程就和之前的Primitive相同。

       可見,GroundPrimitive在渲染的流程上和Primitive並無本質的不同,經過Key1~3的步驟,構造出可以貼地的Primitive,最終還是以Primitive的方式來執行渲染。

createShadowVolume

      什么叫陰影體,一圖以蔽之:

1

圖1

2

圖2

       我想到了大話西游里面,至尊寶在變成齊天大聖的時候跟觀音說的那句話“以前我看事物,是用肉眼去看。但在我死去那一剎那,我開始用心眼去看這個世界”。圖1是我們看到的效果,而圖2才是這個geometry真實的樣子。希望你也能看得前所未有的那么清楚。而在實現中這個過程是先有圖2那樣的陰影體,再有圖1的貼地效果,先有真家伙在做視覺上的假效果。我們還是以Rectangle作為例子來說一下這個過程。這個最簡單,其他的在思路上大同小異,這些幾何算法就不深究了,太專業了,所以只能望而卻步。

RectangleGeometryLibrary.computeOptions

       首先就是做好准備工作。對於Rectangle,默認每個網格的大小是PI/180的弧度,根據其高寬來計算這個矩形需要多少個網格,每個網格大概的高寬,最終返回一個json對象,這就是該Rectangle的幾何信息,下面計算網格的時候就是以此為依據。

constructRectangle

       好比你面對一堵牆,根據之前computePosition的參數,你采購了對應了一批瓷磚,下面的任務就是把瓷磚貼到牆上去。constructRectangle就是貼瓷磚的這個過程。

row) { for (var col = 0; col < width; ++col) { RectangleGeometryLibrary.computePosition(options, row, col, position, st); positions[posIndex++] = position.x; positions[posIndex++] = position.y; positions[posIndex++] = position.z; } } // Key 2 var geo = calculateAttributes(positions, vertexFormat, ellipsoid, options.tangentRotationMatrix); // Key 3 var indicesSize = 6 * (width - 1) * (height - 1); var indices = IndexDatatype.createTypedArray(size, indicesSize); var index = 0; var indicesIndex = 0; for (var i = 0; i < height - 1; ++i) { for (var j = 0; j < width - 1; ++j) { var upperLeft = index; var lowerLeft = upperLeft + width; var lowerRight = lowerLeft + 1; var upperRight = upperLeft + 1; indices[indicesIndex++] = upperLeft; indices[indicesIndex++] = lowerLeft; indices[indicesIndex++] = upperRight; indices[indicesIndex++] = upperRight; indices[indicesIndex++] = lowerLeft; indices[indicesIndex++] = lowerRight; ++index; } ++index; } geo.indices = indices; return geo; }
復制代碼

       position就是每塊瓷磚的左上角位置,也就是vbo中對應的頂點數據,同時在calculateAttributes會根據需要創建法線,切線等數據;每一個瓷磚都可以通過兩個三角形構成,就是一條對角線的工作,這就是indices的事情,對應vbo中的頂點索引:

3

 

constructExtrudedRectangle

       假設我們生活在一個小鎮,在這個小鎮里面,所有人都生活在二維坐標系下,也就是點線面的幾何形狀,其實這個時候我們眼中的面也不過是線。突然有一天,一個球體來到這個小鎮。在這個小鎮居民的眼中,只能看到這個球的一個切面而已,換句話說,即使真的有一個三維的物體來到這個小鎮,在小鎮居民的眼里,他們也和二維物體一樣並無差別(很多人由於自身能力的限制導致了無法突破自身的覺悟)。這個球不停的上下浮動,在小鎮居民的眼里,會發現這條線的長度不停的變化,這時候有一個居民突然開竅了,於是騎在了這個球的身上,看到了一個全新的世界---三維的世界。

       這個故事說的比較倉促,里面蘊涵了一個降維的思想,反其道而行之也可以做到二維升級到三維的過程。這也是constructExtrudedRectangle的思路。

復制代碼
function constructExtrudedRectangle(options) { var topBottomGeo = constructRectangle(options); // 沿橢球切線上移到最高點 var topPositions = PolygonPipeline.scaleToGeodeticHeight(topBottomGeo.attributes.position.values, maxHeight, ellipsoid, false); var length = topPositions.length; var newLength = length*2; var positions = new Float64Array(newLength); positions.set(topPositions); // 沿橢球切線上移到最低點 var bottomPositions = PolygonPipeline.scaleToGeodeticHeight(topBottomGeo.attributes.position.values, minHeight, ellipsoid); positions.set(bottomPositions, length); topBottomGeo.attributes.position.values = positions; for (i = 0; i < indicesLength; i += 3) { newIndices[i + indicesLength] = indices[i + 2] + posLength; newIndices[i + 1 + indicesLength] = indices[i + 1] + posLength; newIndices[i + 2 + indicesLength] = indices[i] + posLength; } // 立方體的四個圍牆 for (i = 0; i < area; i+=width) { wallPositions = addWallPositions(wallPositions, posIndex, i*3, topPositions, bottomPositions); posIndex += 6; if (vertexFormat.st) { wallTextures = addWallTextureCoordinates(wallTextures, stIndex, i*2, topSt); stIndex += 4; } } for (i = area-width; i < area; i++) { } for (i = area-1; i > 0; i-=width) { } for (i = width-1; i >= 0; i--) { } }
復制代碼

       代碼注釋如上,首先,不難理解矩形拉伸的結果是立方體。我們先把該Rectangle上移到最高點,這樣有了一個頂,然后下移到最低點,這樣就有了一個低,這里對低的indice的順序求逆,用來區分正反面。這里,這個上移和下移的方向則是沿着地球橢球體的法線方向,由scaleToGeodeticHeight方法來實現。有了上下兩個面,然后下來四個for循環就是構建這個圍牆。

4

       圍牆構建的思路大致這樣,如上123是頂面的一個邊,abc則是地面對應的邊,最終這面牆的順序為:(1 a 2 b 3 c)組成其頂點數據,然后對應的構建其頂點索引,如綠色的線,最終構建完這面牆,同理構建其他三面圍牆。這樣實現了面拉伸為體。

Shadow Volume Rendering

       5

       上圖取自GPUgems,你會有一個詳細的了解。版本一:大致上就是通過模版緩沖區先渲染一遍,這樣通過正反面的區分,最終獲取該立方體對應的陰影部分。換人類的語言來重新描述一下:版本二:因為我們的立方體的頂面和低面都是取的當前Tile中的極值,所以該立方體肯定是和地球完全相交的,就好比齊天大聖把他的如意金箍棒插到地上,貼地的結果就是這個棍子和地面相交的部分。再看看上圖,是否就能理解了。

       通過版本二的描述,我們理解了這個思路,下面解釋一下版本一,我們可以明白計算機,WebGL的方式來理解這個過程。

6

       如上圖,是一個立方體,下面正好貼地,上面黃色部分是一個光源,照射該物體后,我們能夠看到的區域在模版緩沖區中+1,我們用藍色的標識:

7

       接着,面對我們的視線它的背面區域,我們對這個區域-1,用棕色來標記,如下圖:

8

       we got it!兩個區域合並在一起,相同的區域,在緩沖期里面中和掉,值為0,而剩下的不為零(為1)的區域就是陰影部分,也是該立方體和地面相交的部分。因此,在陰影體渲染中,第一,前提是地球部分要優先繪制,這樣就有深度信息,接着對陰影體渲染兩次,一次是在模版緩沖區中,標識出陰影區域,然后在渲染到紋理或屏幕,這時進行過濾,只對模版緩沖區不為零(或等於1)的部分渲染,完成陰影體渲染。

       如下圖,第一個stencilPreloadRenderState是渲染到模版緩沖區中,可以看到正面為increase,反面為decrease;第二個colorRenderState是渲染到紋理,注意渲染條件為not equal,對應的mask為~0,這里,改為0x1更准確一下,效果也是一樣的:

復制代碼
var stencilPreloadRenderState = { colorMask : { red : false, green : false, blue : false, alpha : false
 }, stencilTest : { enabled : true, frontFunction : StencilFunction.ALWAYS, frontOperation : { fail : StencilOperation.KEEP, zFail : StencilOperation.DECREMENT_WRAP, zPass : StencilOperation.DECREMENT_WRAP }, backFunction : StencilFunction.ALWAYS, backOperation : { fail : StencilOperation.KEEP, zFail : StencilOperation.INCREMENT_WRAP, zPass : StencilOperation.INCREMENT_WRAP }, reference : 0, mask : ~0  }, depthTest : { enabled : false  }, depthMask : false }; var colorRenderState = { stencilTest : { enabled : true, frontFunction : StencilFunction.NOT_EQUAL, frontOperation : { fail : StencilOperation.KEEP, zFail : StencilOperation.KEEP, zPass : StencilOperation.DECREMENT_WRAP }, backFunction : StencilFunction.NOT_EQUAL, backOperation : { fail : StencilOperation.KEEP, zFail : StencilOperation.KEEP, zPass : StencilOperation.DECREMENT_WRAP }, reference : 0, mask : ~0  }, depthTest : { enabled : false  }, depthMask : false, blending : BlendingState.ALPHA_BLEND };
復制代碼

       渲染的過程在GroundPrimitive中createColorCommands中,可以看到對一個Primitive也會渲染兩次,先渲染模版緩沖期,在渲染color,兩個的順序不能反。這個就是構建兩個DrawCommand的過程,基於以前的章節,這個應該不難理解,這里就不再贅述了。

總結

       至此,我們了解了Cesium在Worker線程中createGeometry的大致流程,也通過Rectangle為例,看到了構建陰影體的算法思路,最后也了解了通過模版緩沖區實現渲染陰影體的過程。這一個完成的流程,可以很好的體現Cesium在算法和OpenGL渲染中很全面,很專業的水准,而且這一套規范是以開源的方式來貢獻出來,這也是難能可貴的。仿佛一枚絢麗的明珠放在了人類象牙塔尖,人人都有擁有它的機會,而且沒有人可以據為己有。

原文地址:https://www.cnblogs.com/fuckgiser/p/6216050.html


免責聲明!

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



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