cesium学习(四)-相机

什么是 Camera

Camera 是 Cesium 中控制"从哪里看、往哪里看、怎么看"的核心对象。

它主要由三部分组成:

  • 位置:相机在三维世界中的坐标。
  • 方向:相机朝向哪里。
  • 视锥体:相机能看到的空间范围。
text 复制代码
Camera
 ├─ position   相机位置(Cartesian3,ECEF 坐标)
 ├─ direction  朝向方向(单位向量)
 ├─ up         相机上方向(单位向量)
 ├─ right      相机右方向(单位向量)
 └─ frustum    视锥体

可以把 Camera 理解成真实世界里的摄像机:

text 复制代码
position  = 摄像机放在哪里
direction = 摄像机镜头朝哪里
frustum   = 摄像机能拍到多大范围

Camera 在 Viewer 中的位置

创建 Viewer 后,通过 viewer.cameraviewer.scene.camera 拿到相机对象(两者等价)。

js 复制代码
const viewer = new Cesium.Viewer('cesiumContainer')

const camera = viewer.camera
// 等价于
const camera = viewer.scene.camera

层级关系:

text 复制代码
Viewer
 └─ scene
    └─ camera

视锥体 Frustum

视锥体(Frustum)是相机能看到的空间范围,由 6 个平面围成:

  • left / right / top / bottom:四个侧面
  • near:近平面
  • far:远平面
text 复制代码
       far
    /-------/
   /       /
  /-------/
     near
      |
   Camera

Cesium 用视锥体做裁剪(Frustum Culling):

text 复制代码
在视锥体内的对象   → 可能渲染
不在视锥体内的对象 → 直接剔除,不消耗性能

这是 3D Tiles、地形、影像和 Primitive 能高性能渲染的重要原因之一。

视锥体类型

类型 说明
PerspectiveFrustum 透视投影,近大远小,默认 3D 模式使用
OrthographicFrustum 正交投影,无透视效果,常用于 2D 模式
js 复制代码
// 查看当前视锥体类型
console.log(viewer.camera.frustum)

读取当前相机状态

实际业务中经常需要保存或同步当前视角,可以直接读取相机属性。

读取位置(经纬度 + 高度)

js 复制代码
const cartographic = viewer.camera.positionCartographic

const longitude = Cesium.Math.toDegrees(cartographic.longitude)
const latitude  = Cesium.Math.toDegrees(cartographic.latitude)
const height    = cartographic.height

console.log(longitude, latitude, height)

读取姿态角

js 复制代码
const heading = Cesium.Math.toDegrees(viewer.camera.heading)
const pitch   = Cesium.Math.toDegrees(viewer.camera.pitch)
const roll    = Cesium.Math.toDegrees(viewer.camera.roll)

console.log(heading, pitch, roll)

读取 Cartesian3 位置

js 复制代码
const position = viewer.camera.position // Cartesian3,ECEF 坐标

保存 / 恢复视角

js 复制代码
// 保存
const savedView = {
  destination: viewer.camera.position.clone(),
  orientation: {
    heading: viewer.camera.heading,
    pitch:   viewer.camera.pitch,
    roll:    viewer.camera.roll
  }
}

// 恢复
viewer.camera.setView(savedView)

Heading / Pitch / Roll 详解

heading

heading 表示水平朝向,可以理解为指南针方向。

text 复制代码
0°   → 朝北
90°  → 朝东
180° → 朝南
270° → 朝西

pitch

pitch 表示俯仰角。

text 复制代码
0°   → 平视(水平)
-45° → 斜向下看
-90° → 垂直向下俯视

Cesium 里俯视地图通常用负数:

js 复制代码
pitch: Cesium.Math.toRadians(-45)

roll

roll 表示镜头翻滚,绕朝向轴旋转。一般业务场景很少使用,通常设为 0

Cesium 的角度参数统一使用弧度 ,需要用 Cesium.Math.toRadians() 将角度值转换。


destination 与 orientation 参数

控制相机时最常见的两个参数:

text 复制代码
destination = 相机位置
orientation = 相机姿态

destination

destination 通常是 Cartesian3 坐标:

js 复制代码
const destination = Cesium.Cartesian3.fromDegrees(
  116.39, // 经度
  39.9,   // 纬度
  1000    // 椭球高 HAE(单位:米)
)

注意:这里的高度是椭球高(HAE),不是海拔高(ASL),两者在有地形时有差异。

orientation

orientation 包含三个角度(均为弧度):

参数 含义 常见值
heading 水平方向角 0 表示朝北
pitch 俯仰角 负值向下看
roll 翻滚角 通常为 0
js 复制代码
orientation: {
  heading: Cesium.Math.toRadians(0),
  pitch:   Cesium.Math.toRadians(-45),
  roll:    0
}

控制相机视角

setView ------ 立即跳转

立即切换视角,无动画过程,适合初始化视角、重置视角、快速定位。

js 复制代码
viewer.camera.setView({
  destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 1000),
  orientation: {
    heading: Cesium.Math.toRadians(0),
    pitch:   Cesium.Math.toRadians(-45),
    roll:    0
  }
})

flyTo ------ 飞行动画

带动画的飞行切换,适合点击列表定位、从一个城市飞到另一个城市。

js 复制代码
viewer.camera.flyTo({
  destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 1000),
  orientation: {
    heading: Cesium.Math.toRadians(0),
    pitch:   Cesium.Math.toRadians(-45),
    roll:    0
  },
  duration: 2,            // 动画时长(秒),默认 3
  complete: () => {       // 飞行完成回调
    console.log('arrived')
  },
  cancel: () => {         // 被取消时回调
    console.log('cancelled')
  }
})

viewer.flyTo vs camera.flyTo

两者都能触发飞行动画,但适用场景不同:

API 目标类型 说明
viewer.flyTo(target) Entity、Primitive、Tileset、DataSource 等 自动计算目标包围球,高层封装
camera.flyTo(options) 具体的 destination 坐标 底层,需要手动指定坐标和姿态
js 复制代码
// 飞到 tileset(推荐方式,自动适配位置)
viewer.flyTo(tileset)

// 飞到某个 Entity
viewer.flyTo(entity, { duration: 2 })

// 飞到具体坐标(需要自己算好位置)
viewer.camera.flyTo({
  destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 500)
})

实际开发中,有 Entity 或 Tileset 对象时优先用 viewer.flyTo(),只知道坐标时用 camera.flyTo()


flyToBoundingSphere

当有一个模型、Tileset 或一组对象时,可以用包围球定位。

js 复制代码
viewer.camera.flyToBoundingSphere(tileset.boundingSphere, {
  duration: 2,
  offset: new Cesium.HeadingPitchRange(
    Cesium.Math.toRadians(0),   // heading
    Cesium.Math.toRadians(-30), // pitch
    500                         // 与目标点的距离(米)
  )
})

HeadingPitchRange 三个参数:

  • heading:绕目标点水平旋转角度
  • pitch:俯仰角
  • range:相机与目标点的距离

lookAt

让相机锁定围绕一个目标点观察,适合查看建筑、模型、设备。

js 复制代码
const target = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0)

viewer.camera.lookAt(
  target,
  new Cesium.HeadingPitchRange(
    Cesium.Math.toRadians(0),
    Cesium.Math.toRadians(-45),
    1000
  )
)

使用 lookAt 后,相机会进入以目标点为参考的锁定状态,鼠标旋转会绕目标点转。如果想恢复自由相机,调用:

js 复制代码
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY)

zoomIn / zoomOut

沿相机朝向方向前进或后退一段距离,适合程序化调整远近。

js 复制代码
viewer.camera.zoomIn(500)  // 向前移动 500 米
viewer.camera.zoomOut(500) // 向后移动 500 米

相机位移与旋转方法族

Cesium 提供了一组细粒度的相机控制方法,常用于自定义交互或动画。

位移

js 复制代码
viewer.camera.moveForward(100)   // 沿朝向前进
viewer.camera.moveBackward(100)  // 沿朝向后退
viewer.camera.moveLeft(100)      // 向左平移
viewer.camera.moveRight(100)     // 向右平移
viewer.camera.moveUp(100)        // 向上移动
viewer.camera.moveDown(100)      // 向下移动

旋转

js 复制代码
viewer.camera.rotateLeft(0.1)    // 向左旋转(弧度)
viewer.camera.rotateRight(0.1)   // 向右旋转
viewer.camera.rotateUp(0.1)      // 向上旋转
viewer.camera.rotateDown(0.1)    // 向下旋转

视角调整

js 复制代码
viewer.camera.twistLeft(0.1)     // 逆时针翻滚
viewer.camera.twistRight(0.1)    // 顺时针翻滚
viewer.camera.lookLeft(0.1)      // 原地向左看
viewer.camera.lookRight(0.1)     // 原地向右看
viewer.camera.lookUp(0.1)        // 原地向上看
viewer.camera.lookDown(0.1)      // 原地向下看

cancelFlight ------ 取消飞行

如果 flyToflyToBoundingSphere 正在执行,可以随时取消:

js 复制代码
viewer.camera.cancelFlight()

常见用途:用户在飞行过程中点击了另一个目标,需要先取消当前飞行再发起新的。

js 复制代码
// 先取消当前飞行,再飞向新目标
viewer.camera.cancelFlight()
viewer.camera.flyTo({ destination: newDestination })

坐标转换

屏幕坐标转地球坐标

方式一:pickPosition(有深度信息时)

适合有 3D Tiles、模型或开启深度检测的场景。

js 复制代码
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

handler.setInputAction((movement) => {
  const cartesian = viewer.scene.pickPosition(movement.position)

  if (!Cesium.defined(cartesian)) return

  const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
  const longitude = Cesium.Math.toDegrees(cartographic.longitude)
  const latitude  = Cesium.Math.toDegrees(cartographic.latitude)
  const height    = cartographic.height

  console.log(longitude, latitude, height)
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)

方式二:pickEllipsoid(拾取椭球表面)

当场景没有 3D Tiles 或模型时,pickPosition 可能拿不到结果。此时用 pickEllipsoid 拾取椭球面(忽略地形高度):

js 复制代码
const cartesian = viewer.camera.pickEllipsoid(
  movement.position,
  viewer.scene.globe.ellipsoid
)

两者对比

方法 适用场景 注意
scene.pickPosition() 有模型/3D Tiles/深度信息 需要开启深度测试,结果受深度影响
camera.pickEllipsoid() 纯地球表面 忽略地形起伏,只返回椭球面坐标

世界坐标转屏幕坐标

js 复制代码
const position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100)

const windowPosition = Cesium.SceneTransforms.worldToWindowCoordinates(
  viewer.scene,
  position
)

if (Cesium.defined(windowPosition)) {
  console.log(windowPosition.x, windowPosition.y)
}

常用于 HTML 标注、弹窗跟随、DOM 面板贴合三维坐标。

注意:当目标点在相机背后或屏幕外时,worldToWindowCoordinates 可能返回 undefined 或屏幕外坐标,使用前需判断。


射线拾取

camera.getPickRay 可以获取屏幕某点投射出的射线,配合 globe.pick 拾取地形表面(包含地形高度)。

js 复制代码
const ray = viewer.camera.getPickRay(movement.position)
const cartesian = viewer.scene.globe.pick(ray, viewer.scene)

if (Cesium.defined(cartesian)) {
  const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
  const longitude = Cesium.Math.toDegrees(cartographic.longitude)
  const latitude  = Cesium.Math.toDegrees(cartographic.latitude)
  const height    = cartographic.height
  console.log(longitude, latitude, height)
}

pickPosition 的区别:globe.pick 专门用于地形表面拾取,结果更稳定。


获取当前视野范围

js 复制代码
const rectangle = viewer.camera.computeViewRectangle()

if (Cesium.defined(rectangle)) {
  const west  = Cesium.Math.toDegrees(rectangle.west)
  const south = Cesium.Math.toDegrees(rectangle.south)
  const east  = Cesium.Math.toDegrees(rectangle.east)
  const north = Cesium.Math.toDegrees(rectangle.north)

  console.log(west, south, east, north)
}

常用场景:相机停止后用当前视野范围请求接口数据,或同步鹰眼地图范围。


相机交互控制

Cesium 默认允许用户用鼠标控制相机:

操作 效果
左键拖拽 旋转 / 平移
右键拖拽 缩放
滚轮 缩放
中键拖拽 倾斜视角

这些交互由 screenSpaceCameraController 控制。

开启 / 关闭交互

js 复制代码
const controller = viewer.scene.screenSpaceCameraController

controller.enableRotate    = true   // 旋转
controller.enableTranslate = true   // 平移
controller.enableZoom      = true   // 缩放
controller.enableTilt      = true   // 倾斜
controller.enableLook      = true   // 自由观察

禁止缩放

js 复制代码
viewer.scene.screenSpaceCameraController.enableZoom = false

限制相机高度

js 复制代码
const controller = viewer.scene.screenSpaceCameraController

controller.minimumZoomDistance = 100    // 最近距离(米)
controller.maximumZoomDistance = 50000  // 最远距离(米)

适合限制用户不能钻到地下,或者不能无限拉远。


监听相机变化

moveStart / moveEnd

相机开始移动和停止移动时触发。

js 复制代码
viewer.camera.moveStart.addEventListener(() => {
  console.log('相机开始移动')
})

viewer.camera.moveEnd.addEventListener(() => {
  console.log('相机停止移动')
  // 适合在这里请求接口
})

常见用途:

  • 相机停止后加载业务数据
  • 根据视野范围请求接口
  • 同步鹰眼地图
  • 保存用户当前视角

camera.changed

camera.changed 是一个更细粒度的相机变化事件,每当相机属性变化超过一定比例时触发。

js 复制代码
// percentageChanged 控制灵敏度,默认 0.5,范围 0~1
// 值越小,越灵敏(越小的变化也会触发)
viewer.camera.percentageChanged = 0.01

viewer.camera.changed.addEventListener(() => {
  const cartographic = viewer.camera.positionCartographic
  const height = cartographic.height
  console.log('相机高度变化到', height)
})

moveEnd 的对比:

事件 触发时机 适用场景
moveEnd 相机完全停止后 停止后请求接口、保存视角
camera.changed 相机属性变化超过阈值时 实时同步视野、动态更新 UI

2D / 3D / Columbus View

Cesium 支持三种场景模式:

模式 含义
SCENE3D 三维地球
SCENE2D 二维平面地图
COLUMBUS_VIEW 2.5D 哥伦布视图
js 复制代码
viewer.scene.morphTo3D()
viewer.scene.morphTo2D()
viewer.scene.morphToColumbusView()

不同模式下,相机的交互方式和视锥体都有差异:

  • SCENE2D 下相机只能平移和缩放,视锥体退化为正交投影。
  • COLUMBUS_VIEW 类似 3D 但地球被展开为平面,可以倾斜视角。
  • 大多数三维项目主要使用 SCENE3D

常见问题 FAQ

1. flyTo 飞完看不到目标

常见原因:高度太低、pitch 方向不对、目标点坐标错误,或 3D Tiles 还没加载完成。建议:

  • 先提高 destination 高度
  • 使用 viewer.flyTo(tileset) 自动适配
  • complete 回调里再确认状态

2. 相机飞到地下

可能是高度设置过低,或开启地形后地表高度高于相机高度。设置 minimumZoomDistance,并结合 sampleTerrainMostDetailed 查询实际地面高度后再设置相机位置。

3. 点击地球拿不到坐标

scene.pickPosition 依赖深度信息,无模型时可能失败。改用 camera.pickEllipsoid 拾取椭球面,或 globe.pick(ray) 拾取地形表面。

4. heading / pitch / roll 方向不符合预期

先确认单位是否是弧度。Cesium 的角度参数统一使用弧度,不是角度。

5. HTML 弹窗位置不跟随

需要在每帧或相机移动时重新调用 SceneTransforms.worldToWindowCoordinates,将三维坐标换算成屏幕坐标,并在监听 scene.postRender 时更新 DOM 位置。

js 复制代码
viewer.scene.postRender.addEventListener(() => {
  const pos = Cesium.SceneTransforms.worldToWindowCoordinates(
    viewer.scene,
    targetPosition
  )
  if (Cesium.defined(pos)) {
    el.style.left = pos.x + 'px'
    el.style.top  = pos.y + 'px'
  }
})

6. 相机变化导致频繁请求接口

不要在相机每一帧都请求接口。改用 moveEnd 事件,或在 camera.changed 上加防抖。

7. lookAt 之后鼠标旋转行为异常

lookAt 会锁定相机围绕目标点旋转,不是自由旋转。如需恢复:

js 复制代码
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY)

8. flyTo 飞行中途如何切换目标

js 复制代码
viewer.camera.cancelFlight()
viewer.camera.flyTo({ destination: newDestination })

小结

Camera 是 Cesium 里控制视角的核心对象,实际开发中最常用的 API:

场景 推荐 API
初始化 / 重置视角 camera.setView
飞到某个经纬度 camera.flyTo
飞到模型或 Tileset viewer.flyTo
通过包围球定位 camera.flyToBoundingSphere
围绕目标观察 camera.lookAt
取消正在进行的飞行 camera.cancelFlight
屏幕点转三维坐标 scene.pickPosition / camera.pickEllipsoid / globe.pick
三维坐标转屏幕坐标 SceneTransforms.worldToWindowCoordinates
获取当前相机位置 camera.positionCartographic
获取当前视野范围 camera.computeViewRectangle
限制用户交互 screenSpaceCameraController
相机停止后触发逻辑 camera.moveEnd
实时监听相机变化 camera.changed + percentageChanged
相关推荐
悟空瞎说1 小时前
Git Worktree 实战:多 AI 编码代理并行开发,彻底解决分支切换冲突痛点
前端·git
zeqinjie2 小时前
Skills-Flutter 内测泄漏审核
前端·flutter·app
村上小树2 小时前
非常简单地学习一下shareDB的原理
前端·javascript
认真的薛薛2 小时前
阿里云: A记录 & CNAME
服务器·前端·阿里云
2301_815645383 小时前
css基础
前端·css
Hilaku3 小时前
求求你们🙏 ,别再换打包工具了?
前端·javascript·程序员
用户新3 小时前
V8引擎 精品漫游指南--Ignition篇(下 二) JavaScript 栈帧详解
前端·javascript
账号已注销free3 小时前
box-shadow完整用法
前端
得闲喝茶3 小时前
JavaScript在数据处理的应用
开发语言·前端·javascript·经验分享·笔记