跳到主要内容基于 CesiumJS + React + Go 实现三维无人机编队实时巡航可视化系统 | 极客日志Go / Golang大前端算法
基于 CesiumJS + React + Go 实现三维无人机编队实时巡航可视化系统
介绍使用 CesiumJS、React 和 Go 构建三维无人机编队实时巡航可视化系统的完整方案。系统采用前后端分离架构,后端 Go 负责飞行模拟与 WebSocket 广播,前端 React 结合 CesiumJS 实现 60fps 平滑渲染。核心包含 Haversine 距离计算、航向角解算及指数衰减插值引擎,解决低频推送与高帧率渲染矛盾。支持多机编队同步仿真、真实地理坐标展示、智能相机跟随及沉浸式天气效果。文章涵盖技术选型、架构设计、核心算法实现及性能优化技巧,适合图形学与全栈开发者参考。
落日余晖30 浏览 一、项目概述
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
├── model/
│ └── drone.go
├── router/
│ └── router.go
└── simulator/
└── flight.go
3.2 数据模型定义
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"`
Battery float64 `json:"battery"`
Timestamp int64 `json:"timestamp"`
}
type Waypoint struct {
Lng float64
Lat float64
Alt float64
}
3.3 飞行模拟器核心算法
飞行模拟器是系统的核心,负责计算每架无人机在航线上的实时位置和姿态。
3.3.1 Haversine 距离计算
Haversine 公式用于计算地球表面两点间的大圆距离:
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)
}
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
}
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 连接,并定时广播无人机状态:
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
│ │ └── HUD.tsx
│ ├── hooks/
│ │ └── useDroneWS.ts
│ ├── types/
│ │ └── drone.ts
│ └── styles/
│ └── index.css
4.2 WebSocket Hook 实现
封装 WebSocket 连接逻辑,支持自动重连:
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 渲染。于是设计了一个指数衰减插值引擎:
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
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)
}
}
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)
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
if (scene.postProcessStages) {
scene.postProcessStages.fxaa.enabled = true
}
5.2 React 组件优化
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]
)
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
七、技术难点
- 问题:后端 200ms 推送一次数据(5Hz),但前端需要 60fps(16.67ms/帧)渲染。
- 解决方案:设计
DroneInterpolator 类,使用指数衰减插值将 5Hz 数据'膨胀'到 60fps。这是一种临界阻尼弹簧模型的简化,既保证追赶速度,又无超调抖动。
难点 2:航向角跨越 0°/360° 时的反转问题
- 问题:无人机航向从 350° 变为 10°(向右转 20°),如果直接插值会走 340° 的大弧。
- 解决方案:在
lerpAngleDeg 函数中先计算最短角度差(确保 diff ∈ [-180°, 180°]),再进行插值。
- 问题: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 轨迹分析预测
参考资料
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online