一、项目概述
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 * ( + fs.progress*)
} fs.progress > {
speed = fs.speed * ( + (-fs.progress)*)
}
fs.state = model.DroneState{
DroneID: fs.droneID,
Lng: lng,
Lat: lat,
Alt: alt,
Heading: heading,
Pitch: pitch,
Roll: roll,
Speed: math.Round(speed*) / ,
Battery: math.Round(fs.battery*) / ,
Timestamp: time.Now().Unix(),
}
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 = ()
wsRef. = ws
ws. = {
(!isMounted.) {
ws.()
}
.()
()
}
ws. = {
{
raw = .(evt.)
: [] = .(raw) ? raw : [raw]
: = {}
: [] = []
( s arr) {
newFleet[s.] = s
ids.(s.)
}
(newFleet)
(ids)
} (e) {
.(, e)
}
}
ws. = {
(!isMounted.)
()
reconnectTimer. = .(connect, )
}
ws. = {
ws.()
}
}
()
{
isMounted. =
(reconnectTimer.)
wsRef.?.()
wsRef. =
}
}, [])
{ 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., : s.,
: s., : s., : s.,
})
}
}
() {
speed =
t = - .(-speed * dt)
( [id, tgt] .) {
cur = ..(id)
(!cur)
cur. += (tgt. - cur.) * t
cur. += (tgt. - cur.) * t
cur. += (tgt. - cur.) * t
cur. = (cur., tgt., t)
cur. += (tgt. - cur.) * t
cur. += (tgt. - cur.) * t
}
}
(: ): | {
..(id)
}
}
() {
diff = b - a
(diff > ) diff -=
(diff < -) diff +=
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. = ; bl. = ; bl. = }
} (weather === ) {
scene.. =
scene.. =
scene.. = -
scene.. =
(bl) { bl. = ; bl. = ; bl. = }
}
scene.()
}, [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 # http://localhost:8080 - 启动前端
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 组件优化
特色
- 多机编队同步仿真
- 5Hz → 60fps 帧级平滑渲染
- 真实地理坐标与 3D 模型
- 智能相机跟随系统
- 沉浸式天气系统
技术
- CesiumJS 三维地球引擎的使用
- WebSocket 实时通信的实现
- 帧级插值算法的设计
- React + TypeScript 大型项目架构
- Go 语言并发编程
后续扩展
- 添加更多无人机(支持 10+ 架)
- 实现碰撞检测与避障算法
- 添加历史轨迹回放功能
- 集成真实无人机 API
- 添加 VR/AR 支持
- 支持拓展 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


