之前我们介绍了点聚合图的绘制方案:

潘与其:使用 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 并不会超出。

本文会依次介绍以下内容:

  1. 压缩颜色、半径以及点阵坐标数据
  2. 使用 Chrome MemoryInfra 度量 GPU 内存,对比优化前后效果
  3. 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]

binary32 bits layout

利用好这 24 bits 的精度,我们完全可以将一些特殊类型的数据(int、bool)压缩进来。

例如我们的场景中,点阵坐标 xy 取值只有 -1 和 1,完全可以 + 1 之后(0,2)使用 2 bits 存储。这样每个点阵坐标只需要 4 bits,完全没必要使用 vec2。另外 radius 也不会特别大,16 bits 应该也够用了。因此利用简单的乘法进行位移:

const LEFT_SHIFT18 = 262144.0;
const LEFT_SHIFT20 = 1048576.0;

(extrude[0] + 1) * LEFT_SHIFT20
+ (extrude[1] + 1) * LEFT_SHIFT18
+ radius, // 16 bits

在 shader 中 decode 时也要注意和 encode 顺序保持一致:

attribute float a_packed_data; // radius + extrude.y + extrude.x

#define SHIFT_RIGHT18 1.0 / 262144.0
#define SHIFT_RIGHT20 1.0 / 1048576.0
#define SHIFT_LEFT18 262144.0
#define SHIFT_LEFT20 1048576.0

// unpack data(extrude(4-bit), radius(16-bit))
float compressed = a_packed_data;

// extrude(4-bit)
vec2 extrude;
extrude.x = floor(compressed * SHIFT_RIGHT20);
compressed -= extrude.x * SHIFT_LEFT20;
extrude.x = extrude.x - 1.;

extrude.y = floor(compressed * SHIFT_RIGHT18);
compressed -= extrude.y * SHIFT_LEFT18;
extrude.y = extrude.y - 1.;

// radius(16-bit)
float radius = compressed;
v_radius = radius;

看起来不错,我们将 vec4 + vec2 *2 + float 压缩成了 vec4 + float。当然了解了上述 float unpack 方法我们可以发现 vec4 中存储颜色的两个分量 float(rg) 和 float(ba) 其实也没有充分利用 24 位精度,各还有 8 bits 可以塞入其他数据。

现在我们需要度量优化后的效果,看看到底节省了多少 GPU 内存。

使用 MemoryInfra 度量 GPU 内存

关于 MemoryInfra 的用法我打算单独写一篇文章。从下面的界面中也能看出,要想看懂分析面板中以进程维度展示的内存数据以及详细分类(例如 blink_gc、cc、partition_alloc、gpumemorybuffer 等等),必须要了解一些 Chrome 的进程/线程模型、Blink 渲染引擎以及 GPU 内存在 Chrome 中的使用情况。这里推荐 @易旭昕 的一系列关于 Chrome Blink、cc、viz 的文章。下图来自「How cc Works」[3]

易旭昕:How cc Works 中文译文?

zhuanlan.zhihu.com
图标

这里我们只需要知道 MemoryInfra 是 Chrome 集成在 chrome://tracing 中的一个内存度量工具。Chrome 很多内置组件都会使用它,例如 V8 用它来度量 JS heap,而 GPU 组件则用它度量 OpenGL 和其他 GPU 对象的分配情况。

MemoryInfra 界面

使用上述顶点数据压缩之后,在我的 DEMO 中可以看出 GPU 进程使用内存从 530M 下降到了 445M,而 DEMO 页面对应的 Renderer 进程从 22M 下降到了 14M。

优化前 GPU 进程使用内存
使用顶点数据压缩后,GPU 进程使用内存

来自 Cesium 和 Mapbox 的实践

Mapbox 广泛使用了对于颜色数据的压缩。而 Cesium 的这篇文章「Graphics Tech in Cesium - Vertex Compression」[4],除了上面介绍的 float pack 技巧,还使用了对于法向量、纹理坐标的压缩方案,下面我们就简单介绍一下。

「A Survey of Efficient Representations of Independent Unit Vectors」[5]介绍了一种压缩 3 个分量的单位向量到 「oct」 格式,只需要使用两个 8-bit 分量:

// https://github.com/AnalyticalGraphicsInc/cesium/blob/master/Source/Core/AttributeCompression.js#L43
AttributeCompression.octEncodeInRange = function(vector, rangeMax, result) {
result.x = vector.x / (Math.abs(vector.x) + Math.abs(vector.y) + Math.abs(vector.z));
result.y = vector.y / (Math.abs(vector.x) + Math.abs(vector.y) + Math.abs(vector.z));
if (vector.z < 0) {
var x = result.x;
var y = result.y;
result.x = (1.0 - Math.abs(y)) * CesiumMath.signNotZero(x);
result.y = (1.0 - Math.abs(x)) * CesiumMath.signNotZero(y);
}

result.x = CesiumMath.toSNorm(result.x, rangeMax);
result.y = CesiumMath.toSNorm(result.y, rangeMax);

return result;
};

Cesium 就使用了这种方法来压缩法向量,随后将这两个 8-bit 按照上文介绍的方法又压缩到了一个 float 中。在 shader 中按照 float -> vec2 -> vec3 的顺序进行 unpack,得到原始的 vec3 单位向量:

// 解压缩存储了两个 8-bit 的 float,得到 vec2
vec3 czm_octDecode(float encoded)
{
float temp = encoded / 256.0;
float x = floor(temp);
float y = (temp - x) * 256.0;
return czm_octDecode(vec2(x, y));
}

// 解压缩 vec2 到原始 vec3 单位向量
vec3 czm_octDecode(vec2 encoded)
{
return czm_octDecode(encoded, 255.0);
}
vec3 czm_octDecode(vec2 encoded, float range)
{
if (encoded.x == 0.0 && encoded.y == 0.0) {
return vec3(0.0, 0.0, 0.0);
}

encoded = encoded / range * 2.0 - 1.0;
vec3 v = vec3(encoded.x, encoded.y, 1.0 - abs(encoded.x) - abs(encoded.y));
if (v.z < 0.0) {
v.xy = (1.0 - abs(v.yx)) * czm_signNotZero(v.xy);
}

return normalize(v);
}

对于只需要 12 bits 的纹理坐标,同样可以采用类似之前颜色数据的压缩方法:

// 压缩纹理坐标到 float 中
AttributeCompression.compressTextureCoordinates = function(textureCoordinates) {
// Move x and y to the range 0-4095;
var x = (textureCoordinates.x * 4095.0) | 0;
var y = (textureCoordinates.y * 4095.0) | 0;
return 4096.0 * x + y;
};

在文章[4]的最后,Cesium 也总结了在 BillboardCollection 中的压缩效果:

The BillboardCollection and LabelCollection have a total of 18 attributes per vertex, each with various types and number of components. After packing and compression, the number is down to eight four-component floating-point attributes per vertex. For more details, see the BillboardCollection or its vertex shader.

最后我在查资料的过程中,在 Reddit 上看到了一个讨论[6],里面提到了 「QTangents」[7]。来自 2011 Siggraph Presentation Spherical Skinning with Dual-Quaternions and QTangents, Crytek。使用一个四元数存储 tangent & bitangent。

总结

对于顶点数据的压缩在地理信息展示场景中是很重要的,可以看出 Cesium 在这方面基本做到了极致。

在下篇文章中我会介绍 Chrome MemoryInfra 的使用方法,以及看懂内存分析数据所需要的一些知识。

参考

  1. ^Scalars https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Scalars
  2. ^Single-precision floating-point format https://en.wikipedia.org/wiki/Single-precision_floating-point_format
  3. ^How cc Works https://chromium.googlesource.com/chromium/src/+/master/docs/how_cc_works.md
  4. ^abGraphics Tech in Cesium - Vertex Compression https://cesium.com/blog/2015/05/18/vertex-compression/
  5. ^A Survey of Efficient Representations of Independent Unit Vectors http://jcgt.org/published/0003/02/01/
  6. ^Reddit - vertex compression https://www.reddit.com/r/vulkan/comments/9xvvfj/vertex_compression/
  7. ^qtangents http://dev.theomader.com/qtangents/

推荐阅读:

相关文章