原文連接html
導語:除了畫布寬高以外,3D地圖的可視範圍還受到俯仰角、旋轉角度的影響。在大俯仰角狀況下,爲了下降瓦片加載和渲染的耗時,在地圖遠端不加載瓦片,瓦片邊緣會變得很是突兀。因此須要對瓦片邊緣進行霧化處理,實現漸隱效果,優化用戶體驗。web
地圖的渲染原理請自行了解:Web地圖呈現原理,其中重點了解地圖數據以瓦片爲單位進行加載和繪製這一點便可。在3D地圖中,因爲有俯仰角和水平旋轉角的存在,墨卡託投影上的可視範圍與可視窗口大小並不相同,會呈現一個梯形:bash
當俯仰角增大時,梯形上半部分會快速擴大,致使加載的瓦片過多而影響性能,因此須要對遠端瓦片進行截斷。而截斷後邊緣比較突兀,因此須要對邊緣作霧化處理,優化體驗,以下圖。less
注:下文涉及到3D世界中各座標系統的相互轉換,需提早了解OpenGL座標系統,以及略懂線性代數,不然可能暈頭轉向不知所云(畫圖太麻煩了,見諒)。函數
在投影空間中地圖的俯仰角變化所引發的可視範圍的變化以下圖左側所示,45度時瓦片加載數量大約爲0度時的1.5倍,而且隨着俯仰角增大而快速上漲。爲了減小瓦片加載數量,必須捨棄一部分瓦片。考慮到透視投影形成的近大遠小,遠端的瓦片被壓縮後可分辨度很低,因此能夠捨棄,即對可視空間的頂部進行裁切。性能
如何裁切,這裏須要先介紹一下可視範圍的計算方法。在不考慮裁切的狀況下,可視區域的四個頂點均在地圖平面(z_world=0
平面)上,即頂點在世界空間內的座標可表示爲(x_world, y_world, 0, 1)
,而後通過與視圖投影矩陣相乘獲得裁剪空間座標,裁剪空間可簡單映射到屏幕空間。因此可反推回去,經過屏幕空間的四個頂點,映射到裁剪空間座標,與視圖投影矩陣的逆矩陣相乘後可獲得可視區域的四個頂點座標。其中較難理解的是裁剪空間的z_clip
值沒法肯定,能夠取-1和1兩個值,逆變換後可獲得兩個世界座標肯定一條直線,該線與z_world=0
平面的交點即最終的頂點座標。測試
因此若是要裁切,由上圖所示,在世界空間內咱們切掉的是頂部梯形陰影部分,所對應到屏幕空間切掉的是頂部矩形陰影部分,那麼咱們須要獲得屏幕空間的一條水平裁切線,即圖中的y值。假如以45度時的梯形上半高度爲標準,經過簡單的幾何計算可得其爲1.93185 * view.top
,可近似爲2 * view.top
,那麼可取世界座標(0, 2 * view.top, 0, 1)
做爲裁切點,將其變換到屏幕空間(x_screen, y_screen)
便可獲得y值,記爲fogEdge
。以後再根據上文所述的逆變換,使用(0,y), (1,y), (0,1), (1,1)
四個頂點反推可視區域的四個頂點。優化
至此咱們獲得的俯仰角70度時的瓦片加載效果以下圖: webgl
霧化並不難理解,其本質上是一種顏色混合,將本色與霧色(爲了適應個性化的地圖樣式咱們使用的霧色與大地顏色保持一致)進行混合。隨着混合因子的變化實現漸變效果。可參考WebGL 霧,着色器代碼以下所示:ui
gl_FragColor = originalColor * (1.0 - fogFactor) + fogColor * fogFactor;
複製代碼
地圖是分圖層繪製的,若在每一個圖層的着色器中實現霧化邏輯實在過於冗雜,因此後處理方式更爲合理。所謂後處理,即在幀緩衝中繪製完畢後,將緩衝關聯的紋理做爲輸入進行圖像處理。爲了讓漸變效果更平滑,可使用smoothstep
函數讓霧化因子從0平滑過渡到1,以下圖所示:
着色器代碼爲:
float fogFactor = 1. - smoothstep(fogEdge, fogEdge + fogRange, y_screen);
複製代碼
至此咱們獲得初步的霧化效果以下圖:
可見邊緣已被霧徹底遮蓋了,可是頂部仍顯空洞,能夠加上一點藍天的背景效果,可使用紋理也可使用純色,仍然也須要一個漸變過程,霧化因子變化以下:
獲得效果圖以下:
注:如下內容涉及到深度測試,需提早了解OpenGL深度測試,不然可能暈頭轉向不知所云。
以上是直接根據紋理縱向座標計算霧化因子,雖然效果已符合預期,但仍有不合理之處。好比樓塊,若是樓塊太高,就算其距離視點較近,其頂部仍然會被霧化,不符合天然認知。因此霧化因子應根據深度值進行計算。
通常來講,幀緩衝區對象的深度關聯對象爲渲染緩衝區對象(renderbuffer object),而渲染緩衝區是沒法傳入着色器進行讀取的,因此須要關聯到一個紋理對象。要讓紋理對象支持深度值的寫入,須要使用一個擴展WEBGL_depth_text
,這個擴展使紋理支持gl.DEPTH_COMPONENT
格式,同時能支持gl.UNSIGNED_SHORT
和gl.UNSIGNED_INT
類型,相應代碼以下:
// 開啓深度紋理擴展
if (!gl.getExtension("WEBGL_depth_texture")) {
console.error("depth textures not supported");
}
// 設置紋理大小
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
複製代碼
在幀緩衝上繪製完畢後,將深度紋理傳入着色器,將其可視化可獲得下圖(圖中僅對須要霧化的部分讀取了深度值進行展現):
如上圖所示,雖然咱們可以獲取到深度紋理,但這個深度紋理是不完善的,在沒有樓塊的地方是一片空白,說明深度值爲1即沒有寫入深度。這是由於:(1) 爲了不深度衝突,除了建築物以外其餘圖層繪製時都關閉了深度測試,因此並無寫入深度值;(2) 即便在其餘圖層上開啓了深度測試,使用背景色的地面是沒有繪製物的,仍然沒有深度值。簡單來講,即目前沒法獲取到大地平面的深度值。
如何獲取到大地平面的深度值?其實深度值是能夠計算的,參考OpenGL 投影矩陣。不過並不須要知道它的具體公式,只須要了解到透視投影下深度值與觀察空間中的1/z_view
是成正比的,即depth = A * (1 / z_view) + B
。另外一方面,咱們經過簡單的幾何計算能夠獲得:
由此可得大地平面在裁剪空間中的座標的y值y_clip
與深度值depth
是成正比關係的,即depth = E * y_clip + F
。那麼就能夠選擇兩個平面上的座標點,好比點1(0, 0, 0, 1)
和點2(0, 2 * view.top, 0, 1)
,經過視圖投影矩陣獲得(0, 0, z_clip_1, 1)
和(x_clip_2, y_clip_2, z_clip_2, 1)
,再根據depth = (z_clip + 1) / 2
獲得depth_1
和depth_2
,兩個方程聯立求解便可獲得E
和F
。
float getDepthFromY(float y_clip) {
return min(mix(depth_1, depth_2, y_clip / y_clip_2), 1.);
}
複製代碼
至此能夠獲得紋理上空白處的深度值了,結合從深度紋理中讀取的數值,可視化以下:
霧化因子應相應修改成隨深度變化:
獲得最終的霧化效果以下圖:
能夠再看看此時的高樓效果:
動態效果參見附件視頻。