地形部分的原理介紹的差很少了,但以前還有一個刻意忽略的地方,就是地形的重採樣。通俗的講,若是當前Tile沒有地形數據的話,則會從他父類的地形數據中取它所對應的四分之一的地形數據。打個比方,當咱們快速縮放影像的時候,下一級的影像還沒來得及更新,因此會暫時把當前Level的影像數據放大顯示, 一旦對應的影像數據下載到當前客戶端後再更新成精細的數據。Cesium中對地形也採用了這樣的思路。下面咱們具體介紹其中的詳細內容。git
上圖是一個大概流程,在建立Tile的時候(prepareNewTile),第一時間會獲取該Tile父節點的地形數據(upsampleTileDetails),而後構造出upsampledTerrain對象,它是TileTerrain對象,只是一個包含父類地形信息的空殼。接着,開始建立地形網格(processTerrainStateMachine)。算法
這裏就有兩個邏輯,若是當前沒有地形數據,也就是EllipsoidTerrainProvider的狀況,這樣會直接建立HeightmapTerrainData。所以狀態是TerrainState.RECEIVED,這種狀況下不須要重採樣;若是請求了真實的地形數據,好比CesiumTerrainProvider,不管是請求高度圖仍是STK,只要有異步請求,則會執行processUpsampleStateMachine韓式,最終實現重採樣(sourceData.upsample)。網絡
咱們先了解一下高度圖下的實現。高度圖,顧名思義也是一種圖了,因此這個重採樣的方式和普通的圖片拉伸算法一致。好比一個2*2的圖片,放大至4*4的大小,這裏就有一個插值的過程。好比線性差值,會取相鄰的兩個像素顏色,加權求值,或者雙線性插值,取周邊四個像素,加權求值。這讓我想到了GDI中對圖片是採用線性了,而Photoshop裏面則有不少專業的選項,某些逗逼用戶常常拿着PS拉伸的效果來作對比,說咱們圖片拉伸的效果不如PS。等咱們作完了,又拿CorelDraw來對比矢量效果。但你有很難從技術和產品的角度來和用戶溝通其中的利弊。這是題外話了,咱們來看一下Cesium具體的代碼:異步
for (var j = 0; j < height; ++j) { var latitude = CesiumMath.lerp(destinationRectangle.north, destinationRectangle.south, j / (height - 1)); for (var i = 0; i < width; ++i) { var longitude = CesiumMath.lerp(destinationRectangle.west, destinationRectangle.east, i / (width - 1)); var heightSample = interpolateMeshHeight(buffer, encoding, heightOffset, heightScale, skirtHeight, sourceRectangle, width, height, longitude, latitude, exaggeration); setHeight(heights, elementsPerHeight, elementMultiplier, divisor, stride, isBigEndian, j * width + i, heightSample); } }
這是兩個for循環,遍歷目標圖片中每個經緯度對應父類圖片中該位置的高度值。下面是一個切片四叉樹的示意圖,高度圖也是一個思路,只是其中每個像素不是顏色,而是高度值:ide
如同可見,子類切片的像素大小和父類是同樣的,通常都是256*256的切片,但具體到地理範圍上則只有父類的四分之一,因此顧名思義是四叉樹。這樣,從父類到子類放大的過程當中,父類的一個像素,在子類中佔了4個像素。也就是一個1:4的映射關係。儘管像素都是整數的,但咱們在插值的過程當中會有一個亞像素的概念。這樣,同一個位置,經緯度都是相同的,但在子類和父類中的uv是不同的,在對子類的遍歷中,獲取同一個經緯度對應父類uv的位置,進而得知在父類中相鄰的四個像素和權重,進而插值獲取其高度(顏色),以下是一個示意代碼:函數
function interpolateMeshHeight(){ var fromWest = (longitude - sourceRectangle.west) * (width - 1) / (sourceRectangle.east - sourceRectangle.west); var fromSouth = (latitude - sourceRectangle.south) * (height - 1) / (sourceRectangle.north - sourceRectangle.south); var widthEdge = (skirtHeight > 0) ? width - 1 : width; var westInteger = fromWest | 0; var eastInteger = westInteger + 1; if (eastInteger >= widthEdge) { eastInteger = width - 1; westInteger = width - 2; } var dx = fromWest - westInteger; return southwestHeight + (dX * (northeastHeight - northwestHeight)) + (dY * (northwestHeight - southwestHeight)) }
高度圖畢竟還都是離散的點值,並無構網,於是節點之間尚未創建關聯,差值算法也相對容易一些。而STK的數據,自己已是TIN的三角網結構了。這時,在父類中切割出四分之一來就有點複雜了。再打個比方,若是高度圖至關於一個棋盤上均勻的大米,而後你四等分,取走其中的一份,而TIN三角網則至關於一個錯綜複雜的下水管,你要切走四分之一。這要怎麼作到呢。假設此時咱們有一把利刃,把這個TIN網格橫一刀豎一刀,這時,咱們迅速的把漏水的管道密封(造成新的節點),這樣就實現了TIN三角網重採樣的過程。spa
固然,這個過程相比高度圖要複雜的多,所以Cesium中建立了Worker線程,切割的過程都是在線程中完成。具體到算法則以下:prototype
for (i = 0; i < parentIndices.length; i += 3) { var i0 = parentIndices[i]; var i1 = parentIndices[i + 1]; var i2 = parentIndices[i + 2]; var u0 = parentUBuffer[i0]; var u1 = parentUBuffer[i1]; var u2 = parentUBuffer[i2]; triangleVertices[0].initializeIndexed(parentUBuffer, parentVBuffer, parentHeightBuffer, parentNormalBuffer, i0); triangleVertices[1].initializeIndexed(parentUBuffer, parentVBuffer, parentHeightBuffer, parentNormalBuffer, i1); triangleVertices[2].initializeIndexed(parentUBuffer, parentVBuffer, parentHeightBuffer, parentNormalBuffer, i2); // Clip triangle on the east-west boundary. var clipped = Intersections2D.clipTriangleAtAxisAlignedThreshold(halfMaxShort, isEastChild, u0, u1, u2, clipScratch); // Get the first clipped triangle, if any. clippedIndex = 0; if (clippedIndex >= clipped.length) { continue; } clippedIndex = clippedTriangleVertices[0].initializeFromClipResult(clipped, clippedIndex, triangleVertices); if (clippedIndex >= clipped.length) { continue; } clippedIndex = clippedTriangleVertices[1].initializeFromClipResult(clipped, clippedIndex, triangleVertices); if (clippedIndex >= clipped.length) { continue; } clippedIndex = clippedTriangleVertices[2].initializeFromClipResult(clipped, clippedIndex, triangleVertices); // Clip the triangle against the North-south boundary. clipped2 = Intersections2D.clipTriangleAtAxisAlignedThreshold(halfMaxShort, isNorthChild, clippedTriangleVertices[0].getV(), clippedTriangleVertices[1].getV(), clippedTriangleVertices[2].getV(), clipScratch2); if(clipped2.length == 10 && clipped.length == 10) var i = 10; addClippedPolygon(uBuffer, vBuffer, heightBuffer, normalBuffer, indices, vertexMap, clipped2, clippedTriangleVertices, hasVertexNormals); // If there's another vertex in the original clipped result, // it forms a second triangle. Clip it as well. if (clippedIndex < clipped.length) { clippedTriangleVertices[2].clone(clippedTriangleVertices[1]); clippedTriangleVertices[2].initializeFromClipResult(clipped, clippedIndex, triangleVertices); clipped2 = Intersections2D.clipTriangleAtAxisAlignedThreshold(halfMaxShort, isNorthChild, clippedTriangleVertices[0].getV(), clippedTriangleVertices[1].getV(), clippedTriangleVertices[2].getV(), clipScratch2); addClippedPolygon(uBuffer, vBuffer, heightBuffer, normalBuffer, indices, vertexMap, clipped2, clippedTriangleVertices, hasVertexNormals); } }
這個算法有點複雜,但思路清楚了,也就迎刃而解。首先,遍歷頂點索引,每次+3,由於三個點構成一個三角形,因此完成了對全部三角網遍歷切割的過程。在每次循環中,u0,u1,u2是三角形對應的三個點,而後經過Intersections2D.clipTriangleAtAxisAlignedThreshold實現三角形的切割算法。線程
這個切割的過程其實就是三角形和直線求交的過程,但更直觀一些,由於這個直線是豎直或水平的,若是直線和三角形沒有交點,那表示該三角形要麼全在子類,要麼全不在,不須要切割,咱們不討論這種狀況。若是相交,則有兩種狀況:code
第一種狀況,只保留了原三角形一個頂點,但會產生兩個新的節點(藍色),最終造成一個三角形。針對這種狀況,咱們在作一次水平的切合(水平線和藍色三角形的相交計算),這個算法是一致的。
第二種狀況,保留了原三角形兩個頂點,同時也產生了兩個新的節點(必然是偶數節點,哥尼斯堡七橋問題),這時,會造成兩個三角形,則咱們須要對這兩個三角形單獨作一次水平切割。
通過如上的邏輯,咱們就完成了一個三角形的兩刀切,固然,算法只提供了一個思路,並無考慮特殊狀況,正好通過頂點對半切,這個在實際中須要作一次額外的判斷,避免少算或重複算。細的說裏面有兩個過程,求交點,構造新的三角形,分別經過clipTriangleAtAxisAlignedThreshold和addClippedPolygon函數實現。這時,如是是第二種狀況,則還有一個三角形須要進行水平的切割和構造新三角形的過程。這是爲何會多一個if判斷。
如上,新的,通過重採樣的TIN三角網構建完成,Cesium會先渲染這個略微粗糙的地形,等待精細的地形下載完後在更新。固然,經過這個過程,咱們能意識到,Cesium並不硬性的要求每個地形Tile都可以獲取到,若是其中一個Tile沒有下載到(網絡異常或環境限制),也能很好的自適應,並且也不方案該Tile的子類也能夠正常渲染和更新。但前提是,根節點的地形數據是必須的。不論是蛋生雞仍是雞生蛋,你總得現有同樣。