【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行

前言

【qt openGL入门笔记】FPS相机和轨道相机-CSDN博客分享过原生opengl网格地图下的相机,本篇则是基于OSG特性制作的网格地图飞机飞行案例。

什么是OpenSceneGraph(开源场景图)

OSG是一个基于 OpenGL 的开源、跨平台、高性能 3D 图形渲染引擎/场景图中间件,用标准 C++ 编写,充分利用 STL 和设计模式。

  • 核心思想:把三维世界组织成一棵树状的"场景图"(Scene Graph,有向无环图),节点包括 Group、Transform、Geode、LOD、Switch、Camera 等,每个节点可挂状态集(StateSet)、回调和自定义数据。开发者只需声明"场景长什么样",引擎自动处理渲染顺序与资源生命周期。
  • 性能优化:内置视锥剔除、遮挡剔除、细节层次 LOD / PagedLOD 流式加载、状态排序、VBO、着色器、多线程渲染(Cull/Draw 线程分离)等。
  • 模块化:osg(核心场景管理)、osgUtil(优化与工具)、osgDB(插件式数据读写,支持 50+ 格式如 OBJ、FBX、3DS、DAE、OSGB/OSGT、GDAL)、osgViewer(窗口与渲染循环)、osgGA(交互事件),外加 osgText、osgParticle、osgShadow、osgAnimation、osgTerrain 等扩展。
  • 内存与安全 :用 osg::ref_ptr 引用计数智能指针管理资源。
  • 跨平台:Windows、Linux、macOS、Android、iOS 等,可嵌入 Qt、SDL、MFC 等,支持多 GPU 与集群渲染。
  • 历史与授权:1998 年起源(最初为飞行模拟需求),1999 年开源,采用 OSGPL(基于 LGPL),可免费商用。
  • 用途:虚拟现实、仿真训练、飞行模拟、GIS/数字地球(osgEarth)、数字孪生、科学可视化、工业可视化等。国内很多高校课程、军工仿真和测绘可视化项目都在用。

以上是AI提供的知识点,通俗的说OSG封装了很多场景图以及相关的事件、渲染等,不用自己用opengl来全部实现,但封装的事件、渲染不满足自己的需求时,需要自己重载处理。

效果图

功能详解

飞机模型加载

上一篇文件已分享飞机模型cessna.osg的下载及放置目录的方法【qt openGL入门笔记】windows编译MinGW版本的OpenSceneGraph(3.6.5)-CSDN博客

代码结构

复制代码
Process1/
├── QtOsgDemo.pro
├── main.cpp
├── OSGWidget.h / OSGWidget.cpp    ← 通用渲染组件:写一次全程复用
├── SceneUtil.h                    ← 渲染工具库
├── SituationScene.h / .cpp        ← 核心:网格/航线/飞机/动画
└── MainWindow.h / .cpp            ← 组装 + 时钟驱动

源码及说明

pro文件

复制代码
QT       += core gui widgets
CONFIG   += c++14
TARGET    = Radar
TEMPLATE  = app

OSG_DIR = D:/software/OpenSceneG/install-mingw73

INCLUDEPATH += $$OSG_DIR/include
# OSG 的库分工(记住这个对应关系,缺库报 undefined reference 时按名字补):
#   osg          ------ 场景图核心:Node/Group/Geometry/StateSet...
#   osgUtil      ------ 工具集
#   osgDB        ------ 模型文件读写:readNodeFile
#   osgViewer    ------ 渲染器:Viewer/Camera
#   osgGA        ------ 交互:TrackballManipulator/事件
#   osgText      ------ 三维文字标注
#   OpenThreads  ------ OSG 自带线程库(osg 头文件内联用到其 Mutex,必须链)
LIBS += -L$$OSG_DIR/lib \
        -losg -losgUtil -losgDB -losgViewer -losgGA -losgText -lOpenThreads

SOURCES += main.cpp MainWindow.cpp OSGWidget.cpp \
    situationscene.cpp
HEADERS += MainWindow.h OSGWidget.h \
    SceneUtil.h \
    situationscene.h

main.cpp

复制代码
// ============================================================
// 程序入口:只做三件事 ------ 初始化 Qt、配置 OpenGL 表面、启动主窗口
// ============================================================
#include <QApplication>
#include <QSurfaceFormat>
#include "mainwindow.h"

int main(int argc, char* argv[])
{
    // 告诉 Qt 使用系统桌面 OpenGL(而非 ANGLE/DirectX 转译层)。
    // OSG 基于桌面版 OpenGL,这一行必须放在 QApplication 构造之前。
    QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);

    QApplication app(argc, argv);

    QSurfaceFormat fmt;
    fmt.setDepthBufferSize(24);//   深度 24 位 ------ 3D 遮挡判断(飞机在网格前面还是后面)必需
    fmt.setStencilBufferSize(8);//   模板 8 位 ------ 本项目暂用不到,但留着无害
    fmt.setSamples(4);//   4 倍多重采样 ------ 抗锯齿,让网格线/航线平滑
    QSurfaceFormat::setDefaultFormat(fmt);

    // 命令行第 1 个参数 = 飞机模型路径(可选),例如:
    //   Process1.exe D:/OpenSceneGraph-Data/cessna.osg
    // 不传则先找 cessna.osg,再失败就用黄色占位盒兜底
    MainWindow w(argc > 1 ? QString::fromLocal8Bit(argv[1]) : QString());
    w.setWindowTitle(QStringLiteral("过程1:网格 + 飞机 + 五角星定向飞行"));
    w.resize(1280, 800);
    w.show();
    return app.exec();
}

OSGWidget

这是OSG的核心场景类,渲染OSG场景,以及接收鼠标键盘事件

复制代码
// OSGWidget.h
// ============================================================
// OSGWidget ------ Qt 与 OSG 的"桥"
// 职责:提供一个能渲染 OSG 场景的 Qt 窗口部件,并把 Qt 的鼠标键盘
//       事件翻译给 OSG。这是【通用组件】,四个过程都不需要再改它。
// ============================================================
#ifndef OSGWIDGET_H
#define OSGWIDGET_H

#include <QOpenGLWidget>
#include <QTimer>
#include <osg/ref_ptr>
#include <osgViewer/Viewer>
#include <osgViewer/GraphicsWindow>
#include <osgGA/TrackballManipulator>
#include <osgGA/GUIEventAdapter>

class OSGWidget : public QOpenGLWidget
{
public:
    explicit OSGWidget(QWidget* parent = nullptr,
                       Qt::WindowFlags f = Qt::WindowFlags());
    ~OSGWidget() override;

    // 装入场景,并按场景包围球自动摆一个"斜上方俯视"的初始相机位
    void setScene(osg::Node* node);

    // 调整远/近裁剪面(场景达到公里级时必须放大 far,否则远处被裁掉)
    void setClip(double nearZ, double farZ);

    // 设置背景色
    void setBgColor(float r, float g, float b, float a = 1.f);

protected:
    // Qt 渲染回调:每帧调 viewer.frame() 驱动 OSG 完整渲染一遍
    void paintGL() override;
    void resizeGL(int w, int h) override;

    // ------ 事件转发:Qt 事件 → OSG 事件队列 ------
    void keyPressEvent(QKeyEvent* e) override;
    void mousePressEvent(QMouseEvent* e) override;
    void mouseReleaseEvent(QMouseEvent* e) override;
    void mouseDoubleClickEvent(QMouseEvent* e) override;
    void mouseMoveEvent(QMouseEvent* e) override;
    void wheelEvent(QWheelEvent* e) override;

private:
    static bool qtKeyToOsg(QKeyEvent* e, osgGA::GUIEventAdapter::KeySymbol& out);
    void applyProjection(int w, int h);

    osgViewer::Viewer m_viewer;                          // OSG 渲染器(无窗口版)
    osg::ref_ptr<osgViewer::GraphicsWindowEmbedded> m_gwin; // "嵌入窗口":借用 Qt 的 GL 上下文
    QTimer* m_timer = nullptr;                           // 60Hz 触发重绘
    qreal  m_scaleX = 1.0, m_scaleY = 1.0;               // 高分屏缩放(DPI)
    bool   m_ortho  = false;                             // 是否正交投影(过程5 用)
    double m_half   = 1000.0;                            // 正交视野半宽
    double m_nearZ  = 1.0, m_farZ = 100000.0;            // 裁剪面
};
#endif

//OSGWidget.cpp
#include "OSGWidget.h"
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>

OSGWidget::OSGWidget(QWidget* parent, Qt::WindowFlags f)
    : QOpenGLWidget(parent, f)
{
    setMouseTracking(true);          // 不按键也接收 mouseMove(拖拽预览用)
    setFocusPolicy(Qt::StrongFocus); // 可接收键盘

    // 高分屏适配:Qt 的逻辑像素 ≠ 物理像素,OSG 需要物理像素
    m_scaleX = devicePixelRatioF();
    m_scaleY = devicePixelRatioF();

    // GraphicsWindowEmbedded:OSG 不自己创建窗口和 GL 上下文,
    // 而是借用在 paintGL 时刻 Qt 已绑定好的那个上下文 ------ 这就是
    // "Qt 主导渲染循环、OSG 只负责画" 的集成方案核心。
    m_gwin = new osgViewer::GraphicsWindowEmbedded(x(), y(), width(), height());

    osg::Camera* cam = m_viewer.getCamera();
    cam->setGraphicsContext(m_gwin.get());
    cam->setClearColor(osg::Vec4(0.08f, 0.10f, 0.12f, 1.0f));

    // 单线程渲染:frame() 在 Qt 的 GUI 线程里同步执行,简单且安全。
    // (多线程渲染是性能优化课题,初学阶段单线程最稳)
    m_viewer.setThreadingModel(osgViewer::ViewerBase::SingleThreaded);
    m_viewer.setKeyEventSetsDone(0);  // 禁掉 OSG 默认的 Esc 退出
    m_viewer.realize();

    // 60Hz 定时器触发 Qt 重绘 → paintGL → viewer.frame()
    // 渲染节奏归 Qt,业务数据节奏归 MainWindow 的另一个定时器,互不干扰
    m_timer = new QTimer(this);
    m_timer->setInterval(16);
    connect(m_timer, &QTimer::timeout, this, QOverload<>::of(&QWidget::update));
    m_timer->start();
}

OSGWidget::~OSGWidget() { m_timer->stop(); }

void OSGWidget::setScene(osg::Node* node)
{
    m_viewer.setSceneData(node);

    // 按场景包围球自动算初始相机位置:
    // 眼睛放在"包围球半径 2.5 倍远、斜上方",保证整个场景入画
    osg::Vec3d center(0, 0, 0);
    double radius = 10.0;
    if (node) {
        const osg::BoundingSphere bs = node->getBound();
        if (bs.valid()) { center = osg::Vec3d(bs.center()); radius = bs.radius(); }
    }
    const double dist = radius * 2.5 + 1.0;
    const osg::Vec3d eye = center + osg::Vec3d(0.0, -dist, dist * 0.5);

    // 轨迹球操纵器:左键旋转 / 右键平移 / 滚轮缩放
    osg::ref_ptr<osgGA::TrackballManipulator> manip = new osgGA::TrackballManipulator;
    manip->setAllowThrow(false);   // 关闭"甩动"惯性
    manip->setHomePosition(eye, center, osg::Vec3d(0.0, 0.0, 1.0));
    m_viewer.setCameraManipulator(manip.get());
}

void OSGWidget::setClip(double nearZ, double farZ)
{ m_nearZ = nearZ; m_farZ = farZ; }

void OSGWidget::setBgColor(float r, float g, float b, float a)
{ m_viewer.getCamera()->setClearColor(osg::Vec4(r, g, b, a)); }


// 根据窗口宽高比设置投影矩阵(每次 resize 都要重算,否则画面变形)
void OSGWidget::applyProjection(int w, int h)
{
    osg::Camera* cam = m_viewer.getCamera();
    const double aw = w * m_scaleX, ah = h * m_scaleY;
    const double aspect = (ah > 0) ? aw / ah : 1.0;

    if (m_ortho) {
        // 正交:短边贴合 m_half,长边按宽高比扩展,保证圆不压成椭圆
        if (aspect >= 1.0)
            cam->setProjectionMatrixAsOrtho(-m_half*aspect, m_half*aspect,
                                            -m_half, m_half, m_nearZ, m_farZ);
        else
            cam->setProjectionMatrixAsOrtho(-m_half, m_half,
                                            -m_half/aspect, m_half/aspect,
                                            m_nearZ, m_farZ);
    } else {
        // 透视:30° 垂直视场角(人眼自然视角附近)
        cam->setProjectionMatrixAsPerspective(30.0, aspect, m_nearZ, m_farZ);
    }
}

void OSGWidget::paintGL() { m_viewer.frame(); }

void OSGWidget::resizeGL(int w, int h)
{
    if (!m_gwin.valid()) return;
    m_scaleX = devicePixelRatioF();
    m_scaleY = devicePixelRatioF();
    // 通知 OSG 视口尺寸变了(注意坐标乘了 DPI 缩放)
    m_gwin->getEventQueue()->windowResize(0, 0, w*m_scaleX, h*m_scaleY);
    m_gwin->resized(0, 0, w*m_scaleX, h*m_scaleY);
    m_viewer.getCamera()->setViewport(0, 0, w*m_scaleX, h*m_scaleY);
    applyProjection(w, h);
}

// ------ Qt 键码 → OSG 键码 ------(两套框架的键值枚举不同,需手动映射)
bool OSGWidget::qtKeyToOsg(QKeyEvent* e, osgGA::GUIEventAdapter::KeySymbol& out)
{
    switch (e->key()) {
    case Qt::Key_Left:     out = osgGA::GUIEventAdapter::KEY_Left;      return true;
    case Qt::Key_Right:    out = osgGA::GUIEventAdapter::KEY_Right;     return true;
    case Qt::Key_Up:       out = osgGA::GUIEventAdapter::KEY_Up;        return true;
    case Qt::Key_Down:     out = osgGA::GUIEventAdapter::KEY_Down;      return true;
    case Qt::Key_Space:    out = osgGA::GUIEventAdapter::KEY_Space;     return true;
    case Qt::Key_Home:     out = osgGA::GUIEventAdapter::KEY_Home;      return true;
    case Qt::Key_PageUp:   out = osgGA::GUIEventAdapter::KEY_Page_Up;   return true;
    case Qt::Key_PageDown: out = osgGA::GUIEventAdapter::KEY_Page_Down; return true;
    default: return false;
    }
}

void OSGWidget::keyPressEvent(QKeyEvent* e)
{
    osgGA::GUIEventAdapter::KeySymbol ks;
    if (qtKeyToOsg(e, ks)) m_gwin->getEventQueue()->keyPress(ks);
    else QOpenGLWidget::keyPressEvent(e);
}

void OSGWidget::mousePressEvent(QMouseEvent* e)
{
    // Qt 按钮 → OSG 按钮编号(1左 2中 3右)
    int button = 0;
    if (e->button() == Qt::LeftButton)   button = 1;
    if (e->button() == Qt::MiddleButton) button = 2;
    if (e->button() == Qt::RightButton)  button = 3;
    // ★ Y 轴翻转:Qt 原点在左上角,OpenGL/OSG 原点在左下角
    m_gwin->getEventQueue()->mouseButtonPress(
        e->x() * m_scaleX, (height() - e->y()) * m_scaleY, button);
}

void OSGWidget::mouseReleaseEvent(QMouseEvent* e)
{
    int button = 0;
    if (e->button() == Qt::LeftButton)   button = 1;
    if (e->button() == Qt::MiddleButton) button = 2;
    if (e->button() == Qt::RightButton)  button = 3;
    m_gwin->getEventQueue()->mouseButtonRelease(
        e->x() * m_scaleX, (height() - e->y()) * m_scaleY, button);
}

void OSGWidget::mouseDoubleClickEvent(QMouseEvent* e)
{
    m_gwin->getEventQueue()->mouseDoubleButtonPress(
        e->x() * m_scaleX, (height() - e->y()) * m_scaleY,
        e->button() == Qt::LeftButton ? 1 : 0);
}

void OSGWidget::mouseMoveEvent(QMouseEvent* e)
{
    m_gwin->getEventQueue()->mouseMotion(
        e->x() * m_scaleX, (height() - e->y()) * m_scaleY);
}

void OSGWidget::wheelEvent(QWheelEvent* e)
{
    m_gwin->getEventQueue()->mouseScroll(
        e->angleDelta().y() >= 0 ? osgGA::GUIEventAdapter::SCROLL_UP
                                 : osgGA::GUIEventAdapter::SCROLL_DOWN);
}
功能说明
setScene

把一棵 OSG 场景树挂到 Viewer 上,然后让相机自动退到一个能把整个场景完整装进画面的位置,最后装上轨迹球漫游器,使鼠标可以旋转、平移、缩放。

setClip

调整远/近裁剪面。

setBgColor

设置背景色。

paintGL

Qt 渲染回调,只用了m_viewer.frame()实现渲染效果。触发规则如下:

QTimer → update() → 投一个 Paint 事件 → paintGL → m_viewer.frame()

复制代码
connect(m_timer, &QTimer::timeout, this, QOverload<>::of(&QWidget::update));

注意,update()只触发 Paint 事件,不会触发鼠标键盘事件。

resizeGL

窗口尺寸变化

SceneUtil

渲染工具库

复制代码
SceneUtil.h
// ============================================================
// SceneUtil ------ 渲染小工具库
// 职责:把 OSG 冗长的样板代码封装成一句话工具。
// ============================================================
#ifndef SCENEUTIL_H
#define SCENEUTIL_H

#include <osg/Geometry>
#include <osg/Geode>
#include <osg/LineWidth>
#include <osg/Point>
#include <osg/Depth>
#include <osg/BlendFunc>
#include <osgText/Text>
#include <cmath>

namespace su {

// ------------------------------------------------------------
// gridLines:以原点为中心的正方形网格
//   halfExtent : 网格半宽(米)------ total = 2*halfExtent
//   step       : 网格间距(米)
// 知识点:Geometry 四件套 = 顶点数组 + 颜色数组 + 图元 + 挂到 Geode
// ------------------------------------------------------------
inline osg::Geode* gridLines(double halfExtent, double step,
                             const osg::Vec4& color, float width = 1.f)
{
    osg::ref_ptr<osg::Geometry> g = new osg::Geometry;
    osg::ref_ptr<osg::Vec3Array> v = new osg::Vec3Array;   // 顶点:两个点 = 一条线
    for (double d = -halfExtent; d <= halfExtent + 1; d += step) {
        v->push_back(osg::Vec3(float(d), float(-halfExtent), 0.f)); // 平行 Y 轴的线
        v->push_back(osg::Vec3(float(d), float( halfExtent), 0.f));
        v->push_back(osg::Vec3(float(-halfExtent), float(d), 0.f)); // 平行 X 轴的线
        v->push_back(osg::Vec3(float( halfExtent), float(d), 0.f));
    }
    g->setVertexArray(v.get());
    g->addPrimitiveSet(new osg::DrawArrays(GL_LINES, 0, v->size()));

    // 颜色:一条颜色贯穿所有顶点(BIND_OVERALL)
    osg::ref_ptr<osg::Vec4Array> c = new osg::Vec4Array;
    c->push_back(color);
    g->setColorArray(c.get(), osg::Array::BIND_OVERALL);

    // 渲染状态:网格不受光照影响(否则背光角度会变黑)
    osg::StateSet* ss = g->getOrCreateStateSet();
    ss->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
    ss->setAttributeAndModes(new osg::LineWidth(width));

    osg::ref_ptr<osg::Geode> geode = new osg::Geode;  // Geode = 挂 Drawable 的叶节点
    geode->addDrawable(g.get());
    return geode.release();
}

// ------------------------------------------------------------
// makeLabel:屏幕对齐的文字标注
//   pixelSize 用像素为单位(SCREEN_COORDS)------不管相机多远,
//   字在屏幕上始终一样大。这是态势显示"信息层"的标配。
//   ★ 返回 Text 对象后,调用方负责 setPosition 和 setText。
// ------------------------------------------------------------
inline osgText::Text* makeLabel(float pixelSize, const osg::Vec4& color,
    osgText::TextBase::AlignmentType align = osgText::TextBase::LEFT_BOTTOM)
{
    osg::ref_ptr<osgText::Text> t = new osgText::Text;
    t->setCharacterSize(pixelSize);
    t->setCharacterSizeMode(osgText::TextBase::SCREEN_COORDS); // 像素大小
    t->setColor(color);
    t->setAlignment(align);
    t->setAxisAlignment(osgText::TextBase::SCREEN);      // 永远正对屏幕(billboard)
    t->setBackdropType(osgText::Text::OUTLINE);          // 描边,深色背景上更清晰
    osg::StateSet* ss = t->getOrCreateStateSet();
    ss->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
    // 关闭深度测试:文字永远不被模型/地形遮挡(信息层优先于实景层)
    ss->setAttributeAndModes(new osg::Depth(osg::Depth::ALWAYS));
    return t.release();
}

// 把 Text 包进 Geode( osgText::Text 是 Drawable,必须挂 Geode 才能进场景图)
inline osg::Geode* labelGeode(osgText::Text* t)
{
    osg::ref_ptr<osg::Geode> g = new osg::Geode;
    g->addDrawable(t);
    return g.release();
}

} // namespace su
#endif
功能说明
gridLines

绘制以原点为中心的正方形网格

makeLabel

在飞机模型上显示一个标签,用来做态势标签。

labelGeode

与makeLabel配套使用,把标签加入到叶子几何中。

SituationScene

3D 态势图层:网格 + 五角星航线 + 飞机

复制代码
//SituationScene.h
// ============================================================
// SituationScene ------ 3D 态势图层(网格 + 五角星航线 + 飞机)
// 职责:创建本过程全部静态元素;提供 updateTruth(t) 每帧驱动飞机。
// 增长计划:过程2 加 addPlot();过程3 加 updateTrack();过程4 加标绘。
// ============================================================
#ifndef SITUATIONSCENE_H
#define SITUATIONSCENE_H

#include <osg/Group>
#include <osg/MatrixTransform>
#include <osgText/Text>
#include <osg/Vec3d>

class SituationScene
{
public:
    // root  : 场景根节点(本类把所有图层挂上去)
    // model : 已处理好的飞机模型(缩放/材质已在 MainWindow 处理)
    explicit SituationScene(osg::Group* root, osg::Node* model);

    // ★ 核心:动画 = 时间 → (位置, 姿态) 的映射函数。
    // MainWindow 每帧用仿真时钟调用它,飞机就动起来。
    void updateTruth(double t);

private:
    void buildGrid();        // 网格地面
    void buildStarRoute();   // 五角星航线可视化(航线线 + 顶点标记)
    void buildAircraft();    // 飞机 + TRUTH 标签 + 垂直线

    osg::ref_ptr<osg::Group>             m_root;      // 场景根
    osg::ref_ptr<osg::MatrixTransform>   m_airMt;     // 飞机变换节点(动画作用对象)
    osg::ref_ptr<osgText::Text>          m_airLabel;  // TRUTH 标签

    osg::Vec3d m_star[5];     // 五角星 5 个外顶点(世界坐标,等高)
    int        m_seq[6];      // 飞行顺序(星形序列,首尾相接)
};

#endif

//SituationScene.cpp
#include "situationscene.h"
#include "SceneUtil.h"
#include <osg/Geode>
#include <osg/Geometry>
#include <osg/LineWidth>
#include <osg/Point>
#include <osg/ShapeDrawable>
#include <osg/Matrix>
#include <osg/Material>
#include <osg/Depth>
#include <cmath>
#include <cstdio>

// ------------------------------------------------------------
// 五角星顶点计算(数学小知识,面试可能聊到):
// 5 个顶点均匀分布在外接圆上,相邻顶点间隔 72°。
//   按 0→1→2→3→4 连线  = 正五边形
//   按 0→2→4→1→3 连线  = 五角星(每次"跳过"一个相邻点)
// 通用规律:n 角星的连线步长 = (n-1)/2(n=5 时步长 2)
// 这里第一个顶点放在正北(+Y 方向),星星是"角朝上"的正姿势。
// ------------------------------------------------------------
static osg::Vec3d starVertex(int i, double R, double H)
{
    const double a = osg::PI_2 + i * (2.0 * osg::PI / 5.0); // 90° 起始,逆时针
    return osg::Vec3d(R * std::cos(a), R * std::sin(a), H);
}

SituationScene::SituationScene(osg::Group* root, osg::Node* model)
    : m_root(root)
{
    // ------ 五角星参数:外接圆半径 5km、飞行高度 800m ------
    const double R = 5000.0, H = 800.0;
    for (int i = 0; i < 5; ++i)
        m_star[i] = starVertex(i, R, H);

    // 星形飞行序列:0→2→4→1→3→回到0(首尾相接,共 5 段直线)
    const int seq[6] = { 0, 2, 4, 1, 3, 0 };
    for (int i = 0; i < 6; ++i) m_seq[i] = seq[i];

    buildGrid();
    buildStarRoute();
    buildAircraft();

    if (model) m_airMt->addChild(model);   // 飞机模型挂到变换节点下
    m_root->addChild(m_airMt.get());
}

// ------------------------------------------------------------
// 图层1:网格地面(1km 间距,16km × 16km)
// 作用:给场景提供"尺度参照"------一眼看出航线是公里级、飞机是百米级
// ------------------------------------------------------------
void SituationScene::buildGrid()
{
    m_root->addChild(su::gridLines(8000.0, 1000.0,
                                   osg::Vec4(0.55f, 0.58f, 0.62f, 1.f), 1.f));
}

// ------------------------------------------------------------
// 图层2:五角星航线可视化
//   a) 按星形顺序把 5 顶点连成闭合折线(青色粗线)
//   b) 每个顶点画一个大点(起降点/航路点的感觉)
// 知识点:GL_LINE_LOOP 自动闭合(首尾相连),GL_POINTS 画点
// ------------------------------------------------------------
void SituationScene::buildStarRoute()
{
    // a) 航线折线
    {
        osg::ref_ptr<osg::Geometry> g = new osg::Geometry;
        osg::ref_ptr<osg::Vec3Array> v = new osg::Vec3Array;
        for (int i = 0; i < 5; ++i)              // LOOP 模式自动回到起点闭合
            v->push_back(osg::Vec3(m_star[m_seq[i]]));
        g->setVertexArray(v.get());
        g->addPrimitiveSet(new osg::DrawArrays(GL_LINE_LOOP, 0, v->size()));

        osg::ref_ptr<osg::Vec4Array> c = new osg::Vec4Array;
        c->push_back(osg::Vec4(0.2f, 0.9f, 1.f, 1.f));   // 青色
        g->setColorArray(c.get(), osg::Array::BIND_OVERALL);

        osg::StateSet* ss = g->getOrCreateStateSet();
        ss->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
        ss->setAttributeAndModes(new osg::LineWidth(2.5f));

        osg::ref_ptr<osg::Geode> geode = new osg::Geode;
        geode->addDrawable(g.get());
        m_root->addChild(geode.get());
    }
    // b) 顶点标记
    {
        osg::ref_ptr<osg::Geometry> g = new osg::Geometry;
        osg::ref_ptr<osg::Vec3Array> v = new osg::Vec3Array;
        for (int i = 0; i < 5; ++i)
            v->push_back(osg::Vec3(m_star[i]));
        g->setVertexArray(v.get());
        g->addPrimitiveSet(new osg::DrawArrays(GL_POINTS, 0, v->size()));

        osg::ref_ptr<osg::Vec4Array> c = new osg::Vec4Array;
        c->push_back(osg::Vec4(0.2f, 0.9f, 1.f, 1.f));
        g->setColorArray(c.get(), osg::Array::BIND_OVERALL);

        osg::StateSet* ss = g->getOrCreateStateSet();
        ss->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
        ss->setAttributeAndModes(new osg::Point(10.f));  // 点直径 10 像素

        osg::ref_ptr<osg::Geode> geode = new osg::Geode;
        geode->addDrawable(g.get());
        m_root->addChild(geode.get());
    }
}

// ------------------------------------------------------------
// 图层3:飞机 + TRUTH 标签 + 垂直下引线
//
// ★ 场景图坐标规则(本过程最重要的知识点):
//   挂在 MatrixTransform 下的子节点,坐标是【本地坐标】------
//   父节点每帧 setMatrix 移动时,子节点自动跟着走。
//   所以标签只需设一次本地偏移 (0,0,300),永远不要在 update 里
//   再叠世界坐标(叠了位置就翻倍"飞走",我们踩过这个坑)。
// ------------------------------------------------------------
void SituationScene::buildAircraft()
{
    // 飞机变换节点:动画唯一的作用对象
    m_airMt = new osg::MatrixTransform;

    // a) TRUTH 标签:本地坐标,飞机上方 300m
    //m_airLabel = su::makeLabel(40.f, osg::Vec4(1.f, 0.f, 0.f, 1.f));  // 40px 大红字
    m_airLabel = su::makeLabel(15.f, osg::Vec4(0.85f, 0.95f, 1.f, 1.f));
    m_airLabel->setText("TRUTH");
    m_airLabel->setPosition(osg::Vec3(0.f, 0.f, 300.f));   // ★ 本地偏移,只设这一次
    m_airMt->addChild(su::labelGeode(m_airLabel.get()));

    // b) 垂直下引线:本地 (0,0,0) → (0,0,-800)。
    //    飞机在世界 z=800,本地 -800 正好落地。
    //    (五角星等高飞行,垂线长度恒定 → 纯静态几何,一次设好即可。
    //      若以后高度变化,就需要"动态几何"技术------过程2 的内容。)
    {
        osg::ref_ptr<osg::Geometry> g = new osg::Geometry;
        osg::ref_ptr<osg::Vec3Array> v = new osg::Vec3Array;
        v->push_back(osg::Vec3(0.f, 0.f,    0.f));    // 端点0:飞机脚下
        v->push_back(osg::Vec3(0.f, 0.f, -800.f));    // 端点1:地面投影
        g->setVertexArray(v.get());
        g->addPrimitiveSet(new osg::DrawArrays(GL_LINES, 0, 2));

        osg::ref_ptr<osg::Vec4Array> c = new osg::Vec4Array;
        c->push_back(osg::Vec4(1.f, 1.f, 1.f, 0.75f)); // 白色微透明
        g->setColorArray(c.get(), osg::Array::BIND_OVERALL);

        osg::StateSet* ss = g->getOrCreateStateSet();
        ss->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
        ss->setAttributeAndModes(new osg::LineWidth(2.f));
        ss->setMode(GL_BLEND, osg::StateAttribute::ON);        // 启用半透明
        ss->setAttributeAndModes(new osg::Depth(osg::Depth::ALWAYS)); // 不被网格遮挡

        osg::ref_ptr<osg::Geode> geode = new osg::Geode;
        geode->addDrawable(g.get());
        m_airMt->addChild(geode.get());
    }
}

// ------------------------------------------------------------
// ★★ 本过程核心算法:五角星航线的插值(手写版 AnimationPath)★★
//
// 思路:把一圈飞行拆成 5 段直线,每段耗时 SEG_TIME 秒。
//   仿真时间 t → 圈内时间 tau → 第 seg 段 + 段内进度 u
//   位置 = 起点 + (终点-起点) * u      (线性插值,严格走直线)
//   朝向 = 机头向量(-Y) 对齐 段方向    (到顶点瞬间转向)
//
// 对比 osg::AnimationPath:它内部做的就是同样的事,但旋转用球面
// 插值(slerp),转弯处飞机会"切弯";我们用直线段+瞬时转向,
// 轨迹严格贴合五角星。理解了这个函数,就理解了 AnimationPath。
// ------------------------------------------------------------
void SituationScene::updateTruth(double t)
{
    const double SEG_TIME = 6.0;                       // 每段飞 6 秒
    const double tau = std::fmod(t, SEG_TIME * 5.0);   // 一圈 30 秒,取圈内时间
    const int    seg = int(tau / SEG_TIME);            // 当前段号 0~4
    const double u   = (tau - seg * SEG_TIME) / SEG_TIME; // 段内进度 0~1

    // 本段的起、终点(注意用星形序列 m_seq 索引顶点)
    const osg::Vec3d from = m_star[m_seq[seg]];
    const osg::Vec3d to   = m_star[m_seq[seg + 1]];

    // ① 位置:线性插值。u=0 在起点,u=1 到终点。
    const osg::Vec3d pos = from + (to - from) * u;

    // ② 朝向:机头方向 = 本段运动方向(水平单位向量)
    osg::Vec3d dir = to - from;
    dir.normalize();

    // ③ 构造旋转四元数:把模型本地 -Y 轴(cessna 机头朝向)
    //    旋转到 dir 上。(如果你的模型机头朝 +Y/+X,改这里第一个参数)
    osg::Quat rot;
    rot.makeRotate(osg::Vec3d(0, -1, 0), dir);

    // ④ 矩阵合成(OSG 后乘约定,作用于顶点 = 先转后移):
    //    rotate 在前 translate 在后 → 先绕自身原点对准机头,再平移到位。
    //    顺序写反会变成"绕世界原点公转",飞机姿态也会乱。
    m_airMt->setMatrix(osg::Matrix::rotate(rot) * osg::Matrix::translate(pos));
}
功能说明
SituationScene

构造函数,绘制五角星,设置飞行路线,创建网格、五角星航线、飞机模型位置。

updateTruth

五角星航线轨迹,用MainWindow主框架的定时器调用。

buildGrid

创建网格地面

buildStarRoute

五角星航线可视化(航线线 + 顶点标记)

buildAircraft

绘制飞机 + TRUTH 标签 + 垂直线

MainWindow

UI主框架,

复制代码
//mainwindow.h
// ============================================================
// MainWindow ------ 主窗口(过程1 版)
// 职责:① 加载/准备飞机模型  ② 组装界面  ③ 用 QTimer 维护仿真时钟,
//       每 33ms 推进一次并驱动 SituationScene。
// 增长计划:过程2 在 onTick 里加"雷达产生点迹"分支;过程3 加滤波。
// ============================================================
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTimer>
#include <osg/Group>
#include "OSGWidget.h"

class SituationScene;

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(const QString& modelFile = QString(),
                        QWidget* parent = nullptr);

private slots:
    void onTick();          // 仿真时钟节拍:推进时间 → 驱动场景
    void onPause(bool on);  // 暂停/继续

private:
    OSGWidget*       m_view   = nullptr;   // 3D 视图
    osg::ref_ptr<osg::Group> m_root;       // 场景根
    SituationScene*  m_scene  = nullptr;

    QTimer m_timer;
    double m_simTime = 0.0;              // 仿真时钟(秒)------ 与真实墙钟解耦,
    double m_dt      = 0.033;            // 每帧推进 33ms
};
#endif

//MainWindow.cpp
#include "mainwindow.h"
#include "situationscene.h"
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>
#include <osg/Geode>
#include <osg/ShapeDrawable>
#include <osg/MatrixTransform>
#include <osg/Matrix>
#include <osg/Material>
#include <osgDB/ReadFile>

// 没有模型文件时的兜底"飞机"(黄色十字盒,保证任何机器都能跑通)
static osg::ref_ptr<osg::Node> makePlaceholderAircraft()
{
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    osg::ref_ptr<osg::ShapeDrawable> body = new osg::ShapeDrawable(
        new osg::Box(osg::Vec3(0, 0, 0), 60.f, 260.f, 30.f));      // 机身
    body->setColor(osg::Vec4(0.95f, 0.95f, 0.35f, 1.f));
    osg::ref_ptr<osg::ShapeDrawable> wing = new osg::ShapeDrawable(
        new osg::Box(osg::Vec3(0, 0, 45.f), 340.f, 55.f, 18.f));   // 机翼
    wing->setColor(osg::Vec4(0.9f, 0.5f, 0.1f, 1.f));
    geode->addDrawable(body.get());
    geode->addDrawable(wing.get());
    // 占位件也要统一提亮(和真模型同一套材质逻辑)
    geode->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
    return geode;
}

MainWindow::MainWindow(const QString& modelFile, QWidget* parent)
    : QMainWindow(parent)
{
    // ============ ① 准备飞机模型 ============
    // 加载 → 打日志确认 → 归一化到半径 400m → 强制亮色材质
    osg::ref_ptr<osg::Node> plane;
    if (!modelFile.isEmpty())
        plane = osgDB::readNodeFile(modelFile.toStdString());   // 绝对路径
    if (!plane)
        plane = osgDB::readNodeFile("cessna.osg");              // 相对路径/OSG_FILE_PATH
    if (plane) {
        qDebug() << "[model] loaded, radius =" << plane->getBound().radius();
    } else {
        qWarning() << "[model] NOT loaded -> using placeholder";
        plane = makePlaceholderAircraft();
    }

    // 归一化缩放:不同模型大小差异巨大,统一缩到"包围球半径=400m"。
    // 公里级场景里真实尺寸的目标只有一个像素点,所以态势系统惯例
    // 是把模型【夸大显示】,真实感交给近距离观察时再说。
    {
        const double airmodel = 1000.0;
        const double s = airmodel / plane->getBound().radius();
        osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform;
        mt->setMatrix(osg::Matrix::scale(s, s, s));
        mt->addChild(plane.get());
        plane = mt;
    }

    // 强制统一亮色材质(OVERRIDE 压过模型内部自带的暗色材质)。
    // 知识点:StateSet 沿场景图继承,子节点显式设置会覆盖父节点;
    // 加 OVERRIDE 标志后父节点反过来强制覆盖整棵子树------不改模型
    // 文件就能统一外观,这是工程上处理外部模型的标准手法。
    {
        osg::ref_ptr<osg::Material> mat = new osg::Material;
        mat->setColorMode(osg::Material::OFF);
        mat->setAmbient (osg::Material::FRONT_AND_BACK, osg::Vec4(0.35f,0.35f,0.35f,1.f));
        mat->setDiffuse (osg::Material::FRONT_AND_BACK, osg::Vec4(0.85f,0.85f,0.88f,1.f));
        mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4(0.45f,0.45f,0.50f,1.f)); // 自发光保底亮度
        mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4(0.05f,0.05f,0.05f,1.f));
        osg::StateSet* ss = plane->getOrCreateStateSet();
        ss->setAttributeAndModes(mat.get(),
            osg::StateAttribute::OVERRIDE | osg::StateAttribute::ON);
        ss->setMode(GL_LIGHTING, osg::StateAttribute::OVERRIDE | osg::StateAttribute::ON);
    }

    // ============ ② 组装界面 ============
    QWidget* central = new QWidget(this);
    QVBoxLayout* lay = new QVBoxLayout(central);

    m_view = new OSGWidget;
    lay->addWidget(m_view, 1);          // 视图占满剩余空间

    QPushButton* pauseBtn = new QPushButton(QStringLiteral("暂停"));
    pauseBtn->setCheckable(true);
    connect(pauseBtn, &QPushButton::toggled, this, &MainWindow::onPause);
    lay->addWidget(pauseBtn, 0);        // 按钮固定高度

    setCentralWidget(central);

    // ============ ③ 场景与视图 ============
    m_root  = new osg::Group;
    m_scene = new SituationScene(m_root.get(), plane.get());
    m_view->setScene(m_root.get());
    m_view->setClip(10.0, 80000.0);     // 16km 场景:far 必须拉远,否则网格边缘被裁
    m_view->setBgColor(0.08f, 0.10f, 0.12f);

    // ============ ④ 仿真时钟 ============
    connect(&m_timer, &QTimer::timeout, this, &MainWindow::onTick);
    m_timer.start(33);                  // ~30Hz。渲染另由 OSGWidget 的 60Hz 定时器驱动
}

// 仿真节拍:唯一的职责是"推时钟 + 驱动场景"。
// 注意数据循环(30Hz)与渲染循环(60Hz)是两条独立的线,
// 渲染线程每帧画的都是当前最新的场景状态------这就是"数据与显示解耦"。
void MainWindow::onTick()
{
    m_simTime += m_dt;                  // 暂停时不进这里,时钟冻结
    m_scene->updateTruth(m_simTime);
}

void MainWindow::onPause(bool on)
{ on ? m_timer.stop() : m_timer.start(33); }
功能说明
MainWindow

构造函数,加载、绘制飞机模型,组装界面(OSG界面和暂停按钮),调用SituationScene和OSGWidget绘制场景与视图,设置定时器调用updateTruth更新飞机轨迹。

onTick

仿真时钟节拍,调用updateTruth更新飞机轨迹。

onPause

暂停按钮功能。

相关推荐
西飘客11 小时前
vs 怎么根据dll 生成lib文件
c++·qt
bkspiderx12 小时前
Qt 插件机制:动态扩展应用功能的核心框架
开发语言·qt·元数据·qt 插件·qpluginloader
半杯咖啡半行码15 小时前
Qt开发实战:数据库、MV 模式、QProcess与串口通信全攻略
数据库·qt
时间的拾荒人16 小时前
Qt 界面优化:绘图详解 —— 从基础图形到图片处理
开发语言·qt·面试
MayZork16 小时前
Qt 文件操作:QSaveFile 与 QTemporaryFile(含 QTemporaryDir)实战
开发语言·qt
半杯咖啡半行码19 小时前
Qt 开发基础:版本演进、常用工具与多平台安装
开发语言·qt
qq_401700411 天前
Qt Signal Mapper:一堆按钮,怎么共用一个槽函数
开发语言·qt
Python图像识别1 天前
35-【2027毕设】YOLO11中草药检测识别系统 - Python完整源码+PyQt5界面+训练模型+数据集
python·qt·课程设计