[譯] 如何使用 WebGL 技術進行風力地圖視覺化
翻譯:@四季留歌
部分翻譯。原文:https://blog.mapbox.com/how-i-built-a-wind-map-with-webgl-b63022b5537f
目錄
如果使用 CPU 進行風向視覺化: 慢
有很多風力視覺化線上網站,最出名的莫過於 earth.nullschool.net。它並不是開源的,但是它有一個開源的舊版本,大多數現有方案都基於此實現。
通常,這種視覺化依賴於 Canvas 2D API
,大致的邏輯是這樣的:
- 生成一組隨機粒子並繪製它們
- 對於每個粒子,查詢其位置上的風力值,並用此風力值移動此粒子
- 將一小部分粒子重置,這樣能保證粒子所覆蓋的區域看起來比較豐滿
- 淡化當前幀,並在其上一層繪製新的一幀
這是有效能限制的:
- 風粒子數量不能太多,大約 5000 個
- 每次更新資料或者檢視都會有很大的延遲,因為處理這些資料所用的 JavaScript 執行在 CPU 上,相當耗時
原作者在文章中提出一種使用 WebGL 的新繪製邏輯,這樣速度很快,能畫上百萬個粒子,而且打算和 Mapbox GL 進行結合展示。
原作者找到了 Chris Wellons 寫的關於如何使用 WebGL 粒子物理學的絕棒教程,認為風力視覺化可以使用類似的方。
OpenGL 基礎
簡單概括原文就是,OpenGL 等圖形技術就是在畫三角形,雖然也能畫點和線,但是用的比較少。
這節的重點是,在頂點著色器或者片元著色器新增一個紋理引數,然後在這個紋理上查詢顏色。
這是本文關於風力視覺化的重中之重。
獲取風力資料
美國國家氣象局每 6 個鐘釋出全球天氣資料,這種資料叫做 GFS
,大概就是經緯度的網格攜帶了有關的資料值。這種資料稱為 GRIB,是一種特殊的二進位制格式。可以用一些其他的工具解析為人類可讀的 JSON(工具)。
原作者寫了一些指令碼,將風力資料下載下來並轉為 PNG 影象,風速編碼為 RGB 色彩灰度值 —— 畫素座標代表經緯度,紅色灰度值代表水平風速,綠色灰度值代表垂直風速。大概長這樣:
解析度可以更大,但是原作者認為全球視覺化來說,這個解析度夠用了。
使用 GPU 移動粒子
風粒子是存在 JavaScript 陣列中的,如何通過 GPU 運算去操作這些粒子物件?或許可以上計算著色器,但是裝置相容性會成大問題。
所以只能是這個選擇:使用紋理。
OpenGL 規範不僅僅可以把 GPU 計算的結果畫到螢幕上,還能把它畫到紋理上(這個紋理有個特別的名字,叫幀快取)。
因此,可以把粒子的座標編碼為 RGBA 值,然後傳遞到渲染管線中進行計算,計算完畢後,再編碼到 RGBA 並繪製為新的影象。
為了滿足 X 和 Y 座標的精度,物盡其用,R和G通道這兩個位元組儲存 X,B和A通道則儲存 Y。那麼,\(2^{16}=65536\) 個數字給到每個數字,應該夠了。
一張解析度為 500×500 的影象可以儲存 25w 個粒子:
在片元著色器裡操作這些畫素代表的粒子即可。
下列是從 RGBA 四個通道灰度值解碼、編碼的 glsl 程式碼:
// 從粒子座標紋理中取色值
vec4 color = texture2D(u_particles, v_tex_pos);
// 從 rgba 灰度值解碼成座標
vec2 pos = vec2(
color.r / 255.0 + color.b,
color.g / 255.0 + color.a
);
// ... 此處可以寫移動粒子的座標
// 編碼座標成 RGBA 灰度值
gl_FragColor = vec4(
fract(pos * 255.0),
floor(pos * 255.0) / 255.0
);
在下一幀,就可以繪製出這一個影象來。
每一幀,重複這個過程,即,只需兩個紋理物件,交替計算和繪製就可以實現將風場模擬計算轉移到 GPU 上來。
在極點附近的粒子和赤道附近的粒子相比,沿著 X 軸移動的速度會快得多,因為同樣經度(X軸是緯線)跨度,赤道跨過的距離和極點跨過的距離是不一樣的。
通過下列著色器進行改進:
float distortion = cos(radians(pos.y * 180.0 - 90.0));
// 這樣,使用向量 (velocity.x / distortion, velocity.y) 移動粒子即可
繪製粒子
雖然 WebGL 大多數時候適合繪製三角形,但是這個場景下,繪製點就很合適了。
在頂點著色器中,對粒子紋理進行取樣,以獲取其座標,然後,在風速紋理上取樣獲取 u 和 v 值,計算其風速(\(speed^2=u^2+v^2\)),然後將這個風速對映到漸變色帶上,以進行著色。
此時,大概是這樣的:
還行。看起來有點空空的,沒有風的感覺,需要繪製軌跡線來完成視覺化。
繪製粒子軌跡
繪製粒子到一個紋理上,然後在下一幀時,將其作為背景(略微變暗),並將另一張在上一幀已經用完的紋理設為本幀的繪製目標,實現交換繪製。
插值以獲取風力值
風速資料是經緯網格上特定格網點的一些正北正東向的速度值。例如 (50°N, 30°E)
、(51°N, 30°E)
、(50°N, 31°E)
、(51°N, 31°E)
等。那麼,如何獲取位於這四個點之間的中間值,例如 (50.123°N, 30.744°E)
?
使用 texture2D
函式取樣時,OpenGL 會幫你完成這事兒。
但是,風速資料那張紋理圖片放大後鋸齒、馬賽克效應很明顯,大概這樣:
使用 雙線性插值 演算法,額外獲取某個點附近的 4 個點,可以插值得到比較平滑的結果,這個可以在片元著色器上完成,效果如下:
使用 GPU 上的偽隨機演算法
在著色器程式中還有一個棘手的邏輯要實現,那就是粒子繪製完後,要重置它,如何隨機重置呢?
先說明,著色器程式是沒有內建的隨機數生成器的。但是在 StackOverflow 上有一個用於生成偽隨機數的函式:
float rand(const vec2 co) {
float t = dot(vec2(12.9898, 78.233), co);
return fract(sin(t) * (4375.85453 + t));
}
有了這個函式,就可以判斷是否需要重置粒子狀態了:
if (rand(some_numbers) > 0.99)
reset_particle_position();
難點在於,如何讓粒子重置的時候重置到一個足夠隨機的位置。
直接用粒子的座標是不行的,因為相同的粒子的座標總是會得到一樣的隨機數,即在哪兒產生,就在哪兒消失。
最終,作者決定使用三個值作為隨機數的輸入二維向量:
vec2 seed = (pos + v_tex_pos) * u_rand_seed;
其中,pos
是粒子當前座標,v_tex_pos
是粒子原始座標,u_rand_seed
是每一幀中計算得到的隨機值。
仍舊存在一個小問題,那就是粒子的速度非常快的區域看起來會很稠密,可以通過設定一個“重置率”數值來實現整體平衡:
float dropRate = u_drop_rate + speed_t * u_drop_rate_bump;
其中,speed_t
是一個介於區間 (0, 1)
之間的相對值,u_drop_rate
和 u_drop_rate_bump
是自由調節的兩個引數。
展望
作者感謝的話。不翻譯了。