相机与视角控制
三维场景里「视角」不是缩放级别,而是相机的位置加朝向。位置用经纬高描述, 朝向用 heading、pitch、roll 三个角描述,两者必须一起给,否则画面会侧翻或者看不见球体。
三个定位方法的分工
| 方法 | 行为 | 适用场景 |
|---|---|---|
camera.setView | 瞬间切换,无动画 | 初始化、视角书签、恢复状态 |
camera.flyTo | 带飞行动画,可指定时长与缓动 | 用户触发的跳转、引导视角 |
camera.lookAt | 以某个目标点为中心设置局部参照系 | 绕物体旋转、跟随单个建筑 |
viewer.zoomTo | 按实体或瓦片的包围球自动取景 | 加载完数据后定位,不用自己算高度 |
setView 传参最直观:destination 是相机位置,
orientation 是朝向。两者同时给的时候,相机就以给定位置和角度观察地球:
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(116.391263, 39.907359, 3000),
orientation: {
heading: Cesium.Math.toRadians(20),
pitch: Cesium.Math.toRadians(-45),
roll: 0
}
});
三个角度怎么数
heading 是方位角,从正北开始顺时针为正,0 表示相机朝向北方,90 表示朝向正东。 pitch 是俯仰角,0 表示水平看出去,-90 表示垂直向下看。 roll 是绕视线的自转,0 表示地平线水平,非零会让画面倾斜。
最常见的错误是只给 destination 不给 orientation。
这时 Cesium 会保留相机原有朝向,如果上一次视角是斜的,新位置看起来就会很奇怪。
做固定视角的展示页时,两个参数要一起写。
pitch 不能取正值。相机在地表以上向下看,有效的仰角范围是
[-90°, 0°]。传 30 度并不会让相机抬头,而是会被钳制或导致定位失效。
用包围球取景
手工试高度很费时间:低了穿模,高了看不清。数据本身有包围球时,
直接交给 flyToBoundingSphere,它按视场角反算距离:
var sphere = new Cesium.BoundingSphere(
Cesium.Cartesian3.fromDegrees(116.391263, 39.907359, 0),
500
);
viewer.camera.flyToBoundingSphere(sphere, {
duration: 2,
offset: new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(45), // 从东北方向看
Cesium.Math.toRadians(-35),
1500 // 距离中心 1500 米
)
});
offset 的第三个参数是相机到包围球中心的距离,不是高度。
距离写成 sphere.radius * 3 一类的比例值,换到别的数据上也大致合适。
限制用户操作范围
默认情况下用户可以把相机拉到地下、滚到很远。做行业应用时通常要设边界:
| 属性 | 默认 | 作用 |
|---|---|---|
scene.screenSpaceCameraController.minimumZoomDistance | 1 | 最近可以拉到离地表多少米,设太小会穿进地面 |
scene.screenSpaceCameraController.maximumZoomDistance | Infinity | 最远距离,防止用户滚出太阳系 |
scene.screenSpaceCameraController.enableCollisionDetection | true | 开启后相机不会穿到椭球体以下 |
scene.screenSpaceCameraController.enableTilt | true | 关闭后只能平移缩放,不能改变 pitch |
scene.screenSpaceCameraController.enableRotate | true | 关闭后只能倾斜不能绕点旋转 |
var controller = viewer.scene.screenSpaceCameraController;
controller.minimumZoomDistance = 200;
controller.maximumZoomDistance = 2.0e7;
controller.enableCollisionDetection = true;
保存和恢复视角
相机位置本质上是 ECEF 坐标,直接存 camera.position 不方便复现。
存经纬高加三个角度更稳,换分辨率、换容器尺寸后依然能还原:
function captureView() {
var c = viewer.camera.positionCartographic;
return {
lng: Cesium.Math.toDegrees(c.longitude),
lat: Cesium.Math.toDegrees(c.latitude),
height: c.height,
heading: viewer.camera.heading,
pitch: viewer.camera.pitch,
roll: viewer.camera.roll
};
}
function restoreView(v) {
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(v.lng, v.lat, v.height),
orientation: { heading: v.heading, pitch: v.pitch, roll: v.roll }
});
}
heading、pitch、roll 返回的就是弧度,不用再转换。
存进数据库时注意 height 是海拔高度,不是距离地心的距离。
五个实际会踩到的问题
1. flyTo 没有动画
没写 duration 时默认 3 秒;写了 duration: 0 则是瞬间跳转。
从「瞬间」误判成「动画失效」的情况很常见,先看有没有传 0。
2. 连续点击导致飞行被截断
上一次飞行没结束时再调 flyTo,新的一次会直接接管。
要做平滑串联,用 camera.cancelFlight() 先结束,或者用
flight 完成事件排队。
3. 2D 模式下朝向无效
二维场景里相机固定俯视,heading 只影响旋转方向,pitch 被忽略。
二三维切换的项目要在切换后重新设一次视角。
4. 开了 requestRenderMode 后飞行卡顿
按需渲染模式下,相机运动过程中必须持续触发渲染。飞行这类内置动画会自己请求帧,
但用 setInterval 手改 setView 就不会,
需要显式调用 scene.requestRender()。
5. 相机在球体内部
把 destination 的高度写成负数,或者 Z 值算错,相机就进了地球内部,
画面变成一片黑。排查时打印 camera.positionCartographic.height,
这个值必须是正数。
经纬志(2026)。《相机与视角控制》。https://www.jingweizhi.com/cesium/tutorial/camera.html(访问日期 2026-11-03)。