/** * 相机视锥投影模块 - Camera Frustum Projection Module * * 参考来源: * 1. demo/Cloud-API-Demo-Web-main - DJI Cloud API Web Demo * 2. demo/DJI-Cloud-API-Demo-main - DJI Cloud API Backend Demo * * 核心功能:将云台相机的姿态转换到绝对的位置和姿态,实现相机视锥投影到地图场景中 * * 基于DJI Cloud API Demo中的以下数据结构: * - DockDronePayload: gimbalPitch, gimbalRoll, gimbalYaw * - MediaFileMetadata: gimbalYawDegree, shootPosition, absoluteAltitude * - PayloadControl: CameraAim, PostCameraAimBody */ class CameraFrustumProjection { constructor(cesiumViewer) { this.viewer = cesiumViewer; this.frustumEntities = new Map(); // 存储视锥实体 this.cameraParams = { // 默认相机参数 - 参考DJI M30系列 fov: 84, // 视场角 (度) aspectRatio: 16/9, // 宽高比 near: 1.0, // 近裁剪面 (米) far: 1000.0 // 远裁剪面 (米) }; } /** * 更新相机视锥投影 * 基于DJI Cloud API中的云台姿态数据 * * @param {Object} droneData - 无人机数据 * @param {number} droneData.longitude - 无人机经度 * @param {number} droneData.latitude - 无人机纬度 * @param {number} droneData.altitude - 无人机高度 * @param {Object} gimbalData - 云台姿态数据 (参考DockDronePayload) * @param {number} gimbalData.gimbalPitch - 云台俯仰角 (度) * @param {number} gimbalData.gimbalRoll - 云台滚转角 (度) * @param {number} gimbalData.gimbalYaw - 云台偏航角 (度) * @param {string} droneId - 无人机标识符 */ updateCameraFrustum(droneData, gimbalData, droneId = 'default') { try { // 1. 计算相机在世界坐标系中的位置 const cameraPosition = this.calculateCameraPosition(droneData, gimbalData); // 2. 计算相机姿态矩阵 - 参考DJI Cloud API的姿态数据处理 const orientation = this.calculateCameraOrientation(droneData, gimbalData); // 3. 创建或更新视锥几何体 this.createOrUpdateFrustumEntity(droneId, cameraPosition, orientation); console.log(`Camera frustum updated for drone ${droneId}`, { position: cameraPosition, gimbal: gimbalData }); } catch (error) { console.error('Failed to update camera frustum:', error); } } /** * 计算相机位置 * 基于DJI Cloud API Demo的精确位置计算 * 考虑无人机机体姿态和云台安装偏移 * * @param {Object} droneData - 无人机位置数据 * @param {Object} gimbalData - 云台数据 * @returns {Cesium.Cartesian3} 相机世界坐标位置 */ calculateCameraPosition(droneData, gimbalData) { // 1. 无人机位置(地心坐标系) const droneCartesian = Cesium.Cartesian3.fromDegrees( droneData.longitude, droneData.latitude, droneData.altitude ); // 2. 云台相对于无人机机体的偏移向量 // 基于DJI M30/M30T系列的实际测量数据 const gimbalOffset = new Cesium.Cartesian3(0, 0, -0.12); // 云台位于机体下方12cm // 3. 考虑无人机机体姿态对偏移的影响 let droneAttitude = droneData.attitude || { heading: 0, pitch: 0, roll: 0 }; // 构建机体姿态变换矩阵 const droneHeading = Cesium.Math.toRadians(droneAttitude.heading || 0); const dronePitch = Cesium.Math.toRadians(droneAttitude.pitch || 0); const droneRoll = Cesium.Math.toRadians(droneAttitude.roll || 0); // 4. 计算局部坐标系变换 const eastNorthUpTransform = Cesium.Transforms.eastNorthUpToFixedFrame(droneCartesian); // 机体姿态旋转矩阵 const droneRotation = Cesium.Matrix3.fromHeadingPitchRoll( new Cesium.HeadingPitchRoll(droneHeading, dronePitch, droneRoll) ); // 5. 将云台偏移向量从机体坐标系转换到世界坐标系 // 先应用机体姿态,再应用局部坐标系变换 const localGimbalOffset = Cesium.Matrix3.multiplyByVector( droneRotation, gimbalOffset, new Cesium.Cartesian3() ); const worldGimbalOffset = Cesium.Matrix4.multiplyByPointAsVector( eastNorthUpTransform, localGimbalOffset, new Cesium.Cartesian3() ); // 6. 计算云台/相机的最终世界坐标位置 const cameraPosition = Cesium.Cartesian3.add( droneCartesian, worldGimbalOffset, new Cesium.Cartesian3() ); return cameraPosition; } /** * 计算相机姿态矩阵 * 基于DJI Cloud API Demo的精确姿态转换算法 * 将云台相对姿态转换为绝对姿态 - 关键算法 * * @param {Object} droneData - 无人机数据 * @param {Object} gimbalData - 云台姿态数据 * @returns {Cesium.Quaternion} 相机姿态四元数 */ calculateCameraOrientation(droneData, gimbalData) { // 1. 获取无人机位置用于坐标系计算 const dronePosition = Cesium.Cartesian3.fromDegrees( droneData.longitude, droneData.latitude, droneData.altitude ); // 2. 建立局部东北天(ENU)坐标系变换矩阵 // 这是DJI云台姿态的参考坐标系 const eastNorthUpTransform = Cesium.Transforms.eastNorthUpToFixedFrame(dronePosition); // 3. 处理无人机机体姿态 (如果数据中包含) // 注意:DJI Cloud API中通常云台姿态已经是相对于地理坐标系的绝对姿态 let droneAttitude = droneData.attitude || { heading: 0, pitch: 0, roll: 0 }; // 4. 云台姿态转换 - 基于DJI Cloud API的角度定义和坐标系约定 // DJI标准: // - Pitch(俯仰): 绕Y轴旋转,向下为正 // - Roll(滚转): 绕X轴旋转,右倾为正 // - Yaw(偏航): 绕Z轴旋转,顺时针为正(从上往下看) const gimbalPitch = Cesium.Math.toRadians(gimbalData.gimbalPitch || 0); const gimbalRoll = Cesium.Math.toRadians(gimbalData.gimbalRoll || 0); const gimbalYaw = Cesium.Math.toRadians(gimbalData.gimbalYaw || 0); // 无人机机体姿态 const droneHeading = Cesium.Math.toRadians(droneAttitude.heading || 0); const dronePitch = Cesium.Math.toRadians(droneAttitude.pitch || 0); const droneRoll = Cesium.Math.toRadians(droneAttitude.roll || 0); // 5. 构建复合姿态变换 // 按照DJI的姿态叠加原理:机体姿态 + 云台相对姿态 // 5.1 机体姿态四元数 const droneQuaternion = Cesium.Quaternion.fromHeadingPitchRoll( new Cesium.HeadingPitchRoll(droneHeading, dronePitch, droneRoll) ); // 5.2 云台相对姿态四元数 // 注意:DJI云台的Yaw通常是相对于地理北向的绝对方位,而不是相对于机体 const gimbalQuaternion = Cesium.Quaternion.fromHeadingPitchRoll( new Cesium.HeadingPitchRoll(gimbalYaw, gimbalPitch, gimbalRoll) ); // 5.3 组合姿态计算 // 对于DJI云台,通常gimbalYaw已经是绝对角度,所以可以直接使用 const finalQuaternion = Cesium.Quaternion.multiply( Cesium.Matrix4.getRotation(eastNorthUpTransform, new Cesium.Matrix3()), gimbalQuaternion, new Cesium.Quaternion() ); // 6. 相机坐标系修正 // DJI相机坐标系:X右,Y下,Z前 // Cesium坐标系:X右,Y前,Z上 // 需要进行坐标系转换 const cameraCorrection = Cesium.Quaternion.fromAxisAngle( Cesium.Cartesian3.UNIT_X, Cesium.Math.toRadians(-90) // 绕X轴逆时针旋转90度 ); const correctedOrientation = Cesium.Quaternion.multiply( finalQuaternion, cameraCorrection, new Cesium.Quaternion() ); return correctedOrientation; } /** * 创建或更新视锥实体 * * @param {string} droneId - 无人机ID * @param {Cesium.Cartesian3} position - 相机位置 * @param {Cesium.Quaternion} orientation - 相机姿态 */ createOrUpdateFrustumEntity(droneId, position, orientation) { const entityId = `camera_frustum_${droneId}`; // 移除已存在的视锥 if (this.frustumEntities.has(droneId)) { this.viewer.entities.remove(this.frustumEntities.get(droneId)); } // 计算视锥几何参数 const frustumGeometry = this.createFrustumGeometry(); // 创建新的视锥实体 const frustumEntity = this.viewer.entities.add({ id: entityId, position: position, orientation: orientation, // 使用自定义几何体或Cesium内置几何体 polylineVolume: { positions: frustumGeometry.positions, shape: frustumGeometry.shape, material: Cesium.Color.YELLOW.withAlpha(0.3), outline: true, outlineColor: Cesium.Color.YELLOW }, // 备选方案:使用线框表示 polyline: { positions: frustumGeometry.wireframe, material: Cesium.Color.CYAN, width: 2, clampToGround: false }, label: { text: `Camera ${droneId}`, font: '12pt sans-serif', fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.BLACK, outlineWidth: 1, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(0, -20) } }); this.frustumEntities.set(droneId, frustumEntity); } /** * 创建视锥几何体 * 基于相机参数计算视锥的几何形状 * * @returns {Object} 视锥几何数据 */ createFrustumGeometry() { const { fov, aspectRatio, near, far } = this.cameraParams; // 计算视锥顶点 const fovRad = Cesium.Math.toRadians(fov); const halfHeight = Math.tan(fovRad / 2); const halfWidth = halfHeight * aspectRatio; // 近裁剪面顶点 (相机坐标系) const nearTopLeft = new Cesium.Cartesian3(-halfWidth * near, halfHeight * near, -near); const nearTopRight = new Cesium.Cartesian3(halfWidth * near, halfHeight * near, -near); const nearBottomLeft = new Cesium.Cartesian3(-halfWidth * near, -halfHeight * near, -near); const nearBottomRight = new Cesium.Cartesian3(halfWidth * near, -halfHeight * near, -near); // 远裁剪面顶点 (相机坐标系) const farTopLeft = new Cesium.Cartesian3(-halfWidth * far, halfHeight * far, -far); const farTopRight = new Cesium.Cartesian3(halfWidth * far, halfHeight * far, -far); const farBottomLeft = new Cesium.Cartesian3(-halfWidth * far, -halfHeight * far, -far); const farBottomRight = new Cesium.Cartesian3(halfWidth * far, -halfHeight * far, -far); // 原点 (相机位置) const origin = new Cesium.Cartesian3(0, 0, 0); // 视锥轮廓线 const wireframe = [ // 从相机到近裁剪面的连线 origin, nearTopLeft, origin, nearTopRight, origin, nearBottomLeft, origin, nearBottomRight, // 近裁剪面矩形 nearTopLeft, nearTopRight, nearTopRight, nearBottomRight, nearBottomRight, nearBottomLeft, nearBottomLeft, nearTopLeft, // 到远裁剪面的连线 nearTopLeft, farTopLeft, nearTopRight, farTopRight, nearBottomLeft, farBottomLeft, nearBottomRight, farBottomRight, // 远裁剪面矩形 farTopLeft, farTopRight, farTopRight, farBottomRight, farBottomRight, farBottomLeft, farBottomLeft, farTopLeft ]; return { positions: [origin, farTopLeft, farTopRight, farBottomRight, farBottomLeft], // 用于polylineVolume shape: [new Cesium.Cartesian2(-halfWidth, halfHeight), new Cesium.Cartesian2(halfWidth, halfHeight), new Cesium.Cartesian2(halfWidth, -halfHeight), new Cesium.Cartesian2(-halfWidth, -halfHeight)], // 截面形状 wireframe: wireframe }; } /** * 设置相机参数 * * @param {Object} params - 相机参数 * @param {number} params.fov - 视场角 (度) * @param {number} params.aspectRatio - 宽高比 * @param {number} params.near - 近裁剪面距离 * @param {number} params.far - 远裁剪面距离 */ setCameraParams(params) { this.cameraParams = { ...this.cameraParams, ...params }; } /** * 移除指定无人机的视锥投影 * * @param {string} droneId - 无人机ID */ removeFrustum(droneId) { if (this.frustumEntities.has(droneId)) { this.viewer.entities.remove(this.frustumEntities.get(droneId)); this.frustumEntities.delete(droneId); } } /** * 清除所有视锥投影 */ clearAllFrustums() { this.frustumEntities.forEach((entity, droneId) => { this.viewer.entities.remove(entity); }); this.frustumEntities.clear(); } /** * 基于DJI CameraAim API的目标瞄准功能 * 参考PostCameraAimBody接口 * * @param {Object} aimData - 瞄准数据 * @param {string} aimData.payload_index - 载荷索引 * @param {number} aimData.camera_type - 相机类型 * @param {boolean} aimData.locked - 是否锁定 * @param {number} aimData.x - 屏幕X坐标 (0-1) * @param {number} aimData.y - 屏幕Y坐标 (0-1) * @returns {Object} 瞄准目标的世界坐标 */ calculateAimTarget(aimData, droneData, gimbalData) { // 将屏幕坐标转换为相机射线 const { x, y } = aimData; // 归一化设备坐标 (NDC) const ndcX = (x - 0.5) * 2; // 转换到 [-1, 1] const ndcY = (0.5 - y) * 2; // 转换到 [-1, 1], Y轴翻转 // 计算射线方向 (相机坐标系) const fovRad = Cesium.Math.toRadians(this.cameraParams.fov); const halfHeight = Math.tan(fovRad / 2); const halfWidth = halfHeight * this.cameraParams.aspectRatio; const rayDirection = new Cesium.Cartesian3( ndcX * halfWidth, ndcY * halfHeight, -1 // 相机坐标系中Z轴向前为负 ); // 转换到世界坐标系 const cameraPosition = this.calculateCameraPosition(droneData, gimbalData); const cameraOrientation = this.calculateCameraOrientation(droneData, gimbalData); // 将射线方向转换到世界坐标系 const worldRayDirection = Cesium.Matrix3.multiplyByVector( Cesium.Matrix3.fromQuaternion(cameraOrientation), rayDirection, new Cesium.Cartesian3() ); // 与地面求交 (简化处理,假设地面高度为0) const ray = new Cesium.Ray(cameraPosition, worldRayDirection); const intersection = this.viewer.scene.globe.pick(ray, this.viewer.scene); return intersection; } } // 导出模块 window.CameraFrustumProjection = CameraFrustumProjection; // 使用示例和API说明 /** * 使用示例: * * // 1. 初始化 * const frustumProjection = new CameraFrustumProjection(viewer); * * // 2. 设置相机参数 (可选) * frustumProjection.setCameraParams({ * fov: 84, // DJI M30系列典型视场角 * aspectRatio: 16/9, * near: 1.0, * far: 500.0 * }); * * // 3. 更新视锥投影 * const droneData = { * longitude: 116.3974, * latitude: 39.9093, * altitude: 100 * }; * * const gimbalData = { * gimbalPitch: -30, // 向下30度 * gimbalRoll: 0, // 无滚转 * gimbalYaw: 45 // 向右偏转45度 * }; * * frustumProjection.updateCameraFrustum(droneData, gimbalData, 'drone_001'); * * // 4. 计算瞄准目标 (基于DJI CameraAim API) * const aimData = { * payload_index: "52-0-0", * camera_type: 1, * locked: true, * x: 0.5, // 屏幕中心 * y: 0.5 * }; * * const target = frustumProjection.calculateAimTarget(aimData, droneData, gimbalData); */