前言
【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
暂停按钮功能。