今天來看看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
圖2
我想到了大話西游里面,至尊寶在變成齊天大聖的時候跟觀音說的那句話“以前我看事物,是用肉眼去看。但在我死去那一剎那,我開始用心眼去看這個世界”。圖1是我們看到的效果,而圖2才是這個geometry真實的樣子。希望你也能看得前所未有的那么清楚。而在實現中這個過程是先有圖2那樣的陰影體,再有圖1的貼地效果,先有真家伙在做視覺上的假效果。我們還是以Rectangle作為例子來說一下這個過程。這個最簡單,其他的在思路上大同小異,這些幾何算法就不深究了,太專業了,所以只能望而卻步。
RectangleGeometryLibrary.computeOptions
首先就是做好准備工作。對於Rectangle,默認每個網格的大小是PI/180的弧度,根據其高寬來計算這個矩形需要多少個網格,每個網格大概的高寬,最終返回一個json對象,這就是該Rectangle的幾何信息,下面計算網格的時候就是以此為依據。
constructRectangle
好比你面對一堵牆,根據之前computePosition的參數,你采購了對應了一批瓷磚,下面的任務就是把瓷磚貼到牆上去。constructRectangle就是貼瓷磚的這個過程。
position就是每塊瓷磚的左上角位置,也就是vbo中對應的頂點數據,同時在calculateAttributes會根據需要創建法線,切線等數據;每一個瓷磚都可以通過兩個三角形構成,就是一條對角線的工作,這就是indices的事情,對應vbo中的頂點索引:
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循環就是構建這個圍牆。
圍牆構建的思路大致這樣,如上123是頂面的一個邊,abc則是地面對應的邊,最終這面牆的順序為:(1 a 2 b 3 c)組成其頂點數據,然后對應的構建其頂點索引,如綠色的線,最終構建完這面牆,同理構建其他三面圍牆。這樣實現了面拉伸為體。
Shadow Volume Rendering
上圖取自GPUgems,你會有一個詳細的了解。版本一:大致上就是通過模版緩沖區先渲染一遍,這樣通過正反面的區分,最終獲取該立方體對應的陰影部分。換人類的語言來重新描述一下:版本二:因為我們的立方體的頂面和低面都是取的當前Tile中的極值,所以該立方體肯定是和地球完全相交的,就好比齊天大聖把他的如意金箍棒插到地上,貼地的結果就是這個棍子和地面相交的部分。再看看上圖,是否就能理解了。
通過版本二的描述,我們理解了這個思路,下面解釋一下版本一,我們可以明白計算機,WebGL的方式來理解這個過程。
如上圖,是一個立方體,下面正好貼地,上面黃色部分是一個光源,照射該物體后,我們能夠看到的區域在模版緩沖區中+1,我們用藍色的標識:
接着,面對我們的視線它的背面區域,我們對這個區域-1,用棕色來標記,如下圖:
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