【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行并进行态势标绘(四)

前言

在上一篇【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行并显示航迹(三)-CSDN博客的基础上增加态势标绘功能。

效果图

增加了3个UI交互效果:地图上的圆形区域、扇形区域,以及右上角告警信息。

代码结构变化

文件 功能 新增内容
SituationCore.h 数据层 insideSector:检测目标是否在扇形区域内 insideCircleXY:检测目标是否在圆形区域内
SceneUtil.h 渲染工具库 circleFan(半透明扇形/圆盘)、circleLine(圆环轮廓线)、azimuthRuler(方位刻度盘) makeHudCamera:HUD告警面板相机 HudAlertPanel类:右上角告警列表 cjkFont:显示中文字体
SituationScene 3D态势图层 buildPlotting:标绘图层 updateZones:区域态势联动 setHudSize:窗口尺寸变化时由 MainWindow 转发 buildHud:HUD 告警面板
MainWindow 主框架 eventFilter:窗口大小一变,立刻把新尺寸转告 HUD告警面板,让它永远贴住右上角

SituationCore

在数据层增加态势标绘相关的告警区域(圆形区域和扇形区域)检测,这与电子围栏的概念比较相似(电子围栏更加体系化)。

复制代码
// ============================================================
// 区域包含判断
// 注意:全部在 xy 平面判断 ------ 雷达世界是二维的,高度 z 不参与。
// ============================================================

// 目标是否在扇形区域内(az0/az1 为弧度,数学约定 0°=东逆时针)
inline bool insideSector(const osg::Vec3d& p, double az0, double az1, double maxR)
{
    if (std::hypot(p.x(), p.y()) > maxR) return false;   // 距离条件
    double az = std::atan2(p.y(), p.x());                // (-π, π]
    if (az < 0) az += 2.0 * osg::PI;                     // 归一化到 [0, 2π)
    return az >= az0 && az <= az1;
}

// 目标是否在圆形区域内(圆心 cx,cy;比较用 xy 平面距离)
// ★ 若误用三维距离:飞机 z=800 会被算进去,导致边界附近误判
inline bool insideCircleXY(const osg::Vec3d& p, double cx, double cy, double radius)
{
    const double dx = p.x() - cx, dy = p.y() - cy;
    return dx * dx + dy * dy <= radius * radius;
}

SceneUtil

在渲染工具库中,增加渲染圆形和扇形区域等几项标绘工具,以及增加右上角的告警面板。

复制代码
// ============================================================
// 区域标绘工具
// circleFan:扇形/圆盘【填充面】(GL_TRIANGLE_FAN)
//   center 圆心(世界坐标)  radius 半径(米)
//   a0/a1  起止方位角(弧度,约定:0°=+X东、逆时针为正,与 atan2 一致)
//   segs   弧线细分段数(越大越圆滑)
// 知识点1 GL_TRIANGLE_FAN:第 0 个顶点是公共顶点(扇心),
//   (C,P0,P1)(C,P1,P2)... 依次生成三角形 ------ 天生适合扇形/圆盘,
//   n 个顶点出 n-2 个三角形,无重复顶点、无索引开销。
// 知识点2 半透明渲染三原则(见下方注释逐条对应)
// ============================================================
inline osg::Geometry* circleFan(const osg::Vec3& center, float radius,
                                float a0, float a1, int segs,
                                const osg::Vec4& color)
{
    osg::ref_ptr<osg::Geometry> g = new osg::Geometry;
    setupDynamic(g.get());   // 告警联动要改写颜色 → 必须动态几何

    // 顶点 = 扇心 + 弧上 segs+1 个点(a0 到 a1 首尾都含)
    osg::ref_ptr<osg::Vec3Array> v = new osg::Vec3Array;
    v->reserve(segs + 2);
    v->push_back(center);
    for (int i = 0; i <= segs; ++i) {
        const float a = a0 + (a1 - a0) * (float(i) / float(segs));
        v->push_back(center + osg::Vec3(radius * std::cos(a),
                                        radius * std::sin(a), 0.f));
    }
    g->setVertexArray(v.get());
    g->addPrimitiveSet(new osg::DrawArrays(GL_TRIANGLE_FAN, 0, v->size()));

    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);

    // ------ 半透明三原则 ------
    // ① 开混合。公式:结果 = 源色×α + 目标色×(1−α)
    //    α=0.22 → 22%的橙 + 78%的背景 = 一层"淡橙纱"
    ss->setMode(GL_BLEND, osg::StateAttribute::ON);
    ss->setAttributeAndModes(
        new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
    // ② 深度【测试】保留(不遮挡更近的已画物体),
    //    深度【写入】关闭 ------ 透明面若写深度,会挡住其后绘制的
    //    透明片元,混合顺序错乱(实验2会让你亲眼看到这个 bug)
    {
        osg::ref_ptr<osg::Depth> d = new osg::Depth(osg::Depth::LESS);
        d->setWriteMask(false);
        ss->setAttributeAndModes(d.get());
    }
    // ③ 登记进透明渲染队列:OSG 保证先画全部不透明物体,
    //    再按距离由远及近画透明物体 ------ 透明排序问题的标准解法
    ss->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
    return g.release();
}

// ------------------------------------------------------------
// circleLine:圆弧/圆环【轮廓线】
//   withCenter=false → GL_LINE_STRIP 纯弧线(a1−a0=2π 时视觉闭合为整圆)
//   withCenter=true  → GL_LINE_LOOP:顶点序列 [圆心, 弧...] 自动闭合,
//                      一条命令同时画出 弧线+两条半径边,正适合扇形轮廓
// 填充用半透明、轮廓用实线 ------ 这是标绘制图的标准配色惯例
// ------------------------------------------------------------
inline osg::Geometry* circleLine(const osg::Vec3& center, float radius,
                                 float a0, float a1, int segs,
                                 const osg::Vec4& color, float width,
                                 bool withCenter = false)
{
    osg::ref_ptr<osg::Geometry> g = new osg::Geometry;

    osg::ref_ptr<osg::Vec3Array> v = new osg::Vec3Array;
    if (withCenter) v->push_back(center);
    for (int i = 0; i <= segs; ++i) {
        const float a = a0 + (a1 - a0) * (float(i) / float(segs));
        v->push_back(center + osg::Vec3(radius * std::cos(a),
                                        radius * std::sin(a), 0.f));
    }
    g->setVertexArray(v.get());
    g->addPrimitiveSet(new osg::DrawArrays(
        withCenter ? GL_LINE_LOOP : GL_LINE_STRIP, 0, v->size()));

    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));
    return g.release();
}

// ------------------------------------------------------------
// azimuthRuler:方位刻度盘 ------ 刻度短线 + 外圈整圆 + 角度数字
// 标注沿圆周布点:还是圆参数方程(本工程第三次用它:
//   starVertex 算航线 → RadarSim 算真值 → 这里摆标注)
// 度数用数学约定(0°=东、逆时针)。真实雷达屏常用"0°=北、顺时针",
// 两套约定换算:radarDeg = (90 − mathDeg + 360) % 360
// ------------------------------------------------------------
inline osg::Geode* azimuthRuler(double r0, double r1, double rLab, float z,
                                const osg::Vec4& color
                                = osg::Vec4(0.45f, 0.50f, 0.58f, 1.f))
{
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;

    // a) 刻度短线:每 30° 一条(GL_LINES:24 顶点 = 12 条线)
    {
        osg::ref_ptr<osg::Geometry> g = new osg::Geometry;
        osg::ref_ptr<osg::Vec3Array> v = new osg::Vec3Array;
        for (int d = 0; d < 360; d += 30) {
            const double a = d * osg::PI / 180.0;        // 度 → 弧度
            v->push_back(osg::Vec3(float(r0*std::cos(a)), float(r0*std::sin(a)), z));
            v->push_back(osg::Vec3(float(r1*std::cos(a)), float(r1*std::sin(a)), z));
        }
        g->setVertexArray(v.get());
        g->addPrimitiveSet(new osg::DrawArrays(GL_LINES, 0, v->size()));
        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(1.5f));
        geode->addDrawable(g.get());
    }
    // b) 外圈整圆(复用 circleLine:0 → 2π)
    geode->addDrawable(circleLine(osg::Vec3(0, 0, z), float(r1),
                                  0.f, float(2.0 * osg::PI), 72, color, 1.5f));
    // c) 角度数字标注(Text 也是 Drawable,可多个共存一个 Geode)
    for (int d = 0; d < 360; d += 30) {
        const double a = d * osg::PI / 180.0;
        char buf[8];
        std::snprintf(buf, sizeof buf, "%d", d);
        osg::ref_ptr<osgText::Text> t =
            makeLabel(12.f, color, osgText::TextBase::CENTER_CENTER);
        t->setText(buf);
        t->setPosition(osg::Vec3(float(rLab * std::cos(a)),
                                 float(rLab * std::sin(a)), z));
        geode->addDrawable(t.get());
    }
    return geode.release();
}

// 中文字体查找:按优先级返回第一个存在的字体文件
// 顺序有讲究:simhei 是纯 ttf(最稳),msyh.ttc 是字体集合
// (FreeType 取 face 0,绝大多数 OSG 版本可读,但放在第二位)
inline const std::string& cjkFont()
{
    static std::string s_font;
    if (s_font.empty()) {
        const char* candidates[] = {
            "C:/Windows/Fonts/simhei.ttf",   // 黑体
            "C:/Windows/Fonts/msyh.ttc",     // 微软雅黑
            "C:/Windows/Fonts/simsun.ttc",   // 宋体
            "simhei.ttf", "msyh.ttc"         // 拷进工程目录的兜底
        };
        for (const char* f : candidates)
            if (osgDB::fileExists(f)) { s_font = f; break; }
        if (s_font.empty()) s_font = "fonts/arial.ttf";  // 全无→退回(无中文)
    }
    return s_font;
}


inline osgText::Text* makeLabel(float pixelSize, const osg::Vec4& color,
    osgText::TextBase::AlignmentType align = osgText::TextBase::LEFT_BOTTOM)
{
    //t->setFont("fonts/arial.ttf");//这是英文的方法
    t->setFont(cjkFont());//改成中文的方法
    ... ...
}

// ------------------------------------------------------------
// makeHudCamera:HUD 告警面板相机。所谓 HUD,就是无论三维场景怎么变,
// 它都按【屏幕像素坐标】显示的二维子场景(战斗机抬头显示的由来)。
// 五要素缺一即翻车(症状见括号):
// ① 正交投影 ortho2D(0,w,0,h):1 单位 = 1 像素(不定则坐标错乱)
// ② ABSOLUTE_RF 绝对参考帧:不受任何父变换影响(不定则跟着场景跑)
// ③ 视图矩阵 identity:不受主相机漫游影响(不定则转个视角就飞)
// ④ 只清深度缓冲:不清颜色(清了会把三维场景整个擦掉!)
// ⑤ POST_RENDER 最后绘制:永远盖在三维画面上(不定则被场景盖住)
// ------------------------------------------------------------
inline osg::Camera* makeHudCamera()
{
    osg::ref_ptr<osg::Camera> cam = new osg::Camera;
    cam->setProjectionMatrix(osg::Matrix::ortho2D(0.0, 1280.0, 0.0, 800.0)); // 初值,setSize 修正
    cam->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
    cam->setViewMatrix(osg::Matrix::identity());
    cam->setClearMask(GL_DEPTH_BUFFER_BIT);
    cam->setRenderOrder(osg::Camera::POST_RENDER);
    cam->setAllowEventFocus(false);            // HUD 不抢鼠标键盘
    return cam.release();
}

// ------------------------------------------------------------
// HudAlertPanel:右上角告警列表。
//   固定 N 个 Text 槽位 + 队列:新告警插到最上,旧行逐行下移,
//   超过 N 条丢弃最老 ------ 点迹 FIFO 的同一思想,"容器是文字行"。
//   槽位数量固定、内容每条改写 ------ 速度矢量"定长改写"模式的复用:
//   不 new/delete Text 对象,只在既有对象上 setText/setColor。
// ------------------------------------------------------------
class HudAlertPanel
{
public:
    explicit HudAlertPanel(int maxLines = 6)
        : m_maxLines(maxLines)
    {
        m_cam = makeHudCamera();
        m_anchor = new osg::MatrixTransform;    // 右上角锚点

        // 面板尺寸:宽按最长文案估(约28字符×6.5px),加字段要同步加宽 W
        const float W = 232.f;
        const float H = 18.f * (maxLines + 1) + 16.f;   // 标题+N行+上下留白
        const float Z = -0.1f;                  // 与文字错开(深度已关,双保险)

        // ① 背板(bin 8,最先画 = 垫底)。四边形用过程4的 TRIANGLE_FAN:4点=2三角
        {
            osg::ref_ptr<osg::Geometry> bg = new osg::Geometry;
            osg::ref_ptr<osg::Vec3Array> v = new osg::Vec3Array;
            v->push_back(osg::Vec3(-W, 10.f - H, Z));   // 左下(原点=右上角锚点)
            v->push_back(osg::Vec3(  6, 10.f - H, Z));  // 右下
            v->push_back(osg::Vec3(  6,      10.f, Z)); // 右上
            v->push_back(osg::Vec3(-W,      10.f, Z));  // 左上
            bg->setVertexArray(v.get());
            bg->addPrimitiveSet(new osg::DrawArrays(GL_TRIANGLE_FAN, 0, 4));
            osg::ref_ptr<osg::Vec4Array> c = new osg::Vec4Array;
            //c->push_back(osg::Vec4(0.04f, 0.06f, 0.09f, 0.78f)); // 深蓝黑 78%不透明
            // ① 背板颜色:近黑 → 浅灰(α 提到 0.92,面板感更实)
            c->push_back(osg::Vec4(0.88f, 0.90f, 0.93f, 0.92f));   // 原 (0.04,0.06,0.09,0.78)
            bg->setColorArray(c.get(), osg::Array::BIND_OVERALL);
            osg::StateSet* ss = bg->getOrCreateStateSet();
            ss->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
            ss->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
            ss->setMode(GL_BLEND, osg::StateAttribute::ON);
            ss->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
            ss->setRenderBinDetails(8, "RenderBin");    // ★ 决定先画
            osg::ref_ptr<osg::Geode> g = new osg::Geode;
            g->addDrawable(bg.get());
            m_anchor->addChild(g.get());
        }
        // ② 边框(bin 9)
        {
            osg::ref_ptr<osg::Geometry> bd = new osg::Geometry;
            osg::ref_ptr<osg::Vec3Array> v = new osg::Vec3Array;
            v->push_back(osg::Vec3(-W, 10.f - H, Z));
            v->push_back(osg::Vec3(  6, 10.f - H, Z));
            v->push_back(osg::Vec3(  6,      10.f, Z));
            v->push_back(osg::Vec3(-W,      10.f, Z));
            bd->setVertexArray(v.get());
            bd->addPrimitiveSet(new osg::DrawArrays(GL_LINE_LOOP, 0, 4));
            osg::ref_ptr<osg::Vec4Array> c = new osg::Vec4Array;
            //c->push_back(osg::Vec4(0.45f, 0.60f, 0.80f, 0.9f));
            // ② 边框颜色:亮蓝灰 → 深蓝灰(浅底上亮边框会"融化")
            c->push_back(osg::Vec4(0.30f, 0.42f, 0.58f, 1.f));     // 原 (0.45,0.60,0.80,0.9)
            bd->setColorArray(c.get(), osg::Array::BIND_OVERALL);
            osg::StateSet* ss = bd->getOrCreateStateSet();
            ss->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
            ss->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
            ss->setAttributeAndModes(new osg::LineWidth(1.5f));
            ss->setRenderBinDetails(9, "RenderBin");
            osg::ref_ptr<osg::Geode> g = new osg::Geode;
            g->addDrawable(bd.get());
            m_anchor->addChild(g.get());
        }

        // ③ 文字(默认 bin 10,最后画 = 最上层),每条都加深色描边
        osg::ref_ptr<osg::Geode> textGeode = new osg::Geode;
        //m_title = su::makeLabel(13.f, osg::Vec4(0.85f, 0.92f, 1.f, 1.f),
        //                        osgText::TextBase::RIGHT_TOP);
        // 改后:红色标题
        m_title = su::makeLabel(13.f, osg::Vec4(0.85f, 0.08f, 0.08f, 1.f),
                                osgText::TextBase::RIGHT_TOP);
        m_title->setText("==告警信息==", osgText::String::ENCODING_UTF8);

        hudTextPost(m_title.get());
        textGeode->addDrawable(m_title.get());

        m_slots.reserve(maxLines);
        for (int i = 0; i < maxLines; ++i) {
            osg::ref_ptr<osgText::Text> t =
                su::makeLabel(12.f, osg::Vec4(1, 1, 1, 1),
                              osgText::TextBase::RIGHT_TOP);
            t->setText("");
            t->setPosition(osg::Vec3(0.f, -float(i + 1) * 18.f, 0.f));
            hudTextPost(t.get());
            textGeode->addDrawable(t.get());
            m_slots.push_back(t);
        }
        m_anchor->addChild(textGeode.get());

        m_cam->addChild(m_anchor.get());
        setSize(1280, 800);
    }

    osg::Camera* camera() const { return m_cam.get(); }

    void setSize(int w, int h)   // 与 v1 相同
    {
        m_cam->setProjectionMatrix(osg::Matrix::ortho2D(0.0, double(w), 0.0, double(h)));
        m_cam->setViewport(0, 0, w, h);
        m_anchor->setMatrix(osg::Matrix::translate(double(w) - 24.0, double(h) - 40.0, 0));
    }

    void push(const std::string& msg, const osg::Vec4& color)   // 与 v1 相同
    {
        m_items.insert(m_items.begin(), Item{msg, color});
        if (int(m_items.size()) > m_maxLines) m_items.pop_back();
        refresh();
    }

private:
    struct Item { std::string msg; osg::Vec4 color; };

    void refresh()   // 与 v1 相同
    {
        for (int i = 0; i < m_maxLines; ++i) {
            if (i < int(m_items.size())) {
                //m_slots[i]->setText(m_items[i].msg);
                m_slots[i]->setText(m_items[i].msg, osgText::String::ENCODING_UTF8);
                m_slots[i]->setColor(m_items[i].color);
            } else m_slots[i]->setText("");
        }
    }

    // osgText 自带描边:沿字形画一圈深色轮廓,字在任何背景上都有对比度
    static void hudTextPost(osgText::Text* t)
    {
        t->setBackdropType(osgText::Text::NONE);   // 原 OUTLINE
//        t->setBackdropType(osgText::Text::OUTLINE);
//        t->setBackdropColor(osg::Vec4(0.f, 0.f, 0.f, 0.9f));
//        t->setBackdropOffset(1.2f);
    }

    osg::ref_ptr<osg::Camera>                m_cam;
    osg::ref_ptr<osg::MatrixTransform>       m_anchor;
    osg::ref_ptr<osgText::Text>              m_title;
    std::vector<osg::ref_ptr<osgText::Text>> m_slots;
    std::vector<Item>                        m_items;
    int                                      m_maxLines;
};

SituationScene

在3D态势层,增加了buildPlotting(标绘图层)、updateZones(区域态势联动)、setHudSize(窗口尺寸变化时由 MainWindow 转发)buildHud(HUD 告警面板)。

复制代码
// ============================================================
// 过程4新增:标绘区参数(文件级常量)
// 这些是"操作员手绘标注"性质的注释数据,归视图层所有;
// 若将来区域要联网下发/被多视图共享,再升级进 SituationCore。
// 方位角约定:0°=+X(东)、逆时针为正,与 atan2(y,x) 一致。
// ============================================================
static const double kWarnAz0 = 20.0 * osg::PI / 180.0;  // 警戒扇区起止方位
static const double kWarnAz1 = 70.0 * osg::PI / 180.0;
static const double kWarnR   = 7000.0;                  // 半径(米)
static const double kNoFlyX  = 0.0;                     // 禁飞区圆心
static const double kNoFlyY  = -1900.0;                 // (航线弦 2→4 恰好穿过)
static const double kNoFlyR  = 1200.0;

static const osg::Vec4 kWarnBase (1.0f, 0.45f, 0.10f, 0.22f); // 常态:淡橙纱
static const osg::Vec4 kNoFlyBase(1.0f, 0.10f, 0.10f, 0.25f); // 常态:淡红纱
static const osg::Vec4 kZoneHot  (1.0f, 0.08f, 0.05f, 0.60f); // 告警:浓红
static const osg::Vec4 kAlertIn (0.78f, 0.05f, 0.05f, 1.f);  // 入界:深红(原亮红)
static const osg::Vec4 kAlertOut(0.62f, 0.33f, 0.00f, 1.f);  // 越界:深橙(原亮橙)

SituationScene::SituationScene(osg::Group* root, osg::Node* model)
    : m_root(root)
{
    ...
    buildHud();      // ★ HUD 告警面板
    ...
}

// ------------------------------------------------------------
// 图层5:标绘★
// ------------------------------------------------------------
void SituationScene::buildPlotting()
{
    const float z = 0.15f;  // 标绘层抬高15cm:与 z=0 网格错开,防深度冲突闪烁

    // ① 方位刻度盘(纯静态,也是全场景的"方位尺子")
    m_root->addChild(su::azimuthRuler(8800.0, 9200.0, 9700.0, z));

    // ② 警戒扇区:半透明填充 + 实线轮廓(含两条半径边)+ 标签
    m_warnFan = su::circleFan(osg::Vec3(0, 0, z), float(kWarnR),
                              float(kWarnAz0), float(kWarnAz1), 48, kWarnBase);
    m_root->addChild(m_warnFan.get());

    m_warnBorder = su::circleLine(osg::Vec3(0, 0, z), float(kWarnR),
                                  float(kWarnAz0), float(kWarnAz1), 48,
                                  osg::Vec4(1.f, 0.55f, 0.15f, 1.f), 2.f, true);
    m_root->addChild(m_warnBorder.get());

    m_warnLabel = su::makeLabel(13.f, osg::Vec4(1.f, 0.65f, 0.35f, 1.f),
                                osgText::TextBase::CENTER_CENTER);
    m_warnLabel->setText("WARN ZONE");
    // 位置放在扇区中轴上:方位45°、半径4300 → 4300×(cos45°, sin45°)
    m_warnLabel->setPosition(osg::Vec3(3040.f, 3040.f, z + 0.4f));
    m_root->addChild(su::labelGeode(m_warnLabel.get()));

    // ③ 禁飞区:整圆填充 + 整圆轮廓 + 标签
    m_noFlyFan = su::circleFan(osg::Vec3(float(kNoFlyX), float(kNoFlyY), z),
                               float(kNoFlyR), 0.f, float(2.0 * osg::PI),
                               48, kNoFlyBase);
    m_root->addChild(m_noFlyFan.get());

    m_noFlyBorder = su::circleLine(osg::Vec3(float(kNoFlyX), float(kNoFlyY), z),
                                   float(kNoFlyR), 0.f, float(2.0 * osg::PI),
                                   48, osg::Vec4(1.f, 0.30f, 0.30f, 1.f), 2.f);
    m_root->addChild(m_noFlyBorder.get());

    m_noFlyLabel = su::makeLabel(13.f, osg::Vec4(1.f, 0.55f, 0.55f, 1.f),
                                 osgText::TextBase::CENTER_CENTER);
    m_noFlyLabel->setText("NO-FLY");
    m_noFlyLabel->setPosition(osg::Vec3(float(kNoFlyX), float(kNoFlyY), z + 0.4f));
    m_root->addChild(su::labelGeode(m_noFlyLabel.get()));
}

// ------------------------------------------------------------
// 区域态势联动+告警事件
// 判断在数据层(insideSector/insideCircleXY),视觉切换在视图层。
// 每帧都查(30Hz)------飞机位置每帧都变,区域判断必须同步。
// ------------------------------------------------------------
void SituationScene::updateZones(const osg::Vec3d& p, double t)
{
    const bool inWarn  = situ::insideSector(p, kWarnAz0, kWarnAz1, kWarnR);
    const bool inNoFly = situ::insideCircleXY(p, kNoFlyX, kNoFlyY, kNoFlyR);

    // ============ 过程4.5:边沿检测 → 告警事件 ============
    // 电平 vs 边沿:若把 inNoFly 每帧直接上报,是【电平触发】------
    // 飞机在区内待 2 秒会重复上报 60 次"我还在里面";
    // 与上一帧状态比较、只在跳变瞬间上报,是【边沿触发】------
    // 一次进入恰好一条 ENTER、一次离开恰好一条 EXIT。
    // 告警/事件系统的通用准则:给操作员看的信息必须走边沿。
    char buf[96];
    if (inNoFly != m_inNoFly) {
        std::snprintf(buf, sizeof buf, "[%5.1fs] T01 %s NO-FLY",
                      t, inNoFly ? "ENTER" : "EXIT");
        m_hud.push(buf, inNoFly ? kAlertIn : kAlertOut);
    }
    if (inWarn != m_inWarn) {
        std::snprintf(buf, sizeof buf, "[%5.1fs] T01 %s WARN-ZONE",
                      t, inWarn ? "ENTER" : "EXIT");
        m_hud.push(buf, inWarn ? kAlertIn : kAlertOut);
    }

    // 视觉联动(过程4原逻辑,不动)
    setZoneActive(m_warnFan.get(),  kWarnBase,  inWarn);
    setZoneActive(m_noFlyFan.get(), kNoFlyBase, inNoFly);
    m_warnLabel->setText(inWarn ? "! WARN ZONE" : "WARN ZONE");
    m_warnLabel->setColor(inWarn ? osg::Vec4(1.f, 0.25f, 0.15f, 1.f)
                                 : osg::Vec4(1.f, 0.65f, 0.35f, 1.f));
    m_noFlyLabel->setText(inNoFly ? "! NO-FLY" : "NO-FLY");
    m_noFlyLabel->setColor(inNoFly ? osg::Vec4(1.f, 0.25f, 0.15f, 1.f)
                                   : osg::Vec4(1.f, 0.55f, 0.55f, 1.f));

    m_inWarn  = inWarn;    // 本帧状态存为"上一帧",供下一帧比较
    m_inNoFly = inNoFly;
}

// ------------------------------------------------------------
// HUD 告警面板:屏幕右上角
// 注意它是挂在 m_root 下的独立相机(POST_RENDER),与三维场景
// 平行渲染 ------ 不是场景物体,所以放最后 build
// ------------------------------------------------------------
void SituationScene::buildHud()
{
    m_root->addChild(m_hud.camera());
}

void SituationScene::setHudSize(int w, int h)
{ m_hud.setSize(w, h); }

MainWindow

eventFilter:窗口大小一变,立刻把新尺寸转告 HUD告警面板,让它永远贴住右上角。

复制代码
// ------------------------------------------------------------
// Qt 事件过滤器:不改 OSGWidget 一行源码,就能"截听"它的事件。
// 返回 false = 处理完继续把事件交给目标控件(必须!返回 true
// 会吞掉事件,OSGWidget 自己就收不到 Resize 了)。
// ------------------------------------------------------------
bool MainWindow::eventFilter(QObject* obj, QEvent* ev)
{
    if (obj == m_view && ev->type() == QEvent::Resize) {
        const QSize s  = m_view->size();
        const qreal dpr = m_view->devicePixelRatioF();  // OSG 视口用物理像素
        m_scene->setHudSize(int(s.width() * dpr), int(s.height() * dpr));
    }
    return QMainWindow::eventFilter(obj, ev);
}

源码

源码已上传到本篇资源处。

相关推荐
橙橙笔记4 小时前
QT安卓软件的制作
android·git·qt·软件
芦芭荞5 小时前
QT使用xml模版生成word文档
xml·qt·word
mengzhi啊5 小时前
qt把结构体数据存入json文件
qt·json
kaixin_learn_qt_ing6 小时前
写入二进制文件(Qt)
qt
尘中远8 小时前
QWT7 的科学 Colormap 色彩评估:QwtColorMapPreset 22 种预设的感知均匀性与场景化选
qt·qwt·科学绘图·科学配色
宿州派大星13 小时前
Python实战:PyQt5 + OpenCV + Dlib 打造AI桌面应用
人工智能·python·qt
长沙红胖子Qt1 天前
Qt+TiePieHS3开发笔记(二):HS3发送器发出内置波形、直连示波器,示波器检测内置波形Demo
qt·示波器·tiepiehs3·发生器·同步发射和示波器采集
陈年老古董1 天前
PyQt5 入门指南:环境搭建、PyCharm 外部工具配置与登录界面实战
开发语言·qt·pycharm
zhanghaha13141 天前
Qt 类完整例程【逐词拆解 + 逐步骤执行保姆教程】
开发语言·qt