![f805a918a6e7026ed004d20331fc2880.png](https://i-blog.csdnimg.cn/blog_migrate/e7550b0eca2c61fcf5cfac287c718fad.png)
之前我们介绍了点聚合图的绘制方案:
潘与其:使用 k-d tree 实现点聚合图zhuanlan.zhihu.com![2acd17a6d099afebb5884f0b9565ebb5.png](https://i-blog.csdnimg.cn/blog_migrate/dba8b6a55545a24b1ee79a37beede17c.jpeg)
对于每个圆形我们使用了如下的顶点数据:
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]:
![5204a0271eeb8b71ed0adac2afdd2409.png](https://i-blog.csdnimg.cn/blog_migrate/e7aa43aa036ef5d8d323dbc32b477525.png)
利用好这 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![b98ceb17fd966dfd171d25f7211a8024.png](https://i-blog.csdnimg.cn/blog_migrate/fb3c7a56f0cca37291cfa591d9aceb72.jpeg)
这里我们只需要知道 MemoryInfra 是 Chrome 集成在 chrome://tracing 中的一个内存度量工具。Chrome 很多内置组件都会使用它,例如 V8 用它来度量 JS heap,而 GPU 组件则用它度量 OpenGL 和其他 GPU 对象的分配情况。
![0a3b0c86897593ac63e919e03e359866.png](https://i-blog.csdnimg.cn/blog_migrate/a58f9f05b986474a2639aaaf10945b9d.jpeg)
使用上述顶点数据压缩之后,在我的 DEMO 中可以看出 GPU 进程使用内存从 530M 下降到了 445M,而 DEMO 页面对应的 Renderer 进程从 22M 下降到了 14M。
![2bfb160e645933d35a99ae83657c57a3.png](https://i-blog.csdnimg.cn/blog_migrate/f1b6f14214bc7ddcbce43f00a56a1fdb.jpeg)
![6bc3738b518087e7499ba9f2ce634e4b.png](https://i-blog.csdnimg.cn/blog_migrate/dfeb1f7b1e5adc276c627426ce37f766.jpeg)
来自 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 的使用方法,以及看懂内存分析数据所需要的一些知识。
参考
- ^Scalars https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Scalars
- ^Single-precision floating-point format https://en.wikipedia.org/wiki/Single-precision_floating-point_format
- ^How cc Works https://chromium.googlesource.com/chromium/src/+/master/docs/how_cc_works.md
- ^abGraphics Tech in Cesium - Vertex Compression https://cesium.com/blog/2015/05/18/vertex-compression/
- ^A Survey of Efficient Representations of Independent Unit Vectors http://jcgt.org/published/0003/02/01/
- ^Reddit - vertex compression https://www.reddit.com/r/vulkan/comments/9xvvfj/vertex_compression/
- ^qtangents http://dev.theomader.com/qtangents/