翻譯:@四季留歌
部分翻譯。原文: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
是自由調節的兩個參數。
展望
作者感謝的話。不翻譯了。