直接用glDrawElements切换LOD会卡顿,因其每次切换都需调用glBufferData更新索引缓冲区,触发GPU同步与管线阻塞;正确做法是将多级LOD索引一次性上传至同一GL_ELEMENT_ARRAY_BUFFER,再通过glDrawElementsBaseVertex或偏移+count控制绘制段。

为什么直接用 glDrawElements 切换 LOD 会卡顿
GeoMipmapping 的核心不是“画多张图”,而是避免每帧重建整个索引缓冲区或重传顶点数据。常见错误是每次切换 LOD 都调用 glBufferData 更新 GL_ELEMENT_ARRAY_BUFFER,这会触发 GPU 同步、阻塞渲染管线。真正可行的做法是:把所有 LOD 级别的索引表(比如 0~5 级)一次性上传到一个大 GL_ELEMENT_ARRAY_BUFFER 中,再用 glDrawElementsBaseVertex 或偏移 + count 控制实际绘制哪一段。
关键约束:各 LOD 索引必须按连续块排列,且每个块内索引值需相对于同一顶点基址归一化(即不能直接存原始全局索引)。例如,LOD1 占前 600 个索引,它对应的顶点范围是 [0, 200),那么这 600 个索引值都应在 0~199 范围内。
- 若用
glDrawElements,每次切 LOD 都得更新缓冲区首地址和大小,驱动大概率降级为 CPU 同步路径 -
glDrawElementsBaseVertex允许复用同一份顶点缓冲,只改 base vertex 和 index offset,GPU 可流水线处理 - 顶点数据本身可共享——低 LOD 只是跳过部分顶点,不需另存一套;但法线、UV 等属性若随 LOD 重采样,就得按 LOD 分块存储
如何组织顶点与索引内存布局才不浪费显存
地形网格通常按瓦片(tile)划分,每个瓦片预生成 N 级 LOD 的索引序列。重点在于顶点存储策略:
- 推荐使用“单顶点缓冲 + 多索引缓冲”:顶点位置、高度、世界坐标等静态属性存一份;而 LOD 相关的法线/UV 若差异大,可额外存一张
LOD-dependent attribute texture(用纹理查表),避免顶点缓冲爆炸 - 索引缓冲建议用
GL_UNSIGNED_INT,虽然GL_UNSIGNED_SHORT节省一半空间,但现代 GPU 对 uint 索引的 cache 命中率更高,且支持 >65535 顶点的瓦片 - 每个 LOD 的索引块长度必须是 3 的倍数(保证三角形完整),末尾补 0 不影响绘制,但要确保
count参数严格对齐,否则会读越界或绘错三角形
示例:一个 64×64 高度图瓦片,LOD0 用全分辨率(63×63 个四边形 → 7560 个索引),LOD1 降为 32×32 网格 → 3072 个索引。这两个块连续写入同一 GL_ELEMENT_ARRAY_BUFFER,绘制 LOD1 时调用:
glDrawElements(GL_TRIANGLES, 3072, GL_UNSIGNED_INT, (void*)(7560 * sizeof(uint32_t)));
立即学习“C++免费学习笔记(深入)”;
视锥裁剪 + 屏幕误差(Screen-space error)怎么算才不抖动
单纯按距离切换 LOD 会产生 pop-in;用屏幕投影误差更稳,但容易因浮点精度导致相邻帧在临界值反复横跳。根本解法是引入滞后(hysteresis)和世界空间误差容差。
- 不要每帧重新计算像素误差:先算出当前 LOD 下该瓦片在屏幕上的最大三角形边长(用 MVP 投影三个顶点,取屏幕坐标的最大边长),再除以视口宽高得像素误差;但这个值对视角旋转极敏感
- 更鲁棒的做法:用瓦片包围盒中心到相机的距离
d,结合预计算的该 LOD 下“理论最大误差”表(离线跑一遍不同 d 下的投影误差,拟合成 d→error 的分段线性函数) - 切换阈值必须带 ±5% 滞后区间,例如 LOD1→LOD2 的升档阈值是 80px,降档阈值设为 76px,避免来回震荡
- 注意:误差计算必须在世界空间做,不能在裁剪空间除以 w —— 近平面附近 w 极小,会导致除零或巨大误差假信号
为什么 glMultiDrawElements 在这里反而拖慢性能
有人试图用 glMultiDrawElements 一次提交多个 LOD 瓦片,但实际测下来帧时间上升。问题不在 API 本身,而在数据访问模式:
- 多瓦片混合不同 LOD 时,索引访问完全随机,GPU 的 index cache 命中率暴跌,比顺序 draw call 更伤
- 驱动对
glMultiDrawElements的优化集中在“同参数批量提交”,而 GeoMipmapping 中每个瓦片的count、indices偏移、basevertex全不同,无法合并为硬件指令 - 真正高效的是:按 LOD 分组,同级瓦片用
glMultiDrawElements批量提交;跨 LOD 则老老实实逐个glDrawElementsBaseVertex - 如果启用了 indirect rendering,优先用
glMultiDrawElementsIndirect,把 LOD 切换逻辑移到 compute shader 里预计算 draw commands,CPU 完全不参与每帧决策
最易被忽略的一点:GPU 驱动对 glDrawElementsBaseVertex 的调用有隐式状态校验,若连续 10 帧都用同一 basevertex+offset 组合,某些驱动会缓存该组合的硬件指令模板;但频繁切换 basevertex 值会清空这个缓存,造成微卡顿——所以 basevertex 最好按瓦片 ID 固定分配,而不是按 LOD 动态算。


















