源码:
class Vertebral {
constructor(options: IVertebralOptions) {
this.atlas = options.atlas
this.Create(options.value)
}
public origin!: Cesium.Entity
public atlas!: PointMap
public spotLightCamera!: any;
public primitivesone!: any;
public primitivestwo!: any;
public Create(value: valueType): void {
const positions = Cesium.Cartesian3.fromDegrees(value.droneLon, value.droneLat, value.droneAltitude)
const spotLightCamera = this.spotLightCamera = new Cesium.Camera(this.atlas.viewer.scene)
spotLightCamera.setView({
destination: positions,
orientation: {
heading: Cesium.Math.toRadians(value.aircraftHeading),
pitch: Cesium.Math.toRadians(value.gimbalPitchRotateAngle),
roll: Cesium.Math.toRadians(0)
}
})
const scratchRight = new Cesium.Cartesian3()
const scratchRotation = new Cesium.Matrix3()
const scratchOrientation = new Cesium.Quaternion()
const positionWC = spotLightCamera.positionWC
const directions = spotLightCamera.directionWC
const up = spotLightCamera.upWC
let right = spotLightCamera.rightWC
right = Cesium.Cartesian3.negate(right, scratchRight)
const rotation = scratchRotation
Cesium.Matrix3.setColumn(rotation, 0, right, rotation)
Cesium.Matrix3.setColumn(rotation, 1, up, rotation)
Cesium.Matrix3.setColumn(rotation, 2, directions, rotation)
// 计算视锥姿态
const orientation = Cesium.Quaternion.fromRotationMatrix(rotation, scratchOrientation)
// 摄像机近距离
spotLightCamera.frustum.near = 0.1
// 动态设置摄像机远距离
const altitude = value.droneAltitude
// const altitude = value.altitude + value.droneAltitude
const farDistance = altitude / Math.sin(value.hfov / 2);
spotLightCamera.frustum.far = farDistance;
// @ts-ignore
// 摄像机视野范围
spotLightCamera.frustum.fov = value.vfov
const instanceOutline = new Cesium.GeometryInstance({
geometry: new Cesium.FrustumGeometry({
// @ts-ignore
frustum: spotLightCamera.frustum,
origin: positionWC,
orientation: orientation
}),
// material: Cesium.Color.RED.withAlpha(1),
id: 'pri' + this.atlas.viewer.scene.primitives.length + 1,
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(new Cesium.Color(3 / 255, 215 / 255, 145 / 255, 0.5)),
show: new Cesium.ShowGeometryInstanceAttribute(true)
}
})
const instance = new Cesium.GeometryInstance({
geometry: new Cesium.FrustumOutlineGeometry({
// @ts-ignore
frustum: spotLightCamera.frustum,
origin: positionWC,
orientation: orientation
}),
// material: Cesium.Color.RED.withAlpha(0.1),
id: 'pri0' + this.atlas.viewer.scene.primitives.length + 1,
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(new Cesium.Color(3 / 255, 215 / 255, 145 / 255, 1)),
show: new Cesium.ShowGeometryInstanceAttribute(true)
}
})
this.primitivesone = this.atlas.viewer.scene.primitives.add(new Cesium.Primitive({
geometryInstances: instance,
appearance: new Cesium.PerInstanceColorAppearance({
translucent: true,
flat: true,
}),
asynchronous: false
}))
this.primitivestwo = this.atlas.viewer.scene.primitives.add(new Cesium.Primitive({
geometryInstances: instanceOutline,
appearance: new Cesium.PerInstanceColorAppearance({
translucent: true,
flat: true
}),
asynchronous: false
}))
this.primitivesone.type = 'vertebral'
this.primitivestwo.type = 'vertebral'
}
public Destroy(): void {
new Promise(resolve => {
if (this.primitivesone) {
this.atlas.viewer.entities.remove(this.primitivesone)
this.atlas.viewer.entities.remove(this.primitivestwo)
this.primitivesone.destroy()
this.primitivestwo.destroy()
this.spotLightCamera = null
}
resolve(true)
})
}
public async UpDateVertebral(value: valueType) {
await this.Destroy()
this.Create(value)
}
}
使用
在别的cesium实例类里面
const cameraVertebralParams = {
aircraftHeading, //偏航角
gimbalPitchRotateAngle: pointInfo.payload[0].gimbalPitch,//俯仰角
focalLength, //变焦倍数
droneAltitude,//海拔高
droneLat,
droneLon,
altitude: 1700,//没有用到
}
let hfov = 2 * Math.atan(SENSOR_WIDTH / (2 * cameraVertebralParams.focalLength));
let vfov = 2 * Math.atan(SENSOR_HEIGHT / (2 * cameraVertebralParams.focalLength));
this.pointCamera && this.pointCamera.Destroy()
this.pointCamera = new Vertebral({
atlas: this,(cesium)
value: {
...cameraVertebralParams,
vfov,
hfov
},
})
这些代码的核心作用是在 Cesium 3D 场景中实时创建和更新无人机相机的 “视锥体”(视野范围可视化),通过Vertebral
类实现视锥体的创建、销毁,结合相机参数计算视场角,最终在场景中渲染出无人机当前姿态下的视野范围(类似一个 “可视锥”)。以下是详细解析:
一、Vertebral
类:视锥体的创建与管理
Vertebral
类是核心,负责视锥体的初始化(Create
方法)和销毁(Destroy
方法),本质是对 Cesium 几何体 API 的封装,将无人机参数转换为 3D 可视化的 “锥体”。
1. 类属性说明
typescript
class Vertebral {
public origin!: Cesium.Entity; // 预留的原点实体(未实际使用)
public atlas!: any; // 外部传入的场景上下文(通常是CesiumMap实例,包含viewer)
public spotLightCamera!: any; // 视锥体对应的相机实例(用于计算姿态和视野)
public primitivesone!: any; // 视锥体轮廓线的Primitive(Cesium的3D渲染对象)
public primitivestwo!: any; // 视锥体填充区域的Primitive
}
atlas
:关键依赖,需要包含 Cesium 的viewer
实例(场景核心),用于添加 / 移除 3D 对象。spotLightCamera
:模拟无人机相机的虚拟相机,用于计算视锥体的姿态(方向)和视野范围(视场角)。primitivesone
/primitivestwo
:Cesium 中用于渲染几何体的Primitive
对象,分别对应视锥体的 “轮廓线” 和 “填充区域”。
2. constructor
构造函数
typescript
constructor(options: IVertebralOptions) {
this.atlas = options.atlas; // 接收外部传入的场景上下文(含viewer)
this.Create(options.value); // 初始化时立即调用Create方法,创建视锥体
}
- 作用:接收初始化参数(
options
),保存场景上下文(atlas
),并触发视锥体创建。 IVertebralOptions
类型:应包含atlas
(场景上下文)和value
(无人机 / 相机参数)。
3. Create
方法:核心逻辑(创建视锥体)
该方法是视锥体可视化的核心,通过 6 个步骤将无人机参数转换为 3D 视锥体:
步骤 1:计算无人机位置(世界坐标)
typescript
// 将无人机的经纬度、海拔转换为Cesium的世界坐标(Cartesian3)
const positions = Cesium.Cartesian3.fromDegrees(
value.droneLon, // 经度
value.droneLat, // 纬度
value.droneAltitude // 海拔高度(米)
);
- 作用:确定视锥体的 “顶点” 位置(无人机当前位置)。
步骤 2:初始化虚拟相机(spotLightCamera
)
typescript
// 创建与场景绑定的虚拟相机(用于模拟无人机相机姿态)
const spotLightCamera = this.spotLightCamera = new Cesium.Camera(this.atlas.viewer.scene);
// 设置相机位置和姿态(与无人机一致)
spotLightCamera.setView({
destination: positions, // 相机位置 = 无人机位置
orientation: {
heading: Cesium.Math.toRadians(value.aircraftHeading), // 偏航角(转为弧度)
pitch: Cesium.Math.toRadians(value.gimbalPitchRotateAngle), // 俯仰角(转为弧度)
roll: Cesium.Math.toRadians(0) // 横滚角(默认水平,无倾斜)
}
});
- 关键:虚拟相机的姿态完全匹配无人机的姿态(偏航角控制水平方向,俯仰角控制上下倾斜),确保视锥体的朝向与无人机实际拍摄方向一致。
- 单位转换:Cesium 的角度参数需用弧度,因此通过
Cesium.Math.toRadians
将传入的角度(如aircraftHeading
)转换为弧度。
步骤 3:计算视锥体的姿态(方向矩阵→四元数)
视锥体的姿态由相机的三个方向向量(右、上、前)决定,通过矩阵和四元数表示:
typescript
// 初始化临时变量(优化性能,避免频繁创建对象)
const scratchRight = new Cesium.Cartesian3();
const scratchRotation = new Cesium.Matrix3();
const scratchOrientation = new Cesium.Quaternion();
// 获取相机的三个基础方向向量(Cesium相机自带)
const positionWC = spotLightCamera.positionWC; // 相机世界坐标
const directions = spotLightCamera.directionWC; // 前向向量(拍摄方向)
const up = spotLightCamera.upWC; // 上向向量
let right = spotLightCamera.rightWC; // 右向向量
// 右向向量取反(修正坐标系方向,确保视锥体与Cesium场景坐标系匹配)
right = Cesium.Cartesian3.negate(right, scratchRight);
// 用三个方向向量构建旋转矩阵(描述视锥体在空间中的姿态)
const rotation = scratchRotation;
Cesium.Matrix3.setColumn(rotation, 0, right, rotation); // X轴:右向(取反后)
Cesium.Matrix3.setColumn(rotation, 1, up, rotation); // Y轴:上向
Cesium.Matrix3.setColumn(rotation, 2, directions, rotation); // Z轴:前向
// 旋转矩阵转换为四元数(Cesium中更高效的姿态表示方式,用于几何体定位)
const orientation = Cesium.Quaternion.fromRotationMatrix(rotation, scratchOrientation);
- 作用:通过相机的方向向量计算视锥体的空间姿态,确保视锥体的朝向与无人机相机完全一致(比如无人机朝东偏航,视锥体也朝东)。
- 为什么右向向量取反?Cesium 相机的
rightWC
默认方向可能与视锥体几何体的坐标系相反,取反后可确保视锥体左右方向正确。
步骤 4:配置视锥体的核心参数(近平面、远平面、视场角)
视锥体的 “形状” 由近平面(near)、远平面(far)和视场角(fov)决定:
// 1. 近平面:相机到最近可见平面的距离(过滤过近的物体,避免遮挡)
spotLightCamera.frustum.near = 0.1; // 单位:米
// 2. 远平面:相机到最远可见平面的距离(视锥体的“长度”)
const altitude = value.droneAltitude; // 无人机海拔高度(米)
// 计算公式:远距 = 海拔高度 / sin(水平视场角/2)(基于三角函数,确保视锥体覆盖地面)
const farDistance = altitude / Math.sin(value.hfov / 2);
spotLightCamera.frustum.far = farDistance;
// 3. 垂直视场角:控制视锥体的“高度”范围(与水平视场角hfov共同决定视野宽高比)
spotLightCamera.frustum.fov = value.vfov; // vfov为垂直视场角(弧度)
- 远平面计算逻辑:假设无人机在高空
altitude
处,水平视场角为hfov
,则视锥体底部边缘到无人机正下方地面的距离为altitude / sin(hfov/2)
,确保视锥体刚好覆盖相机能拍摄到的最远距离。 - 视场角关系:
hfov
(水平)和vfov
(垂直)的比例与相机传感器的宽高比一致(你的传感器宽 9.6mm、高 7.2mm,比例 4:3,因此 hfov:vfov≈4:3)。
步骤 5:创建视锥体的几何体(填充区域 + 轮廓线)
通过 Cesium 的FrustumGeometry
(视锥体几何体)和FrustumOutlineGeometry
(视锥体轮廓几何体)创建 3D 模型:
// 1. 视锥体填充区域(半透明实体)
const instanceOutline = new Cesium.GeometryInstance({
geometry: new Cesium.FrustumGeometry({
frustum: spotLightCamera.frustum, // 关联相机的视锥体参数(near/far/fov)
origin: positionWC, // 视锥体原点(无人机位置)
orientation: orientation // 视锥体姿态(四元数,之前计算的方向)
}),
id: 'pri' + ..., // 唯一ID(用于后续销毁)
attributes: {
// 颜色:青绿色,透明度0.5(半透明,避免遮挡场景其他元素)
color: Cesium.ColorGeometryInstanceAttribute.fromColor(new Cesium.Color(3/255, 215/255, 145/255, 0.5)),
show: new Cesium.ShowGeometryInstanceAttribute(true) // 初始显示
}
});
// 2. 视锥体轮廓线(边框,不透明,增强可视性)
const instance = new Cesium.GeometryInstance({
geometry: new Cesium.FrustumOutlineGeometry({ // 轮廓几何体
frustum: spotLightCamera.frustum,
origin: positionWC,
orientation: orientation
}),
id: 'pri0' + ..., // 唯一ID
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(new Cesium.Color(3/255, 215/255, 145/255, 1)), // 不透明
show: new Cesium.ShowGeometryInstanceAttribute(true)
}
});
GeometryInstance
:Cesium 中几何体的 “实例”,包含几何体数据和样式属性(颜色、是否显示)。- 填充区域 vs 轮廓线:填充区域用半透明青绿色展示视野范围,轮廓线用同色不透明线条勾勒边缘,确保 3D 场景中清晰可见。
步骤 6:将几何体添加到场景中(渲染视锥体)
通过Cesium.Primitive
将几何体添加到场景的primitives
集合中,完成渲染:
// 1. 添加轮廓线(线)
this.primitivesone = this.atlas.viewer.scene.primitives.add(new Cesium.Primitive({
geometryInstances: instance, // 轮廓线几何体实例
appearance: new Cesium.PerInstanceColorAppearance({ // 样式:按实例定义的颜色
translucent: true, // 半透明(线条轻微透明,避免过于刺眼)
flat: true // 无光照效果(平面着色,性能更好)
}),
asynchronous: false // 同步渲染(参数变化时立即更新,适合动态场景)
}));
// 2. 添加填充区域(体)
this.primitivestwo = this.atlas.viewer.scene.primitives.add(new Cesium.Primitive({
geometryInstances: instanceOutline, // 填充区域几何体实例
appearance: new Cesium.PerInstanceColorAppearance({
translucent: true, // 半透明(核心:能看到视锥体内的场景元素)
flat: true
}),
asynchronous: false
}));
// 标记类型,方便后续批量管理
this.primitivesone.type = 'vertebral';
this.primitivestwo.type = 'vertebral';
scene.primitives.add
:将Primitive
添加到场景中,Cesium 会自动渲染。asynchronous: false
:关闭异步加载,确保视锥体参数变化时立即重新渲染(无人机移动或姿态变化时实时更新)。
3. Destroy
方法:销毁视锥体(释放资源)
public Destroy(): void {
new Promise(resolve => {
if (this.primitivesone) {
// 尝试从场景中移除并销毁Primitive
this.atlas.viewer.entities.remove(this.primitivesone);
this.atlas.viewer.entities.remove(this.primitivestwo);
this.primitivesone.destroy();
this.primitivestwo.destroy();
this.spotLightCamera = null; // 清空相机实例
}
resolve(true);
});
}
- 作用:移除场景中的视锥体渲染对象,释放内存,避免重复渲染导致的性能问题。
- 潜在问题:
Primitive
是添加到scene.primitives
中的,而entities.remove
用于移除Entity
对象,这里应该用scene.primitives.remove
:typescript
// 正确的移除方式:从scene.primitives中移除Primitive this.atlas.viewer.scene.primitives.remove(this.primitivesone); this.atlas.viewer.scene.primitives.remove(this.primitivestwo);
二、使用逻辑:创建 / 更新视锥体
// 1. 收集无人机和相机参数
const cameraVertebralParams = {
aircraftHeading, // 偏航角(无人机水平方向,如朝向正东为90°)
gimbalPitchRotateAngle: pointInfo.payload[0].gimbalPitch, // 俯仰角(相机上下倾斜角度,如俯视30°为-30°)
focalLength, // 相机焦距(mm,如7mm)
droneAltitude, // 无人机海拔高度(米)
droneLat, // 无人机纬度
droneLon, // 无人机经度
altitude: 1700, // 预留参数(未使用)
};
// 2. 计算水平和垂直视场角(关键:决定视锥体的宽窄)
let hfov = 2 * Math.atan(SENSOR_WIDTH / (2 * cameraVertebralParams.focalLength)); // 水平视场角
let vfov = 2 * Math.atan(SENSOR_HEIGHT / (2 * cameraVertebralParams.focalLength)); // 垂直视场角
// 3. 先销毁旧的视锥体,再创建新的(更新逻辑)
this.pointCamera && this.pointCamera.Destroy();
this.pointCamera = new Vertebral({
atlas: this, // 传入场景上下文(含viewer)
value: {
...cameraVertebralParams, // 扩展无人机参数
vfov, // 传入计算好的垂直视场角
hfov // 传入计算好的水平视场角
},
});
关键步骤解析
-
参数收集:
cameraVertebralParams
包含无人机的位置(经纬度、海拔)、姿态(偏航角、俯仰角)和相机参数(焦距),是视锥体计算的基础。 -
视场角计算:
- 公式:
hfov = 2 * arctan(传感器宽度/(2*焦距))
(水平),vfov = 2 * arctan(传感器高度/(2*焦距))
(垂直)。 - 你的传感器参数:宽 9.6mm、高 7.2mm,焦距 7mm 时,
hfov≈68°
,vfov≈53.6°
(宽高比 4:3,与传感器一致)。 - 意义:视场角决定视锥体的 “宽窄”—— 焦距越小(广角),视场角越大,视锥体越 “胖”;焦距越大(长焦),视场角越小,视锥体越 “瘦”。
- 公式:
-
更新逻辑:
this.pointCamera && this.pointCamera.Destroy()
确保先销毁旧的视锥体,再创建新实例,避免场景中存在多个视锥体导致混乱。
三、整体作用与业务价值
这套代码的核心价值是将无人机相机的抽象参数(位置、姿态、焦距)转换为 3D 场景中可视化的 “视锥体”,在无人机应用中具体作用:
- 直观判断拍摄范围:操作人员能实时看到无人机当前参数下能拍摄到的区域,避免漏拍(如测绘时确保覆盖目标区域)。
- 参数调试辅助:调整焦距(变焦)时,通过视锥体的宽窄变化可直观验证参数是否合适(如长焦时视锥体细长,适合拍摄远处细节)。
- 动态监控:无人机移动或姿态变化时,视锥体实时更新,帮助判断是否对准目标(如巡检时是否覆盖待检测的设备)。