OpenSceneGraph: Integration with the Qt framework

  • Tutorial


Introduction


On the one hand, the OpenSceneGraph engine itself has an advanced window management, user input event handling, user message sending and receiving subsystem. We talked about this in some detail in previous articles in this series. In general, in sum with the capabilities of C ++ / STL, this is quite enough for the development of arbitrarily complex applications.

An example of integrating OSG into an application developed in QtDesigner. This example will be discussed in detail below.


On the other hand, to speed up C ++ development, both third-party libraries are used, which extend the capabilities of this language (like boost), as well as whole frameworks, which make it possible to develop cross-platform applications of a wide functional purpose easily and naturally. One such framework is the ultra popular Qt. No matter how much Qt is blamed for its meta-object compiler and other shortcomings and inconveniences, Qt is powerful in an extensive class library that solves all conceivable tasks of cross-platform development, as well as in the "signals-slots" concept that implements the messaging subsystem between classes. Signals and slots are also based on how the application interacts with the operating system, as well as interprocess communication.

And, damn it, it would be very interesting to combine two technologies: Qt and OSG. A similar task had to be solved by my team, which I already wrote in one of my publications . However, I would like to reveal this question a little wider, and this article will be just about this topic.

There are two ways to integrate OSG and Qt:

  1. Using Qt Signals and Slots to Interact Objects within an OSG Application
  2. Integration of the OSG viewer in a graphical user interface developed in C ++ / Qt, including the use of the QtDesigner form designer

The first option is applicable when the use of GUI elements provided by Qt is not required, but it is required to ensure the interaction of application components by means of signals and slots. For example, I had such a need to integrate an OSG application with an interprocess communication library via TCP sockets using Qt.

The second option is necessary when the integration of the OSG engine and a graphical application developed using Qt is required. Signals and slots are becoming available to us, and besides them, the whole range of standardized GUI elements provided by Qt.

1. Qt signals in the OSG window system


The first example will be somewhat synthetic: we will write a simple OSG application with a primitive scene; We will create two classes, one of which will handle keystrokes, and the other will display in the console a message about which key is pressed. In this case, the handler will generate a Qt signal, with a message about the key pressed as a parameter.

To integrate with Qt, it is sufficient to satisfy the following three conditions.

  1. Inherit QObject Interactive Classes
  2. Arrange a signal processing loop
  3. Create an instance of the QApplication class (or QCoreApplication) that exists in memory during the operation of the application

The full code of the example can be seen here , in my OSG-lessons repository , where all the lessons for this cycle are collected.

To begin, create a class that will "turn" the signal processing queue

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

Everything is very simple - this class is a standard OSG event handler that, when a frame is drawn, initiates Qt's signal queue processing

QCoreApplication::processEvents(QEventLoop::AllEvents);

Now we will create a class that processes the keyboard, again, using the mechanism built into OSG, but still capable of sending a Qt signal. To do this, we will apply the anathema committed recently to the multiple inheritance of

keyhandler.h

#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

The class will process the keystroke message and send a signal with a message containing the key code. This signal will be received by a class that is not related in any way to OSG, which is a QObject successor and contains a single slot that prints a message to the standard output stream

receiver.h

#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

Now let's put everything together by writing an OSG application

main.h

#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();
}

First, we create an instance of the QCoreApplication class.

QCoreApplication app(argc, argv);

This is necessary for the work of the described technology. However, we will not call the QCoreApplication :: exec () method ! Instead, the signal processing cycle will be rotated inside the osgViewer :: Viewer :: run () loop, for which we create and register the corresponding handler.

viewer.addEventHandler(new QtEventsHandler);

Create instances of classes that will interact through Qt signals, associating the signal of one with the slot of another

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

Register the keyboard handler

viewer.addEventHandler(keyboardHandler);

All, run the viewer


return viewer.run();

and see such a picture


Yes, the example is somewhat contrived, but it illustrates the main principles of integrating code using Qt mechanisms into an application using OSG. This idea, taken from the book OpenSceneGraph 3. Cookbook , saved me and my development team a lot of time and nerves by allowing us to use a Qt-based module that was debugged and standardized within our codebase.

What if we want to use OSG inside a Qt GUI application?

2. The osgQt library


osgQt is an integration library intended for:

  1. Встраивания трехмерной сцены, реализованной на OSG в графический интерфейс приложения, разрабатываемого на Qt
  2. Встраивания виджетов Qt на поверхности трехмерной геометрии внутри сцены OSG. Да, вы не ослышались — виджеты Qt могут преспокойно работать внутри виртуального мира. Когда-нибудь я обязательно это продемонстрирую

There were certain problems with this library that were overcome by carefully studying the examples attached to it and reading the already mentioned OpenSceneGraph 3. Cookbook The

library should be compiled, and this process is similar to the engine assembly itself, described in detail in the very first article of the cycle . The only note is that -DCMAKE_INSTALL_PREFIX should be chosen the same as was specified when building the engine - so osgQt will be installed next to the engine, and it will be convenient to use it during development.

3. Integrating osgViewer :: Viewer into Qt GUI


The following example will be quite useful. We will write a viewer that allows you to load * .osg format models using standard Qt controls. And for development of the graphic interface we use QtDeisgner.

Let's create a new project like “Qt Widgets Application”.



This will generate the main application window with a menu preset, toolbar and status bar. In QtDesigner, add a QFrame component to this window. In this frame we place an OSG viewer. The OSG viewer will essentially be a Qt widget; to implement it, we will write the QViewerWidget class. Full source code will put on the spoiler, so as not to zamylativ statement sheets of code





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


The main idea of ​​the implementation is to use the osgQt :: GraphicsWindow class, which creates a graphic window based on the QGLWidget class. To create this window is the method


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

The window is configured in accordance with the geometry parameters transmitted to the input and the required settings of the three-dimensional OSG render. The returned pointer is the OSG graphics context that should be passed to the camera. Therefore, the next step is to initialize the camera.


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

Actually call


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

and passes the camera the required context associated with the QGLWidget widget. The entire routine for creating a widget is placed in the class constructor.


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

Here we set up a viewer and pay special attention to the challenge.


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

switching viewer in single-threaded mode. This is a necessary measure when integrating OSG into Qt, since in some Linux distributions the program will fall out into a segfolt when using multi-threaded rendering, which OSG uses by default. The reasons for this require separate debriefing, so go ahead and pay attention to this code.


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

in which we create a layer, interrupting the QGLWidget in it, returned from the camera's graphic context, converted to the pointer osgQt :: GraphicsWindows. The created layer is added to our QViewerWidget widget by calling


this->setLayout(layout);

In order for our widget, and with it, the scene to be updated when the window is updated, you need to redefine the QPaintEvent event handler.


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

in which we initiate frame rendering by calling the osgViewer :: Viewer :: frame () method.

Ok, the code of our widget is ready, now we build it into the frame located on the form. To do this, in the constructor of the class MainWindow we write such code


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

or rather, we are still interested in this part of it


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

where we create a layer, create our widget with dimensions equal to the frame size, add the created widget to the layer, and attach the layer to the frame. And, in order not to bother in this example with the layout, we stretch the frame to the entire client area of ​​the window, designating it as the central widget.

To perform rendering, you should organize a periodic update of the window by timer. To do this, we create a timer with an interval of 40 milliseconds (25 frames per second) and associate its timeout signal with the window update slot. I do it like this using Qt5 syntax


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

pre-defining the update slot for the window class in this way


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

Why so, because you can directly link the timer signal to the QMainWindow :: update slot in the way shown in most osgQt usage examples.

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

The fact is that the syntax with the SIGNAL () and SLOT () macros has been declared obsolete, and should be abandoned in anticipation of the transition to Qt6. At the same time, the QMainWindow class does not overload the update () slot without parameters, which will cause an error on the binding call when compiling. To do this, I had to define my update () slot without parameters, calling the base QMainWindow :: update () in it, passing the window client area to it.

Having added this program to this place and running the program, we will get a certain result.



Pressing “S” we can activate the OSG statistics monitor and make sure that our widget works as it should, by drawing an empty scene.

What kind of statistics monitor?
In order not to overload the article I will write about it here. OSG has a built-in monitor that displays engine statistics in real time. To add it to the viewer, connect the header file


#include    <osgViewer/ViewerEventHandlers>

and add a handler to the viewer


viewer.addEventHandler(new osgViewer::StatsHandler);

then at any time by pressing "S" display a lot of useful information.

4. Finish our viewer: add menu


In the form designer, we customize the menu by applying “mouse-oriented” programming (to which I am indifferent, but yes, it is sometimes convenient). In the end, we will get something like this. Now we will create the appropriate slot handlers to load the model along the path selected from the dialog box, clear the scene and exit the application.





Menu Handler Code

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
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();
}


After that we will get a very convenient model viewer * .osg.



Demonstration of his work is shown in the video at the beginning of the article. Full source code for this example is available here.

Conclusion


As we have seen, the integration of OSG and Qt is not particularly difficult either in understanding or in implementation. This is an excellent tool for creating cross-platform applications for technical visualization, and possibly games.

This article opens the continuation of the OSG cycle, where complex development techniques will be presented. I think she was successful. Thank you for your attention and see you soon!

Also popular now: