文章目录
全球网格和局部三维网格面对的是不同问题。相机可以从整颗地球一路拉近到街区,固定层级要么太稀疏,要么在远景生成不可接受的网格数量。

实现思路导图
flowchart TD
ROOT[全球动态网格]
ROOT --> CAMERA[相机状态]
CAMERA --> LEVEL[高度选择层级]
CAMERA --> RECT[计算可见范围]
ROOT --> GEO[地理处理]
GEO --> DATE[日期线拆分]
GEO --> CURVE[网格线贴合椭球]
ROOT --> BUILD[Worker 构建几何]
ROOT --> RENDER[Primitive 批量提交]
ROOT --> STATE[快照版本淘汰旧结果]
根据相机状态选择层级
案例监听相机变化,根据高度和配置范围计算当前显示层级。层级变化后不立刻在每个相机事件中重建,而是合并短时间内的连续变化:
viewer.camera.changed.addEventListener(() => {
clearTimeout(refreshTimer);
refreshTimer = setTimeout(refreshVisibleGrid, 120);
});
这段防抖只减少重复安排。真正防止旧计算覆盖新结果,还需要给 Worker 请求附带版本号。
只生成当前可见区域
全球网格不应一次铺满所有层级。程序从相机视锥和地球交点估算可见范围,只生成当前视图可能出现的单元,并设置单帧数量上限。
跨越经度 ±180° 时需要拆分范围。若直接用 west > east 的包围盒参与普通循环,会漏掉日期变更线附近的网格。
二维地表格网与三维体素
地表模式把单元边界贴到椭球或地形。三维模式增加高度层,可以用不同颜色区分层级或高度:
const style = {
showSurface: true,
showVoxels: false,
colorMode: "level",
lineAlpha: 0.72,
};
两种模式的计算量差异明显。体素数量是二维单元数乘以高度层数,远景应自动关闭或限制体素显示。
光标编码查询
鼠标移动时先拾取地球位置,再把经纬度和高度送入编码函数。查询结果与显示网格使用同一套层级参数,才能保证光标提示的编码与画面一致。
const cartographic = viewer.camera.pickEllipsoid(
movement.endPosition,
viewer.scene.globe.ellipsoid,
);
在开启真实地形时,应优先使用场景或地形拾取,否则查询高度只是椭球面高度。
性能控制
LOD 切换、Worker 计算和 Primitive 替换需要形成一条带版本的异步链。旧请求可以完成,但不能提交到场景。销毁页面时终止 Worker、移除相机监听并删除网格 Primitive。
全球模式为什么不能直接复用局部循环
局部网格知道一个小包围盒,可以枚举行列。全球视角下,一个四级格子横跨数千公里,而十二级格子数量已经非常大。程序必须先根据相机高度决定层级,再按可见范围裁剪。
截图中相机高度约 2200 万米,自动 LOD 选择第 4 级,当前视图只生成 12 × 6 × 1 个格网。拉近到北京后,层级升高,但可见经纬范围变小,总数量仍受网格预算控制。
LOD 的输入不是只有高度
相机高度是主信号,视口范围和网格预算负责纠偏。相同高度下,俯视地球中心与掠地平线观察的可见范围不同。实现会先估算视锥与椭球交区,再检查预计单元数是否超过预算。
候选层级 = heightToLevel(cameraHeight + lodOffset)
可见范围 = estimateViewRectangle(camera, ellipsoid)
若单元数 > budget:逐级降低
LOD 偏移 用于人为增加或降低一档精度。它不应该绕过预算,否则一个滑块就能在全球视角生成百万格网。
跨日期变更线的范围拆分
当可见范围跨越 180° 经线时,Cesium Rectangle 可能出现 west > east。正确做法是拆成 [west, 180°] 与 [-180°, east] 两段生成,再合并结果。
极区也要限制纬度索引。经线在极点收敛,按普通矩形画出的单元会变成狭长楔形;包围球和拾取逻辑都应使用实际角点计算,不能只用赤道尺寸。
地表线如何贴合地球
每条网格边界不是 ECEF 中的一条直线。程序沿经线或纬线插入中间点,再通过 Cartesian3.fromRadians 放到椭球或指定高度。分段不足时,长边会穿进地球;分段过多则增加顶点量。
三维体素模式在基准高度和顶部高度各生成一圈边界,再连接竖边。它适合展示分层编码,但全球远景默认关闭,避免透明体素覆盖整个地球。
光标查询与显示版本一致
鼠标提示使用当前已经提交的 renderedLevel,不是正在计算中的候选层级。否则相机刚移动、Worker 尚未返回时,提示编码会领先画面一版。
这是一个典型的异步快照问题:相机状态是当前值,Worker 请求参数是快照,画面层级是最后完成的快照。文章中把三者区分开,才能解释快速缩放时为何旧请求要被淘汰。
这个案例的主线是:相机高度选择层级,可见范围限制单元数量,Worker 构建几何,版本号淘汰过期结果。
从零复现:全球动态网格教程
案例源码位于 src/demos/cesiumdemos/global-beidou-grid/。全球网格不能一次生成全部单元,教程的重点是根据视角只构建当前需要的部分。
第 1 步:建立高度到层级的映射
先定义若干相机高度区间,每个区间对应一个网格层级。加入滞回范围,避免相机停在阈值附近时不断重建。
function chooseLevel(height, previousLevel) {
if (previousLevel === 8 && height < 1_200_000) return 8;
if (height < 1_000_000) return 9;
return 8;
}
完成标志:缓慢缩放穿过阈值时只发生一次层级切换,面板层级与网格密度同步变化。
第 2 步:求当前可见经纬范围
优先使用相机视锥与椭球求交得到可见区域;俯视时也可用 camera.computeViewRectangle。无交点或相机在太空远景时回退到全球范围。
给范围增加半个网格的缓冲带,避免相机轻微移动就暴露边缘空白。缓冲后再按网格步长向外取整。
第 3 步:处理日期线
当 west > east 时,可见范围跨越 ±180°。把它拆成 [west, 180] 和 [-180, east] 两段分别生成,不能直接从 west 循环到 east。
const ranges =
west <= east
? [[west, east]]
: [
[west, 180],
[-180, east],
];
两段在日期线处共享边界线。合并前按量化后的经纬 key 去重,避免线条加亮。
第 4 步:生成贴合椭球的线段
纬线和经线不能只连接两个 Cartesian3 端点,否则会穿过椭球内部。按角度间隔插入中间点,再用 Cartesian3.fromDegrees 投到椭球表面。
相机越近,中间点间隔应越小。几何采样步长和网格层级是两个参数:前者决定曲线贴合度,后者决定单元大小。
第 5 步:在 Worker 中构建批量几何
主线程发送层级、范围和采样精度。Worker 返回位置 TypedArray、颜色索引和单元统计;主线程再创建 Geometry 与 Primitive。
Worker 不应访问 Cesium Viewer。经纬转 Cartesian 可以在主线程完成,或在 Worker 中使用独立数学函数;这样职责和运行环境才明确。
第 6 步:用快照提交结果
每次相机触发更新时保存 {requestId, level, rectangle}。结果返回后只与最新 id 匹配才替换 Primitive。替换顺序应为先创建新对象、确认可用,再移除旧对象,避免计算期间地图闪空。
第 7 步:限制更新频率
监听 camera.changed,但用距离、角度和时间阈值合并事件。拖动期间可以降低网格质量,moveEnd 后再生成最终版本。
验收与排错
- 日期线附近缺半个地球:检查跨线范围拆分。
- 网格线钻进地球:增加曲线中间采样点。
- 缩放时层级反复闪烁:为高度阈值增加滞回。
- 快速拖动后出现旧范围:检查 Worker 结果是否比较请求版本。