前言
在上一篇【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);
}
源码
源码已上传到本篇资源处。