翻譯:@四季留歌
部分翻譯,原文:https://blog.mapbox.com/how-i-built-a-wind-map-with-webgl-b63022b5537f
目錄
- 如果使用 CPU 進行風向可視化: 慢
- OpenGL 基礎
- 獲取風力資料
- 使用 GPU 移動粒子
- 繪制粒子
- 繪制粒子軌跡
- 插值以獲取風力值
- 使用 GPU 上的偽隨機演算法
- 展望
如果使用 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 是自由調節的兩個引數,
展望
作者感謝的話,不翻譯了,
附贈源代碼:https://github.com/mapbox/webgl-wind
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/296162.html
標籤:其他
