學廢了系列 - WebGIS vs WebGL圖形程式設計

JunpengZ 發表於 2021-06-17
WebGL

目前工作中有不少涉及到地圖的專案,我參加了幾次技術評審,前端夥伴們在 WebGIS 方面的知識儲備稍有不足,這次分享的主要目的是科普一些在前端領域比較常用的 WebGIS 知識。另外,我之前的工作中積攢了一些從零開始搭建 WebGL 地圖引擎的微薄經驗,雖然最終遺憾沒有上線,但在其中學到的一些WebGL知識還是值得分享一下。WebGL 可以說是前端視覺化技術領域難度最大的一項圖形程式設計技術,所以今天就結合 WebGIS 這個話題順帶分享一些 WebGL 的相關知識,不會太深入,很細節的技術點在後續文章裡再講解。

一 WebGIS 常用概念

在前端領域需要關注的 WebGIS 知識最主要的是搞清楚電子地圖中的各種座標系,其次需要對路網有一些基本的認知,包含路網的特徵以及尋路演算法的複雜度量級,其中對演算法複雜度的瞭解不用精確到數字,只需要有一個大致的概念即可。

路網定址是一套非常複雜的演算法,除了路網本身的有向圖特徵以外,還需要將路況、天氣甚至民生、政治等因素考慮在內。這是一項單獨的研究課題,前端研發不需要關注太細節的東西。

1.1 座標系

我們日常接觸的地理座標最多的是經緯度座標,地球是一個橢球體,經緯度是球面座標系。但是我們平時使用的電子地圖都是平面的,如何把球面座標系下的經緯度座標對映為電子地圖的平面座標系(數學上稱謂是笛卡爾直角座標系)呢?這個對映過程就是投影變換,目前在 WebGIS 領域國際上統一使用墨卡託投影實現。

下面就分別介紹一下以上兩種座標系以及對映原理。

經緯度座標

表面上看是兩種,經緯度和墨卡託,但準確的說應該是三種(甚至N種)。因為我們日常接觸到的經緯度座標都是經過加密演算法處理之後的偏移座標,與地理上真實的經緯度座標有一定的偏移量。

真實的地理經緯度座標系是國際標準,稱為WGS84標準,此標準下的座標系稱為地球座標系或地理座標系。絕大多數電子地圖服務商都不會(或者說不準)直接使用 WGS84 座標,因為地理資訊是涉及國家安全的重要資訊,所以一般都需要進行加密。

我們國家目前使用的加密標準是國家測繪局2002年制定GCJ02 標準,經過加密後的座標系被稱為火星座標系。在我國的所有電子地圖都必須至少經過 GCJ02 加密一次才可以上線使用。請注意,至少的意思是經過 GCJ02 加密之後,地圖廠商還可以進行二次甚至三次加密,比如百度地圖使用的 BD09 標準就是在 GCJ02 加密之後進行二次加密的結果。

下圖顯示的是同一個經緯度座標在不同地圖上的位置:

圖片

墨卡託座標

墨卡託座標是球面座標經過墨卡託投影之後得到的笛卡爾直角二維座標,墨卡託投影全名叫做正軸等角圓柱墨卡託投影。其原理是假設地球被圍在一箇中空的圓柱裡,其基準緯線(赤道)與圓柱相切接觸,然後再假想地球中心有一盞燈,把球面上的圖形投影到圓柱體上,再把圓柱體展開,這就是一幅選定基準緯線上的“墨卡託投影”繪製出的地圖,見下圖:

圖片

為了便於建模和計算,墨卡託投影在真實的地球模型上做了以下幾個假設:

  • 假設一:地球自轉是“垂直的”。之所以加引號,是因為在宇宙角度上討論垂直和水平沒有任何意義。大家都知道地球的自轉軸(也就是南極點和北極點的連線)是有一個傾斜角的,所以我們見到的地球儀都是傾斜的;
  • 假設二:地球是一個正球體。嚴格來說,這條假設並不是墨卡託投影賦予的,而是來自Web墨卡託投影。原生墨卡託投影得到的平面地圖是一個長方形,Web 墨卡託投影在原生墨卡託投影基礎上的再次簡化,將地球假設為一個正球體,投影后得到的平面地圖是一個正方形。正方形方便瓦片切圖(關於瓦片切圖的知識下文會講),這樣能夠提前將地圖資料切片儲存,提高使用者的使用體驗。缺點是Y軸存在0.33%的誤差;

墨卡託投影有兩個致命的缺點:

  • 第一,形變非常嚴重。越接近兩極的位置越嚴重,而且投影后視覺上的平面“面積”遠遠大於真實的地理球面面積。所以在某個特殊時期,墨卡託投影被個別北美洲國家鍾愛,因為他們的國家在投影之後“看上去”非常大。
  • 第二,南北極緯度丟失。墨卡託投影能覆蓋的緯度區間大概是 [-85.05, 85.05](單位度deg),區間之外的兩極地區的經緯度座標經過投影計算得到的值趨近無限大和無限小,無法在平面圖上表達,所以目前市面上的網際網路地圖兩極地區都是“黑洞”。請看下面這張圖:

圖片

現實問題:計算兩點之間的距離

計算兩個POI點之間的“直線”距離是我們日常專案中出現概率很高的一種需求,之所以“直線”兩字加引號是因為在現實中地球上的兩個點不存在絕對的直線距離,在地理上都是球面距離,也就是數學上的弧長。球面上兩點之間的弧長計算是比較複雜的,而且地球是橢球體,進一步加大了複雜度。

這個問題有了墨卡託投影的輔助就很好解決了,墨卡託投影的計量單位是米(m),首先將兩個POI點的經緯度座標換算為墨卡託座標,剩下的就是簡單的勾股定理計算了。

經緯度與墨卡託座標之間的轉換沒有絕對統一的換算公式,每個地圖廠商根據自己的加密演算法都多少存在一些差異,一般不能跨地圖廠商使用

1.2 電子地圖製圖

電子地圖的製圖是一項非常複雜的流程,技術的縱深涉及前端、後端、(空間)資料庫等等,除了技術層面以外,還涉及民生、政治等因素。篇幅有限,這些細節就不一一列舉了,只挑選在前端範圍內以及現有專案中涉及的知識點講一下,主要有兩個方面:

  1. 瓦片切圖;
  2. 路網結構。

其中第一點是出於技術層面考慮,對從事 WebGIS 的前端開發者來說是必須具備的,因為我們對地圖只是使用,不會涉及這麼深入的知識,所以大家可以當這點為科普內容;第二點的目的是讓大家對路網定址演算法的複雜度有大概的認知,從而在進行與路網相關需求的技術評審時能夠全面考慮,從而制定更合理的研發週期。

下面就分別展開講一講。

瓦片金字塔

參照下面這張圖理解後續的內容:

圖片

球面的經緯度座標經過墨卡託投影之後是一張二維的平面圖,圖中的大部分內容的變動頻率是非常低的,比如上圖中展示的大陸和海洋板塊,除非遇到地殼運動,否則基本不會變動。為了持久化儲存,在webgis領域引入了「瓦片」的概念,意思是將墨卡託座標系的二維地圖按照既定的規則切成一個個小方塊儲存到伺服器,然後前端的應用程式在繪製地圖時將這些方塊按順序拼接為完整的地圖,這些小方塊被稱為瓦片-tiles

Tile 直接翻譯是“瓷磚”,倒是很貼切,電子地圖就是用一個個 tile 拼起來的,至於為啥被翻譯成“瓦片”我也不清楚,行業術語,跟著叫就是了。

還記得前面提到的墨卡託投影的第二個假設嗎?將地球假設為正球體,投影之後得到的平面地圖是一個正方形,被切割成一個個瓦片也是正方形,這樣能夠大大降低計算複雜度。因為長方形需要考慮長和寬兩個計算因子,而正方形只需要考慮邊長一個因子即可。

瓦片的尺寸是固定的,普清瓦片邊長是256畫素,高清瓦片邊長在普清基礎上乘以2也就是512畫素。但即便是高清瓦片在瀏覽器中渲染的時候也是被壓縮成256畫素,這裡我先不解釋為什麼,大家也先不要看下文,先思考一下為什麼這麼做。

留空思考時間..

5...
4...
3...
2...
1...
下面揭曉答案。

所謂高清和普清的區別在於:在相同物理尺寸上的畫素密集程度。高清瓦片是為了讓地圖在高清螢幕上看起來更清晰,高清電子螢幕的准入標準是DPR=2(retina屏),當然目前市面上有很多高清屏已經突破了這個值。DPR是螢幕物理畫素與獨立畫素的比值,前端開發者應該清楚 DPR 對於圖形的影響,也就能夠理解為何高清瓦片被壓縮一倍了,我就不在贅述了。

看到這裡,前端夥伴們是不是覺得 WebGIS 其實也並沒有那麼神祕?其實跟我們日常開發所用的技術有很多共同點和相似性。

上面介紹了瓦片的基本概念,在地圖中還有另外一個重要概念:比例尺-Scale。可以類比成望遠鏡的放大倍數,倍數越大,看到的東西就越多越清晰,地圖比例尺就類似望遠鏡的放大倍數。在墨卡託投影的平面地圖中比例尺代表每個畫素等價的以米(meter)為單位的地理距離

地圖從巨集觀到微觀被切分為不同的級別(level),相鄰level的比例尺一般成兩倍關係(並不絕對,下文解釋)。請再次參考上面的圖片,每放大一個級別(即level+1),每個瓦片都會被切割為4張新瓦片,比如level 1 的1號瓦片在level 2中被切割為1-0、1-1、1-2、1-3四張瓦片,但這四張瓦片代表的地理範圍與 level 1和1號瓦片是完全相同的,只是細節更多了(類比望遠鏡就是看到的東西更清楚了)。

在這樣的切割規則下,從巨集觀到微觀,瓦片的數量隨著地圖 level 的增長成四倍增長關係(4^n),以數量為維度,所有的瓦片構成了一個金字塔結構,這就是 WebGIS 領域的術語:瓦片金字塔 - Tiles Pyramid。如下圖:

圖片

上面介紹的其實是理論上的行業標準,但在現實工作中一般不會嚴格按照這份標準落地。在瓦片切割方面一般由3 個不同於標準的地方:

  1. 相鄰 level 不一定是嚴格的兩倍關係;
  2. 基於第一點,各level的瓦片不一定是無耦合的,部分瓦片可能被相鄰的2個甚至N個 level 共享使用;
  3. 不同的地圖廠商(準確的說應該是地圖資料服務商)使用的 level 上下限邊界可能不同,以搜狗地圖為例,level 最小值=3,最大值=19。

基於以上3點區別,不同的地圖在一些涉及瓦片和level的計算規則上也有差異,另外再加上座標加密演算法的區別,所以大部分地圖的資料是無法共通的。

路網結構

對於路網這部分知識的科普,主要目的是讓大家對路網定址演算法的複雜程度和計算量級有一個大概的認知,從而針對目前以及後續專案中涉及到定址功能的需求大家能夠對技術上的可行性、成本以及排期有更加理性的評估。

下面這張圖是在電子地圖上的某個區域的路網示意圖:

學廢了系列 - WebGIS vs WebGL圖形程式設計

路網在數學上的模型是圖(Graph)。圖論是離散數學的一個分支,在計算機應用科學領域,《資料結構與演算法》這門課中有專門的圖論演算法,而且佔比非常大。但由於相比較其他內容,圖論演算法的複雜度高出很多,所以即便教材裡有這一部分的內容,但很多高校在實際教學中不會教也不會考(反正我當時沒學~囧)。

圖片

最簡單的圖是一個二元組,由頂點(vertex)和邊(edge)組成,表示式為:

G = (V,E)

在 WebGIS 領域,路網在是一種有向帶權圖。所謂帶權圖可以簡單的理解為每條邊有一些額外的屬性,比如路況、方向等等。

路網定址的需求主要是用在路徑規劃和導航場景下,這兩種場景有一個共同點:起點和終點是確定。在這個前提下,路網定址其實就是圖論中經典的最小路徑定址演算法,這種演算法已經非常成熟了,而且複雜度也已經被很多前人反覆驗證和改良過,目前各家地圖使用的此類演算法都是在時間複雜度和空間複雜度之間權衡的最優解,而且還要綜合考慮出行方式、交通、天氣等現實因素(這些在數學模型中都是帶權圖結構中edge的「權」)。

但是(沒錯,什麼都有但是),高效的定址演算法背後,請一定要注意「起點和終點是確定的」這個重要的前提。如果沒有了這個前提,複雜度會呈指數型增長,甚至可以說以現在的計算機硬體技術,這個複雜度是沒有上限的。為什麼這麼講,且看下文。

在地圖的業務場景中還有一個非常典型的功能:POI檢索。比如以某個點為中心在指定半徑的圓形區域內檢索特定型別的POI。或者在地圖上自定義指定幾個點,然後在以這些點為頂點的不規則圖形內進行POI檢索。這兩種都是典型的POI檢索場景,跟路網定址一毛錢關係都沒有。

然而有時候我們還期望另外一種檢索方式:

  1. 指定某個點為起點座標;
  2. 指定出行的方式以及最長出行時長或者最長出行距離;
  3. 在前面兩條要求下,找到在出行範圍之內的特定型別(比如酒店、加油站等)的POI。

針對這種需求,我在搜狗工作期間寫了一個專利,但是在商業軟體領域基本不具備可行性,因為計算量太大了。這個專利純粹是為了完成老闆交付的任務~哈哈。

我們可以設想一下應該按照什麼樣的流程去解決這個需求。

第一種是正向解法:從起點開始沿著路網圖的邊遞進檢索,直到到達出行範圍的最遠邊界。這是符合現實規律的一種方法,就好比我想找一家便利店,最遠不能超過步行30分鐘,然後我就從當前位置開始沿著路走啊走,遇到路口就隨機選一個方向接著走,運氣好的話選的路邊有家店,運氣不好的話只能回到路口再隨機選一個方向試著找找,以此類推。當然現實跟演算法的區別就是人的體力有限,一是不可能多執行緒,二是體力堅持不了走所有的路。

第二種是逆向解法。就是在進行定址演算法之前儘量做減法,以給定的條件儘量縮小檢索範圍。比如指定步行最長距離是5公里,起點在中關村科貿大廈,按照以下步驟進行:

  1. 首先以科貿大廈為圓心,5公里為半徑,檢索圓形區域內的所有指定型別的POI,得到一個list;
  2. 然後依次以list中的每個POI為終點,科貿大廈為起點進行路徑規劃,得到所有POI與起點的真實地理距離,篩選出小於等於5公里的POI。

事實上,前文提到的兩種POI檢索場景(圓形和自定義多邊形)都是逆向解法。POI在資料庫中的模型除了座標以外,還有其他附加屬性,比如國家、城市、行政區域、甚至在哪條路等資訊,就是為了縮小檢索範圍從而減輕計算量。

逆向解法比正向解法的計算量小很多,但是兩種解法的計算量都會隨著出行時長和距離的增加呈指數型增長,幾乎沒有上限(當然這麼說不準確,肯定是在地球範圍之內~)。

如果地圖廠商自己想要不計成本地實現這個需求還是有一定可行性的,因為他們自己擁有路網和POI資料。但是如果我們想實現就很困難了,首先我們沒有資料,所以正向解法絕無可能;其次,我們是採買的地圖廠商的服務,而商業化的服務都是有限制的,比如每天的POI檢索量上限,如果限制在比較小的範圍內同時檢索量沒有超過上限,逆向解法是有一定可行性的。但是(是的,還有但是),對於我們來說,這個可行性必須建立兩個前提下:

  • 第一,如果是以出行距離為邊界,可行性相對高一些;
  • 第二,如果是以出行時間為邊界,則必須約束出行方式為步行或騎行。這兩種方式下的路網定址演算法一般不需要考慮交通等影響出行時長的因素,這樣在任何一方向上的最遠邊界距離都是一致的,即半徑=速度 x 時長。而如果是機動車出行,則必須考慮交通因素,不僅複雜度高,而且每個方向上的最遠邊界距離很大可能不一致,也就是說先圈定一個圓形區域的逆向解法中的“減法”不成立。

路網相關的知識分享到這裡,大家應該對定址演算法的計算量級有大概的認知了吧。作為科普,對 WebGIS 的瞭解到這個程度就可以了,其中還有很多WebGIS領域內的技術細節,篇幅有限就不一一列舉了。下半部分是跟前端技術相關性比較高內容,以電子地圖的渲染流程為引,介紹一下 WebGL 的一些基礎知識。

二 WebGIS 與前端

這塊內容分為兩部分,第一部分介紹一下電子地圖的渲染流程,期間按照瓦片的兩種型別(靜態/動態)分別講一下涉及的前端技術;第二部分以當前主流的向量地圖為引,簡單介紹一下 WebGL 的一些基礎知識。關於 WebGL 的知識不會很深入,目的是讓大家的對 WebGL 以及圖形程式設計有大概的認知,後續前端組會制定一套資料視覺化技術的系列課程,到時再深入到各項技術的細節知識。

2.1 地圖渲染流程

先講一點預備知識,電子地圖涉及幾種座標系,每種座標的計量單位如下:

  • 經緯度是球面座標,我們日常使用經緯度單位的是角度(deg),在進行投影計算時需要換算為弧度(rad);
  • 墨卡託投影得到的二維座標單位是米(m);
  • 電子螢幕座標的單位是畫素(px)。

前端拿到的地圖資料中絕大多數是墨卡託座標,很小一部分是經緯度座標。墨卡託或經緯度座標需要先被換算成螢幕座標,最後被CSS拼接或WebGL渲染。

這裡的螢幕座標準確的說應該是畫布(canvas)座標,前端常規認知的螢幕座標是CSS座標,在柵格地圖中CSS座標與canvas座標是相等的,在向量地圖中根據螢幕的DPR值,CSS座標與canvas座標成倍數關係。

web地圖的渲染流程大致如下:

圖片

地圖在進入渲染流程之前有一些必要的前置條件:

  • 地圖level,可以從快取中讀取或者使用預設值;
  • 地圖的中心點座標,可以通過瀏覽器的地理定位API獲取,也可以從快取中讀取,如果都取不到,就必須有一個預設值;
  • 瀏覽器畫布的尺寸,如果是高清屏還需要DPR值。

以上幾個條件的目的是為了計算地圖當前的視野範圍(bounds),進而計算出當前視野包含的瓦片編號列表。

柵格地圖

前半部分介紹了瓦片切圖,準確地說應該是「瓦片切割」,早期web地圖使用的瓦片是一張張靜態的png圖片,前端開發者使用CSS position按照瓦片編號拼接成一張完整的二維地圖。對前端來說,瓦片就等同於是圖片,所以“瓦片切圖”這個叫法一直被延續下來。

但地圖資料本身是一個個座標值並不是圖片,之所以將瓦片儲存為圖片格式是因為早期的瀏覽器沒有能夠繪製海量資料的圖形技術,也就是大家熟知的 WebGL。在這個前提下,地圖廠商會在服務端搭建一套瓦片切圖預處理的流程,簡單理解就是先用 OpenGL 將地圖資料視覺化,然後按照既定的規則把每個 level的地圖切割成一張張 256 * 256 的圖片託管到靜態檔案伺服器,最後前端開發者取圖片拼接。以圖片拼接而成的web地圖叫做「柵格地圖」。

圖片

注意上圖裡的切圖服務中包含「瓦片-data」和「瓦片-png」,兩者的內容一般是不同的。瓦片data的功能一方面是為了瓦片圖片切割,另一方面是提供給其他支援向量圖形技術的平臺使用,比如 app。

柵格地圖的優點是:

  • 前端的計算量非常小,效能相對高一點,對使用者體驗很友好;
  • 瀏覽器相容性很好,由於技術原始,所以很多老舊瀏覽器都能夠相容,比如搜狗的PC地圖即便是現在也能在 IE5 裡無bug執行(這可能是唯一值得吹一下的優點了~囧)。

基於以上兩個優點,目前仍然有很多地圖的JavaScript SDK使用柵格瓦片或者柵格混合向量資料(一般是底圖用柵格瓦片,建築物和poi用向量資料)的形式。不過柵格地圖也有很明顯的缺點:

  • 相對於資料,圖片的體積更大,儲存成本相對更高一些;
  • 點陣圖是非向量的,縮放會失真,視覺體驗不佳;
  • 基於上一條,每個瓦片圖片都不能被相鄰level共享,否則會嚴重失真,這進一步加大了圖片數量和儲存成本;
  • 無法3D化。

向量地圖

隨著大部分主流瀏覽器對 WebGL實現了支援,很多地圖廠商都陸續開始研發並上線了向量地圖。向量地圖同樣需要預處理的切圖服務,但是預處理的產出並不是圖片格式的瓦片,而是與app一樣的瓦片data,換句話說,向量web地圖可以與app地圖使用同一份資料,這意味著所有平臺的地圖資料可以統一維護和迭代

“可以”的意思是可行但不一定,分業務場景。比如導航是app地圖獨有的功能,導航場景使用的地圖資料稱為“市街圖-street map”,這些資料是web地圖用不到的。

圖片

向量地圖說白了就是把原本OpenGL乾的活交給了WebGL幹,說起來簡單做起來難,WebGL 是非常底層的圖形程式設計技術,幾乎沒有任何上層封裝,接近純粹的計算機圖形學。相關的研發人才非常稀缺,圖形程式設計本身就是一個相對小眾的垂直領域,WebGL 圖形程式設計則更加小眾,雖然同屬於前端技術領域,但 WebGL 研發人員的招聘和培養難度比常規web前端研發人員要難很多,所以有能力開發 WebGL 向量地圖的廠商要麼是有足夠的人才儲備想為產品錦上添花,比如高德和百度的WebGL地圖第一個產品是自家的PC地圖;要麼是有充分的客戶需求兌現商業價值,比如騰訊的WebGL地圖第一個產品是B端的 JavaScript SDK(2020年初上線),截止到今天PC地圖也沒有接入WebGL。否則單純靠愛發電很難落地,比如搜狗地圖的WebGL引擎開發到80%的時候被叫停,之後再也沒有撿起來過。

2.2 向量地圖與WebGL

WebGL 圖形程式設計與常規web網站是完全不同的一套知識體系,雖然都使用JavaScript語言,但細節技術點完全不同,比如 WebGL 中被大量使用的 buffer、TypedArray、Protobuf等知識點在常規web網站中幾乎不會被涉及,另外還有一套類似C++的shader語言-GLSL。這些細節知識點會在後續的文章中講解,今天就簡單科普一下WebGL的渲染管線以及WebGL向量地圖中常用的幾種演算法。

WebGL渲染管線

WebGL 是 canvas的一種渲染上下文(context),canvas有兩種context:2D和WebGL。二者沒有任何關係,相同點是都需要藉助canvas輸出影像。目前大部分瀏覽器都支援 WebGL1.0,對 WebGL 2.0 的相容很不理想,下文的討論都是針對 1.0 版本。

下面這段程式碼是建立WebGL 上下文的API以及幾個常用配置項:

const canvas = <htmlcanvaselement>createElement('canvas');
const gl: WebGLRenderingContext = canvas.getContext("webgl",{
  // 是否開啟自動抗鋸齒,建議關閉,瀏覽器相容性差開了也沒用,就算有用效能也很差(因為瀏覽器用的抗鋸齒演算法是效果很好同時效能很差的一種),大多是自己寫程式碼實現
antialias: false,
// 是否開啟透明通道,一般建議關閉,效能損耗嚴重,自己寫程式碼根據透明值計算出混合色值更高效。如果開啟的話,對研發人員的技術能力有更高要求
alpha: false,
// 是否開啟 stencil(模板) 緩衝區支援,資料量大的應用建議開啟,配合stencil test能夠減少無效渲染
stencil: true,
// 是否開啟 depth(深度) 快取區支援,簡易的webgl地圖基本用不到depth test,一般是關閉的。像mapbox這類複雜的webgl地圖引擎是開啟的
depth: false
});

WebGL 中有幾個核心概念:

  • shader - 著色器,分為兩種:
    • vertex shader - 頂點著色器,用於確定圖元頂點的座標;
    • fragment shader - 片段著色器,用於處理光柵化之後的點陣畫素資訊,包括色值、透明度等等。

除了以上兩種shader以外,OpenGL 還支援 geometry shader-幾何著色器,不過也不常用。WebGL不支援幾何著色器,

  • program(沒有準確翻譯),用於繫結(attach)兩種著色器。

基於上面的幾個核心概念,WebGL 執行渲染的API呼叫流程是:分別建立兩種shader -> 建立一個program -> 將program與兩個shader繫結 -> 連結(link)program ->啟用(use)program -> 傳參給shader -> 傳值&渲染。如下:

// 1.1-建立vertex shader instance
const vShader:WebGLShader = gl.createShader(gl.VERTEX_SHADER);
// 1.2-指定vertex shader源-vShadersStr,字串格式
gl.shaderSource(vShader, vShadersStr);
// 1.3-編譯vertex shader
gl.compileShader(vShader);
// 2.1-建立fragmentshader instance
constfShader:WebGLShader = gl.createShader(gl.FRAGMENT_SHADER);
// 2.2-指定fragmentshader源-fShadersStr,字串格式
gl.shaderSource(fShader,fShadersStr);
// 2.3-編譯fragmentshader
gl.compileShader(fShader);
// 3-建立program
const program: WebGLProgram = gl.createProgram();
// 4-繫結program與兩個shader
gl.attachShader(program, vShader);
gl.attachShader(program, fShader);
// 5-連結program
gl.linkProgram(program);
// 6-啟用program
gl.useProgram(program);
// 7-傳值&渲染相關API下文再講

接下來就是傳值和執行渲染,這部分需要了解WebGL shader中的三種變數型別:

  • attribute變數是由JavaScript API 傳給頂點著色器的資料,術語為vertexBufferObject-VBO,顧名思義是一種二進位制的buffer,在JavaScript中的表達是型別陣列-TypedArray。根據精度的不同需求最常用的有Float32ArrayUint8Array。attitude主要是包含頂點座標,但是並沒有嚴格的限制,可以傳遞任何其他用途的資料,比如色值-color,前提是資料精度相同;
  • uniform變數也是由JavaScript API傳遞給著色器,不過可以同時被頂點和片段著色器訪問,通常用於傳遞所有頂點共用的資料,比如MVP矩陣(下文介紹)、畫布解析度、色值等等。uniform不是常量,著色器中有常量的定義規範-defined,語法類似C++如下:
#define PI 3.1415926538
  • varying變數不是由JavaScript API傳入著色器,而是在頂點著色器中根據其他資料(attribute/uniform/defined)計算出來,然後傳遞給片段著色器中同名varying變數。目的有兩種:
    • 減少GPU的計算壓力。因為頂點著色器只會計算指定圖元的頂點數量,而片段著色器需要在圖元覆蓋的所有畫素點都計算一次;
    • 片段著色器無法訪問attribute資料,varying變數可以傳遞一些與attribute相關的資料。

結合上文的幾種變數型別,WebGL的渲染流程大致如下圖所示(條紋框表示GPU內部流程,開發者無法干預):

圖片

  1. 在CPU側(也就是JavaScript側)計算出必要的資料,包括VBO和uniform,然後傳遞給著色器;
  2. 頂點著色器計算出制定圖元的頂點座標和必要的varying變數;
  3. 接下來是開發者不可控的GPU內部邏輯,包括圖元裝配和光柵化:
    1. 圖元裝配:根據JavaScript呼叫的繪圖API所指定的圖元型別(點/線段/三角形)和頂點座標組裝成對應的幾何圖形;
    2. 光柵化:將裝配好的幾何圖形轉化為二維影像,影像中的每個點都對應一個物理畫素點,叫做片元或片段(fragment);
  4. 片段著色器在圖元覆蓋的畫素點依次計算出色值結果;
  5. 接下來是測試混合(Test&Blending)階段,之後會生成幀快取FBO,這部分也是開發者不可控的;
  6. 最後電子螢幕取幀快取資料進行展示。

圖片

MVP矩陣

簡單聊一下上文提到的 MVP 矩陣,細節的技術實現方案後續的分享中再說。

MVP 矩陣是仿射變換過程中三種變換矩陣的統稱:

  • M代表Model,Model矩陣即模型矩陣,可以簡單理解為圖形本身的變換矩陣,經過Model矩陣變換後得到頂點在世界空間中的座標值;
  • V代表View,View矩陣即觀察矩陣,作用是將世界空間的頂點座標對映到可以簡單理解為攝像機(即觀察者,camera是一個抽象物件)為中心的觀察空間中;
  • P代表Projection,Projection矩陣即投影矩陣,圖形程式設計中兩種投影方式:正向投影和透視投影。Projection矩陣的作用是將觀察空間的三維座標對映到二維的裁剪空間中,可以理解成將三維的圖形投影到二維的畫布上。

頂點的原始座標需要依次經過Model矩陣、View矩陣和Projection矩陣變換(左乘)之後才能夠得到它在裁剪空間中的最終座標值。如下程式碼所示:

precision mediump float;
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform mat4 u_mMatrix;
uniform mat4 u_vMatrix;
uniform mat4 u_projMatrix;
void main() {
    position = (u_vMatrix*u_mMatrix*vec4(a_position,0,1)).xy;
    
    gl_Position = u_projMatrix*vec4((position / u_resolution * 2.0 - 1.0)*vec2(1,-1), 0, 1);
}

上面程式碼中的u_resolution是畫布的尺寸,Model和View矩陣的數值一般是與畫布的座標使用相同的計量單位(px),Projection矩陣一般是歸一化的矩陣。

三種矩陣在數學上沒有區別只是計算邏輯上的三種抽象,都是4*4矩陣,都可以包含位移、縮放、斜切等形變資訊。一般Projection矩陣是單獨的,Model和View矩陣可以分開也可以在CPU側計算之後得到一個Model&View矩陣再傳入頂點著色器。

WebGIS常用演算法

最後這部分介紹兩種 WebGIS 領域常用的演算法,準確地說應該是 WebGIS 繪圖領域,一種是多邊形三角剖分演算法,一種是R-Tree演算法。這兩種演算法與 WebGIS本身並沒有太大關係,屬於計算機圖形學通用的演算法。

三角剖分演算法

計算機圖形學中只有三種基本圖元:點、線段、三角形。點和線段的適用面很窄,極少被使用,

繪圖過程中絕大部分的圖形底層都是一個個三角形組成的,如下圖所示:

圖片圖片

喜歡玩3D遊戲的人可能知道,建模對遊戲的視覺效果影響很大,除了模型本身的設計風格以外,建模的精細度也很重要,而衡量精細度的核心指標之一便是三角形的數量。雖然數量不是唯一指標,但細緻的3D模型的三角形數量一定非常龐大,一般數量越多,模型的邊緣越平滑,視覺效果越好。反面例子比如下圖展示學動畫三年系列,人物(姑且算是個人吧)模型邊緣有非常明顯的稜角,過渡非常不順滑。

圖片

回到 WebGIS 領域,我們看到的電子地圖是由一個個不規則的多邊形(Polygon)和線(Line)組成,三角剖分演算法的作用就是把這些多邊形分割成一個個三角形,然後才能夠被 WebGL 繪製出來。

其實線也是多邊形,因為 WebGL 1.0 不支援寬於1畫素的線,所以寬線必須以多邊形的形式繪製。

圖片圖片

三角剖分演算法有兩種型別,一種是多邊形三角剖分,一種是點集三角剖分,後者在圖形程式設計領域不常用,我們只需要關注多邊形三角剖分。

三角剖分是典型的動態規劃演算法,對於多邊形三角剖分最簡單的場景就是三個點,也就是三角形,這種根本不需要分割。再複雜一點就是矩形,前端小夥伴們可以想像一下我們常用的 CSS盒子,html佈局就是一個個矩形拼起來的,對於一個矩形來說需要2個三角形組成。然後依次再遞增多邊形的頂點個數,比如6個:

圖片

這時候需要4個三角形。

很細節的演算法實現就不講了,其實我也沒搞太懂哈哈。對於前端工程師來說,從零實現這套演算法的代價太大,更別提還要很細化地調優,我們直接使用經過大量實踐驗證的開源演算法和工具就可以了。WebGL圖形程式設計常用的三角剖分工具是Libtess,這套演算法也是OpenGL程式設計常用的,非常高效。

R-Tree演算法

R-Tree是一種樹狀資料結構,在 GIS領域主要用於空間資料的儲存。在繪圖方面,R-Tree較多地被用於圖形衝突檢測。

柵格地圖的POI點座標是在瓦片預處理過程中被計算好的,哪個顯示哪個不顯示都被預定義好了,前端拿到資料之後按照既定的座標渲染出來即可。而向量地圖則不然,前文提到,向量地圖實際上就是讓WebGL幹了OpenGL的活,不單是繪圖,繪圖過程中的任何事情都變成了前端的事情,POI衝突檢測就是其中一項。

先看下面這張圖:

圖片

圖中有兩個POI點:微電子與納電子學系(下文簡稱POI點A)和超導量子資訊處理實驗室(下文簡稱POI點B),每個點都有圖示和文字兩部分,點A和點B的文字都位於圖示的下方。

POI有一個「權重-rank」的屬性,繪圖時要保障權重高的優先渲染,如果畫布空間有限則要合理地調整低權重POI的佈局甚至不渲染。仍然以上圖為例,假設點A的權重高於點B:

  1. 先渲染點A,圖示必須渲染出來;
  2. (偽)隨機選一個方位放置文字,圖中選的是圖示下方;
  3. 渲染點B,點B的圖示與點A的圖示和文字都不衝突,正常渲染;
  4. 渲染點B的文字,可選四個方位-上下左右(複雜情況下可選八個方位),使用R-Tree描述文字的矩形盒子,檢測發現上左右都會與點A的文字發生位置衝突,只有下方可行。

以上便是使用R-Tree進行位置衝突檢測的簡易流程。除了POI位置檢測以外,繪圖中R-Tree另一個使用場景是道路名稱的位置標註演算法,如下圖中的「雙清路」「荷清路」文字:

圖片

R-Tree衝突檢測的開源工具推薦rbush

POI的位置佈局(POI Placement)演算法也是單獨的一項研究課題,有大量論文,大家有興趣可以自行查閱相關資料。

其實R-Tree不僅僅適用於圖形程式設計,在常規前端領域也有可借鑑的場景。比如下圖展示的一個報表看板:

圖片

圖中的佈局亂了,報表之間存在遮擋情況,如果這種情形需要前端實現一個自動佈局,也就是圖中的「一鍵美化」功能,你可能考慮怎麼辦?

這時候就可以嘗試用R-Tree解決,每個報表的容器都是一個個矩形盒子,使用rbush可以檢測出所有矩形的衝突情況,然後再嘗試自動調整佈局直到rbush檢測不衝突為止。R-Tree提供了一種解決思路和搭配的工具,在此基礎之上可以進一步完善細化的佈局調整邏輯。

三 總結

以上是今天分享的全部內容,簡單總結一下。

第一部分介紹了 WebGIS 領域的一些基礎知識,包括座標體系、製圖繪圖流程和路網結構。對於日常工作中涉及地圖的專案,對這些基礎知識有個大概瞭解可以對工作有輔助作用比如技術評審。

第二部分介紹了兩種地圖型別以及向量地圖所使用的圖形技術WebGL,簡單分享了WebGL的渲染管線和常用的兩種演算法。電子地圖不像遊戲、動畫等高複雜度圖形應用對WebGL技術有很苛刻的要求,地圖引擎頂多發揮了WebGL 三分之一的能力,我們日後在資料視覺化方面的技術需求,可能涉及WebGL的部分甚至不如地圖那麼複雜,所以今天我們對WebGL先有一個大概的認知,後續再一步步學習內部的細節知識。

常用開源工具

參考材料

  1. WebGL渲染管線
  2. 使用Actor模型管理Web Worker多執行緒