[譯] 如何使用 WebGL 技術進行風力地圖可視化

翻譯:@四季留歌

部分翻譯。原文:https://blog.mapbox.com/how-i-built-a-wind-map-with-webgl-b63022b5537f



如果使用 CPU 進行風向可視化: 慢

有很多風力可視化在線網站,最出名的莫過於 earth.nullschool.net。它並不是開源的,但是它有一個開源的舊版本,大多數現有方案都基於此實現。

image

通常,這種可視化依賴於 Canvas 2D API,大致的邏輯是這樣的:

  1. 生成一組隨機粒子並繪製它們
  2. 對於每個粒子,查詢其位置上的風力值,並用此風力值移動此粒子
  3. 將一小部分粒子重置,這樣能保證粒子所覆蓋的區域看起來比較豐滿
  4. 淡化當前幀,並在其上一層繪製新的一幀

這是有性能限制的:

  • 風粒子數量不能太多,大約 5000 個
  • 每次更新數據或者視圖都會有很大的延遲,因爲處理這些數據所用的 JavaScript 運行在 CPU 上,相當耗時

原作者在文章中提出一種使用 WebGL 的新繪製邏輯,這樣速度很快,能畫上百萬個粒子,而且打算和 Mapbox GL 進行結合展示。

原作者找到了 Chris Wellons 寫的關於如何使用 WebGL 粒子物理學的絕棒教程,認爲風力可視化可以使用類似的方。

OpenGL 基礎

簡單概括原文就是,OpenGL 等圖形技術就是在畫三角形,雖然也能畫點和線,但是用的比較少。

這節的重點是,在頂點着色器或者片元着色器添加一個紋理參數,然後在這個紋理上查找顏色。

這是本文關於風力可視化的重中之重。

獲取風力數據

美國國家氣象局每 6 個鐘發佈全球天氣數據,這種數據叫做 GFS,大概就是經緯度的網格攜帶了有關的數據值。這種數據稱爲 GRIB,是一種特殊的二進制格式。可以用一些其他的工具解析爲人類可讀的 JSON(工具)。

原作者寫了一些腳本,將風力數據下載下來並轉爲 PNG 圖像,風速編碼爲 RGB 色彩灰度值 —— 像素座標代表經緯度,紅色灰度值代表水平風速,綠色灰度值代表垂直風速。大概長這樣:

image

分辨率可以更大,但是原作者認爲全球可視化來說,這個分辨率夠用了。

使用 GPU 移動粒子

風粒子是存在 JavaScript 數組中的,如何通過 GPU 運算去操作這些粒子對象?或許可以上計算着色器,但是設備兼容性會成大問題。

所以只能是這個選擇:使用紋理。

OpenGL 規範不僅僅可以把 GPU 計算的結果畫到屏幕上,還能把它畫到紋理上(這個紋理有個特別的名字,叫幀緩存)。

因此,可以把粒子的座標編碼爲 RGBA 值,然後傳遞到渲染管線中進行計算,計算完畢後,再編碼到 RGBA 並繪製爲新的圖像。

image

爲了滿足 X 和 Y 座標的精度,物盡其用,R和G通道這兩個字節存儲 X,B和A通道則存儲 Y。那麼,\(2^{16}=65536\)​ 個數字給到每個數字,應該夠了。

一張分辨率爲 500×500 的圖像可以存儲 25w 個粒子:

image

在片元着色器裏操作這些像素代表的粒子即可。

下列是從 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\)),然後將這個風速映射到漸變色帶上,以進行着色。

此時,大概是這樣的:

image

還行。看起來有點空空的,沒有風的感覺,需要繪製軌跡線來完成可視化。

繪製粒子軌跡

繪製粒子到一個紋理上,然後在下一幀時,將其作爲背景(略微變暗),並將另一張在上一幀已經用完的紋理設爲本幀的繪製目標,實現交換繪製。

插值以獲取風力值

風速數據是經緯網格上特定格網點的一些正北正東向的速度值。例如 (50°N, 30°E)(51°N, 30°E)(50°N, 31°E)(51°N, 31°E) 等。那麼,如何獲取位於這四個點之間的中間值,例如 (50.123°N, 30.744°E)

使用 texture2D 函數採樣時,OpenGL 會幫你完成這事兒。

但是,風速數據那張紋理圖片放大後鋸齒、馬賽克效應很明顯,大概這樣:

image

使用 雙線性插值 算法,額外獲取某個點附近的 4 個點,可以插值得到比較平滑的結果,這個可以在片元着色器上完成,效果如下:

image

使用 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_rateu_drop_rate_bump 是自由調節的兩個參數。

展望

作者感謝的話。不翻譯了。

附贈源代碼:https://github.com/mapbox/webgl-wind

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章