什么是Cesium通视分析?
简单来说,通视分析的核心就是「模拟人眼(或设备)的视线」,判断三维场景中两个或多个点位之间,是否存在地形、建筑、模型等障碍物遮挡,进而输出视线状态、遮挡位置、可见范围等信息。在实际开发中,常用军事、安防、通信、无人机等业务场景中。
核心功能拆解
功能实现分为以下三个功能:首先是地图选点,其次插值计算,然后判断是否可视,若存在遮挡,标注出遮挡的具体位置和高度。
地图选点
地图选点,是cesium中交互式功能中很常见的功能,通过鼠标左键的点击,在地图标注观测点,目标点,难点在于实时获取鼠标左键点击的经纬度信息,这个功能在前面的有过封装和介绍。点击查看
插值计算比较
这里的插值计算主要是Cartesian2和Cartesian3的插值计算。这样做的原因在于通过对Cartesian2坐标的插值进行高度拾取,然后与Cartesian3插值的坐标比较,如果拾取的坐标高度大于实际Cartesian3插值的坐标高度,那就证明不可视,有遮挡,返回第一个遮挡点坐标。具体实现如下:
- 坐标转换
js
// 1、左键点击获取的坐标
let start = new cesium.Cartesian3(0, 0, 0)
let end = new cesium.Cartesian3(0, 0, 0)
list.forEach((el, index) => {
const entity = viewer.value.entities.getById(`${el.longitude + el.latitude + el.height}`)
const text = index === 0 ? '观测点' : '目标点'
entity.label.text = text
index === 0 ? start = cesium.Cartesian3.fromDegrees(el.longitude, el.latitude, el.height) : end = cesium.Cartesian3.fromDegrees(el.longitude, el.latitude, el.height)
})
// 2、空间坐标转屏幕坐标,模拟拾取高程数据
const sp = convertCartesian3ToCartesian2(start)
const ep = convertCartesian3ToCartesian2(end)
- 插值计算
js
// Cartesian坐标线性插值公式:start + (end - start) * (interval / length)
function getC2ByPixelInterval(sPoint, ePoint, interval, length) {
const result = new cesium.Cartesian2(0, 0)
// 距离不足直接返回
if (length < interval) {
return result
}
// 插值比例 t
const t = interval / length
cesium.Cartesian2.lerp(sPoint, ePoint, t, result)
return result
}
- 高度拾取
js
let ray = viewer.value.camera.getPickRay(c2Position)
let cartesianTerrain = viewer.value.scene.globe.pick(ray, viewer.value.scene)
- 判断模拟拾取坐标和实际插值坐标
js
let theoryPosition = cesium.Cartographic.fromCartesian(current.cartesian)
let actualPosition = cesium.Cartographic.fromCartesian(c3Temp)
if (theoryPosition.height < actualPosition.height) {
record = c3Temp
}
完整代码如下:
js
/**
* 通视分析
* @param {viewer} viewer
* @param {Cartesian3} start
* @param {Cartesian3} end
*/
export const perspectiveAnalysis = (start, end) => {
// 初始化遮挡点为原点,标识无遮挡
let record = cesium.Cartesian3.ZERO
// 将Cartesian3空间坐标转为Cartesian2屏幕坐标用来模拟拾取高程数据
const sp = convertCartesian3ToCartesian2(start)
const ep = convertCartesian3ToCartesian2(end)
// 计算空间之间的两点之间的距离,用于插值计算
const c3Length = cesium.Cartesian3.distance(start, end)
// 计算屏幕之间的像素距离
const c2Length = cesium.Cartesian2.distance(sp, ep)
// 计算步长
const c3Step = c3Length / 100
const c2Step = c2Length / 100
for (let index = 0; index < 100; index++) {
// 根据三维坐标间隔计算当前空间坐标点
let c3Temp = getC3ByPixelInterval(start, end, c3Step * index, c3Length)
// 根据二维坐标间隔计算当前屏幕坐标点
let c2Temp = getC2ByPixelInterval(sp, ep, c2Step * index, c2Length)
// // 拾取当前模拟的屏幕坐标的空间坐标
let current = getPickCartesian(c2Temp)
// 将理论坐标转为经纬高,与当前步长的实际空间坐标的经纬高进行比较
let theoryPosition = cesium.Cartographic.fromCartesian(current.cartesian)
// 将当前空间坐标点转为地理坐标,即经纬高
let actualPosition = cesium.Cartographic.fromCartesian(c3Temp)
// 比较地表高度和当前的高度
if (theoryPosition.height - actualPosition.height > 1.8) {
// 如果地表更高,记录当前空间坐标点,结束循环
record = c3Temp
break
}
}
return record
}
/**
* Cartesian2坐标线性插值
* @param {Cartesian2} sPoint
* @param {Cartesian2} ePoint
* @param {Number} interval
* @param {Number} length
* @returns {Cartesian2}
*/
function getC2ByPixelInterval(sPoint, ePoint, interval, length) {
const result = new cesium.Cartesian2(0, 0)
// 距离不足直接返回
if (length < interval) {
return result
}
// 插值比例 t
const t = interval / length
cesium.Cartesian2.lerp(sPoint, ePoint, t, result)
return result
}
/**
* Cartesian3坐标线性插值
* @param {Cartesian3} start
* @param {Cartesian3} end
* @param {Number} interval
* @param {Number} length
* @returns {Cartesian3}
*/
function getC3ByPixelInterval(start, end, interval, length) {
const result = new cesium.Cartesian3(0, 0, 0)
if (length < interval) {
return result
}
// 比例 t
const t = interval / length
cesium.Cartesian3.lerp(start, end, t, result)
return result
}
/**
* 根据屏幕坐标获取高程数据
* @param {Cartesian2} c2Position
*/
export const getPickCartesian =(c2Position)=>{
if (!c2Position || !cesium.defined(c2Position)) {
return {}
}
// 方法1:从深度缓冲区拾取坐标(适用于3D模型)
let cartesianModel = viewer.value.scene.pickPosition(c2Position)
// 方法2:从相机发射射线拾取地形坐标
let ray = viewer.value.camera.getPickRay(c2Position)
let cartesianTerrain = viewer.value.scene.globe.pick(ray, viewer.value.scene)
if (!cartesianModel && !cartesianTerrain) {
return {}
}
// 构造返回结果
let result = {
cartesian: cartesianModel || cartesianTerrain,
cartesianModel,
cartesianTerrain,
c2Position
}
if (cartesianModel && cartesianTerrain) {
result.altitudeMode =
cartesianModel.z.toFixed(0) !== cartesianTerrain.z.toFixed(0)
? cesium.HeightReference.NONE
: cesium.HeightReference.CLAMP_TO_GROUND
} else {
result.altitudeMode = cesium.HeightReference.CLAMP_TO_GROUND
}
return result
}
误区
虽然上述代码也完成了功能,但是总是感觉好像不太对,在实际使用中偶尔也会出现反差,后来在看webGL三维正射投影,透视投影的时候发现,想要由屏幕坐标变换成三维坐标时要经过一系列的矩阵变换才能得到,那么反过来上述代码在进行空间坐标转屏幕坐标时,是不是也会受到影响,查看了相关资料发现,从代码逻辑上来看,是没有任何问题的,但是Cesium 是透视相机,遵循近大远小投影规则,所以屏幕插值出来的点,反向拾取后不在原始三维视线直线上,那么对应的高程数据也就失去了参考意义。
再次实现
既然屏幕坐标插值会出现偏差,那就直接使用空间坐标进行插值计算,借助插值后的空间坐标进行转换成经纬度,通过特定API来获取地形的高度。修改如下:
js
let theoryPosition = cesium.Cartographic.fromCartesian(c3Temp)
const actualHeight = viewer.value.scene.globe.getHeight(theoryPosition);
if (actualHeight - theoryPosition.height > 1.8) {
record = c3Temp
break
}
总结
在 Cesium 开发中,通过屏幕坐标模拟空间分析是一个非常典型的误区。虽然pickPosition可以获取到包含地形与模型的真实高程,但它依赖屏幕渲染、深度缓冲、相机视角,但常适用于鼠标交互拾取,对用于通视分析、可视域分析、剖面分析等真正的 GIS 空间分析场景还是有些问题需要解决的。
正确的空间分析还是要基于三维空间直线插值与射线相交检测(RayCasting) ,实现与屏幕、视角无关的稳定计算。
