WebGL风向图

前面关于webgl的基本知识点就没翻译,原文地址


概述

制作风向图通常步骤
1. 在屏幕上生成一系列随机粒子位置并绘制粒子。
2. 对于每一个粒子,查询风数据以获得其当前位置的粒子速度,并相应地移动它。
3. 将一小部分粒子重置为一个随机的位置。这就确保了风从不会变成空的区域。
4. 淡出当前屏幕,并在顶部绘制新定位的粒子。

这有后续的性能限制:
* 风粒子的数量应该保持在低水平(例如地球使用<5k)。
* 每次更新数据或查看(例如2秒),都有很大的延迟,因为数据处理开销很大,而且CPU方面也发生了变化。

此外,将它作为一个交互式基于webgl的地图(如Mapbox)的一部分进行集成,您必须在每个框架上将画布元素的像素内容上载到GPU上,这将大大降低性能。

获取风向数据

每隔6个小时,美国国家气象局就会以纬度/经度网格(包括风速)的形式,发布全球的气象数据,即GFS。它以一种特殊的二进制格式编码,称为GRIB,它可以用一组特殊的工具解析成人类可读的JSON。

我编写了一些小脚本,下载并将风数据转换成一个简单的PNG图像,以RGB颜色为风速编码。在每个像素中,水平速度为红色,垂直速度为绿色。它看起来像这样:

WebGL风向图_第1张图片

有更高分辨率的版本可以下载(2x和4x),但360×180网格足够low-zoom可视化。PNG压缩非常适合这种类型的数据,上面的图像通常大小约80KB。

在GPU上移动粒子

现有的风可视化粒子状态存储在JavaScript数组中。我们如何在GPU方面存储和操纵这个状态?一个新的GL特性,叫做计算着色器(存在于OpenGL ES 3.1和WebGL 2.0规范)允许您在任意数据上运行shader代码(不需要任何呈现)。不幸的是,在浏览器和移动设备上对新规范的支持是很弱的,因此我们只剩下一个实用的选项:纹理。

OpenGL不仅允许您绘制屏幕,而且还可以绘制纹理(通过一个称为framebuffer的概念)。因此,我们可以将粒子位置编码为RGBA颜色的图像,将其加载到GPU上,根据片段着色器中的风速计算新的位置,再将它们编码成RGBA颜色,并将其绘制成新的图像。

为了存储足够精度的X和Y分量,我们分别将每个分量存储在两个字节中,分别为RG和BA,为每个分量提供了65536个不同的值。
WebGL风向图_第2张图片

一个500 × 500的示例图像将要加载250,000个粒子,我们会用片段着色器移动每个粒子。所生成的图像将如下所示:
WebGL风向图_第3张图片

下面的代码是从RGBA中如何解码位置,和在片段着色器中如何再编码成RGBA:

// 查找粒子像素的颜色
vec4 color = texture2D(u_particles, v_tex_pos);

// 从像素RGBA颜色中解码粒子位置(x,y)
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上。

这种方法非常快————之前在浏览器上每秒只更新5000个粒子60次,现在我们可以处理100万个。

要记住的一件事是,在两极附近,与赤道上的粒子相比,粒子的运动速度要快得多,因为相同的经度度代表的距离要小得多。这可以考虑到以下的着色代码:

float distortion = cos(radians(pos.y * 180.0 - 90.0));
// 通过(velocity.x / distortion, velocity.y)来移动粒子

绘制粒子

正如我前面提到的,除了三角形之外,我们还可以绘制基本点——很少使用,但对于像这样的1像素粒子来说是完美的。

为了绘制每一个粒子,我们只需在顶点着色器的粒子状态纹理上查找它的像素颜色来确定它的位置;然后通过从风的纹理中查找当前的速度来确定片元着色器中的粒子颜色;最后把它映射到一个漂亮的颜色渐变(我从 ColorBrewer2中挑选了颜色)。在这一点上,它看起来是这样的:
WebGL风向图_第4张图片

但单靠粒子运动很难获得风的方向。我们需要添加粒子轨迹。

绘制粒子轨迹

我尝试使用的第一个方法是使用preserveDrawingBuffer这个WebGL选项,它使屏幕状态在帧之间保持,这样我们就可以在每一帧移动的时候,在每一帧上画出粒子。然而,这个WebGL特性是一个巨大的性能冲击,许多WebGL文章建议不要使用它。

相反,类似于我们如何处理粒子状态纹理,我们可以将粒子画成纹理(这将会被绘制到屏幕上),然后在下一帧上使用这个纹理作为背景(稍微暗一点),并将输入/目标纹理交换到每一帧。除了更好的性能外,这种方法的优点是我们可以将它直接移植到原生代码(它没有perservedrawingbuffer相同的效果)。

风属性查找插值

WebGL风向图_第5张图片
风数据对纬度/经度网格的特定点有值,例如(50,30),(51,30),(50,31),(51,31)地理位置。如何得到一个任意的中间值,例如(50.123,30.744)?

当你查找纹理颜色的时候OpenGL自带插值。然而,它仍然导致了块状、像素化的模式。这里有一个例子,在风纹理中缩放的时候:
WebGL风向图_第6张图片
原生GL的线性插值

幸运的是,我们可以通过在每个风测点中查找4个相邻像素来平滑这些工件,并在原生的一个片段着色器上做手工双线性插值运算。它虽然代价更高,但修复了问题,并可以实现更流畅的风可视化。
WebGL风向图_第7张图片
在片元着色器上的双线性插值

在GPU上的伪随机生成器

在GPU上实现的一个棘手的逻辑是随机重新设置粒子位置。如果没有这个,即使是大量的风粒子也会退化成屏幕上的几条线,因为风吹过的区域随着时间的推移变得空空如也:
WebGL风向图_第8张图片

问题是,着色器没有随机数生成器。我们如何随机决定一个粒子是否需要重置?

在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是你可以在最终的可视化中进行调整的参数。下面是它如何影响结果的一个例子:
WebGL风向图_第9张图片

WebGL风向图_第10张图片

你可能感兴趣的:(地图技术,webgl)