站内搜索

输入关键词开始搜索

Cesium 坐标转换与地图拾取:从屏幕像素到经纬度

文章目录

地图点击事件给出的是屏幕像素,业务需要的通常是经纬度和高度。中间必须经过场景拾取和坐标转换,不能把鼠标的 x、y 直接当成经纬度。

一个稳定的拾取工具还要区分三种目标:已经渲染的模型或 3D Tiles、真实地形、没有地形时的椭球面。

先分清三套坐标

Cesium 交互中最常见的是三种对象:

类型表示内容单位
Cartesian2Canvas 内的屏幕位置像素
Cartesian3地心地固坐标系中的世界位置米
Cartographic经度、纬度和椭球高弧度、弧度、米

经纬度转世界坐标可以直接使用 fromDegrees:

function lonLatToCartesian3(lon: number, lat: number, height = 0) {
  return Cesium.Cartesian3.fromDegrees(lon, lat, height)
}

世界坐标转经纬度要先得到 Cartographic,再把弧度转成角度:

function cartesian3ToLonLat(cartesian: Cesium.Cartesian3) {
  const cartographic = Cesium.Cartographic.fromCartesian(cartesian)

  return {
    lon: Cesium.Math.toDegrees(cartographic.longitude),
    lat: Cesium.Math.toDegrees(cartographic.latitude),
    height: cartographic.height,
  }
}

Cartesian3 的 x、y、z 不是经度、纬度和高度。它们是相对于地球中心的三维直角坐标。

为什么只调用 pickPosition 不够

最短的拾取代码是:

const position = viewer.scene.pickPosition(screenPosition)

pickPosition 根据当前帧的深度缓冲恢复世界坐标,适合拾取已经渲染的地形、模型和 3D Tiles。但它依赖浏览器对深度纹理的支持,也依赖目标已经出现在当前视图的深度缓冲中。

只使用它会遇到几类问题:

  • 设备不支持深度拾取。
  • 点击天空或场景空白区域。
  • 当前底图只有椭球,没有可用地形深度。
  • 透明对象没有写入拾取所需的深度。

因此更稳妥的做法是建立降级链。

三级拾取

export function pickWorldPosition(
  viewer: Cesium.Viewer,
  screenPosition: Cesium.Cartesian2,
): Cesium.Cartesian3 | undefined {
  const { scene, camera } = viewer

  // 1. 模型、3D Tiles 和当前帧深度
  if (scene.pickPositionSupported) {
    const depthPosition = scene.pickPosition(screenPosition)
    if (Cesium.defined(depthPosition)) return depthPosition
  }

  // 2. 相机射线和地形求交
  const ray = camera.getPickRay(screenPosition)
  if (ray) {
    const terrainPosition = scene.globe.pick(ray, scene)
    if (Cesium.defined(terrainPosition)) return terrainPosition
  }

  // 3. 回退到 WGS84 椭球
  const ellipsoidPosition = camera.pickEllipsoid(
    screenPosition,
    scene.globe.ellipsoid,
  )

  return ellipsoidPosition ?? undefined
}

它的执行顺序是:

屏幕坐标
  ├─ 深度缓冲有结果 → 模型、3D Tiles 或地形表面
  ├─ 否则发射相机射线 → 与当前地形求交
  └─ 仍然失败 → 与 WGS84 椭球求交

三个结果都返回 Cartesian3,后续业务不需要知道拾取发生在哪一层。

高度应该保留还是归零

有些地图标点需求会把负高度改成零:

const cartographic = Cesium.Cartographic.fromCartesian(position)
if (cartographic.height < 0) cartographic.height = 0

这是业务规则,不是通用坐标转换规则。

如果系统需要表达地下管线、隧道、矿井或海底位置,负高度必须保留。更合理的方式是把它做成参数:

function normalizeHeight(
  position: Cesium.Cartesian3,
  clampNegativeHeight = false,
) {
  if (!clampNegativeHeight) return position

  const point = Cesium.Cartographic.fromCartesian(position)
  point.height = Math.max(point.height, 0)

  return Cesium.Cartesian3.fromRadians(
    point.longitude,
    point.latitude,
    point.height,
  )
}

把拾取接入鼠标事件

const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

handler.setInputAction(
  (movement: { position: Cesium.Cartesian2 }) => {
    const cartesian = pickWorldPosition(viewer, movement.position)
    if (!cartesian) return

    const { lon, lat, height } = cartesian3ToLonLat(cartesian)

    console.log({ lon, lat, height })
  },
  Cesium.ScreenSpaceEventType.LEFT_CLICK,
)

movement.position 是点击发生时的 Canvas 坐标。鼠标移动事件使用的是 movement.endPosition,两者不要混用。

在拾取位置添加图钉

function addPin(viewer: Cesium.Viewer, position: Cesium.Cartesian3) {
  const { lon, lat } = cartesian3ToLonLat(position)

  return viewer.entities.add({
    position,
    point: {
      pixelSize: 14,
      color: Cesium.Color.CYAN,
      outlineColor: Cesium.Color.WHITE,
      outlineWidth: 3,
      disableDepthTestDistance: Number.POSITIVE_INFINITY,
    },
    label: {
      text: `${lon.toFixed(6)}°, ${lat.toFixed(6)}°`,
      verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
      pixelOffset: new Cesium.Cartesian2(0, -20),
      fillColor: Cesium.Color.WHITE,
      outlineColor: Cesium.Color.BLACK,
      outlineWidth: 3,
      style: Cesium.LabelStyle.FILL_AND_OUTLINE,
    },
  })
}

disableDepthTestDistance: Number.POSITIVE_INFINITY 会让图钉始终显示在地形前面。它适合交互标记,但不适合需要真实遮挡关系的物体。

多个图钉应保存 Entity 引用,清理时逐个移除:

const pins: Cesium.Entity[] = []

pins.push(addPin(viewer, position))

function clearPins() {
  pins.forEach(pin => viewer.entities.remove(pin))
  pins.length = 0
}

世界坐标转回屏幕坐标

信息气泡如果使用普通 HTML,需要在每帧或相机变化时把世界位置投影回屏幕:

function worldToScreen(
  viewer: Cesium.Viewer,
  position: Cesium.Cartesian3,
) {
  const result = Cesium.SceneTransforms.worldToWindowCoordinates(
    viewer.scene,
    position,
  )

  return result ? { x: result.x, y: result.y } : undefined
}

它只负责投影。目标位于地球背面时,还需要结合相机方向或遮挡检测决定是否隐藏 HTML 元素。

销毁事件处理器

ScreenSpaceEventHandler 不会因为局部变量被覆盖就自动解除 Canvas 事件。组件销毁时要显式调用 destroy:

function dispose() {
  handler.destroy()
  clearPins()
}

坐标拾取的主线可以收成一句话:先从 Canvas 像素获得一个可靠的世界坐标,再把它转换成业务需要的经纬度和高度。深度拾取、地形求交和椭球求交分别解决不同场景,不能互相替代。

参考资料

相关文章

Cesium 三维风场:用 WebGL 2 GPU 粒子描绘全球气流

拆解案例中的风场数据、GPU 粒子更新、轨迹渲染、颜色映射和 Vue 参数控制

Cesium 北斗三维网格:在城市 3D Tiles 上做 GeoSOT-3D 剖分

介绍区域体素生成、建筑表面采样、结构碰撞检测、Worker 几何构建和网格编码查询