WebGL-3D地圖大俯仰角的霧化處理

如下內容轉載自掘金文章《 WebGL-3D地圖大俯仰角的霧化處理
做者: 多多洛愛學習
連接: http://www.javashuo.com/article/p-semjwova-eh.html
來源:掘金
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

導語:除了畫布寬高以外,3D地圖的可視範圍還受到俯仰角、旋轉角度的影響。在大俯仰角狀況下,爲了下降瓦片加載和渲染的耗時,在地圖遠端不加載瓦片,瓦片邊緣會變得很是突兀。因此須要對瓦片邊緣進行霧化處理,實現漸隱效果,優化用戶體驗。如下爲使用騰訊位置服務Javascript API GL所遇到實際問題。javascript

問題背景

地圖的渲染原理請自行了解:Web地圖呈現原理,其中重點了解地圖數據以瓦片爲單位進行加載和繪製這一點便可。在3D地圖中,因爲有俯仰角和水平旋轉角的存在,墨卡託投影上的可視範圍與可視窗口大小並不相同,會呈現一個梯形:html

3D投影變換

當俯仰角增大時,梯形上半部分會快速擴大,致使加載的瓦片過多而影響性能,因此須要對遠端瓦片進行截斷。而截斷後邊緣比較突兀,因此須要對邊緣作霧化處理,優化體驗,以下圖。java

瓦片截斷和霧化處理

解決方案

瓦片加載限制

注:下文涉及到3D世界中各座標系統的相互轉換,需提早了解OpenGL座標系統,以及略懂線性代數,不然可能暈頭轉向不知所云(畫圖太麻煩了,見諒)。web

在投影空間中地圖的俯仰角變化所引發的可視範圍的變化以下圖左側所示,45度時瓦片加載數量大約爲0度時的1.5倍,而且隨着俯仰角增大而快速上漲。爲了減小瓦片加載數量,必須捨棄一部分瓦片。考慮到透視投影形成的近大遠小,遠端的瓦片被壓縮後可分辨度很低,因此能夠捨棄,即對可視空間的頂部進行裁切。less

可視空間裁切

如何裁切,這裏須要先介紹一下可視範圍的計算方法。在不考慮裁切的狀況下,可視區域的四個頂點均在地圖平面(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)四個頂點反推可視區域的四個頂點。post

至此咱們獲得的俯仰角70度時的瓦片加載效果以下圖:性能

瓦片加載限制效果圖

霧化處理

霧化並不難理解,其本質上是一種顏色混合,將本色與霧色(爲了適應個性化的地圖樣式咱們使用的霧色與大地顏色保持一致)進行混合。隨着混合因子的變化實現漸變效果。可參考WebGL 霧,着色器代碼以下所示:學習

gl_FragColor = originalColor * (1.0 - fogFactor) + fogColor * fogFactor;

地圖是分圖層繪製的,若在每一個圖層的着色器中實現霧化邏輯實在過於冗雜,因此後處理方式更爲合理。所謂後處理,即在幀緩衝中繪製完畢後,將緩衝關聯的紋理做爲輸入進行圖像處理。爲了讓漸變效果更平滑,可使用smoothstep函數讓霧化因子從0平滑過渡到1,以下圖所示:測試

霧化因子-單側漸變

着色器代碼爲:

float fogFactor = 1. - smoothstep(fogEdge, fogEdge + fogRange, y_screen);

至此咱們獲得初步的霧化效果以下圖:

image

可見邊緣已被霧徹底遮蓋了,可是頂部仍顯空洞,能夠加上一點藍天的背景效果,可使用紋理也可使用純色,仍然也須要一個漸變過程,霧化因子變化以下:

image

獲得效果圖以下:

image

根據深度進行霧化

注:如下內容涉及到深度測試,需提早了解OpenGL深度測試,不然可能暈頭轉向不知所云。

以上是直接根據紋理縱向座標計算霧化因子,雖然效果已符合預期,但仍有不合理之處。好比樓塊,若是樓塊太高,就算其距離視點較近,其頂部仍然會被霧化,不符合天然認知。因此霧化因子應根據深度值進行計算。

建築頂部被霧化

深度紋理

通常來講,幀緩衝區對象的深度關聯對象爲渲染緩衝區對象(renderbuffer object),而渲染緩衝區是沒法傳入着色器進行讀取的,因此須要關聯到一個紋理對象。要讓紋理對象支持深度值的寫入,須要使用一個擴展WEBGL_depth_text,這個擴展使紋理支持gl.DEPTH_COMPONENT格式,同時能支持gl.UNSIGNED_SHORTgl.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_clip與z_view關係推導

由此可得大地平面在裁剪空間中的座標的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_1depth_2,兩個方程聯立求解便可獲得EF

float getDepthFromY(float y_clip) {
    return min(mix(depth_1, depth_2, y_clip / y_clip_2), 1.);
}

至此能夠獲得紋理上空白處的深度值了,結合從深度紋理中讀取的數值,可視化以下:

image

霧化因子應相應修改成隨深度變化:

image

獲得最終的霧化效果以下圖:

霧化處理-深度霧化

能夠再看看此時的高樓效果:

建築頂部合理顯示

動態效果參見附件視頻。

問題總結

  1. 不管是可視範圍裁切仍是深度計算都須要熟練掌握3D的各個空間之間的相互變換關係,須要正反雙向思惟能力
  2. 霧化本質上是一種漸變的顏色混合,漸變的維度可自由把握
  3. 透視投影下深度值與觀察空間座標的1/z成正比
  4. 深度緩衝和模板緩衝通常是關聯到renderbuffer對象,renderbuffer是沒法傳入着色器進行讀取的,在這裏使用了texture對象替代了renderbuffer用於存儲深度值,在其餘應用場景下也能夠直接經過着色器繪製深度或模板到顏色緩衝上,提供更高的精度,也能實現更加自定義的功能
相關文章
相關標籤/搜索