OpenSceneGraph:与Qt框架集成



引言


一方面,OpenSceneGraph引擎本身具有高级子系统,用于管理窗口,处理用户输入事件,发送和接收用户消息。 我们在本系列的前几篇文章中对此进行了详细讨论。 通常,再加上C ++ / STL的功能,这对于开发任意复杂的应用程序已经足够了。

将OSG集成到QtDesigner开发的应用程序中的示例。 该示例将在下面详细讨论。


另一方面,为了加快C ++的开发速度,使用了两个第三方库来扩展该语言的功能(例如boost),还使用了整个框架,这些框架使开发具有广泛功能目的的跨平台应用程序变得容易和容易。 一种这样的框架是非常流行的Qt。 不管他们如何批评Qt的元对象编译器以及其他缺点和不便之处,Qt的优势都在于它可以解决跨平台开发中所有可能的任务的扩展类库,以及实现类之间消息传递子系统的“信号-插槽”的概念。 信号和时隙还基于应用程序与操作系统之间的交互方法以及进程间通信。

而且,将两种技术结合起来将非常有趣:Qt和OSG。 我的团队不得不解决一个类似的问题,我已经在我的一篇出版物中写过。 但是,我想进一步扩大这个问题,本文将讨论这个主题。

集成OSG和Qt有两种选择:

  1. 使用Qt信号和插槽与OSG应用程序中的对象进行交互
  2. 将OSG查看器集成到用C ++ / Qt开发的图形界面中,包括使用QtDesigner表单设计器

第一个选项适用于不需要使用Qt提供的GUI元素,但您想确保应用程序组件通过信号和插槽进行交互的情况。 例如,我提出了使用Qt通过TCP套接字将OSG应用程序与进程间通信库集成的需求。

当需要OSG引擎集成和使用Qt开发的图形应用程序时,需要第二个选项。 信号和插槽可供我们使用,除此以外,Qt还提供了标准化的GUI元素的整个范围。

1. OSG窗口系统中的Qt信号


第一个示例有些综合:我们编写一个具有原始场景的简单OSG应用程序; 创建两个类,其中一个将处理击键,另一个类向控制台显示有关按下哪个键的消息。 在这种情况下,处理程序将生成Qt信号,并将有关按键的消息作为参数。

为了与Qt集成,满足以下三个条件即可

  1. 从QObject继承交互类
  2. 组织信号处理循环
  3. 创建在应用程序操作期间内存中存在的QApplication(或QCoreApplication)类的实例

完整的示例代码可以在我的OSG课程存储库中找到 ,该存储库包含该周期的所有课程。

首先,创建一个将“扭曲”信号处理队列的类。

qt-events.h

#ifndef QT_EVENTS_H #define QT_EVENTS_H #include <osgGA/GUIEventHandler> #include <QCoreApplication> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class QtEventsHandler : public osgGA::GUIEventHandler { public: QtEventsHandler(); virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: }; #endif // QT_EVENTS_H 

qt-events.cpp

 #include "qt-events.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ QtEventsHandler::QtEventsHandler() { } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ bool QtEventsHandler::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { Q_UNUSED(aa) switch (ea.getEventType()) { case osgGA::GUIEventAdapter::FRAME: { // Process qt signals and event QCoreApplication::processEvents(QEventLoop::AllEvents); break; } default: break; } return false; } 

一切都非常简单-此类是标准的OSG事件处理程序,在渲染帧时,可通过调用来启动Qt信号队列处理

 QCoreApplication::processEvents(QEventLoop::AllEvents); 

现在,让我们使用OSG内置的机制再次创建一个处理键盘的类,但同时能够发送Qt信号。 为此,我们将使用最近经过分析的多重继承

密钥处理程序

 #ifndef KEY_HANDLER_H #define KEY_HANDLER_H #include <osgGA/GUIEventHandler> #include <QObject> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class KeyboardHandler : public QObject, public osgGA::GUIEventHandler { Q_OBJECT public: KeyboardHandler(QObject *parent = Q_NULLPTR) : QObject(parent) , osgGA::GUIEventHandler () { } bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { switch (ea.getEventType()) { case osgGA::GUIEventAdapter::KEYDOWN: emit sendMessage("Pressed key " + QString(ea.getKey())); break; default: break; } return false; } signals: void sendMessage(QString msg); private: }; #endif // KEY_HANDLER_H 

该类将处理有关已按下键的消息,并发送包含已按下键代码的消息。 该信号将由未以任何方式与OSG连接的类接收,该类是QObject的后代,并且包含一个将消息打印到标准输出的单个插槽

接收器

 #ifndef RECEIVER_H #define RECEIVER_H #include <QObject> #include <iostream> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class Receiver : public QObject { Q_OBJECT public: Receiver(QObject *parent = Q_NULLPTR) : QObject(parent) {} public slots: void printMessage(QString msg) { std::cout << msg.toStdString() << std::endl; } }; #endif // RECEIVER_H 

现在,通过编写OSG应用程序将它们放在一起

主文件

 #ifndef MAIN_H #define MAIN_H #include <osgViewer/Viewer> #include <osgDB/ReadFile> #include <QCoreApplication> #include "qt-events.h" #include "keyhandler.h" #include "receiver.h" #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); osg::ref_ptr<osg::Node> scene = osgDB::readNodeFile("../data/cessnafire.osg"); osgViewer::Viewer viewer; viewer.setSceneData(scene.get()); viewer.addEventHandler(new QtEventsHandler); viewer.setUpViewInWindow(0, 0, 1024, 768); KeyboardHandler *keyboardHandler = new KeyboardHandler; Receiver *receiver = new Receiver; QObject::connect(keyboardHandler, &KeyboardHandler::sendMessage, receiver, &Receiver::printMessage); viewer.addEventHandler(keyboardHandler); return viewer.run(); } 

首先,我们创建QCoreApplication类的实例。

 QCoreApplication app(argc, argv); 

对于所描述的技术来说,这是必需的。 但是,我们不会调用QCoreApplication :: exec()方法 ! 相反,信号处理循环将在osgViewer :: Viewer :: run()循环内旋转,为此我们创建并注册相应的处理程序

 viewer.addEventHandler(new QtEventsHandler); 

我们创建将通过Qt信号交互的类的实例,将一个的信号与另一个的插槽连接

 KeyboardHandler *keyboardHandler = new KeyboardHandler; Receiver *receiver = new Receiver; QObject::connect(keyboardHandler, &KeyboardHandler::sendMessage, receiver, &Receiver::printMessage); 

注册键盘处理程序

 viewer.addEventHandler(keyboardHandler); 

全部运行查看器

 return viewer.run(); 

看到这样的照片


是的,该示例有些人为设计,但它说明了使用Qt机制将代码集成到使用OSG的应用程序中的主要原理。 这个想法来自OpenSceneGraph 3. Cookbook ,为我和我的开发团队节省了大量时间和精力,使我可以使用基于Qt的模块,该模块已在我们的代码库中的代码库中进行了调试和标准化。

但是,如果我们仍然想在Qt GUI应用程序中使用OSG,该怎么办?

2. osgQt库


osgQt是一个用于以下目的的集成库:

  1. 在Qt上开发的应用程序的图形界面中嵌入在OSG上实现的3D场景
  2. 在OSG场景内的3D几何图形表面上嵌入Qt小部件。 是的,您没听错-Qt小部件可以在虚拟世界中安静地工作。 总有一天我会示范

该库存在某些问题,我们通过仔细研究与该库相连的示例并阅读已经提到的OpenSceneGraph 3 ,设法解决了这些问题

库应该被组装,并且该过程类似于引擎本身的组装,这将在本周期的第一篇文章中进行详细描述。 唯一要注意的是,应选择-DCMAKE_INSTALL_PREFIX来选择与构建引擎时指定的名称相同的名称-因此os​​gQt将安装在引擎旁边,并且在开发过程中将很方便使用。

3.将osgViewer :: Viewer集成到Qt GUI中


以下示例将非常有用。 我们将编写一个查看器,使您可以使用标准Qt控件加载* .osg格式的模型。 此外,要开发图形界面,我们使用QtDeisgner。

让我们创建一个新项目,例如“ Qt Widgets Application”



在这种情况下,将使用菜单栏,工具栏和状态栏生成主应用程序窗口。 在QtDesigner中,将QFrame组件添加到此窗口。



我们将OSG查看器放在此框架中。 OSG查看器本质上将是Qt小部件,对于其实现,我们将编写QViewerWidget类。 我将全部资料放在扰流板上,以免使代码片段模糊演示文稿。

qviewerwidget.h
 #ifndef QVIEWER_WIDGET_H #define QVIEWER_WIDGET_H #include <QWidget> #include <osgViewer/Viewer> #include <osgQt/GraphicsWindowQt> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class QViewerWidget : public QWidget { public: QViewerWidget(const QRect &geometry); virtual ~QViewerWidget(); osg::Group *getScene(); osgViewer::Viewer *getViewer(); protected: osg::ref_ptr<osg::Group> scene; osgViewer::Viewer viewer; private: osgQt::GraphicsWindowQt *createGraphicsWindow(const QRect &geometry); void initCamera(const QRect &geometry); void paintEvent(QPaintEvent *); }; #endif // QVIEWER_WIDGET_H 


qviewerwidget.cpp
 include "qviewerwidget.h" #include <osgViewer/ViewerEventHandlers> #include <osgGA/TrackballManipulator> #include <QGridLayout> //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ QViewerWidget::QViewerWidget(const QRect &geometry) : QWidget() , scene(new osg::Group) { initCamera(geometry); viewer.setSceneData(scene); viewer.addEventHandler(new osgViewer::StatsHandler); viewer.setCameraManipulator(new osgGA::TrackballManipulator); viewer.setThreadingModel(osgViewer::Viewer::SingleThreaded); osgQt::GraphicsWindowQt *gw = static_cast<osgQt::GraphicsWindowQt *>(viewer.getCamera()->getGraphicsContext()); QGridLayout *layout = new QGridLayout; if (layout != Q_NULLPTR) { layout->addWidget(gw->getGLWidget()); this->setLayout(layout); } } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ QViewerWidget::~QViewerWidget() { } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osg::Group *QViewerWidget::getScene() { return scene.get(); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osgViewer::Viewer *QViewerWidget::getViewer() { return &viewer; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osgQt::GraphicsWindowQt *QViewerWidget::createGraphicsWindow(const QRect &geometry) { osg::DisplaySettings *ds = osg::DisplaySettings::instance().get(); osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits; traits->windowName = ""; traits->windowDecoration = false; traits->x = geometry.x(); traits->y = geometry.y(); traits->width = geometry.width(); traits->height = geometry.height(); if (traits->height == 0) traits->height = 1; traits->doubleBuffer = true; traits->alpha = ds->getMinimumNumAlphaBits(); traits->stencil = ds->getMinimumNumStencilBits(); traits->sampleBuffers = ds->getMultiSamples(); traits->samples = ds->getNumMultiSamples(); return new osgQt::GraphicsWindowQt(traits.get()); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void QViewerWidget::initCamera(const QRect &geometry) { osg::Camera *camera = viewer.getCamera(); osg::ref_ptr<osgQt::GraphicsWindowQt> gw = createGraphicsWindow(geometry); gw->setTouchEventsEnabled(true); camera->setGraphicsContext(gw.get()); const osg::GraphicsContext::Traits *traits = gw->getTraits(); camera->setClearColor(osg::Vec4(0.7f, 0.7f, 0.7f, 1.0f)); camera->setViewport(0, 0, traits->width, traits->height); double aspect = static_cast<double>(traits->width) / static_cast<double>(traits->height); camera->setProjectionMatrixAsPerspective(30.0, aspect, 1.0, 1000.0); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void QViewerWidget::paintEvent(QPaintEvent *) { viewer.frame(); } 


实现的主要思想是使用osgQt :: GraphicsWindow类,该类基于QGLWidget类创建图形窗口。 若要创建此窗口,请使用方法

 osgQt::GraphicsWindowQt *QViewerWidget::createGraphicsWindow(const QRect &geometry) { osg::DisplaySettings *ds = osg::DisplaySettings::instance().get(); osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits; traits->windowName = ""; traits->windowDecoration = false; traits->x = geometry.x(); traits->y = geometry.y(); traits->width = geometry.width(); traits->height = geometry.height(); if (traits->height == 0) traits->height = 1; traits->doubleBuffer = true; traits->alpha = ds->getMinimumNumAlphaBits(); traits->stencil = ds->getMinimumNumStencilBits(); traits->sampleBuffers = ds->getMultiSamples(); traits->samples = ds->getNumMultiSamples(); return new osgQt::GraphicsWindowQt(traits.get()); } 

根据传输到输入的几何参数以及OSG 3D渲染的所需设置来配置窗口。 返回的指针是应该传递给相机的OSG图形上下文。 因此,下一步是初始化相机

 void QViewerWidget::initCamera(const QRect &geometry) { osg::Camera *camera = viewer.getCamera(); osg::ref_ptr<osgQt::GraphicsWindowQt> gw = createGraphicsWindow(geometry); gw->setTouchEventsEnabled(true); camera->setGraphicsContext(gw.get()); const osg::GraphicsContext::Traits *traits = gw->getTraits(); camera->setClearColor(osg::Vec4(0.7f, 0.7f, 0.7f, 1.0f)); camera->setViewport(0, 0, traits->width, traits->height); double aspect = static_cast<double>(traits->width) / static_cast<double>(traits->height); camera->setProjectionMatrixAsPerspective(30.0, aspect, 1.0, 1000.0); } 

实际通话

 camera->setGraphicsContext(gw.get()); 

并将与QGLWidget小部件关联的所需上下文传递给相机。 我们将整个小部件例程放入类构造函数中

 QViewerWidget::QViewerWidget(const QRect &geometry) : QWidget() , scene(new osg::Group) { initCamera(geometry); viewer.setSceneData(scene); viewer.addEventHandler(new osgViewer::StatsHandler); viewer.setCameraManipulator(new osgGA::TrackballManipulator); viewer.setThreadingModel(osgViewer::Viewer::SingleThreaded); osgQt::GraphicsWindowQt *gw = static_cast<osgQt::GraphicsWindowQt *>(viewer.getCamera()->getGraphicsContext()); QGridLayout *layout = new QGridLayout; if (layout != Q_NULLPTR) { layout->addWidget(gw->getGLWidget()); this->setLayout(layout); } } 

在这里,我们配置查看器并特别注意挑战

 viewer.setThreadingModel(osgViewer::Viewer::SingleThreaded); 

将查看器切换到单线程模式。 这是将OSG集成到Qt中时的必要措施,因为在某些Linux发行版中,默认情况下使用OSG使用的多线程渲染时,程序将崩溃。 这样做的原因需要单独汇报,因此请继续注意此代码

 osgQt::GraphicsWindowQt *gw = static_cast<osgQt::GraphicsWindowQt *>(viewer.getCamera()->getGraphicsContext()); QGridLayout *layout = new QGridLayout; if (layout != Q_NULLPTR) { layout->addWidget(gw->getGLWidget()); this->setLayout(layout); } 

在其中我们创建了一个图层,该图层干扰了从相机的图形上下文返回的QGLWidget,并转换为osgQt :: GraphicsWindows指针。 我们通过调用将创建的图层添加到我们的小部件QViewerWidget中

 this->setLayout(layout); 

为了使我们的窗口小部件以及更新窗口时要更新的场景,您需要重新定义QPaintEvent事件处理程序

 void QViewerWidget::paintEvent(QPaintEvent *) { viewer.frame(); } 

在其中我们通过调用osgViewer :: Viewer :: frame()方法来启动框架的渲染。

好的,小部件的代码已经准备好,现在我们将其嵌入到窗体上的框架中。 为此,在MainWindow类的构造函数中,我们编写了这样的代码

 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , qviewer(Q_NULLPTR) { ui->setupUi(this); QGridLayout *layout = new QGridLayout; qviewer = new QViewerWidget(QRect(0, 0, ui->frame->width(), ui->frame->height())); layout->addWidget(qviewer); ui->frame->setLayout(layout); this->setCentralWidget(ui->frame); connect(&timer, &QTimer::timeout, this, &MainWindow::update); timer.start(40); connect(ui->actionQuit, &QAction::triggered, this, &MainWindow::quit); connect(ui->actionClean, &QAction::triggered, this, &MainWindow::clean); connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::open); this->setWindowTitle("QViewerWidget example"); } 

或者更确切地说,我们仍然对此部分感兴趣

 QGridLayout *layout = new QGridLayout; qviewer = new QViewerWidget(QRect(0, 0, ui->frame->width(), ui->frame->height())); layout->addWidget(qviewer); ui->frame->setLayout(layout); this->setCentralWidget(ui->frame); 

在这里我们创建一个图层,创建尺寸等于框架大小的窗口小部件,将创建的窗口小部件添加到该图层,然后将该图层附加到框架。 并且,为了避免打扰此示例中的布局,我们将框架拉伸到窗口的整个工作区,使其成为中央窗口小部件。

要进行渲染,您应该安排按计时器定期更新窗口。 为此,创建一个间隔为40毫秒(每秒25帧)的计时器,并将其超时信号与窗口更新插槽关联。 我使用Qt5语法这样做

 connect(&timer, &QTimer::timeout, this, &MainWindow::update); timer.start(40); 

以前已经以这种方式为窗口类定义了更新槽

 void MainWindow::update() { QMainWindow::update(this->geometry()); } 

为什么这样,因为您可以按照大多数osgQt示例所示的相同方式将计时器信号与QMainWindow :: update插槽直接关联

 connect(&timer, SIGNAL(timeout), this, SLOT(update)); 

事实是,不赞成使用SIGNAL()和SLOT()宏的语法,并且由于期望过渡到Qt6,必须将其丢弃。 同时,QMainWindow类没有参数的update()插槽没有重载,这将在编译期间导致绑定调用错误。 为此,我必须定义不带参数的update()插槽,在其中调用基本QMainWindow :: update(),并在其中传递窗口的客户区。

添加到这个地方并运行程序,我们将得到一定的结果



通过按“ S”,我们可以激活OSG统计监视器,并确保我们的小部件能够正常工作,并绘制一个空白场景。

什么样的统计监控器?
为了不使文章过载,我将在此处进行介绍。 OSG具有内置的监视器,可实时显示引擎统计信息。 要将其添加到查看器中,我们包括头文件

 #include <osgViewer/ViewerEventHandlers> 

并向查看器添加一个处理程序

 viewer.addEventHandler(new osgViewer::StatsHandler); 

然后随时按“ S”以显示很多有用的信息。

4.完成我们的查看器:添加菜单


在表单设计器中,我们使用“面向鼠标”编程来自定义菜单(对此我并不在意,但是是的,有时很方便)。 最后我们会得到这样的东西



现在,我们将启动适当的处理器插槽,使您可以沿从对话框中选择的路径加载模型,清除场景并退出应用程序

菜单处理程序代码
 //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void MainWindow::open() { osg::Group *scene = qviewer->getScene(); if (scene == nullptr) return; QString path = QFileDialog::getOpenFileName(Q_NULLPTR, tr("Open model file"), "./", tr("OpenSceneGraph (*.osg *.osgt *.osgb *.ivi)")); if (path.isEmpty()) return; scene->removeChildren(0, scene->getNumChildren()); osg::ref_ptr<osg::Node> model = osgDB::readNodeFile(path.toStdString()); scene->addChild(model.get()); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void MainWindow::clean() { osg::Group *scene = qviewer->getScene(); if (scene == nullptr) return; scene->removeChildren(0, scene->getNumChildren()); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ void MainWindow::quit() { QApplication::quit(); } 


之后,我们将获得非常方便的* .osg格式模型的查看器。



本文开头的视频中展示了他的作品的演示。 此示例的完整源代码在此处。

结论


如我们所见,OSG和Qt的集成在理解或实现方面并不是特别困难。 这对于为技术可视化以及可能的游戏创建跨平台应用程序提供了很大的帮助。

本文打开了OSG系列的续篇,它将概述复杂的开发技术。 我认为她成功了。 谢谢,再见!

Source: https://habr.com/ru/post/zh-CN438654/


All Articles