站内搜索

输入关键词开始搜索

Cesium 地形可视域:径向采样与最大遮挡仰角

文章目录

地形可视域回答的是:从观察点向指定方向看,哪些地形单元没有被近处山体挡住。案例运行山地示例后采样 100281 个位置,并把可见区画成绿色、遮挡区画成红色。

地形可视域计算结果、红绿分类面片和观察点视角快照

实现思路导图

flowchart TD
  ROOT[地形可视域]
  ROOT --> OBSERVER[观察参数]
  OBSERVER --> POINT[观察点与眼高]
  OBSERVER --> FOV[方向视角与距离]
  ROOT --> SAMPLE[地形采样]
  SAMPLE --> ENU[ENU 射线点]
  SAMPLE --> HEIGHT[批量地形高度]
  ROOT --> TEST[地平线判断]
  TEST --> ANGLE[维护最大仰角]
  TEST --> REFINE[细化遮挡边界]
  ROOT --> RENDER[红绿扇区可视化]

采样计划由视场和半径共同决定

案例不是固定写死射线数。getSamplingPlan 根据半径、水平视角和质量档位计算射线数、距离环数、角步长和距离步长。

const plan = getSamplingPlan(
  params.quality,
  params.radius,
  params.horizontalViewAngle,
);

截图中的标准质量使用 351 条射线和 215 个距离环。扩大半径时,程序会在总采样预算内重新平衡横向和纵向分辨率。

观察点使用真实地形高度

观察点高度是“地表高程 + 观察高度”,不是绝对海拔。程序先拾取地面位置,再通过地形服务获得高程,最后在观察点建立 ENU 局部坐标系。

局部坐标中,水平方向距离和高度差可以直接计算仰角:

const angle = Math.atan2(local.z, Math.hypot(local.x, local.y));

从近到远维护地平线

每条射线独立遍历。当前点的仰角若高于之前的最大遮挡仰角,则可见;否则被前方地形挡住。

let maxBlockingAngle = -Infinity;

for (const sample of ray.samples) {
  sample.visible = sample.angle >= maxBlockingAngle - tolerance;
  maxBlockingAngle = Math.max(maxBlockingAngle, sample.angle);
}

垂直视场角之外的点不参加当前分析。它们不是“遮挡”,统计时需要单独排除。

分类结果批量渲染

相邻两条射线和两个距离环组成一个地表单元。四个角的状态决定单元颜色,红绿混合边界还可以追加局部细化。所有三角形合并为少量 Geometry 和 Primitive,不能每个单元创建一个 Entity。

观察点视角

右侧小窗不是第二个持续运行的 Cesium Viewer。分析完成后,主场景临时切换到观察点姿态并把画面拷贝到 Canvas,再恢复原相机。这样能提供视角快照,又不承担双场景的持续渲染成本。

该算法只读取 TerrainProvider。3D Tiles 建筑和独立模型不会形成遮挡。它适合地形通视,不等同于完整场景可视域。

截图中的统计不是简单相加

截图显示总采样点 100281,可见 15889,遮挡 59563。可见与遮挡之和小于总采样点,因为垂直视场角之外的点只参与地形采样,不进入可视分类。

绿色面片表示仰角达到当前地平线的单元,红色表示被前方更高仰角挡住。青色点是观察者。右下角画面是观察点快照,用来核对“算法判断的方向”与人眼观察方向是否一致。

采样点如何落到地球表面

程序在观察点 ENU 平面内按航向和距离生成局部点,再变换到 ECEF,转为 Cartographic 后交给地形服务补高度。

const local = new Cesium.Cartesian3(
  Math.sin(heading) * distance,
  Math.cos(heading) * distance,
  0,
);

const world = Cesium.Matrix4.multiplyByPoint(
  enuToFixed,
  local,
  new Cesium.Cartesian3(),
);

这种方法适合几公里范围。半径继续扩大时,ENU 平面与椭球曲面的差异增大,应改用测地线正算生成经纬度。

最大遮挡仰角为什么能判断通视

一条射线上,从观察点到任意地形点只有一个仰角。此前出现过的最大仰角形成当前地平线。后续点低于地平线,就一定被产生该最大角的近处地形挡住。

算法复杂度是 O(rayCount × stepCount),不需要任意两点之间互相做射线求交。代价是遮挡只在已采样距离和角度上表达,窄山脊可能落在两条射线之间。

tolerance 吸收地形量化和浮点误差。它按角度设置,而不是按米设置,因此远近点使用一致的视线标准。

红绿边界为什么需要二次采样

若一个四边形单元的四个角有红有绿,真实边界一定穿过单元。案例只对这些混合单元生成更密的局部采样,而不是把整个扇形都升到最高质量。

这是自适应细化:计算量集中在信息变化最大的区域。细化单元数仍有上限,避免复杂山地让第二阶段采样无限增长。

重新计算的状态管理

调整半径、视角或质量后,当前结果已经过期。paramsDirty 标记“参数值已变但画面仍是旧结果”。开启自动重算时使用 700ms 延迟合并连续滑动;关闭时保留旧画面,并明确提示点击重新计算。

这里有三个状态:面板当前参数、上一次分析使用的参数快照、当前显示结果。文章如果只说“参数实时生效”,会掩盖异步采样期间的真实状态。

从零复现:地形可视域教程

案例源码位于 src/demos/cesiumdemos/terrain-viewshed/。这个方案计算的是地形遮挡,不包含建筑模型遮挡;两者的数据来源和求交方式不同。

第 1 步:设置观察点和观察参数

点击地形得到观察点,经 sampleTerrainMostDetailed 修正地面高程,再加观察者身高。保存方位角、水平视角、最大距离和采样质量。

const observerHeight = terrainHeight + eyeHeight;
const observer = Cesium.Cartesian3.fromRadians(lon, lat, observerHeight);

完成标志:观察点标记应落在地面上方指定高度;从观察点切换视角时,相机方向与面板方位角一致。

第 2 步:建立局部 ENU 平面

所有射线先在 ENU 中按米生成。第 i 条射线方位角为视域起始角加角度步长;第 j 个样本距离为 j * distanceStep。

const east = Math.sin(azimuth) * distance;
const north = Math.cos(azimuth) * distance;
const local = new Cesium.Cartesian3(east, north, 0);

把局部点变换为世界坐标,再转 Cartographic 批量采样地形。不要用经纬度直接增加固定小数模拟米制距离。

第 3 步:批量请求地形高度

一次生成所有方位角与距离的 Cartographic 列表,批量调用最精细地形采样。保存样本与射线索引的对应关系。

样本数等于 rayCount × distanceCount。截图中的 100281 个样本来自这两个维度的乘积,因此质量滑块会同时影响等待时间和结果边界精度。

第 4 步:维护地平线最大仰角

沿一条射线由近到远计算样本相对观察点的仰角:

angle = atan2(sampleHeight - observerHeight, horizontalDistance)

若当前角度不低于此前最大角度,则当前点可见并更新最大值;否则它被前方更高地形遮挡。这个算法是每条射线独立的一维地平线扫描。

第 5 步:细化红绿转换边界

相邻两个样本的可见状态不同时,在距离区间内做若干次二分采样,逼近遮挡开始位置。若不细化,低质量模式会出现宽大的红绿锯齿。

边界细化只处理状态转换区间,不需要把整条射线采样密度翻倍,因此能用较少请求改善视觉边缘。

第 6 步:构建扇区面片

相邻两条射线与相邻距离样本组成一个四边形。根据可见状态拆为绿色或红色 GeometryInstance,再用同一 Primitive 分批渲染。

边界点略高于地形,避免深度闪烁。观察点、方向线和视域外框使用独立 Entity,便于交互编辑。

第 7 步:区分当前参数和结果快照

点击计算时复制一份参数快照。计算期间再次拖动滑块只设置 paramsDirty,不改正在运行的数组。自动重算使用防抖,并用 requestId 淘汰旧结果。

验收与排错

  • 山后仍全部为绿色:检查最大仰角是否沿距离持续保留。
  • 近处出现大块红色:确认观察者高度包含地形高程。
  • 扇区方向偏转 90°:检查 ENU 中方位角的正弦、余弦对应关系。
  • 拖动参数后旧结果覆盖新结果:为异步采样增加版本判断。

相关文章

Cesium 地形可视域分析:用采样射线判断通视关系

不依赖 ShadowMap,从观察点建立径向采样网格,通过最大遮挡仰角计算地形可见区并细化红绿边界

Cesium 地形抬升:从真实地表生成可编辑地块

说明地形采样、影像纹理、抬升顶面、侧壁坑体、质量档位、进度取消和工程量分析