
Présentation
D'une part, le moteur OpenSceneGraph lui-même dispose d'un sous-système avancé pour la gestion des fenêtres, le traitement des événements d'entrée utilisateur, l'envoi et la réception de messages utilisateur. Nous en avons parlé en détail dans les articles précédents de cette série. En général, avec les capacités de C ++ / STL, cela suffit pour développer des applications arbitrairement complexes.
Un exemple d'intégration d'OSG dans une application développée par QtDesigner. Cet exemple sera discuté en détail ci-dessous.D'un autre côté, pour accélérer le développement en C ++, des bibliothèques tierces sont utilisées pour étendre les capacités de ce langage (comme boost), ainsi que des cadres entiers qui facilitent et développent facilement des applications multiplateformes à large objectif fonctionnel. Un tel cadre est le Qt ultra populaire. Peu importe la façon dont ils ont réprimandé Qt pour son compilateur de méta-objets et d'autres inconvénients et inconvénients, la puissance de Qt réside dans une bibliothèque de classes étendue qui résout toutes les tâches imaginables de développement multiplateforme, ainsi que dans le concept de "signaux - emplacements" qui implémente le sous-système de messagerie entre les classes. Les signaux et les créneaux sont également basés sur les méthodes d'interaction entre l'application et le système d'exploitation, ainsi que sur la communication interprocessus.
Et bon sang, il serait très intéressant de combiner deux technologies: Qt et OSG. Mon équipe a dû résoudre un problème similaire, dont j'ai déjà parlé dans
une de mes publications . Cependant, je voudrais ouvrir cette question un peu plus loin, et cet article sera sur ce sujet.
Il existe deux options pour intégrer OSG et Qt:
- Utilisation de signaux Qt et d'emplacements pour interagir des objets dans une application OSG
- Intégration de la visionneuse OSG dans une interface graphique développée en C ++ / Qt, y compris l'utilisation du concepteur de formulaires QtDesigner
La première option est applicable lorsque vous n'avez pas besoin d'utiliser les éléments GUI fournis par Qt, mais que vous souhaitez assurer l'interaction des composants d'application via les signaux et les slots. Par exemple, un tel besoin est apparu pour moi d'intégrer une application OSG avec une bibliothèque de communication interprocessus sur des sockets TCP à l'aide de Qt.
La deuxième option est nécessaire lorsque l'intégration du moteur OSG et une application graphique développée à l'aide de Qt sont requises. Les signaux et les emplacements deviennent disponibles pour nous, et en plus d'eux, toute la gamme d'éléments GUI standardisés fournis par Qt.
1. Signaux Qt dans un système de fenêtres OSG
Le premier exemple sera quelque peu synthétique: nous écrivons une application OSG simple avec une scène primitive; créez deux classes, dont l'une gèrera les frappes, et l'autre pour envoyer un message à la console sur la touche sur laquelle vous appuyez. Dans ce cas, le gestionnaire va générer un signal Qt, avec un message sur la touche enfoncée comme paramètre.
Pour s'intégrer à Qt, il suffit de remplir les trois conditions suivantes
- Hériter des classes interactives de QObject
- Organiser une boucle de traitement du signal
- Créez une instance de la classe QApplication (ou QCoreApplication) qui existe en mémoire pendant le fonctionnement de l'application
L'exemple de code complet peut être vu
ici dans
mon référentiel de leçons OSG , qui contient toutes les leçons de ce cycle.
Tout d'abord, créez une classe qui "déformera" la file d'attente de traitement du signal.
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"
Tout est extrêmement simple - cette classe est un gestionnaire d'événements OSG standard qui, lors du rendu d'une trame, lance le traitement de la file d'attente de signaux Qt en appelant
QCoreApplication::processEvents(QEventLoop::AllEvents);
Créons maintenant une classe qui traite le clavier, encore une fois, en utilisant le mécanisme intégré à OSG, mais en même temps capable d'envoyer un signal Qt. Pour ce faire, nous utiliserons l'héritage multiple récemment anathématisé
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
La classe traitera un message sur une touche enfoncée et enverra un signal avec un message contenant le code de la touche enfoncée. Ce signal sera reçu par une classe qui n'est en aucun cas connectée à OSG, qui est un descendant de QObject et contient un seul emplacement qui imprime un message sur la sortie standard
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
Maintenant, assemblez le tout en écrivant une application OSG
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"
Tout d'abord, nous créons une instance de la classe QCoreApplication.
QCoreApplication app(argc, argv);
Cela est nécessaire pour que la technologie décrite fonctionne. Cependant, nous
n'appellerons pas la méthode QCoreApplication :: exec () ! Au lieu de cela, la boucle de traitement du signal tournera à l'intérieur de notre boucle osgViewer :: Viewer :: run (), pour laquelle nous créons et enregistrons le gestionnaire correspondant
viewer.addEventHandler(new QtEventsHandler);
Nous créons des instances de classes qui interagiront via des signaux Qt, en connectant le signal de l'une avec la fente de l'autre
KeyboardHandler *keyboardHandler = new KeyboardHandler; Receiver *receiver = new Receiver; QObject::connect(keyboardHandler, &KeyboardHandler::sendMessage, receiver, &Receiver::printMessage);
Enregistrer le gestionnaire de clavier
viewer.addEventHandler(keyboardHandler);
Tous exécutent la visionneuse
return viewer.run();
et voir une telle image
Oui, l'exemple est quelque peu artificiel, mais il illustre les principes principaux de l'intégration de code à l'aide des mécanismes Qt dans une application utilisant OSG. Cette idée, tirée du
livre de recettes OpenSceneGraph 3. , m'a fait gagner beaucoup de temps et de nerfs, ainsi qu'à mon équipe de développement, me permettant d'utiliser un module basé sur Qt qui a été débogué et standardisé dans notre base de code dans notre base de code.
Mais que se passe-t-il si nous voulons toujours utiliser OSG dans une application graphique Qt?
2. Bibliothèque osgQt
osgQt est une bibliothèque d'intégration conçue pour:
- Intégration d'une scène 3D implémentée sur OSG dans l'interface graphique d'une application développée sur Qt
- Incorporation de widgets Qt sur une surface de géométrie 3D à l'intérieur d'une scène OSG. Oui, vous avez bien entendu - les widgets Qt peuvent fonctionner tranquillement dans le monde virtuel. Un jour, je vais certainement le démontrer
Il y avait certains problèmes avec cette bibliothèque, que nous avons réussi à surmonter en étudiant attentivement les exemples qui y sont attachés et en lisant le
livre de recettes OpenSceneGraph 3. déjà mentionné
.La bibliothèque doit être assemblée, et ce processus est similaire à l'assemblage du moteur lui-même, décrit en détail dans le tout
premier article du cycle . La seule remarque est que -DCMAKE_INSTALL_PREFIX doit être sélectionné avec le même qui a été spécifié lors de la construction du moteur - donc osgQt sera installé à côté du moteur, et il sera pratique à utiliser pendant le développement.
3. Intégration d'osgViewer :: Viewer dans l'interface graphique de Qt
L'exemple suivant sera très utile. Nous allons écrire une visionneuse qui vous permet de charger des modèles au format * .osg en utilisant des contrôles Qt standard. De plus, pour développer une interface graphique, nous utilisons QtDeisgner.
Créons un nouveau projet comme «Qt Widgets Application»

Dans ce cas, la fenêtre principale de l'application sera générée avec une barre de menus, une barre d'outils et une barre d'état. Dans QtDesigner, ajoutez un composant QFrame à cette fenêtre.

Nous placerons la visionneuse OSG dans ce cadre. Le visualiseur OSG sera essentiellement un widget Qt, pour son implémentation nous écrirons la classe QViewerWidget. Je vais mettre les sources complètes sur le spoiler, afin de ne pas brouiller la présentation avec des feuilles de 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(); }
L'idée principale de l'implémentation est d'utiliser la classe osgQt :: GraphicsWindow, qui crée une fenêtre graphique basée sur la classe QGLWidget. Pour créer cette fenêtre, utilisez la méthode
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()); }
La fenêtre est configurée en fonction des paramètres de géométrie transmis à l'entrée et des paramètres requis du rendu 3D OSG. Le pointeur renvoyé est le contexte graphique OSG qui doit être transmis à la caméra. Par conséquent, l'étape suivante consiste à initialiser la caméra
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); }
L'appel réel
camera->setGraphicsContext(gw.get());
et transmet le contexte souhaité associé au widget QGLWidget à la caméra. Nous mettons toute la routine widget dans le constructeur de classe
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); } }
Ici, nous configurons la visionneuse et prêtons une attention particulière au défi
viewer.setThreadingModel(osgViewer::Viewer::SingleThreaded);
Basculer la visionneuse en mode monothread. C'est une mesure nécessaire lors de l'intégration d'OSG dans Qt, car sur certaines distributions Linux, le programme plantera lors de l'utilisation du rendu multi-thread utilisé par OSG par défaut. Les raisons de cela nécessitent un compte rendu séparé, alors allez-y et faites attention à ce 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); }
dans lequel nous créons une couche, interférant avec un QGLWidget renvoyé du contexte graphique de la caméra, converti en un pointeur osgQt :: GraphicsWindows. Nous ajoutons la couche créée à notre widget QViewerWidget en appelant
this->setLayout(layout);
Pour que notre widget, et avec lui, la scène soit mise à jour lorsque la fenêtre est mise à jour, vous devez redéfinir le gestionnaire d'événements QPaintEvent
void QViewerWidget::paintEvent(QPaintEvent *) { viewer.frame(); }
dans lequel nous initialisons le rendu du cadre en appelant la méthode osgViewer :: Viewer :: frame ().
Ok, le code de notre widget est prêt, maintenant nous l'intégrons dans un cadre situé sur le formulaire. Pour ce faire, dans le constructeur de la classe MainWindow, nous écrivons un tel 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"); }
ou plutôt, nous sommes toujours intéressés par cette partie
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);
où nous créons un calque, créons notre widget, avec des dimensions égales à la taille du cadre, ajoutez le widget créé au calque et attachez le calque au cadre. Et, pour ne pas déranger la mise en page dans cet exemple, nous étendons le cadre à toute la zone client de la fenêtre, ce qui en fait le widget central.
Pour effectuer le rendu, vous devez organiser une mise à jour périodique de la fenêtre par minuterie. Pour ce faire, créez un temporisateur avec un intervalle de 40 millisecondes (25 images par seconde) et associez son signal de temporisation au créneau de mise à jour de fenêtre. Je le fais de cette façon en utilisant la syntaxe Qt5
connect(&timer, &QTimer::timeout, this, &MainWindow::update); timer.start(40);
ayant préalablement défini l'emplacement de mise à jour pour la classe de fenêtre de cette manière
void MainWindow::update() { QMainWindow::update(this->geometry()); }
Pourquoi, car vous pouvez associer directement le signal de la minuterie à l'emplacement QMainWindow :: update de la même manière que la plupart des exemples osgQt le montrent
connect(&timer, SIGNAL(timeout), this, SLOT(update));
Le fait est que la syntaxe avec les macros SIGNAL () et SLOT () est obsolète, et en prévision de la transition vers Qt6, elle doit être abandonnée. Dans le même temps, la classe QMainWindow n'a pas de surcharge de l'emplacement update () sans paramètres, ce qui provoquera une erreur sur l'appel de liaison lors de la compilation. Pour ce faire, j'ai dû définir mon emplacement update () sans paramètres, en y appelant la base QMainWindow :: update (), en y passant la zone client de la fenêtre.
En ajoutant à cet endroit et en exécutant le programme, nous obtiendrons un certain résultat

En appuyant sur "S", nous pouvons activer le moniteur de statistiques OSG et nous assurer que notre widget fonctionne comme il se doit, dessinant une scène vide.
Quel type de moniteur de statistiques?Afin de ne pas surcharger l'article, j'écrirai à ce sujet ici. OSG a un moniteur intégré qui affiche les statistiques du moteur en temps réel. Pour l'ajouter à la visionneuse, nous incluons le fichier d'en-tête
#include <osgViewer/ViewerEventHandlers>
et ajouter un gestionnaire à la visionneuse
viewer.addEventHandler(new osgViewer::StatsHandler);
puis à tout moment en appuyant sur "S" pour afficher de nombreuses informations utiles.
4. Terminez notre visionneuse: ajoutez un menu
Dans le concepteur de formulaires, nous personnalisons le menu en utilisant une programmation «orientée souris» (à laquelle je suis indifférent, mais oui, parfois c'est pratique). À la fin, nous aurons quelque chose comme ça

Nous allons maintenant démarrer les emplacements de processeur appropriés, vous permettant de charger le modèle le long du chemin sélectionné dans la boîte de dialogue, d'effacer la scène et de quitter l'application
Code du gestionnaire de menus Après cela, nous aurons une visionneuse très pratique des modèles au format * .osg.

Une démonstration de son travail est montrée dans la vidéo au début de l'article. Le code source complet de cet exemple est
disponible ici.Conclusion
Comme nous l'avons vu, l'intégration d'OSG et de Qt n'est pas particulièrement difficile à comprendre ou à mettre en œuvre. Il s'agit d'une aide précieuse pour la création d'applications multiplateformes pour la visualisation technique, et éventuellement des jeux.
Cet article ouvre une suite de la série OSG, qui présentera les techniques de développement complexes. Je pense qu'elle a réussi. Merci et à bientôt!