Cesium(二) 实现无人机巡检、无人机扫描地面动画效果、无人机喷洒农药、低空经济效果

Cesium(二) 实现无人机巡检、无人机扫描地面动画效果、无人机喷洒农药、低空经济效果

随着低空经济的快速崛起和智慧农业的普及,无人机在农田巡检、农药喷洒、地面扫描等场景中的应用越来越广泛。Cesium 作为一款强大的 3D 地理空间可视化引擎,能够完美还原无人机作业的真实场景,为智慧农业、低空经济相关系统提供直观的可视化支撑。

本文将基于 Vue3 + Cesium,通过核心代码片段快速实现无人机沿指定区域飞行、喷洒农药可视化、已喷洒区域标记等核心效果,打造完整的智慧农业无人机作业 3D 场景。

一、场景需求与核心技术栈

1. 核心需求

  1. 加载天地图卫星底图,构建真实地理空间环境
  2. 加载无人机 3D 模型,实现沿农田区域匀速飞行
  3. 可视化无人机喷洒锥,同步跟随无人机移动
  4. 实时标记已喷洒区域,提供开始 / 暂停交互控制
  5. 优化性能,避免内存泄漏

2. 核心技术栈

  • 前端框架:Vue3(<script setup> 语法糖)
  • 3D 可视化引擎:Cesium
  • 底图服务:天地图 WMTS 地图服务
  • 3D 模型:GLB 格式无人机模型
  • 核心技术:Cesium 实体管理、路径动画、Primitive 几何绘制

二、前期准备(极简版)

  1. 安装 Cesium 依赖:npm install cesium --save
  2. 获取天地图开发者 Key(勾选 WMTS 权限),存入src/config.js
  3. 准备 GLB 格式无人机模型,放入public/static/modal/目录

三、核心代码实现(关键片段)

1. 页面核心模板

vue

<template> <div> <!-- 左侧交互控制面板 --> <div> <div> <button v-if="!hasStarted" @click="startMission" :disabled="!isViewerReady" > 开始 </button> <template v-if="hasStarted"> <button @click="togglePause" :class="['control-button', isPaused ? 'secondary' : 'warning']" > {{ isPaused ? '继续' : '暂停' }} </button> </template> </div> </div> <!-- Cesium 3D场景容器 --> <div ref="cesiumContainer"></div> </div> </template> 

2. 核心配置抽离(可快速修改参数)

javascript

运行

import { ref, onMounted, onUnmounted } from 'vue'; import { TD_MAP_KEY } from '../config.js'; // 核心配置(一键修改,无需改动业务逻辑) const CONFIG = { drone: { modelUrl: '/static/modal/CesiumDrone.glb', scale: 3.0, // 无人机模型大小 height: 60, // 飞行高度(米) minimumPixelSize: 10, // 远处可见最小像素 maximumScale: 500 // 最大缩放比例 }, area: { // 农田区域经纬度坐标 coordinates: [ [133.083935, 47.729674], [133.083704, 47.719309], [133.099809, 47.718895], [133.099513, 47.729559], [133.084099, 47.729731], [133.083935, 47.729674] ] }, mission: { height: 50, // 喷洒高度(米) speed: 20, // 飞行速度(米/秒) sprayWidth: 50 // 喷洒宽度(米) } }; // 状态变量(核心) const cesiumContainer = ref(null); const isViewerReady = ref(false); const hasStarted = ref(false); const isPaused = ref(false); const droneEntity = ref(null); const frustumPrimitives = ref([]); // 喷洒锥实例集合 const sprayedAreas = ref([]); // 已喷洒区域集合 const isMissionActive = ref(false); 

3. 天地图图层加载核心代码

javascript

运行

// 创建天地图卫星底图/标注图层 const createTDLayer = (viewer, layerType) => { const layerConfigs = { satellite: { url: `https://t{s}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${TD_MAP_KEY}`, zIndex: 0 }, label: { url: `https://t{s}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${TD_MAP_KEY}`, zIndex: 1 } }; const config = layerConfigs[layerType]; if (!config) return null; const layer = viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({ url: config.url, subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'], credit: '天地图', maximumLevel: 18 })); layer.zIndex = config.zIndex; return layer; }; 

4. Cesium 初始化与无人机模型加载

javascript

运行

// 初始化Cesium场景与无人机模型 const initCesium = () => { try { if (!cesiumContainer.value) return; // 销毁旧实例,避免内存泄漏 if (window.cesiumViewer) { window.cesiumViewer.destroy(); window.cesiumViewer = null; } // 初始化Cesium Viewer const newViewer = new Cesium.Viewer(cesiumContainer.value, { timeline: true, animation: true, baseLayerPicker: false, geocoder: false, fullscreenButton: false, imageryProvider: false, shouldAnimate: true }); // 加载天地图 createTDLayer(newViewer, 'satellite'); createTDLayer(newViewer, 'label'); // 绘制农田高亮边框 newViewer.entities.add({ name: '飞行区域', polygon: { hierarchy: new Cesium.PolygonHierarchy( CONFIG.area.coordinates.map(coord => Cesium.Cartesian3.fromDegrees(coord[0], coord[1])) ), material: Cesium.Color.TRANSPARENT, outline: true, outlineColor: Cesium.Color.fromCssColorString('#00FF00'), outlineWidth: 5 } }); // 存储Viewer实例,创建无人机模型 window.cesiumViewer = newViewer; isViewerReady.value = true; droneEntity.value = createDroneEntity(newViewer); // 相机自动飞至最佳观测视角 setTimeout(() => { window.cesiumViewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(133.084112, 47.7281881, 359), orientation: { heading: Cesium.Math.toRadians(1.5), pitch: Cesium.Math.toRadians(-70), roll: 0.01 }, duration: 5 }); }, 1500); } catch (error) { console.error('Cesium 初始化失败:', error); } }; // 创建无人机3D模型 const createDroneEntity = (viewer) => { const startCoord = CONFIG.area.coordinates[0]; const position = Cesium.Cartesian3.fromDegrees( startCoord[0], startCoord[1], CONFIG.drone.height ); const hpr = new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(135), 0, 0); const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr); return viewer.entities.add({ name: '农药无人机', position: position, orientation: orientation, model: { uri: CONFIG.drone.modelUrl, scale: CONFIG.drone.scale, minimumPixelSize: CONFIG.drone.minimumPixelSize, shouldAnimate: true, shadows: Cesium.ShadowMode.DISABLED // 禁用阴影,优化性能 } }); }; 

5. 无人机沿路径匀速飞行核心代码

javascript

运行

// 生成顺时针飞行路径 const generateFlightPath = (coordinates, altitude = 50) => { const path = []; const orderedCoordinates = [ coordinates[0], coordinates[3], coordinates[2], coordinates[1], coordinates[0] ]; orderedCoordinates.forEach(coord => { const [lon, lat] = coord; const position = Cesium.Cartesian3.fromDegrees(lon, lat, altitude); path.push(position); }); return path; }; // 生成飞行路径 const FLIGHT_PATH = generateFlightPath(CONFIG.area.coordinates, CONFIG.drone.height); // 无人机沿路径匀速飞行(核心) const flyAlongPath = (viewer, droneEntity) => { const start = Cesium.JulianDate.fromDate(new Date()); const positionProperty = new Cesium.SampledPositionProperty(); // 计算总飞行距离与时间 let totalDistance = 0; const distances = []; for (let i = 0; i < FLIGHT_PATH.length - 1; i++) { const distance = Cesium.Cartesian3.distance(FLIGHT_PATH[i], FLIGHT_PATH[i + 1]); distances.push(distance); totalDistance += distance; } const totalTime = totalDistance / CONFIG.mission.speed; const stop = Cesium.JulianDate.addSeconds(start, totalTime, new Cesium.JulianDate()); // 绑定路径点与时间 let currentTime = start; FLIGHT_PATH.forEach((pos, index) => { positionProperty.addSample(currentTime, pos); if (index < distances.length) { const segmentTime = distances[index] / CONFIG.mission.speed; currentTime = Cesium.JulianDate.addSeconds(currentTime, segmentTime, new Cesium.JulianDate()); } }); // 配置插值与朝向 positionProperty.setInterpolationOptions({ interpolationDegree: 1, interpolationAlgorithm: Cesium.LinearApproximation }); droneEntity.position = positionProperty; droneEntity.orientation = new Cesium.VelocityOrientationProperty(positionProperty); // 配置时钟与动画循环 viewer.clock.startTime = start.clone(); viewer.clock.currentTime = start.clone(); viewer.clock.stopTime = stop.clone(); viewer.clock.shouldAnimate = true; viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; // 实时标记已喷洒区域 let lastPosition = null; let updateInterval = setInterval(() => { if (!isMissionActive.value) clearInterval(updateInterval); const currentPosition = droneEntity.position.getValue(viewer.clock.currentTime); if (currentPosition && lastPosition && Cesium.Cartesian3.distance(lastPosition, currentPosition) > CONFIG.mission.sprayWidth / 2) { markSprayedArea(viewer, lastPosition, currentPosition); lastPosition = currentPosition; } if (!lastPosition) lastPosition = currentPosition; }, 1000); }; 

6. 喷洒效果可视化(喷洒锥 + 已喷洒区域)

javascript

运行

// 初始化喷洒锥(四棱锥) const initSprayCone = (viewer, droneEntity) => { // 清理旧喷洒锥 frustumPrimitives.value.forEach(primitive => viewer.scene.primitives.remove(primitive)); frustumPrimitives.value = []; const near = 1.0; const far = CONFIG.mission.height; const sprayWidth = CONFIG.mission.sprayWidth; const fov = 2 * Math.atan((sprayWidth / 2) / far); const position = droneEntity.position.getValue(Cesium.JulianDate.now()); const hpr = new Cesium.HeadingPitchRoll(0, Cesium.Math.toRadians(180), 0); const orientation = Cesium.Transforms.headingPitchRollQuaternion(Cesium.Cartesian3.ZERO, hpr); // 创建半透明黄色喷洒锥 const frustum = new Cesium.PerspectiveFrustum({ fov, aspectRatio: 1, near, far }); const instanceGeo = new Cesium.GeometryInstance({ geometry: new Cesium.FrustumGeometry({ frustum, origin: Cesium.Cartesian3.ZERO, orientation }), attributes: { color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.YELLOW.withAlpha(0.5)) } }); const frustumPrimitive = viewer.scene.primitives.add(new Cesium.Primitive({ geometryInstances: instanceGeo, appearance: new Cesium.PerInstanceColorAppearance({ translucent: true, closed: true }), modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(position) })); frustumPrimitives.value.push(frustumPrimitive); // 喷洒锥跟随无人机移动 viewer.scene.preUpdate.addEventListener((scene, time) => { const dronePos = droneEntity.position.getValue(time); if (dronePos) frustumPrimitive.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(dronePos); }); }; // 标记已喷洒区域 const markSprayedArea = (viewer, startPos, endPos) => { const sprayWidth = CONFIG.mission.sprayWidth; const flightDirection = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(endPos, startPos, new Cesium.Cartesian3()), new Cesium.Cartesian3()); const upVector = new Cesium.Cartesian3(0, 0, 1); const perpendicularDirection = Cesium.Cartesian3.normalize(Cesium.Cartesian3.cross(flightDirection, upVector, new Cesium.Cartesian3()), new Cesium.Cartesian3()); // 计算喷洒区域四个角点 const halfWidth = sprayWidth / 2; const topLeft = Cesium.Cartesian3.add(startPos, Cesium.Cartesian3.multiplyByScalar(perpendicularDirection, halfWidth, new Cesium.Cartesian3()), new Cesium.Cartesian3()); const topRight = Cesium.Cartesian3.add(endPos, Cesium.Cartesian3.multiplyByScalar(perpendicularDirection, halfWidth, new Cesium.Cartesian3()), new Cesium.Cartesian3()); const bottomRight = Cesium.Cartesian3.add(endPos, Cesium.Cartesian3.multiplyByScalar(perpendicularDirection, -halfWidth, new Cesium.Cartesian3()), new Cesium.Cartesian3()); const bottomLeft = Cesium.Cartesian3.add(startPos, Cesium.Cartesian3.multiplyByScalar(perpendicularDirection, -halfWidth, new Cesium.Cartesian3()), new Cesium.Cartesian3()); // 添加已喷洒区域多边形 const sprayedArea = viewer.entities.add({ name: '已喷洒区域', polygon: { hierarchy: new Cesium.PolygonHierarchy([topLeft, topRight, bottomRight, bottomLeft]), material: Cesium.Color.YELLOW.withAlpha(0.3) } }); sprayedAreas.value.push(sprayedArea); }; 

7. 交互控制与资源销毁

javascript

运行

// 开始任务 const startMission = () => { if (!window.cesiumViewer || !droneEntity.value) return; hasStarted.value = true; isMissionActive.value = true; initSprayCone(window.cesiumViewer, droneEntity.value); flyAlongPath(window.cesiumViewer, droneEntity.value); }; // 暂停/继续切换 const togglePause = () => { if (!window.cesiumViewer) return; isPaused.value = !isPaused.value; window.cesiumViewer.clock.shouldAnimate = !isPaused.value; }; // 销毁资源,避免内存泄漏(核心) const destroyCesium = () => { isMissionActive.value = false; if (window.cesiumViewer) { // 清理所有可视化实例 frustumPrimitives.value.forEach(p => window.cesiumViewer.scene.primitives.remove(p)); sprayedAreas.value.forEach(a => window.cesiumViewer.entities.remove(a)); window.cesiumViewer.destroy(); window.cesiumViewer = null; } droneEntity.value = null; }; // 生命周期钩子 onMounted(() => cesiumContainer.value && initCesium()); onUnmounted(() => destroyCesium()); 

8. 核心样式(精简版)

css

<style scoped> .app-container { position: relative; width: 100vw; height: 100vh; overflow: hidden; } .cesium-container { width: 100vw; height: 100vh; } .control-panel { position: absolute; top: 30px; left: 30px; z-index: 1000; } .control-button { padding: 12px 20px; font-size: 15px; font-weight: 600; color: white; border: none; border-radius: 10px; cursor: pointer; } .control-button.primary { background: rgba(76, 175, 80, 0.85); } .control-button.secondary { background: rgba(33, 150, 243, 0.85); } .control-button.warning { background: rgba(255, 152, 0, 0.85); } </style> 

四、核心性能优化要点

  1. 禁用模型阴影:shadows: Cesium.ShadowMode.DISABLED,减少渲染计算
  2. 及时清理资源:组件卸载时销毁 Viewer、移除实体与 Primitive
  3. 分段标记喷洒区域:基于喷洒宽度判断标记时机,避免高频更新
  4. 合理设置模型缩放参数:避免远距离模型过度渲染

五、扩展方向

  1. 自定义农田区域:实现鼠标绘制多边形农田
  2. 实时参数调整:添加飞行高度、速度、喷洒宽度的实时修改控件
  3. 多无人机协同作业:扩展多实体管理,实现集群喷洒效果
  4. 数据可视化:叠加农田墒情、作物生长状态等数据图层

总结

  1. 核心实现依赖 Cesium 的SampledPositionProperty(路径动画)与FrustumGeometry(喷洒锥绘制),快速还原无人机作业场景。
  2. 配置与逻辑分离的设计,便于后续参数调整和功能扩展,降低维护成本。
  3. 资源销毁是避免内存泄漏的关键,需在组件卸载时清理所有 Cesium 实例与事件监听。

完整Demo

<template> <div> <!-- 左侧控制面板 --> <div> <!-- 控制按钮组 --> <div> <button v-if="!hasStarted" @click="startMission" :disabled="!isViewerReady" > 开始 </button> <template v-if="hasStarted"> <button @click="togglePause" :class="['control-button', isPaused ? 'secondary' : 'warning']" > {{ isPaused ? '继续' : '暂停' }} </button> </template> </div> </div> <!-- 绘制区域组件 --> <Tools/> <!-- Cesium 容器 --> <div ref="cesiumContainer"></div> </div> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue'; // 引入配置文件 import { TD_MAP_KEY } from '../config.js'; // 引入自定义组件 import Tools from '../components/Tools.vue'; // ============================================== // 核心配置(可随时修改) // ============================================== const CONFIG = { drone: { modelUrl: '/static/modal/CesiumDrone.glb', scale: 3.0, // 无人机模型大小 height: 60, // 飞行高度(米) pitch: -15, minimumPixelSize: 10, // 最小像素大小,确保远处可见 maximumScale: 500, // 最大缩放比例 heightOffset: 0 // 无人机向上偏移 }, area: { coordinates: [ [133.083935, 47.729674], [133.083704, 47.719309], [133.099809, 47.718895], [133.099513, 47.729559], [133.084099, 47.729731], [133.083935, 47.729674] ] }, mission: { height: 50, // 喷洒高度(米),棱锥底面距离地面高度 radius: 6, // 喷洒半径(米) speed: 20, // 飞行速度(米/秒) sprayWidth: 50, // 喷洒宽度(米),与视角棱锥大小一致 sprayOverlap: 0.8 // 喷洒重叠率,确保覆盖均匀 } }; // ============================================== // 状态变量 // ============================================== const cesiumContainer = ref(null); // 容器 DOM 引用 const isViewerReady = ref(false); // 标记viewer是否已初始化完成 const hasStarted = ref(false); // 标记任务是否已开始 const isPaused = ref(false); // 标记任务是否已暂停 const droneEntity = ref(null); // 无人机实体 const sprayCone = ref(null); // 喷洒锥(旧版圆锥) const frustumPrimitives = ref([]); // 视椎体 primitive 数组 const frustumOutlinePrimitives = ref([]); // 视椎体轮廓线 primitive 数组 const sprayedAreas = ref([]); // 已喷洒的区域 const isMissionActive = ref(false); // 任务状态 const savedClockTime = ref(null); // 保存暂停时的时间 const preUpdateListeners = ref([]); // 场景更新事件监听器数组 // 暴露CONFIG对象给模板 defineExpose({ CONFIG }); // 生成无人机沿边缘飞行路径 const generateFlightPath = (coordinates, altitude = 50) => { // console.log('生成地块边缘飞行路径...'); const path = []; // 确保按照顺时针方向飞行(调整坐标顺序) // 标准矩形顺时针顺序:左上 → 右上 → 右下 → 左下 → 左上 const orderedCoordinates = [ coordinates[0], // 左上 coordinates[3], // 右上 coordinates[2], // 右下 coordinates[1], // 左下 coordinates[0] // 回到左上 ]; // console.log('调整后的飞行顺序:', orderedCoordinates); // 按照调整后的顺序生成飞行路径 for (const coord of orderedCoordinates) { const [lon, lat] = coord; const position = Cesium.Cartesian3.fromDegrees(lon, lat, altitude); if (position && !isNaN(position.x) && !isNaN(position.y) && !isNaN(position.z)) { path.push(position); // console.log('添加路径点:', { lon, lat, altitude }); } } // console.log('生成的飞行路径包含', path.length, '个点'); return path; }; // 飞行路径 const FLIGHT_PATH = generateFlightPath(CONFIG.area.coordinates, CONFIG.drone.height + CONFIG.drone.heightOffset); //创建天地图图层 const createTDLayer = (viewer, layerType) => { const layerConfigs = { satellite: { url: `https://t{s}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${TD_MAP_KEY}`, zIndex: 0 }, label: { url: `https://t{s}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${TD_MAP_KEY}`, zIndex: 1 } }; const config = layerConfigs[layerType]; if (!config) return null; const layer = viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({ url: config.url, subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'], credit: '天地图', maximumLevel: 18, minimumLevel: 0, maximumScreenSpaceError: 0, disableDepthTestAgainstTerrain: true })); layer.zIndex = config.zIndex; return layer; }; //初始化 Cesium 并加载无人机模型 const initCesium = () => { try { // 确保DOM元素存在 if (!cesiumContainer.value) { console.error('Cesium容器DOM元素不存在'); return; } // console.log('开始初始化Cesium...'); // 初始化前先清空之前的viewer实例 if (window.cesiumViewer) { // console.log('发现之前的Cesium Viewer实例,正在销毁...'); window.cesiumViewer.destroy(); window.cesiumViewer = null; // console.log('之前的Cesium Viewer实例已销毁'); } // 初始化Cesium Viewer const newViewer = new Cesium.Viewer(cesiumContainer.value, { timeline: true, animation: true, homeButton: false, sceneModePicker: false, navigationHelpButton: false, baseLayerPicker: false, infoBox: false, selectionIndicator: false, navigationInstructionsInitiallyVisible: false, fullscreenButton: false, geocoder: false, // 去掉搜索按钮 imageryProvider: false, shouldAnimate: true,//是否显示模型动画 }); // 调整场景光照,增加亮度 newViewer.scene.light = new Cesium.DirectionalLight({ direction: new Cesium.Cartesian3(0.0, -1.0, -1.0), color: Cesium.Color.WHITE, intensity: 2.0 // 增加光照强度 }); // 调整模型的光照因子 newViewer.scene.globe.enableLighting = true; // 将viewer实例存储到window对象中,供其他组件使用 window.cesiumViewer = newViewer; // console.log('Cesium Viewer创建成功,并已存储到window.cesiumViewer'); // console.log('window.cesiumViewer:', window.cesiumViewer); // console.log('window.cesiumViewer类型:', typeof window.cesiumViewer); // 标记viewer已初始化完成 isViewerReady.value = true; // 创建并配置天地图图层 const imageryLayers = {}; imageryLayers.satellite = createTDLayer(newViewer, 'satellite'); imageryLayers.label = createTDLayer(newViewer, 'label'); // console.log('天地图图层创建成功'); // 添加多边形实体来圈出区域 const areaPolygon = newViewer.entities.add({ name: '飞行区域', polygon: { hierarchy: new Cesium.PolygonHierarchy( CONFIG.area.coordinates.map(coord => Cesium.Cartesian3.fromDegrees(coord[0], coord[1])) ), material: Cesium.Color.TRANSPARENT, // 区域内部透明,无颜色 outline: true, outlineColor: Cesium.Color.fromCssColorString('#00FF00'), // 使用亮绿色作为高亮颜色 outlineWidth: 5, // 增加线宽,确保可见 show: true } }); // 同时创建一个折线实体来增强边框效果 const areaBorder = newViewer.entities.add({ name: '飞行区域边框', polyline: { positions: CONFIG.area.coordinates.map(coord => Cesium.Cartesian3.fromDegrees(coord[0], coord[1])), width: 5, material: new Cesium.PolylineGlowMaterialProperty({ glowPower: 0.2, color: Cesium.Color.fromCssColorString('#00FF00') }), clampToGround: true, show: true } }); // console.log('区域边框创建成功,坐标:', CONFIG.area.coordinates); // console.log('区域多边形创建成功,坐标:', CONFIG.area.coordinates); // console.log('区域中心点:', AREA_CENTER); // 确保相机控制器的所有功能都被启用 newViewer.scene.screenSpaceCameraController.enableTranslate = true; // 启用平移 newViewer.scene.screenSpaceCameraController.enableZoom = true; // 启用缩放 newViewer.scene.screenSpaceCameraController.enableRotate = true; // 启用旋转 newViewer.scene.screenSpaceCameraController.enableTilt = true; // 启用倾斜 newViewer.scene.screenSpaceCameraController.enableLook = true; // 启用观察 // console.log('Cesium Viewer初始化完成,正在创建无人机模型...'); // 创建无人机实体 const entity = createDroneEntity(newViewer); droneEntity.value = entity; // 保存无人机实体引用 // console.log('无人机实体创建完成,开始加载模型...'); // console.log('无人机初始位置:', CONFIG.area.coordinates[0]); // console.log('飞行路径点数量:', FLIGHT_PATH.length); // 延迟一段时间,确保地球和地图加载完成后,再飞向模型 setTimeout(() => { if (window.cesiumViewer && entity) { // console.log('地球和地图加载完成,开始飞向无人机模型...'); // 计算模型的包围球 // const modelPosition = entity.position.getValue(Cesium.JulianDate.now()); // const boundingSphere = new Cesium.BoundingSphere(modelPosition, 50); // 半径50米 // 平滑飞行到模型位置,类似DynamicWallView.vue的实现 // window.cesiumViewer.camera.flyToBoundingSphere(boundingSphere, { // offset: new Cesium.HeadingPitchRange( // Cesium.Math.toRadians(0), // Cesium.Math.toRadians(-40), // 300 // 距离球体中心的距离,更近一些,便于观察喷洒效果 // ), // duration: 5, // easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT // }); window.cesiumViewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(133.084112, 47.7281881, 359), orientation: { heading: Cesium.Math.toRadians(1.5), pitch: Cesium.Math.toRadians(-70), roll: 0.01 }, duration: 5, easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT }); } }, 1500); // 延迟 确保地球和地图已经加载 } catch (error) { console.error('Cesium 初始化失败:', error); console.error('错误详细信息:', error.stack); } }; /** * 创建无人机模型 * @param {Cesium.Viewer} viewer Cesium Viewer实例 * @param {string} url 模型URL * @param {number} height 飞行高度 */ const createModel = (viewer, url, height) => { console.log('创建模型,URL:', url, '高度:', height); // 移除所有无人机实体(保留区域多边形) const entities = viewer.entities.values; for (let i = entities.length - 1; i >= 0; i--) { const entity = entities[i]; if (entity.name === '农药无人机') { viewer.entities.remove(entity); } } // 设置模型位置和朝向(使用地块边界的第一个点作为起始位置) const startCoord = CONFIG.area.coordinates[0]; const position = Cesium.Cartesian3.fromDegrees( startCoord[0], // 经度 startCoord[1], // 纬度 height + CONFIG.drone.heightOffset ); // console.log('无人机初始位置设置为地块边界起点:', startCoord); const heading = Cesium.Math.toRadians(135); const pitch = Cesium.Math.toRadians(0); // 设置为0,确保无人机保持水平 const roll = 0; const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll); const orientation = Cesium.Transforms.headingPitchRollQuaternion( position, hpr ); // 创建模型实体 const entity = viewer.entities.add({ name: '农药无人机', position: position, orientation: orientation, model: { uri: url, minimumPixelSize: CONFIG.drone.minimumPixelSize, // 减小像素大小,提高性能 maximumScale: CONFIG.drone.maximumScale, // 减小最大缩放,提高性能 scale: CONFIG.drone.scale, show: true, shouldAnimate: true, shadows: Cesium.ShadowMode.DISABLED, // 禁用阴影,提高性能 color: Cesium.Color.WHITE.withAlpha(1.0) // 增加亮度,使模型更亮 }, }); return entity; }; /** * 创建无人机实体(兼容旧方法) * @param {Cesium.Viewer} viewer Cesium Viewer实例 */ const createDroneEntity = (viewer) => { return createModel(viewer, CONFIG.drone.modelUrl, CONFIG.drone.height); }; /** * 开始喷洒任务(由用户点击按钮触发) */ const startMission = () => { // console.log('用户点击开始按钮,开始执行喷洒任务...'); if (!window.cesiumViewer || !droneEntity.value) { console.error('无法开始任务:viewer或无人机实体不存在'); return; } hasStarted.value = true; isPaused.value = false; startSprayingMission(window.cesiumViewer, droneEntity.value); }; /** * 切换暂停/继续状态 */ const togglePause = () => { if (!window.cesiumViewer) return; if (isPaused.value) { // 继续任务 // console.log('继续喷洒任务...'); isPaused.value = false; window.cesiumViewer.clock.shouldAnimate = true; } else { // 暂停任务 // console.log('暂停喷洒任务...'); isPaused.value = true; window.cesiumViewer.clock.shouldAnimate = false; savedClockTime.value = window.cesiumViewer.clock.currentTime.clone(); } }; /** * 开始喷洒任务 * @param {Cesium.Viewer} viewer Cesium Viewer实例 * @param {Cesium.Entity} droneEntity 无人机实体 */ const startSprayingMission = (viewer, droneEntity) => { // console.log('开始喷洒农药任务...'); // 标记任务为活动状态 isMissionActive.value = true; // 初始化喷洒锥效果 initSprayCone(viewer, droneEntity); // 开始沿着路径飞行 flyAlongPath(viewer, droneEntity); }; /** * 初始化喷洒雷达四棱锥效果 * @param {Cesium.Viewer} viewer Cesium Viewer实例 * @param {Cesium.Entity} droneEntity 无人机实体 */ const initSprayCone = (viewer, droneEntity) => { // console.log('初始化喷洒视椎体效果...'); // console.log('初始化前状态 - 视觉棱锥数量:', frustumPrimitives.value.length, '轮廓线数量:', frustumOutlinePrimitives.value.length, '事件监听器数量:', preUpdateListeners.value.length); if (!viewer || !droneEntity) { console.error('初始化视椎体失败:参数无效'); return; } // 清理旧的棱锥效果 // console.log('清理旧的棱锥效果...'); for (const primitive of frustumPrimitives.value) { // console.log('移除旧视觉棱锥:', primitive); viewer.scene.primitives.remove(primitive); } frustumPrimitives.value = []; for (const primitive of frustumOutlinePrimitives.value) { // console.log('移除旧视觉棱锥轮廓线:', primitive); viewer.scene.primitives.remove(primitive); } frustumOutlinePrimitives.value = []; // 清理旧的事件监听器 for (const listener of preUpdateListeners.value) { // console.log('移除旧事件监听器:', listener); viewer.scene.preUpdate.removeEventListener(listener); } preUpdateListeners.value = []; // console.log('旧棱锥效果清理完成'); const near = 1.0; const far = CONFIG.mission.height; // 计算视场角,确保底面宽度与喷洒宽度一致 // 喷洒宽度(米) const sprayWidth = CONFIG.mission.sprayWidth; // 视场角 = 2 * arctan(喷洒宽度 / 2 / 喷洒高度),直接使用弧度 const fov = 2 * Math.atan((sprayWidth / 2) / far); const aspectRatio = 1; // 正方形底面 // console.log('视角棱锥大小计算:', { sprayWidth, far, fov: fov * (180 / Math.PI) }); // console.log('视角棱锥底面宽度:', sprayWidth); // console.log('视椎体参数:', { fov: fov * (180 / Math.PI), aspectRatio, near, far, sprayWidth }); let position; try { position = droneEntity.position && droneEntity.position.getValue ? droneEntity.position.getValue(Cesium.JulianDate.now()) : null; } catch (e) { position = null; } if (!position) { position = Cesium.Cartesian3.fromDegrees( CONFIG.area.coordinates[0][0], CONFIG.area.coordinates[0][1], CONFIG.drone.height + CONFIG.drone.heightOffset ); } console.log('无人机位置:', position); try { const heading = Cesium.Math.toRadians(0); const pitch = Cesium.Math.toRadians(180); const roll = Cesium.Math.toRadians(0); const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll); const orientation = Cesium.Transforms.headingPitchRollQuaternion(Cesium.Cartesian3.ZERO, hpr); const frustum = new Cesium.PerspectiveFrustum({ fov: fov, aspectRatio: aspectRatio, near: near, far: far }); const instanceGeo = new Cesium.GeometryInstance({ geometry: new Cesium.FrustumGeometry({ frustum: frustum, origin: Cesium.Cartesian3.ZERO, orientation: orientation, vertexFormat: Cesium.VertexFormat.POSITION_ONLY }), attributes: { color: Cesium.ColorGeometryInstanceAttribute.fromColor( Cesium.Color.fromCssColorString('#ffff00').withAlpha(0.5) ) } }); const initialModelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(position); const frustumPrimitive = viewer.scene.primitives.add(new Cesium.Primitive({ geometryInstances: instanceGeo, appearance: new Cesium.PerInstanceColorAppearance({ translucent: true, closed: true }), modelMatrix: initialModelMatrix })); frustumPrimitives.value.push(frustumPrimitive); // console.log('创建视觉棱锥:', frustumPrimitive, '当前数量:', frustumPrimitives.value.length); const instanceGeoLine = new Cesium.GeometryInstance({ geometry: new Cesium.FrustumOutlineGeometry({ frustum: frustum, origin: Cesium.Cartesian3.ZERO, orientation: orientation }), attributes: { color: Cesium.ColorGeometryInstanceAttribute.fromColor( Cesium.Color.fromCssColorString('#ffff00').withAlpha(0.51) ) } }); const frustumOutlinePrimitive = viewer.scene.primitives.add(new Cesium.Primitive({ geometryInstances: instanceGeoLine, appearance: new Cesium.PolylineColorAppearance({ translucent: true }), modelMatrix: initialModelMatrix })); frustumOutlinePrimitives.value.push(frustumOutlinePrimitive); // console.log('创建视觉棱锥轮廓线:', frustumOutlinePrimitive, '当前数量:', frustumOutlinePrimitives.value.length); // console.log('喷洒视椎体初始化完成'); // console.log('视椎体配置: FOV ' + fov * (180 / Math.PI) + '°, 宽高比 ' + aspectRatio + ', 远裁剪面 ' + far + 'm, 喷洒宽度 ' + sprayWidth + 'm, 黄色半透明'); // 保存当前创建的棱锥引用 const currentFrustumPrimitive = frustumPrimitive; const currentFrustumOutlinePrimitive = frustumOutlinePrimitive; // 创建并保存事件监听器引用 const listener = function(scene, time) { if (droneEntity) { let dronePos; try { dronePos = droneEntity.position && droneEntity.position.getValue ? droneEntity.position.getValue(time) : null; } catch (e) { dronePos = null; } if (dronePos) { const newModelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(dronePos); if (currentFrustumPrimitive) { currentFrustumPrimitive.modelMatrix = newModelMatrix; } if (currentFrustumOutlinePrimitive) { currentFrustumOutlinePrimitive.modelMatrix = newModelMatrix; } } } }; // 添加事件监听器到数组 preUpdateListeners.value.push(listener); // console.log('创建事件监听器:', listener, '当前数量:', preUpdateListeners.value.length); // 添加事件监听器 viewer.scene.preUpdate.addEventListener(listener); // console.log('事件监听器已添加到场景'); } catch (error) { console.error('创建视椎体失败:', error); } }; /** * 沿着路径飞行(基于速度而非时间) * @param {Cesium.Viewer} viewer Cesium Viewer实例 * @param {Cesium.Entity} droneEntity 无人机实体 */ const flyAlongPath = (viewer, droneEntity) => { // console.log('开始沿着平行扫描路径飞行...'); // 验证参数 if (!viewer || !droneEntity) { console.error('开始沿路径飞行失败:参数无效'); return; } // 配置时间轴和时钟 const start = Cesium.JulianDate.fromDate(new Date()); // 创建位置属性 const positionProperty = new Cesium.SampledPositionProperty(); // 计算总飞行时间(基于速度) let totalDistance = 0; const distances = []; // 计算每段路径的距离 for (let i = 0; i < FLIGHT_PATH.length - 1; i++) { const distance = Cesium.Cartesian3.distance(FLIGHT_PATH[i], FLIGHT_PATH[i + 1]); distances.push(distance); totalDistance += distance; } // console.log('总飞行距离(米):', totalDistance); // console.log('飞行速度(米/秒):', CONFIG.mission.speed); // 计算总飞行时间(秒) const totalTime = totalDistance / CONFIG.mission.speed; // console.log('预计总飞行时间(秒):', totalTime); // 计算停止时间 const stop = Cesium.JulianDate.addSeconds(start, totalTime, new Cesium.JulianDate()); // 添加路径点到位置属性(基于速度) let currentTime = start; for (let i = 0; i < FLIGHT_PATH.length; i++) { positionProperty.addSample(currentTime, FLIGHT_PATH[i]); // 如果不是最后一个点,计算下一个点的时间 if (i < distances.length) { const segmentTime = distances[i] / CONFIG.mission.speed; currentTime = Cesium.JulianDate.addSeconds(currentTime, segmentTime, new Cesium.JulianDate()); } } // 设置位置插值选项,使用线性插值确保直线飞行 positionProperty.setInterpolationOptions({ interpolationDegree: 1, // 线性插值,确保直线飞行 interpolationAlgorithm: Cesium.LinearApproximation }); // 创建速度方向属性 const velocityOrientation = new Cesium.VelocityOrientationProperty(positionProperty); // 更新无人机位置 droneEntity.position = positionProperty; // 设置无人机朝向,基于速度 droneEntity.orientation = velocityOrientation; // 移除路径可视化 // 飞行轨迹线已禁用 // 设置可用性,与时间轴关联 droneEntity.availability = new Cesium.TimeIntervalCollection([ new Cesium.TimeInterval({ start: start, stop: stop }) ]); // 更新时钟和时间轴 viewer.clock.startTime = start.clone(); viewer.clock.currentTime = start.clone(); viewer.clock.stopTime = stop.clone(); viewer.clock.multiplier = 1; // 时间速率为1,实时播放 viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; // 循环执行 viewer.clock.shouldAnimate = true; // 启用动画 // 配置时间轴 viewer.timeline.zoomTo(start, stop); // 实时标记喷洒区域 let lastPosition = null; let isFirstPosition = true; let updateInterval = setInterval(() => { if (!isMissionActive.value) { clearInterval(updateInterval); return; } try { const currentTime = viewer.clock.currentTime; const currentPosition = droneEntity.position.getValue(currentTime); if (currentPosition) { if (isFirstPosition) { // 第一次获取到位置时,立即标记 lastPosition = currentPosition; isFirstPosition = false; // 延迟一小段时间后标记第一个区域,确保无人机已经开始飞行 setTimeout(() => { if (lastPosition && isMissionActive.value) { markSprayedArea(viewer, lastPosition, lastPosition); } }, 500); } else if (lastPosition) { // 检查位置是否有效 if (!isNaN(currentPosition.x) && !isNaN(currentPosition.y) && !isNaN(currentPosition.z) && !isNaN(lastPosition.x) && !isNaN(lastPosition.y) && !isNaN(lastPosition.z)) { // 计算距离,使用喷洒宽度的一半作为标记距离,确保不重复覆盖 const distance = Cesium.Cartesian3.distance(lastPosition, currentPosition); if (distance > CONFIG.mission.sprayWidth / 2) { // 使用喷洒宽度的一半,确保不重复覆盖 markSprayedArea(viewer, lastPosition, currentPosition); lastPosition = currentPosition; } } } } } catch (error) { console.error('实时标记喷洒区域失败:', error); } }, 1000); // 每1000毫秒更新一次 // console.log('飞行路径与时间轴关联完成,开始实时标记喷洒区域'); }; /** * 标记已喷洒的区域 * @param {Cesium.Viewer} viewer Cesium Viewer实例 * @param {Cesium.Cartesian3} startPos 起始位置 * @param {Cesium.Cartesian3} endPos 结束位置 */ const markSprayedArea = (viewer, startPos, endPos) => { try { // 喷洒宽度(米),与棱锥底面一致 const sprayWidthMeters = CONFIG.mission.sprayWidth; // console.log('喷洒区域宽度:', sprayWidthMeters); // 计算飞行方向向量 const flightDirection = new Cesium.Cartesian3(); Cesium.Cartesian3.subtract(endPos, startPos, flightDirection); // 检查飞行方向是否有效 const flightDirectionLength = Cesium.Cartesian3.magnitude(flightDirection); if (flightDirectionLength < 0.00001) return; // 忽略非常短的线段 // 归一化飞行方向向量 Cesium.Cartesian3.normalize(flightDirection, flightDirection); // 计算垂直于飞行方向的向量 // 使用叉乘计算垂直向量(假设向上向量为Z轴) const upVector = new Cesium.Cartesian3(0, 0, 1); const perpendicularDirection = new Cesium.Cartesian3(); Cesium.Cartesian3.cross(flightDirection, upVector, perpendicularDirection); Cesium.Cartesian3.normalize(perpendicularDirection, perpendicularDirection); // 计算喷洒宽度的一半(米),与视角棱锥保持一致 // 确保喷洒区域与视角棱锥大小完全相同 const halfSprayWidth = CONFIG.mission.sprayWidth / 2; // console.log('喷洒区域半宽:', halfSprayWidth); // 计算四个角点,确保喷洒区域与视角棱锥大小一致 // 左上角:起始位置 + 垂直方向 * 半宽 const topLeft = new Cesium.Cartesian3(); Cesium.Cartesian3.multiplyByScalar(perpendicularDirection, halfSprayWidth, topLeft); Cesium.Cartesian3.add(startPos, topLeft, topLeft); // 右上角:结束位置 + 垂直方向 * 半宽 const topRight = new Cesium.Cartesian3(); Cesium.Cartesian3.multiplyByScalar(perpendicularDirection, halfSprayWidth, topRight); Cesium.Cartesian3.add(endPos, topRight, topRight); // 右下角:结束位置 - 垂直方向 * 半宽 const bottomRight = new Cesium.Cartesian3(); Cesium.Cartesian3.multiplyByScalar(perpendicularDirection, -halfSprayWidth, bottomRight); Cesium.Cartesian3.add(endPos, bottomRight, bottomRight); // 左下角:起始位置 - 垂直方向 * 半宽 const bottomLeft = new Cesium.Cartesian3(); Cesium.Cartesian3.multiplyByScalar(perpendicularDirection, -halfSprayWidth, bottomLeft); Cesium.Cartesian3.add(startPos, bottomLeft, bottomLeft); // 创建喷洒区域多边形 const polygonHierarchy = new Cesium.PolygonHierarchy([ topLeft, topRight, bottomRight, bottomLeft ]); // 检查多边形层次结构是否有效 if (!polygonHierarchy.positions || polygonHierarchy.positions.length < 3) return; const sprayedArea = viewer.entities.add({ name: '已喷洒区域', polygon: { hierarchy: polygonHierarchy, material: Cesium.Color.YELLOW.withAlpha(0.3), show: true } }); // 保存喷洒区域引用 sprayedAreas.value.push(sprayedArea); } catch (error) { console.error('标记喷洒区域失败:', error); } }; /** * 销毁 Cesium 资源,避免内存泄漏 */ const destroyCesium = () => { isViewerReady.value = false; isMissionActive.value = false; if (window.cesiumViewer) { console.log('清理喷洒任务相关资源...'); if (sprayCone.value) { window.cesiumViewer.entities.remove(sprayCone.value); sprayCone.value = null; } // 清理所有视觉棱锥 for (const primitive of frustumPrimitives.value) { window.cesiumViewer.scene.primitives.remove(primitive); } frustumPrimitives.value = []; // 清理所有视觉棱锥轮廓线 for (const primitive of frustumOutlinePrimitives.value) { window.cesiumViewer.scene.primitives.remove(primitive); } frustumOutlinePrimitives.value = []; // 清理所有事件监听器 for (const listener of preUpdateListeners.value) { window.cesiumViewer.scene.preUpdate.removeEventListener(listener); } preUpdateListeners.value = []; for (const area of sprayedAreas.value) { if (area) { window.cesiumViewer.entities.remove(area); } } sprayedAreas.value = []; } if (droneEntity.value && window.cesiumViewer) { window.cesiumViewer.entities.remove(droneEntity.value); droneEntity.value = null; } if (window.cesiumViewer) { window.cesiumViewer.destroy(); window.cesiumViewer = null; } }; // Vue 生命周期钩子 onMounted(() => { if (cesiumContainer.value) { initCesium(); } }); onUnmounted(() => { destroyCesium(); }); </script> <style scoped> /* 主容器样式 */ .app-container { position: relative; width: 100vw; height: 100vh; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } /* Cesium 容器样式 */ .cesium-container { width: 100vw; height: 100vh; overflow: hidden; } /* 左侧控制面板 */ .control-panel { position: absolute; top: 30px; left: 30px; z-index: 1000; /* background: rgba(255, 255, 255, 0.15); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); */ border-radius: 16px; padding: 24px; /* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); */ max-width: 360px; /* border: 1px solid rgba(255, 255, 255, 0.2); */ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); } .control-panel:hover { background: rgba(255, 255, 255, 0.2); box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15); transform: translateY(-2px); } /* 面板标题 */ .panel-header { margin-bottom: 24px; text-align: center; } .panel-header h2 { margin: 0; font-size: 20px; font-weight: 600; color: rgba(255, 255, 255, 0.95); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } /* 参数部分 */ .params-section { margin-bottom: 28px; } .params-section:last-child { margin-bottom: 32px; } .section-title { margin: 0 0 16px 0; font-size: 14px; font-weight: 600; color: rgba(255, 255, 255, 0.85); text-transform: uppercase; letter-spacing: 0.5px; background: rgba(255, 255, 255, 0.1); padding: 8px 12px; border-radius: 8px; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); } /* 参数网格 */ .params-grid { display: grid; grid-template-columns: 1fr; gap: 12px; } .param-item { background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-radius: 8px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; transition: all 0.3s ease; border: 1px solid rgba(255, 255, 255, 0.1); } .param-item:hover { background: rgba(255, 255, 255, 0.15); transform: translateX(4px); } .param-label { font-size: 13px; color: rgba(255, 255, 255, 0.75); font-weight: 500; } .param-value { font-size: 14px; color: rgba(255, 255, 255, 0.95); font-weight: 600; background: rgba(255, 255, 255, 0.1); padding: 4px 8px; border-radius: 4px; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } /* 按钮部分 */ .button-section { display: flex; flex-direction: column; gap: 14px; } /* 通用按钮样式 */ .control-button { padding: 12px 20px; font-size: 15px; font-weight: 600; color: white; border: none; border-radius: 10px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.2); position: relative; overflow: hidden; } .control-button::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.6s; } .control-button:hover::before { left: 100%; } .control-button:hover:not(:disabled) { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); } .control-button:active:not(:disabled) { transform: translateY(0); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } /* 按钮颜色 */ .control-button.primary { background: rgba(76, 175, 80, 0.85); } .control-button.primary:hover:not(:disabled) { background: rgba(76, 175, 80, 0.95); } .control-button.secondary { background: rgba(33, 150, 243, 0.85); } .control-button.secondary:hover { background: rgba(33, 150, 243, 0.95); } .control-button.warning { background: rgba(255, 152, 0, 0.85); } .control-button.warning:hover { background: rgba(255, 152, 0, 0.95); } .control-button.danger { background: rgba(244, 67, 54, 0.85); } .control-button.danger:hover { background: rgba(244, 67, 54, 0.95); } .control-button:disabled { background: rgba(204, 204, 204, 0.5); cursor: not-allowed; transform: none; box-shadow: none; opacity: 0.6; } /* 响应式设计 */ @media (max-width: 768px) { .control-panel { top: 20px; left: 20px; max-width: 300px; padding: 20px; } .panel-header h2 { font-size: 18px; } .params-section { margin-bottom: 24px; } .params-section:last-child { margin-bottom: 28px; } .control-button { padding: 14px 18px; } } /* 深色模式适配 */ @media (prefers-color-scheme: dark) { .control-panel { background: rgba(20, 20, 20, 0.7); border: 1px solid rgba(255, 255, 255, 0.1); } .panel-header h2 { color: rgba(255, 255, 255, 0.9); } .section-title { color: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.08); } .param-item { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08); } .param-label { color: rgba(255, 255, 255, 0.7); } .param-value { color: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.08); } } </style>

Read more

技术反思:Agent平台的泡沫与未来——从低代码智能体工具看ToB AI落地的真实路径

截至2025年12月,AI Agent(智能体)开发平台如Coze、Dify等在市场中经历了短暂的高光后迅速陷入增长瓶颈。尽管这些平台以“低代码”、“快速构建AI应用”为卖点,在C端和轻量级场景中取得了一定传播效应,但在真正需要深度集成、复杂业务逻辑和高可靠性的ToB企业级市场,其失败率极高。 这背后并非技术不成熟,而是企业路线选择的根本性错误:我们把Agent误当成了一个可封装的产品形态,而非一种面向AI原生架构的设计思想。真正的突破不在“平台”,而在“框架”。 一、产品定位错位:低代码之殇 vs 高代码之需 当前主流Agent平台的核心问题是产品定位的严重偏差。 1. 低代码的本质是“预设流程 + 功能复用” * Coze、Dify等平台强调的是可视化编排、节点拖拽、Prompt模板库。 * 它们的设计哲学是“让非技术人员也能做AI应用”,目标是实现MVP(最小可行产品)的快速验证。 * 这种模式适用于C端小场景、实验性项目或营销类轻应用。 但问题在于:当进入ToB深水区时,业务流程不再标准化,需求高度定制化,所谓的“工作流”变得极其复杂,

VRM4U插件完整指南:在Unreal Engine 5中高效处理VRM模型

VRM4U插件完整指南:在Unreal Engine 5中高效处理VRM模型 【免费下载链接】VRM4URuntime VRM loader for UnrealEngine4 项目地址: https://gitcode.com/gh_mirrors/vr/VRM4U 还在为Unreal Engine 5中VRM模型导入的各种技术问题而烦恼吗?今天我要为你详细介绍一款能够彻底优化VRM工作流程的专业工具——VRM4U插件!这款专为UE5设计的VRM文件导入解决方案,让你能够专注于创意实现,而不是技术细节。 项目核心价值:为什么VRM4U是你的最佳选择 VRM4U插件不仅仅是一个格式转换器,它是一套完整的3D角色处理生态系统。通过智能化的技术实现,它解决了VRM模型在UE5环境中面临的多重挑战。 核心问题解决方案: * 自动化的材质系统转换 * 完整的骨骼结构映射 * 动画数据的无缝衔接 * 跨平台性能优化 快速入门:5分钟完成插件配置 获取插件资源 首先需要下载VRM4U插件,使用以下命令获取完整代码库: git clone https://gitcode

FPGA开发必看!Xilinx Vivado付费IP核License状态解读与获取/vivado最新license获取

FPGA开发必看!Xilinx Vivado付费IP核License状态解读与获取/vivado最新license获取

Xilinx(AMD) vivado软件全部付费IP核及license许可介绍和获取 制作不易,记得三连哦,给我动力,持续更新!!! License或IP src源码 文件下载:Xilinx IP 完整license获取 (点击蓝色字体获取)(可提供IP源码) 一、介绍 Vivado是Xilinx(现属AMD)FPGA开发的核心工具,其内置的IP核资源库极为丰富。这些IP核根据来源可分为两大类: 一类是Xilinx官方提供的IP核,另一类则来自第三方供应商。从授权方式来看,又可划分为免费授权和商业授权两种类型。对于需要商业授权的IP核,用户必须获取对应的License文件方可正常使用。 二、Xilinx IP核 2.1 Xilinx 免费IP Xilinx(AMD)自主开发的IP核主要提供基础功能模块和必要接口组件,涵盖数字信号处理、通信协议、存储控制等通用功能。这类IP核已集成在Vivado开发环境中,用户完成软件安装后即可直接调用,无需额外授权文件。其完整支持设计全流程,包括功能仿真、逻辑综合、布局布线以及比特流生成。在Vivado的License管理界面中,

医疗连续体机器人模块化控制界面设计与Python库应用研究(下)

医疗连续体机器人模块化控制界面设计与Python库应用研究(下)

软件环境部署 系统软件架构以实时性与兼容性为核心设计目标,具体配置如下表所示: 类别配置详情操作系统Ubuntu 20.04 LTS,集成RT_PREEMPT实时内核补丁(调度延迟<1 ms)开发环境Python 3.8核心库组件PyQt5 5.15.4(图形界面)、OpenCV 4.5.5(图像处理)、NumPy 1.21.6(数值计算) 该环境支持模块化控制界面开发与传感器数据的实时融合处理,为连续体机器人的逆运动学求解(如FB CCD算法测试)提供稳定运行基础[16]。 手眼协调校准 为实现视觉引导的精确控制,需完成相机与机器人基坐标系的空间映射校准,具体流程如下: 1. 标识点布置:在机器人末端及各段首尾、中间位置共固定7个反光标识点,构建臂型跟踪特征集[29]; 2. 数据采集:采用NOKOV度量光学动作捕捉系统(8台相机,