基于 CesiumJS + React + Go 实现三维无人机编队实时巡航可视化系统

本文详细介绍如何使用 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 数据流设计

  1. 后端数据流:Ticker(200ms) → 各 FlightSimulator.Tick() → 聚合为 []DroneState→ JSON → 广播至所有 WebSocket 客户端

  2. 前端数据流: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)

这种方法的优势:

  1. 平滑过渡:不会出现突变或抖动

  2. 自适应:距离目标越远,追赶速度越快

  3. 零超调:不会超过目标值再回调

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)

启动步骤

  1. 启动后端

cd api

go mod tidy

go run main.go

http://localhost:8080

  1. 启动前端

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

八、总结

本文介绍了一个完整的三维无人机编队实时巡航可视化系统的实现过程。项目涵盖了:

  1. 后端:Go 语言实现飞行模拟器、WebSocket 实时推送

  2. 前端:React + CesiumJS 实现三维可视化、帧级插值引擎

  3. 核心算法:Haversine 距离计算、Bearing 航向角计算、指数衰减插值

  4. 性能优化:Cesium 渲染优化、React 组件优化

特色

1.多机编队同步仿真

2.5Hz → 60fps 帧级平滑渲染

3.真实地理坐标与 3D 模型

4.智能相机跟随系统

5.沉浸式天气系统

技术

  1. CesiumJS 三维地球引擎的使用

  2. WebSocket 实时通信的实现

  3. 帧级插值算法的设计

  4. React + TypeScript 大型项目架构

  5. Go 语言并发编程

后续扩展

1.添加更多无人机(支持 10+ 架)

2.实现碰撞检测与避障算法

3.添加历史轨迹回放功能

4.集成真实无人机 API

  1. 添加 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)

结语

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,如有任何问题,欢迎在评论区留言讨论。

仓库地址:lien0219/3D-Scene-Flight-Trajectory-of-UAV: 基于 CesiumJS + React + Go 的三维无人机编队实时巡航轨迹可视化系统 | Real-time 3D UAV swarm flight visualization with smooth rendering

相关推荐
ppppppatrick1 小时前
【深度学习基础篇05】从AlexNet到ResNet:经典卷积神经网络的演进
人工智能·深度学习·cnn
henry1010101 小时前
DeepSeek生成的HTML5小游戏 -- 投篮小能手
前端·javascript·css·游戏·html5
Zhu_S W1 小时前
EasyExcel:让Excel操作变得简单优雅
java·前端
GISer_Jing1 小时前
从零到架构师:Taro 全链路学习与实战指南
前端·react.js·taro
菜鸡儿齐1 小时前
leetcode-分割回文串
算法·leetcode·职场和发展
phltxy1 小时前
快速上手 ElementPlus:核心用法精讲
前端·javascript·vue.js
重生之我是Java开发战士1 小时前
【优选算法】链表:两数相加,两两交换节点,重排链表,合并K个升序链表,K个一组反转链表
数据结构·算法·链表
菜鸡儿齐2 小时前
leetcode-组合总和
算法·leetcode·深度优先
滴滴答滴答答2 小时前
LeetCode Hot100 之 19 接雨水
算法·leetcode·职场和发展