之前我们介绍了点聚合图的绘制方案:
潘与其:使用 k-d tree 实现点聚合图?zhuanlan.zhihu.com对于每个圆形我们使用了如下的顶点数据:
attribute vec2 a_pos; // 瓦片坐标
attribute float a_radius; // 半径
attribute vec2 a_extrude; // 拉伸后的点阵坐标
attribute vec4 a_color; // 颜色
随著特性的增加,后续需要存储的顶点数据类型也会增多。例如后续增加基于要素的拾取,就需要存储 pickingId。
如果我们能尽量利用 vec4 存储这些顶点数据并采用一定的压缩技术,无疑能减少 CPU 侧向 GPU 侧传递数据的时间并节省大量 GPU 内存。另外,OpenGL 支持的 attribute 数目是有上限的,当然我们这个简单 DEMO 并不会超出。
本文会依次介绍以下内容:
- 压缩颜色、半径以及点阵坐标数据
- 使用 Chrome MemoryInfra 度量 GPU 内存,对比优化前后效果
- Cesium、Mapbox 中的实践,包括对于其他类型数据的压缩方案
压缩方案
首先以下的压缩方案都是需要在 CPU 侧 JS 中压缩,在 vertex shader 中解压。因此必然会牺牲一定运行时性能,但是在地理信息海量要素展示的场景下,换取的 GPU 内存收益是很客观的。
对于颜色数据每个分量其实只需要 8 bits 就够了,因此一个 16-bit float 就可以存储两个分量,这样就只需要 vec2 存储颜色数据,JS 中压缩方法如下:
function packUint8ToFloat(a: number, b: number) {
a = clamp(Math.floor(a), 0, 255);
b = clamp(Math.floor(b), 0, 255);
return 256 * a + b;
}
// vec2
packUint8ToFloat(r, g);
packUint8ToFloat(b, a);
相应的,在 vertex shader 中进行解压:
vec2 unpack_float(const float packedValue) {
int packedIntValue = int(packedValue);
int v0 = packedIntValue / 256;
return vec2(v0, packedIntValue - v0 * 256);
}
vec4 decode_color(const vec2 encodedColor) {
return vec4(
unpack_float(encodedColor[0]) / 255.0,
unpack_float(encodedColor[1]) / 255.0
);
}
这样我们就只需要一个 vec4 存储瓦片坐标和颜色数据:
attribute vec4 a_pos_color; // 瓦片坐标 + 颜色
vec2 tile_pos = a_pos_color.xy;
vec2 color = a_pos_color.zw;
接下来我们还有点阵坐标(vec2)和半径(float),有没有可能只使用一个 float 存储它们呢?
GLSL 中 float 是单精度浮点数[1],即 IEEE-754 single-precision floating point[2]: