乐趣区

关于javascript:译How-I-built-a-wind-map-with-WebGL

引子

对风场可视化的成果感兴趣,搜材料的时候发现这篇文章,读了后感觉翻译一下以便再次查阅。

  • 原文:How I built a wind map with WebGL
  • Origin
  • My GitHub

注释

查看我的基于 WebGL 的风场模仿演示!让咱们深刻理解它的工作原理。

我要坦率的说:在 Mapbox 工作的最初几年里,我像规避瘟疫一样防止间接的 OpenGL/WebGL 编程。起因之一:OpenGL API 和术语让我深感恐怖。它看起来总是那么简单、凌乱、俊俏和简短,以至于我永远都无奈投入其中。只有听到 stencil masks、mipmap、depth culling、blend functions、normal maps 等术语,我就会有一种不安的感觉。

往年,我最终决定直面我的恐怖,应用 WebGL 构建一些有意义的货色。2D 风场模仿看起来是一个完满的机会——它很有用,视觉上令人惊叹,而且具备挑战性,但在能力范畴内感觉依然能够实现。我诧异地发现,它远没有看上去那么可怕!

基于 CPU 的风场可视化

网上有很多风场可视化的例子,但最受欢迎和最有影响的是 Cameron Beccario 的驰名我的项目 earth.nullschool.net。它自身不是开源的,但它有一个旧的开源版本,大多数其它实现都是基于这个版本编写代码的。一个驰名的开源派生是 Esri Wind JS。应用该技术的风行气象服务包含 Windy 和 VentuSky。

通常,浏览器中的这种可视化依赖于 Canvas 2D API,大抵如下所示:

  1. 在屏幕上生成一组随机粒子地位并绘制粒子。
  2. 对于每个粒子,查问风的数据以获取其以后地位的粒子速度,并相应地挪动它。
  3. 将一小部分粒子重置到随机地位。这样能够确保风吹走的区域永远不会齐全变空。
  4. 逐步淡出以后屏幕,并在顶部绘制新定位的粒子。

这样做会有随之而来的性能限度:

  • 风粒子的数量需放弃较低(例如,地球示例应用~5k)。
  • 每次更新数据或视图时都会有很大的提早(例如,地球示例大概 2 秒),因为数据处理老本很高,而且产生在 CPU 端。

此外,要将其集成为基于 WebGL 的交互式地图(如 Mapbox)的一部分,你必须在每一帧上将画布元素的像素内容加载到 GPU,这将大大降低性能。

我始终在寻找一种办法,用 WebGL 在 GPU 端从新实现残缺的逻辑,这样它会很快,可能绘制数百万个粒子,并且能够集成到 Mapbox GL 地图中,而不会造成很大的性能损失。侥幸的是,我偶尔发现了 Chris Wellons 所写对于 WebGL 中粒子物理 的精彩教程,并意识到风场可视化能够应用雷同的办法。

OpenGL 根底

令人困惑的 API 和术语使得 OpenGL 图形编程十分难以学习,但从外表上看,这个概念非常简单。这里有一个实用的定义:

OpenGL 为高效绘制三角形提供了 2D API。

所以基本上你用 GL 所做的就是画三角形。除了可怕的 API 之外,艰难还来自于执行此操作所需的各种数学和算法。它还能够绘制点和根本线(无平滑或圆形连贯 / 封口),但很少应用。

OpenGL 提供了一种非凡的类 C 语言—— GLSL ——来编写由 GPU 间接执行的程序。每个程序分为两局部,称为着色器——顶点着色器和片元着色器。

顶点着色器 提供用于转换坐标的代码。例如,将三角形坐标乘以 2,使咱们的三角形看起来两倍大。咱们在绘图时传递给 OpenGL 的每个坐标都将运行一次。一个根本的例子:

attribute vec2 coord;
void main() {gl_Position = vec4(2.0 * coord, 0, 1);
}

片元着色器 提供用于确定每个绘制像素色彩的代码。你能够用它做很多很酷的数学运算,但最终它相似“把三角形的以后像素画成绿色”。示例:

void main() {gl_FragColor = vec4(0, 1, 0, 1);
}

在顶点着色器和片元着色器中,都能够做的一件很酷的事件是增加一个图像(称为纹理)作为参数,而后在该图像的任何点中查找像素色彩。咱们将在风场可视化中很依赖这个。

片元着色器代码的执行是大规模并行的,并且硬件加速很快,因而通常比 CPU 上的等效计算快很多数量级。

获取风场数据

美国国家气象局每 6 小时公布一次寰球天气数据,称为 GFS,以纬度 / 经度网格的模式公布相干数值(包含风速)。它以一种称为 GRIB 的非凡二进制格局编码,能够应用一组非凡的工具将其解析为人类可读的 JSON。

我编写了几个小脚本,下载并将风数据转换成一个简略的 PNG 图像,风速编码为 RGB 色彩——每个像素的程度速度为红色,垂直速度为绿色。看起来是这样的:

你能够下载更高分辨率的版本(2x 和 4x),但 360×180 网格对于低缩放可视化来说曾经足够了。PNG 压缩非常适合这种数据,下面的图像通常只有 80 KB 左右。

基于 GPU 挪动粒子

现有风场可视化将粒子状态存储在 JavaScript 数组中。咱们如何在 GPU 端存储和操作该状态?一种称为计算着色器(在 OpenGL ES 3.1 和等效的 WebGL 2.0 标准中)的新 GL 性能容许你在任意数据上运行着色器代码(无需任何渲染)。可怜的是,跨浏览器和挪动设施对新标准的反对十分无限,因而咱们只剩下一个实用选项:纹理。

OpenGL 不仅容许你绘制屏幕,还容许绘制纹理(通过称为帧缓冲区的概念)。因而,咱们能够将粒子地位编码为图像的 RGBA 色彩,将其加载到 GPU,在片着色器中依据风速计算新地位,将其从新编码为 RGBA 色彩,并将其绘制到新图像中。

X 和 Y 为了存储足够的精度,咱们将每个组件存储在两个字节中——别离为 RG 和 BA,为每个组件提供 65536 个不同值的范畴。

一个 500×500 的示例图像将包容 250000 个粒子,咱们将应用片元着色器挪动每个粒子。生成的图像如下所示:

以下是在片元着色器中如何从 RGBA 中解码和编码地位:

// lookup particle pixel color
vec4 color = texture2D(u_particles, v_tex_pos);
// decode particle position (x, y) from pixel RGBA color
vec2 pos = vec2(
    color.r / 255.0 + color.b,
    color.g / 255.0 + color.a);
... // move the position
// encode the position back into RGBA
gl_FragColor = vec4(fract(pos * 255.0),
    floor(pos * 255.0) / 255.0);

在下一帧中,咱们能够将这个新图像作为以后状态,并将新状态绘制到另一个图像中,以此类推,每帧替换两个状态。因而,借助两个粒子状态纹理,咱们能够将所有风模仿逻辑挪动到 GPU。

这种办法的速度十分快,咱们不须要在浏览器上每秒更新 5000 个粒子 60 次,而是能够忽然解决 一百万个

须要记住的一点是,在两极左近,粒子沿 X 轴的挪动速度应该比赤道上的粒子快得多,因为雷同的经度示意的间隔要小得多。以下着色器代码能够解决这一点:

float distortion = cos(radians(pos.y * 180.0 - 90.0));
// move the particle by (velocity.x / distortion, velocity.y)

绘制粒子

正如我后面提到的,除了三角形,咱们还能够绘制根本的点——很少应用,但非常适合这样的 1 像素粒子。

要绘制每个粒子,咱们只需在顶点着色器中的粒子状态纹理上查找其像素色彩以确定其地位;而后通过从风纹理查找其以后速度来确定片元着色器中的粒子色彩;最初将其映射到一个丑陋的色彩突变(我从牢靠的 ColorBrewer2 中抉择色彩)。在这一点上,它看起来是这样的:

如果有点空隙,那是一些货色。但单凭粒子静止很难取得风向感。咱们须要增加粒子轨迹。

绘制粒子轨迹

我尝试的第一种绘制轨迹的办法是应用 WebGL 的 PreserveDrawingBuffer 选项,它使屏幕状态在帧之间放弃不变,这样咱们能够在粒子挪动时在每一帧上重复绘制粒子。然而,这个 WebGL 个性是一个微小的性能打击,许多文章倡议不要应用它。

相同,与应用粒子状态纹理的形式相似,咱们能够将粒子绘制到纹理中(该纹理顺次绘制到屏幕上),而后在下一帧上应用该纹理作为背景(略微变暗),并每一帧替换输出 / 指标纹理。除了更好的性能之外,这种办法的一个长处是咱们能够将其间接移植到本机代码(没有与 preserveDrawingBuffer 等效的代码)。

风场插值查找

在纬度 / 经度栅格上,风数据针对特定点有对应的值,例如(50,30)、(51,30)、(50,31)、(51,31)天文点。如何取得任意两头值,例如(50.123,30.744)?

OpenGL 在查找纹理色彩时提供自带插值。然而,它依然会导致块状、像素化的图案。以下是在缩放时,在风纹理中这些瑕疵的示例:

侥幸的是,咱们能够通过在每个风探测器中查找 4 个相邻像素,并在片元着色器中的本地像素上对其进行手动双线性插值计算,来平滑瑕疵。它的老本更高,但修复了瑕疵并产生更晦涩的风场可视化。以下是与此技术雷同的区域:

GPU 上的伪随机生成器

还有一个辣手的逻辑须要在 GPU 上实现——随机重置粒子地位。如果不这样做,即便是大量的风粒子也会变为屏幕上的几行,因为风吹走的区域会随着工夫变空:

问题是着色器没有随机数生成器。咱们如何随机决定粒子是否须要重置?

我在 StackOverflow 上找到了一个解决方案——一个用于生成伪随机数的 GLSL 函数,它承受一对数字作为输出:

float rand(const vec2 co) {float t = dot(vec2(12.9898, 78.233), co);
    return fract(sin(t) * (4375.85453 + t));
}

这个奇异的函数依赖于 sin 的后果变动。而后咱们能够这样做:

if (rand(some_numbers) > 0.99)
    reset_particle_position();

这里的挑战在于为每个粒子抉择一个足够“随机”的输出,以便生成的值在整个屏幕上是统一的,并且不会显示奇怪的图案。

应用以后粒子地位作为源并不完满,因为雷同的粒子地位将始终生成雷同的随机数,因而某些粒子将在同一区域隐没。

应用在状态纹理中的粒子地位也不起作用,因为雷同的粒子将始终隐没。

我最终失去的后果取决于粒子地位和状态地位,再加上在每一帧上计算并传递给着色器的随机值:

vec2 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 是能够在最终可视化中调整的参数。以下是它如何影响后果的示例:


下一步是什么?

后果是一个齐全由 GPU 驱动的风场可视化,能够以 60fps 的速度渲染一百万个粒子。试着在演示中应用滑块,并查看最终的代码——总共大概 250 行,我致力使其尽可能的可读。

下一步是将其集成到能够摸索的实时地图中。我在这方面获得了一些停顿,但还不足以分享一个实时演示。这里有一些局部片段:

感激你的浏览,请持续关注更多更新!如果你错过了,请查看我上一篇对于空间算法的文章。

非常感谢我的 Mapbox 队友 kkaefer 和 ansis,他们急躁地答复了我所有对于图形编程的傻问题,给了我很多贵重的提醒,帮忙我学到了很多货色。❤️

参考资料

  • How I built a wind map with WebGL
退出移动版