图幅较大的图纸在 WebGL 中一次性绘制会带来两个问题:需要上传的顶点数据量可能超出显存承受范围;在浮点精度有限的情况下,量级很大的坐标之间的细小差别会被舍入掉。分块加载把图纸切成瓦片、按视口增量提供数据,浮动原点则在数值层面缩小坐标量级,两者通常配合使用。
这条路径上的每个环节都有明确的取舍:瓦片切得越细,索引越灵活但维护成本越高;保精细度就需要更多顶点,而精度又受浮点表示限制。下面按索引、判定、层级、调度与精度的顺序展开。
空间索引与瓦片切分
瓦片切分把图纸的包围盒按固定尺寸的网格划分,块的边界与网格对齐,便于用整数行列号定位。图元与瓦片的关系有两类组织方式:
| 组织方式 | 索引结构 | 特点 | 代价 |
|---|---|---|---|
| 单瓦片归属 | 网格到图元列表 | 结构简单 | 跨块图元需复制或裁剪 |
| 多瓦片注册 | 网格到图元列表 | 查询直接 | 绘制时可能出现重复 |
| 层级树 | 四叉树或瓦片金字塔 | 支持多分辨率访问 | 结构维护成本上升 |
多数实现把图元的包围盒与瓦片网格求交,建立「瓦片到图元」的倒排表,绘制时只遍历可见瓦片对应的列表。图元跨越多个瓦片时,可对其按瓦片边界裁剪,保留裁剪后的部分,从而避免同一图元被重复提交。
可见瓦片判定
二维视图下,可见区域就是一个矩形。把视图的显示范围换算到图纸坐标后与瓦片矩形求交,就得到可见瓦片集合。由于网格规则,不必逐块测试,可由范围的四个边界直接算出起始与结束的行列号:
// 由视图范围求可见瓦片的行列区间
function visibleTiles(view, tile) {
const halfW = view.width / (2 * view.scale);
const halfH = view.height / (2 * view.scale);
return {
c0: Math.floor((view.center.x - halfW) / tile.size),
c1: Math.floor((view.center.x + halfW) / tile.size),
r0: Math.floor((view.center.y - halfH) / tile.size),
r1: Math.floor((view.center.y + halfH) / tile.size),
};
}
视图带旋转角时会打破轴对齐的假设。此时可以先用视线范围的外接矩形做粗略判定,再对候选瓦片做精确的矩形相交测试,避免旋转后的角落出现漏画。判定结果一般与上一帧做差集和并集,只有新进入视口的瓦片才需要准备数据。
层级细节与瓦片树
当图纸范围远大于屏幕时,若只用一个层级切分,细小瓦片的数量会急剧增长。常见做法是建立多级瓦片树,每层的瓦片在图纸上覆盖的范围逐级放大,层与层构成金字塔:
| 层级 | 单瓦片覆盖范围 | 单瓦片内容 | 使用场合 |
|---|---|---|---|
| 0(根) | 全图 | 聚合后的简化图元 | 全图概览 |
| 1 | 约四分之一图幅 | 中等密度 | 中等缩放 |
| 2 | 约十六分之一图幅 | 较细 | 局部查看 |
| 更深层级 | 逐级细分 | 原图元 | 高倍缩放 |
选择层级的依据通常是瓦片在屏幕上的投影尺寸:投影小于阈值时改用父层瓦片,大于阈值时下钻到子层。这样屏幕上每块瓦片覆盖的像素数大致处在同一区间,绘制调用数量与几何精度之间取得平衡。层与层之间的图元并非简单包含关系,父层需要事先生成简化表示。
瓦片调度与渐进式渲染
调度需要决定在什么时候准备并上传哪些瓦片。常见策略包括:
- 可见瓦片按优先级排序,通常按与视口中心的距离由近及远
- 邻近瓦片预取,即在视口外扩一圈先行准备,平移时可结合移动方向偏重前进侧
- 每帧限制上传数量,避免长时间占用主线程与总线
- 为瓦片维护就绪状态,未就绪时以父层瓦片或简化几何占位
缓存需要一套淘汰规则,容量可以按瓦片数量或显存字节数计算,超出后按使用时间淘汰,并优先保留视口附近的瓦片。渐进式渲染在此基础上确定默认策略:先用低层级瓦片铺满视口,随后用就绪的高层级瓦片整块替换。这样首屏时间取决于低层级瓦片的准备速度,而不受全部细节数据量的约束。替换以瓦片为单位进行,可避免高低层级并存造成的细节边界错位。
顶点缓冲复用与显存占用
瓦片的顶点数据通常以顶点缓冲对象上传,索引缓冲用于复用顶点。相邻瓦片之间不共享图元,因此顶点数大体随瓦片数量线性增长。可以复用的部分包括:
- 跨瓦片重复出现的图元保存在共享缓冲中,各瓦片只记录对应的索引区间
- 同一几何在不同层级下共用一份缓冲,仅改变绘制用的索引范围
- 对形状相同、仅位置不同的符号,用实例化绘制把每份变换放进实例属性
显存占用的估算方式与GPU 纹理上传与瓦片渲染中讨论的思路一致:顶点数乘以每顶点的字节数,加上索引与辅助数据的开销,再考虑金字塔各层的总量。
大坐标精度与局部原点
WebGL 的顶点属性一般以 32 位浮点存储,其有效位数约为 7 位十进制。若图纸坐标量级达到百万级,相邻两点相差 1 个单位时,32 位浮点已难以区分,表现为顶点抖动、几何粘连或剔除判定失效。
浮动原点的做法是:为当前视图选取一个靠近视口的参考点,把顶点坐标减去该参考点后再上传,参考点本身作为模型矩阵里的平移量参与变换。这样顶点数值的量级保持在视口尺度内,相对误差随之降低。
// 浮动原点:顶点相对参考点存储,参考点放进模型矩阵
const originX = Math.round(view.center.x / 1000) * 1000;
const originY = Math.round(view.center.y / 1000) * 1000;
// 上传的顶点坐标为 (x - originX, y - originY)
modelMatrix.translate(originX, originY);
参考点在视图移动后需要更新:可以按新参考点重算缓冲中的坐标,也可以保留缓冲,把原点偏移量作为统一量传入着色器由其完成平移,后者避免重建缓冲,代价是每帧都要更新这个量。
小结
分块加载把显存与带宽的压力从一次性上传整幅图转为按视口增量上传,其核心是空间索引、可见性判定与调度:索引决定查询成本,判定决定每帧触及的数据范围,调度决定数据到达的时机。浮动原点把顶点坐标的量级压到视口尺度内,使 32 位浮点的精度足以支撑绘制。这些机制共同决定了大幅面图纸在浏览器中可加载的规模。