cesium封装限制相机范围接口

在开发 Cesium 三维应用时,相机控制是用户交互体验的核心部分。有时我们需要限制用户的视角范围,比如在展示特定区域时防止用户迷失方向,或者在虚拟导览中保持合理的观察视角。本文将详细介绍如何在 Cesium 中实现相机约束,限制用户可以浏览的角度、高度和区域范围。

在三维 GIS 应用中,相机约束有多种实用场景:

  1. 导览体验优化:在虚拟导览中,限制视角可以提供更有引导性的体验
  2. 防止无效视角:避免用户旋转到奇怪的角度或者深入地下
  1. 聚焦关注区域:保持用户视线在特定区域内,突出重点
  2. 减少用户迷失:在大型场景中,限制可浏览范围防止用户迷失
  3. 场景性能优化:限制相机可以减少渲染和数据加载的范围,提升性能

相机约束的类型

Cesium 的相机约束主要包括以下几种:

1. 俯仰角约束(Pitch)

限制相机的上下俯仰角度,防止用户看到地下或转到不适合的角度。

2. 航向角约束(Heading)

限制相机的水平旋转角度,控制用户可以环视的范围。

3. 缩放距离约束(Distance)

限制相机与目标点的距离,控制用户可以放大和缩小的程度。

4. 高度约束(Height)

限制相机距离地表的高度,防止用户飞得过高或过低。

5. 地理范围约束(Geographic Bounds)

限制相机在特定地理区域内移动,确保用户不会飞离关注区域。

实现方案

下面是一个完整的实现方案,通过扩展 Cesium 的 Camera 类,添加自定义的视角限制功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
* 限制相机范围
* @param {Object} [cameraVisionOptions] 限范围对象.
*/
Camera.prototype.setCameraVision = function (cameraVisionOptions) {
const scene = this._coreMap.scene;
const camera = this._camera;

if (cameraVisionOptions.limitDistance) {
scene.screenSpaceCameraController.minimumZoomDistance = cameraVisionOptions.limitDistance[0];
scene.screenSpaceCameraController.maximumZoomDistance = cameraVisionOptions.limitDistance[1];
}
this.preRenderListener = () => {
const cameraPosition = camera.positionCartographic;

const currentHeading = Cesium.Math.toDegrees(camera.heading);
const currentPitch = Cesium.Math.toDegrees(camera.pitch);

let needUpdate = false;
let newHeading = currentHeading;
let newPitch = currentPitch;
let newHeight = cameraPosition.height;

if (cameraVisionOptions.limitHeading) {
if (
currentHeading < cameraVisionOptions.limitHeading[0] ||
currentHeading > cameraVisionOptions.limitHeading[1]
) {
needUpdate = true;
newHeading = Cesium.Math.clamp(
currentHeading,
cameraVisionOptions.limitHeading[0],
cameraVisionOptions.limitHeading[1]
);
}
}

if (cameraVisionOptions.limitPitch) {
if (
currentPitch < cameraVisionOptions.limitPitch[0] ||
currentPitch > cameraVisionOptions.limitPitch[1]
) {
needUpdate = true;
newPitch = Cesium.Math.clamp(
currentPitch,
cameraVisionOptions.limitPitch[0],
cameraVisionOptions.limitPitch[1]
);
}
}
if (cameraVisionOptions.limitHeight) {
if (
cameraPosition.height < cameraVisionOptions.limitHeight[0] ||
cameraPosition.height > cameraVisionOptions.limitHeight[1]
) {
needUpdate = true;
newHeight = Cesium.Math.clamp(
cameraPosition.height,
cameraVisionOptions.limitHeight[0],
cameraVisionOptions.limitHeight[1]
);
}
}
if (needUpdate) {
camera.setView({
destination: Cesium.Cartesian3.fromRadians(
cameraPosition.longitude,
cameraPosition.latitude,
newHeight
),
orientation: {
heading: Cesium.Math.toRadians(newHeading),
pitch: Cesium.Math.toRadians(newPitch),
roll: camera.roll,
},
});
}

if (cameraVisionOptions.center && cameraVisionOptions.radius) {
const centerCartesian = Cesium.Cartesian3.fromDegrees(
cameraVisionOptions.center[0],
cameraVisionOptions.center[1]
);
const currentPosition = camera.position;
const distance = Cesium.Cartesian3.distance(currentPosition, centerCartesian);

if (distance > cameraVisionOptions.radius) {
const direction = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(currentPosition, centerCartesian, new Cesium.Cartesian3()),
new Cesium.Cartesian3()
);
const newPosition = Cesium.Cartesian3.multiplyByScalar(
direction,
cameraVisionOptions.radius,
new Cesium.Cartesian3()
);
Cesium.Cartesian3.add(centerCartesian, newPosition, newPosition);
camera.position = newPosition;
}
}
};
scene.preRender.addEventListener(this.preRenderListener);
};

/**
* 取消相机限制
*/
Camera.prototype.removeCameraVision = function () {
const scene = this._coreMap.scene;

scene.screenSpaceCameraController.minimumZoomDistance = 1.0;
scene.screenSpaceCameraController.maximumZoomDistance = Number.POSITIVE_INFINITY;

if (this.preRenderListener) {
this._coreMap.scene.preRender.removeEventListener(this.preRenderListener);
this.preRenderListener = null;
}
};

代码解析

setCameraVision 方法

这个方法用于设置相机约束,接受一个包含多种限制参数的配置对象:

  1. 距离限制设置:
1
2
3
4
if (cameraVisionOptions.limitDistance) {
scene.screenSpaceCameraController.minimumZoomDistance = cameraVisionOptions.limitDistance[0];
scene.screenSpaceCameraController.maximumZoomDistance = cameraVisionOptions.limitDistance[1];
}

这里直接使用 Cesium 内置的缩放距离限制属性,非常直观。

  1. preRender 事件监听:
1
2
3
4
this.preRenderListener = () => {
// 约束逻辑
};
scene.preRender.addEventListener(this.preRenderListener);

设置一个在每帧渲染前执行的监听器,这是实现动态约束的关键。

  1. 俯仰角和航向角约束:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (cameraVisionOptions.limitHeading) {
if (
currentHeading < cameraVisionOptions.limitHeading[0] ||
currentHeading > cameraVisionOptions.limitHeading[1]
) {
needUpdate = true;
newHeading = Cesium.Math.clamp(
currentHeading,
cameraVisionOptions.limitHeading[0],
cameraVisionOptions.limitHeading[1]
);
}
}

if (cameraVisionOptions.limitPitch) {
// 类似逻辑
}

检查当前角度是否超出限制,如果是则计算新的合法角度。

  1. 高度约束:
1
2
3
if (cameraVisionOptions.limitHeight) {
// 类似逻辑
}

限制相机的高度范围。

  1. 应用新视图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (needUpdate) {
camera.setView({
destination: Cesium.Cartesian3.fromRadians(
cameraPosition.longitude,
cameraPosition.latitude,
newHeight
),
orientation: {
heading: Cesium.Math.toRadians(newHeading),
pitch: Cesium.Math.toRadians(newPitch),
roll: camera.roll,
},
});
}

如果需要更新,则设置新的相机视图。

  1. 地理范围约束:
1
2
3
4
if (cameraVisionOptions.center && cameraVisionOptions.radius) {
// 计算相机到中心点的距离
// 如果超出范围,强制相机返回到允许的最远距离
}

这是一个圆形地理围栏,限制相机不能离开指定的圆形区域。

removeCameraVision 方法

这个方法用于移除相机约束:

1
2
3
4
5
6
7
8
9
10
11
Camera.prototype.removeCameraVision = function () {
const scene = this._coreMap.scene;

scene.screenSpaceCameraController.minimumZoomDistance = 1.0;
scene.screenSpaceCameraController.maximumZoomDistance = Number.POSITIVE_INFINITY;

if (this.preRenderListener) {
this._coreMap.scene.preRender.removeEventListener(this.preRenderListener);
this.preRenderListener = null;
}
};

这里重置了缩放距离限制,并移除了 preRender 事件监听器,完全释放相机约束。

使用方法

下面是几个使用示例:

示例 1:限制俯仰角和航向角

1
2
3
4
5
6
7
8
// 只允许向下30°到向上30°的俯仰角,以及0°到180°的航向角
const pitchOnlyOptions = {
limitPitch: [-30, 30],
limitHeading: [0, 180],
};

// 应用约束
viewer.camera.setCameraVision(pitchOnlyOptions);

示例 2:设置完整约束

1
2
3
4
5
6
7
8
9
10
11
12
// 设置完整的相机约束
const fullConstraints = {
limitPitch: [-45, 0], // 俯仰角限制(度)
limitHeading: [-180, 180], // 航向角限制(度)
limitDistance: [1000, 10000], // 缩放距离限制(米)
limitHeight: [500, 5000], // 高度限制(米)
center: [116.3912, 39.9073], // 限制区中心点经纬度(北京)
radius: 50000, // 限制区域半径(米)
};

// 应用约束
viewer.camera.setCameraVision(fullConstraints);

示例 3:创建虚拟导览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 设置第一人称导览约束
const tourConstraints = {
limitPitch: [-10, 45], // 适合查看建筑物的俯仰角
limitHeight: [1.7, 100], // 人眼高度到较低空中视角
limitDistance: [0, 1000], // 近距离观察
center: [120.1551, 30.2741], // 西湖中心
radius: 3000, // 限制在西湖周围3公里
};

// 应用约束
viewer.camera.setCameraVision(tourConstraints);

// 设置初始视角
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(120.1551, 30.2741, 30),
orientation: {
heading: Cesium.Math.toRadians(45),
pitch: Cesium.Math.toRadians(0),
roll: 0,
},
});

示例 4:移除约束

1
2
// 当需要恢复自由视角时,移除约束
viewer.camera.removeCameraVision();
作者

wangyawei

发布于

2025-03-20

更新于

2025-03-21

许可协议