本文详细介绍如何使用 CesiumJS、React 和 Go 构建一个完整的三维无人机编队实时巡航轨迹可视化系统。系统实现了多架无人机在真实地理场景中的同步飞行模拟,通过 WebSocket 实时推送数据,前端以 60fps 进行平滑渲染。文章涵盖技术选型、架构设计、核心算法实现、性能优化等全流程,适合有一定图形学基础和前后端的开发者。
前言
随着无人机技术的快速发展,无人机编队飞行、实时监控、轨迹可视化等应用场景越来越广泛。传统的二维地图展示已经无法满足复杂的三维空间可视化需求。本文介绍如何从零开始,构建一个工业级的三维无人机编队实时巡航可视化系统(工业标准,仅供参考)。
项目亮点
多机编队同步仿真:同时模拟 3 架无人机在不同航线上独立飞行
帧级平滑渲染:后端 5Hz 推送,前端 60fps 渲染,零卡顿体验
真实地理坐标:基于深圳市真实经纬度,高德卫星底图
智能相机系统:追尾、俯瞰、自由三种视角模式
沉浸式天气系统:晴天、大雾、阴天三种天气效果
军事级 HUD 仪表:实时显示完整飞行参数
一、项目概述
1.1 系统架构
前后端分离架构:
后端(Go):负责多无人机飞行模拟、航线解算、状态推算,通过 WebSocket 以 200ms 间隔向所有客户端广播编队状态
前端(React + CesiumJS):基于 Cesium 三维地球引擎,实时接收数据并以 60fps 帧级插值渲染
1.2 技术选型
3D 引擎CesiumJS:业界最成熟的三维地球引擎,支持大规模地理数据渲染 |
前端框架:React 18
Go:高并发性能,WebSocket 支持完善
二、技术架构设计
2.1 整体架构图
┌─────────────────────────────────────────────────────────┐
│ Browser (Client) │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ CesiumJS │ │ React 18 │ │ WebSocket │ │
│ │ 3D Engine │ │ Components │ │ Client Hook │ │
│ │ + Resium │ │ (HUD/Scene) │ │ (useDroneWS) │ │
│ └──────┬──────┘ └──────┬───────┘ └───────┬────────┘ │
│ │ │ │ │
│ └────────────────┴───────────────────┘ │
│ │ 60fps rAF Loop │
│ ┌───────┴────────┐ │
│ │ DroneInterpolator│ ← 帧级平滑插值引擎 │
│ └───────┬────────┘ │
└──────────────────────────┼──────────────────────────────┘
│ WebSocket (JSON)
│ 200ms / tick
┌──────────────────────────┼──────────────────────────────┐
│ Go API Server │
│ ┌──────────────┐ ┌─────┴──────┐ ┌─────────────────┐ │
│ │ Router │ │ WS Hub │ │ FlightSimulator│ │
│ │ (net/http) │ │ (broadcast)│ │ × 3 (多机) │ │
│ │ + CORS │ │ │ │ 航线解算/状态 │ │
│ └──────────────┘ └────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 数据流设计
-
后端数据流:Ticker(200ms) → 各 FlightSimulator.Tick() → 聚合为 []DroneState→ JSON → 广播至所有 WebSocket 客户端
-
前端数据流:WebSocket 接收 → useDroneWS Hook → DroneInterpolator 插值 → requestAnimationFrame 渲染 → Cesium 实体更新
三、后端实现详解
3.1 项目结构
api/
├── main.go # 入口文件
├── config/
│ └── config.go # 航线配置
├── handler/
│ └── ws.go # WebSocket Hub
├── model/
│ └── drone.go # 数据模型
├── router/
│ └── router.go # HTTP 路由
└── simulator/
└── flight.go # 飞行模拟器
3.2 数据模型定义
义无人机状态数据结构:
// api/model/drone.go
package model
type DroneState struct {
DroneID string `json:"droneId"`
Lng float64 `json:"lng"` // 经度
Lat float64 `json:"lat"` // 纬度
Alt float64 `json:"alt"` // 高度(米)
Heading float64 `json:"heading"` // 航向角(度)
Pitch float64 `json:"pitch"` // 俯仰角(度)
Roll float64 `json:"roll"` // 横滚角(度)
Speed float64 `json:"speed"` // 速度(m/s)
Battery float64 `json:"battery"` // 电量(%)
Timestamp int64 `json:"timestamp"`
}
type Waypoint struct {
Lng float64 // 经度
Lat float64 // 纬度
Alt float64 // 高度(米)
}
3.3 飞行模拟器核心算法
飞行模拟器是系统的核心,负责计算每架无人机在航线上的实时位置和姿态。
3.3.1 Haversine 距离计算
Haversine 公式用于计算地球表面两点间的大圆距离:
// api/simulator/flight.go
func haversine(lat1, lng1, lat2, lng2 float64) float64 {
const R = 6371000 // 地球半径(米)
dLat := (lat2 - lat1) * math.Pi / 180
dLng := (lng2 - lng1) * math.Pi / 180
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*
math.Sin(dLng/2)*math.Sin(dLng/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return R * c // 返回距离(米)
}
3.3.2 航向角(Bearing)计算
航向角表示从起点到终点的真北方向角度:
func bearing(lat1, lng1, lat2, lng2 float64) float64 {
dLng := (lng2 - lng1) * math.Pi / 180
lat1R := lat1 * math.Pi / 180
lat2R := lat2 * math.Pi / 180
x := math.Sin(dLng) * math.Cos(lat2R)
y := math.Cos(lat1R)*math.Sin(lat2R) - math.Sin(lat1R)*math.Cos(lat2R)*math.Cos(dLng)
brng := math.Atan2(x, y) * 180 / math.Pi
return math.Mod(brng+360, 360) // 返回 0-360 度
}
3.3.3 飞行模拟器 Tick 逻辑
func (fs *FlightSimulator) Tick() model.DroneState {
fs.mu.Lock()
defer fs.mu.Unlock()
// 航线循环:到达终点后回到起点
if fs.currentIdx >= len(fs.route)-1 {
fs.currentIdx = 0
fs.progress = 0
}
from := fs.route[fs.currentIdx]
to := fs.route[fs.currentIdx+1]
// 计算航段距离
segDist := haversine(from.Lat, from.Lng, to.Lat, to.Lng)
// 计算进度推进
dt := fs.tickInterval.Seconds()
stepRatio := (fs.speed * dt) / segDist
fs.progress += stepRatio
// 到达下一航点
if fs.progress >= 1.0 {
fs.progress = 0
fs.currentIdx++
if fs.currentIdx >= len(fs.route)-1 {
fs.currentIdx = 0
}
from = fs.route[fs.currentIdx]
to = fs.route[fs.currentIdx+1]
}
// 位置插值
lng := lerp(from.Lng, to.Lng, fs.progress)
lat := lerp(from.Lat, to.Lat, fs.progress)
alt := lerp(from.Alt, to.Alt, fs.progress)
// 航向角计算
heading := bearing(from.Lat, from.Lng, to.Lat, to.Lng)
// 俯仰角:根据高度差计算
altDiff := to.Alt - from.Alt
pitch := math.Atan2(altDiff, segDist) * 180 / math.Pi
// 横滚角:模拟飞行中的微小抖动
roll := math.Sin(float64(time.Now().UnixMilli())/1000.0) * 2.0
// 电池衰减
fs.battery -= 0.001
if fs.battery < 0 {
fs.battery = 100.0
}
// 速度变化:在航段起止 10% 区间内模拟加减速
speed := fs.speed
if fs.progress < 0.1 {
speed = fs.speed * (0.5 + fs.progress*5)
} else if fs.progress > 0.9 {
speed = fs.speed * (0.5 + (1-fs.progress)*5)
}
// 更新状态
fs.state = model.DroneState{
DroneID: fs.droneID,
Lng: lng,
Lat: lat,
Alt: alt,
Heading: heading,
Pitch: pitch,
Roll: roll,
Speed: math.Round(speed*100) / 100,
Battery: math.Round(fs.battery*10) / 10,
Timestamp: time.Now().Unix(),
}
return fs.state
}
3.4 WebSocket Hub 实现
Hub 负责管理所有 WebSocket 连接,并定时广播无人机状态:
// api/handler/ws.go
type Hub struct {
clients map[*websocket.Conn]bool
mu sync.RWMutex
simulators []*simulator.FlightSimulator
}
func (h *Hub) StartBroadcast() {
ticker := time.NewTicker(time.Duration(config.TickInterval) * time.Millisecond)
go func() {
for range ticker.C {
states := make([]model.DroneState, len(h.simulators))
for i, sim := range h.simulators {
states[i] = sim.Tick()
}
data, err := json.Marshal(states)
if err != nil {
log.Println("marshal error:", err)
continue
}
h.broadcast(data)
}
}()
}
func (h *Hub) broadcast(msg []byte) {
h.mu.RLock()
defer h.mu.RUnlock()
for conn := range h.clients {
err := conn.WriteMessage(websocket.TextMessage, msg)
if err != nil {
log.Println("write error:", err)
conn.Close()
delete(h.clients, conn)
}
}
}
四、前端实现详解
4.1 项目结构
web/
├── src/
│ ├── App.tsx # 应用入口
│ ├── components/
│ │ ├── CesiumViewer.tsx # 核心 3D 场景组件
│ │ └── HUD.tsx # 飞行仪表面板
│ ├── hooks/
│ │ └── useDroneWS.ts # WebSocket Hook
│ ├── types/
│ │ └── drone.ts # TypeScript 类型定义
│ └── styles/
│ └── index.css # 全局样式
4.2 WebSocket Hook 实现
封装 WebSocket 连接逻辑,支持自动重连:
// web/src/hooks/useDroneWS.ts
import { useEffect, useRef, useState } from 'react'
import type { DroneState, DroneFleet } from '../types/drone'
const WS_URL = `ws://${window.location.hostname}:8080/ws`
export function useDroneWS() {
const [fleet, setFleet] = useState<DroneFleet>({})
const [connected, setConnected] = useState(false)
const [droneIds, setDroneIds] = useState<string[]>([])
const wsRef = useRef<WebSocket | null>(null)
const reconnectTimer = useRef<number>()
const isMounted = useRef(false)
useEffect(() => {
isMounted.current = true
function connect() {
if (!isMounted.current) return
if (wsRef.current?.readyState === WebSocket.OPEN) return
const ws = new WebSocket(WS_URL)
wsRef.current = ws
ws.onopen = () => {
if (!isMounted.current) {
ws.close()
return
}
console.log('WebSocket connected')
setConnected(true)
}
ws.onmessage = (evt) => {
try {
const raw = JSON.parse(evt.data)
// 兼容单无人机和多无人机格式
const arr: DroneState[] = Array.isArray(raw) ? raw : [raw]
const newFleet: DroneFleet = {}
const ids: string[] = []
for (const s of arr) {
newFleet[s.droneId] = s
ids.push(s.droneId)
}
setFleet(newFleet)
setDroneIds(ids)
} catch (e) {
console.error('Parse error:', e)
}
}
ws.onclose = () => {
if (!isMounted.current) return
setConnected(false)
reconnectTimer.current = window.setTimeout(connect, 3000)
}
ws.onerror = () => {
ws.close()
}
}
connect()
return () => {
isMounted.current = false
clearTimeout(reconnectTimer.current)
wsRef.current?.close()
wsRef.current = null
}
}, [])
return { fleet, droneIds, connected }
}
4.3 帧级插值引擎(核心算法)
前端系统的核心点。后端以 5Hz(200ms)推送数据,但前端需要 60fps 渲染。于是设计了一个指数衰减插值引擎:
// web/src/components/CesiumViewer.tsx
interface InterpState {
lng: number; lat: number; alt: number
heading: number; pitch: number; roll: number
}
class DroneInterpolator {
targets = new Map<string, InterpState>()
currents = new Map<string, InterpState>()
setTarget(id: string, s: DroneState) {
this.targets.set(id, {
lng: s.lng, lat: s.lat, alt: s.alt,
heading: s.heading, pitch: s.pitch, roll: s.roll,
})
if (!this.currents.has(id)) {
this.currents.set(id, {
lng: s.lng, lat: s.lat, alt: s.alt,
heading: s.heading, pitch: s.pitch, roll: s.roll,
})
}
}
tick(dt: number) {
const speed = 8 // 插值追赶速度
const t = 1 - Math.exp(-speed * dt) // 指数衰减系数
for (const [id, tgt] of this.targets) {
const cur = this.currents.get(id)
if (!cur) continue
// 位置插值
cur.lng += (tgt.lng - cur.lng) * t
cur.lat += (tgt.lat - cur.lat) * t
cur.alt += (tgt.alt - cur.alt) * t
// 角度插值(处理 0°/360° 跨越)
cur.heading = lerpAngleDeg(cur.heading, tgt.heading, t)
cur.pitch += (tgt.pitch - cur.pitch) * t
cur.roll += (tgt.roll - cur.roll) * t
}
}
get(id: string): InterpState | undefined {
return this.currents.get(id)
}
}
// 角度插值:处理 0°/360° 跨越问题
function lerpAngleDeg(a: number, b: number, t: number) {
let diff = b - a
while (diff > 180) diff -= 360
while (diff < -180) diff += 360
return a + diff * t
}
原理:
指数衰减插值公式:current += (target - current) × (1 - e^(-speed × dt))
target:WebSocket 最新数据
current:当前渲染帧的插值状态
speed = 8:插值追赶速度(越大越灵敏)
dt:帧间隔时间(约 16.67ms)
这种方法的优势:
-
平滑过渡:不会出现突变或抖动
-
自适应:距离目标越远,追赶速度越快
-
零超调:不会超过目标值再回调
4.4 60fps 渲染循环
requestAnimationFrame实现 60fps 渲染:
useEffect(() => {
let rafId: number
let lastTime = performance.now()
const animate = (now: number) => {
rafId = requestAnimationFrame(animate)
const dt = Math.min((now - lastTime) / 1000, 0.05) // 限制最大 dt
lastTime = now
// 所有无人机平滑插值
interpolator.tick(dt)
// 相机跟随选中无人机
const viewer = viewerRef.current
const selId = selectedIdRef.current
const cur = selId ? interpolator.get(selId) : undefined
if (viewer && cur && followModeRef.current !== 'free') {
// 相机平滑跟随
viewer.scene.requestRender()
}
}
rafId = requestAnimationFrame(animate)
return () => cancelAnimationFrame(rafId)
}, [interpolator])
4.5 智能相机跟随系统
实现三种相机模式:
追尾视角
if (followMode === 'chase') {
const offsetDist = sceneMode === '2d' ? 0 : 0.003
const offsetAlt = sceneMode === '2d' ? 600 : 120
tLng = cur.lng - Math.sin(headingRad) * offsetDist
tLat = cur.lat - Math.cos(headingRad) * offsetDist
tAlt = cur.alt + offsetAlt
}
俯瞰视角
else if (followMode === 'top') {
tLng = cur.lng
tLat = cur.lat
tAlt = cur.alt + 800
}
相机位置也使用指数衰减插值,确保切换视角时无突变。
4.6 天气系统实现
Cesium的scene多层渲染参数实现天气效果:
useEffect(() => {
const v = viewerRef.current
if (!v) return
const scene = v.scene
const bl = v.imageryLayers.length > 0 ? v.imageryLayers.get(0) : null
if (weather === 'clear') {
scene.fog.enabled = false
scene.skyAtmosphere.hueShift = 0
scene.globe.atmosphereLightIntensity = 10
if (bl) { bl.brightness = 1; bl.contrast = 1; bl.saturation = 1 }
} else if (weather === 'foggy') {
scene.fog.enabled = true
scene.fog.density = 0.0008
scene.fog.minimumBrightness = 0.6
scene.skyAtmosphere.saturationShift = -0.8
scene.globe.atmosphereLightIntensity = 3
if (bl) { bl.brightness = 1.3; bl.contrast = 0.7; bl.saturation = 0.3 }
} else if (weather === 'overcast') {
scene.fog.enabled = true
scene.fog.density = 0.0003
scene.skyAtmosphere.hueShift = -0.04
scene.globe.atmosphereLightIntensity = 4
if (bl) { bl.brightness = 0.85; bl.contrast = 0.9; bl.saturation = 0.5 }
}
scene.requestRender()
}, [weather])
五、性能优化技巧
5.1 Cesium 渲染优化
// 按需渲染
scene.requestRenderMode = true
scene.maximumRenderTimeChange = 0.0
// 瓦片缓存
globe.tileCacheSize = 1000
globe.maximumScreenSpaceError = 1.5
globe.preloadSiblings = true
// FXAA抗锯齿
if (scene.postProcessStages) {
scene.postProcessStages.fxaa.enabled = true
}
5.2 React 组件优化
// CallbackProperty避免每帧创建新对象
const positionProp = useMemo(
() => new CallbackProperty(() => {
const c = interpolator.get(droneId)
if (!c) return Cartesian3.fromDegrees(0, 0, 0)
return Cartesian3.fromDegrees(c.lng, c.lat, c.alt)
}, false),
[droneId, interpolator]
)
// useMemo缓存静态属性
const model = useMemo(() => ({
uri: DRONE_MODEL_URI,
scale: DRONE_SCALE,
...
}), [droneId, isSelected, droneColor])
六、运行
环境要求
Go ≥ 1.22
Node.js≥ 18
pnpm(或 npm)
启动步骤
- 启动后端
cd api
go mod tidy
go run main.go
- 启动前端
cd web
pnpm install
pnpm dev
七、技术难点
难点1:低频推送与高帧率渲染的矛盾
问题:后端 200ms 推送一次数据(5Hz),但前端需要 60fps(16.67ms/帧)渲染。
解决方案:设计 `DroneInterpolator` 类,使用指数衰减插值将 5Hz 数据"膨胀"到 60fps。这是一种临界阻尼弹簧模型的简化,既保证追赶速度,又无超调抖动。
难点2:航向角跨越 0°/360° 时的反转问题
问题:无人机航向从 350° 变为 10°(向右转 20°),如果直接插值会走 340° 的大弧。
解决方案:在 lerpAngleDeg 函数中先计算最短角度差(确保 diff ∈ [-180°, 180°]),再进行插值。
难点3:多无人机同时渲染的性能瓶颈
问题:3 架无人机 × 每帧更新位置和姿态,计算量大。
解决方案:
使用Cesium的CallbackProperty零分配
将DroneEntity拆为独立子组件,隔离 re-render
提取常量为模块级ConstantProperty
八、总结
本文介绍了一个完整的三维无人机编队实时巡航可视化系统的实现过程。项目涵盖了:
-
后端:Go 语言实现飞行模拟器、WebSocket 实时推送
-
前端:React + CesiumJS 实现三维可视化、帧级插值引擎
-
核心算法:Haversine 距离计算、Bearing 航向角计算、指数衰减插值
-
性能优化:Cesium 渲染优化、React 组件优化
特色
1.多机编队同步仿真
2.5Hz → 60fps 帧级平滑渲染
3.真实地理坐标与 3D 模型
4.智能相机跟随系统
5.沉浸式天气系统
技术
-
CesiumJS 三维地球引擎的使用
-
WebSocket 实时通信的实现
-
帧级插值算法的设计
-
React + TypeScript 大型项目架构
-
Go 语言并发编程
后续扩展
1.添加更多无人机(支持 10+ 架)
2.实现碰撞检测与避障算法
3.添加历史轨迹回放功能
4.集成真实无人机 API
- 添加 VR/AR 支持
6.支持拓展AI轨迹分析预测
参考资料
CesiumJS 官方文档(https://cesium.com/learn/cesiumjs/)
React 官方文档(https://react.dev/)
Go WebSocket 教程(https://github.com/gorilla/websocket)
Haversine 公式(https://en.wikipedia.org/wiki/Haversine_formula)
结语
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,如有任何问题,欢迎在评论区留言讨论。