北斗网格性能优化(一):总体架构

背景: 我国正在发展低空经济,我们做的是飞行服务(飞服)项目,要把低空空域划成能计算、能查询、能下发的单元。选的是北斗网格:按经纬度和高度把地理空间编码成一个个三维盒子,一个编码对应一块确定的空间范围。在 Cesium 里加载 L5、L6、L7 三档网格,给一个多边形和一段高度范围,就能把落在里面的格子填出来,得到一块禁飞区或管控区。三档的水平格边长依次约 123.7、61.8、7.73 米:比 L5 粗的看不出区域形状,比 L7 细的在这个业务尺度上没有意义。

原因: 麻烦来自多出来的那个方向。二维地图只要横向铺满,三维还要按高度一层层堆;而且高度步长不是独立参数,它由水平步长推出来,水平细一倍、高度也跟着细一倍。L6 到 L7 每边 8 倍,三个方向乘起来就是 512 倍 —— 换一档不是画得密一点,是同一片空间里的格子数量直接乘 512。

压测示例用了六个真实的天津、北京管控区,它们在 L7 下的理论总量是 70,986,328 个格子;而视口配的活动预算上限是 180,000,也就是任何一帧最多允许约 0.25% 的格子存在。剩下那 99.75% 必须永远不落地 —— 因为瓶颈不在光栅化,在生成:这些格子还没开始画,光实例矩阵和颜色就是 7000 万 × 80 字节 = 5.68 GB。

阅读更多

北斗网格性能优化(二):GPU Instancing 与 DrawCommand

GPU Instancing 让一批形状相同的物体共享一份几何,各自只提供变换、颜色这些实例属性,一次提交画完整批。 它减少的是 CPU 反复组织和提交绘制的开销,不减少 GPU 要处理的实例数量。

DrawCommand 是 Cesium 对一次绘制任务的描述,把几何缓冲、Shader、渲染状态和绘制数量组织在一起。给顶点属性配上 instanceDivisor、给命令填上 instanceCount,Instancing 就接进了 Cesium 的渲染流程;不带这些配置时,它描述的也是一次普通绘制。

这套北斗网格的做法是:一份中心在原点的单位立方体,每个格子存一份 64 字节矩阵和 16 字节颜色,按 Tile 分批,每个 Tile 最多两条 DrawCommand —— 一条画面,一条画棱。两条命令共用同一批实例缓冲,只有索引和着色器不同,所以只显示线框时不必重传任何实例数据。

GPU 仍要逐实例处理顶点。所以后面还需要 LOD、视锥剔除和活动预算,一起控制真正参与绘制的格子数量。

阅读更多

北斗网格性能优化(三):LOD 与视锥剔除

LOD(Level of Detail,细节层次)决定网格画多细;视锥剔除判断哪些空间块可能出现在画面里;活动预算限制同时参与渲染的数据规模。 三者分别减少无法分辨的细节、视野外的工作,以及设备暂时承担不起的负载。

这套北斗网格的做法是:用相机到 Tile 中心的三维距离选层级,用区域 OBB 和 Tile 包围体做两级剔除,再按距离和预算安排加载;相机移动期间暂缓新增 L7,停下来约 120 毫秒后补细节。

代价是细节分阶段出现,Tile 级筛选会保留少量实际看不见的数据,而且视锥剔除不处理遮挡 —— 楼房和地形挡住的块仍在视锥内,仍然要生成。

这篇从一个常见问题展开:已经用了 GPU Instancing,为什么大规模网格还是会卡?

阅读更多

北斗网格性能优化(四):Web Worker 与 Transferable

Web Worker 是浏览器里独立于页面主线程的 JavaScript 执行环境,主线程通过消息派任务、收结果。Worker 不能访问 DOM,也拿不到 Cesium 的 WebGL 上下文 —— 它只能算,不能画。

Transferable 用来在线程之间转移 ArrayBuffer 的所有权:缓冲区不复制,发送方失去访问权,接收方直接拿到这块内存。这也是唯一一个能把大数组跨线程搬过去、又不付出一次完整拷贝的办法。

这套北斗网格的做法是:一个 Worker,主线程前面配一个按距离排序的队列,同一时刻只跑一个任务;Worker 把实例矩阵、颜色、网格索引、占用标记四组类型化数组用 Transferable 交回主线程。

它把生成搬出主线程,不减少计算量。GPU 上传仍然在主线程完成,Transferable 只免掉线程之间的复制,不会把数据直接变成显存。

阅读更多

北斗网格性能优化(五):解决大坐标精度问题——RTE 与局部坐标系

RTE(Relative-To-Eye)是一种相对相机计算坐标的技术:先从物体位置里减去相机位置,再把这个较小的差值交给 GPU。 它解决的是大世界坐标进入 Float32 之后,小尺度细节丢失的问题。

局部坐标系则给一小块空间挑一个附近的参考原点。 在地球场景里,项目让同一个 Tile 内的网格共用一套 ENU(东、北、上)坐标系,每个实例只保存相对 Tile 参考中心的局部位置。

这套北斗网格的坐标链路可以概括为:CPU 用双精度把 ECEF 世界坐标转成 Tile 局部坐标,并算好参考中心减相机的位置;GPU 再把局部偏移旋转回世界方向,得到相对相机的坐标,最后只做视图旋转和投影。

它不是让 Float32 变成 Double,也不解决所有闪烁。它做的是重新安排减法顺序 —— 让最容易损失精度的「大数减大数」先在 CPU 的双精度里完成。

阅读更多

北斗网格性能优化(六):L7 的水平微块与高度切片

空间分块把一个大区域划成若干可以独立生成、绘制、隐藏和回收的空间块。 切得好,一块就是一次 Worker 任务、一次实例化绘制、一个缓存条目;切得不好,包围体太粗、单次任务太重,前面所有优化都会被抵消。

L7 微块是 L7 层级专用的水平分组,高度切片是在它之上再按采样层切的一刀。 两者合起来把一根根竖直的格子柱拆成可以单独调度的小块。

这套北斗网格的做法是:一个水平微块每边跨 2 个 L6 步长,也就是 16×16 = 256 根格子柱;每根柱子按最多 32 个采样层分成一片,满片 8192 个实例。 微块的列号行号取自全局经纬度,不取自区域边界,所以相邻的两个基础块看到同一片空间会算出同一个编号。

这篇只讲 L7。L6 的切法完全不同 —— 它按区域预估工作量反推块大小,放在下一篇和缓存一起说。

阅读更多

北斗网格性能优化(七):Tile、缓存与资源回收

Tile 是一块可以独立加载、绘制和回收的空间数据,一个 Tile 可以包含成千上万个网格实例。 它把大区域拆成可管理的批次,让视口筛选、Worker 生成和 GPU Instancing 使用同一种处理粒度。

LRU(Least Recently Used)是一种优先淘汰最久没用过的数据的缓存策略。 地图浏览有很强的时间局部性 —— 刚离开的地方常常很快又要看;保留最近用过的 Tile,就能省掉重新生成和上传。

这套北斗网格的做法是:块离开活动集合后先尝试进缓存,进不去就直接销毁;缓存的准入同时受块数、网格数和层级三个维度限制,超出时按 lastUsedAt 淘汰最旧的;共享的单位立方体几何用引用计数,最后一个使用者退出才真正销毁。

分块决定一次工作有多大,缓存决定哪些工作可以复用,回收决定资源能不能长期保持可控。

阅读更多