OpenSceneGraph: Integración con Qt Framework



Introduccion


Por un lado, el motor OpenSceneGraph tiene un subsistema avanzado para administrar ventanas, procesar eventos de entrada de usuario, enviar y recibir mensajes de usuario. Hablamos de esto con cierto detalle en artículos anteriores de esta serie. En general, junto con las capacidades de C ++ / STL, esto es suficiente para desarrollar aplicaciones arbitrariamente complejas.

Un ejemplo de integración de OSG en una aplicación desarrollada por QtDesigner. Este ejemplo se discutirá en detalle a continuación.


Por otro lado, para acelerar el desarrollo en C ++, se utilizan ambas bibliotecas de terceros que amplían las capacidades de este lenguaje (como boost), y los marcos completos le permiten desarrollar fácil y naturalmente aplicaciones multiplataforma de amplio propósito funcional. Uno de esos marcos es el ultra popular Qt. No importa cómo regañaron a Qt por su compilador de metaobjetos y otras deficiencias e inconvenientes, el poder de Qt está en una extensa biblioteca de clases que resuelve todas las tareas concebibles de desarrollo multiplataforma, así como en el concepto de "señales - espacios" que implementa el subsistema de mensajería entre clases. Las señales y las ranuras también se basan en los métodos de interacción entre la aplicación y el sistema operativo, así como en la comunicación entre procesos.

Y demonios, sería muy interesante combinar dos tecnologías: Qt y OSG. Mi equipo tuvo que resolver un problema similar, sobre el cual ya escribí en una de mis publicaciones . Sin embargo, me gustaría abrir esta pregunta un poco más, y este artículo será sobre este tema.

Hay dos opciones para integrar OSG y Qt:

  1. Uso de señales Qt y ranuras para interactuar objetos dentro de una aplicación OSG
  2. Integración del visor OSG en una interfaz gráfica desarrollada en C ++ / Qt, incluido el uso del diseñador de formularios QtDesigner

La primera opción es aplicable cuando no necesita utilizar los elementos GUI proporcionados por Qt, pero desea garantizar la interacción de los componentes de la aplicación a través de señales y ranuras. Por ejemplo, tal necesidad surgió para mí para integrar una aplicación OSG con una biblioteca de comunicación entre procesos a través de sockets TCP utilizando Qt.

La segunda opción es necesaria cuando se requiere la integración del motor OSG y una aplicación gráfica desarrollada usando Qt. Las señales y las ranuras están disponibles para nosotros, y además de ellas, toda la gama de elementos GUI estandarizados proporcionados por Qt.

1. Señales Qt en un sistema de ventanas OSG


El primer ejemplo será algo sintético: escribimos una aplicación OSG simple con una escena primitiva; cree dos clases, una de las cuales manejará las pulsaciones de teclas y la otra para mostrar un mensaje a la consola sobre qué tecla se presiona. En este caso, el controlador generará una señal Qt, con un mensaje sobre la tecla presionada como parámetro.

Para integrarse con Qt, es suficiente satisfacer las siguientes tres condiciones

  1. Heredar clases interactivas de QObject
  2. Organice un bucle de procesamiento de señal
  3. Cree una instancia de la clase QApplication (o QCoreApplication) que existe en la memoria durante la operación de la aplicación

El código de ejemplo completo se puede ver aquí en mi repositorio de lecciones OSG , que contiene todas las lecciones para este ciclo.

Primero, cree una clase que "tuerza" la cola de procesamiento de señal.

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

Todo es extremadamente simple: esta clase es un controlador de eventos OSG estándar que, al representar una trama, inicia el procesamiento de la cola de señal Qt llamando

 QCoreApplication::processEvents(QEventLoop::AllEvents); 

Ahora creemos una clase que procese el teclado, nuevamente, usando el mecanismo incorporado en OSG, pero al mismo tiempo capaz de enviar una señal Qt. Para hacer esto, utilizaremos la herencia múltiple anatematizada recientemente

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 clase procesará un mensaje sobre una tecla presionada y enviará una señal con un mensaje que contiene el código de la tecla presionada. Esta señal será recibida por una clase que no está conectada de ninguna manera con OSG, que es un descendiente de QObject y contiene una sola ranura que imprime un mensaje a la salida estándar

receptor.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 

Ahora ponlo todo junto escribiendo una aplicación 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" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ 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(); } 

Primero, creamos una instancia de la clase QCoreApplication.

 QCoreApplication app(argc, argv); 

Esto es necesario para que la tecnología descrita funcione. Sin embargo, ¡ no llamaremos al método QCoreApplication :: exec () ! En cambio, el bucle de procesamiento de señal girará dentro de nuestro bucle osgViewer :: Viewer :: run (), para el cual creamos y registramos el controlador correspondiente

 viewer.addEventHandler(new QtEventsHandler); 

Creamos instancias de clases que interactuarán a través de señales Qt, conectando la señal de una con la ranura de la otra.

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

Registre el controlador de teclado

 viewer.addEventHandler(keyboardHandler); 

Todos corren el visor

 return viewer.run(); 

y ver esa foto


Sí, el ejemplo es un tanto artificial, pero ilustra los principios principales de la integración de código utilizando mecanismos Qt en una aplicación que utiliza OSG. Esta idea, obtenida del OpenSceneGraph 3. Cookbook , me ahorró mucho tiempo y nervios a mí y a mi equipo de desarrollo, lo que me permitió usar un módulo basado en Qt que fue depurado y estandarizado dentro de nuestra base de código en nuestra base de código.

Pero, ¿qué pasa si todavía queremos usar OSG dentro de una aplicación Qt GUI?

2. biblioteca osgQt


osgQt es una biblioteca de integración diseñada para:

  1. Incrustar una escena 3D implementada en OSG en la interfaz gráfica de una aplicación desarrollada en Qt
  2. Incrustar widgets Qt en una superficie de geometría 3D dentro de una escena OSG. Sí, has oído bien: los widgets Qt pueden funcionar silenciosamente dentro del mundo virtual. Algún día definitivamente lo demostraré

Hubo ciertos problemas con esta biblioteca, que logramos superar estudiando cuidadosamente los ejemplos adjuntos y leyendo el ya mencionado OpenSceneGraph 3. Cookbook

La biblioteca debe ensamblarse, y este proceso es similar al ensamblaje del motor en sí, descrito en detalle en el primer artículo del ciclo . La única observación es que -DCMAKE_INSTALL_PREFIX debe seleccionarse con el mismo que se especificó al construir el motor, por lo que osgQt se instalará al lado del motor y será conveniente usarlo durante el desarrollo.

3. Integración de osgViewer :: Viewer en la interfaz gráfica de usuario de Qt


El siguiente ejemplo será bastante útil. Escribiremos un visor que le permita cargar modelos en formato * .osg usando controles Qt estándar. Además, para desarrollar una interfaz gráfica, utilizamos QtDeisgner.

Creemos un nuevo proyecto como "Aplicación Qt Widgets"



En este caso, la ventana principal de la aplicación se generará con una barra de menú, barra de herramientas y barra de estado. En QtDesigner, agregue un componente QFrame a esta ventana.



Colocaremos el visor OSG en este marco. El visor OSG será esencialmente un widget Qt, para su implementación escribiremos la clase QViewerWidget. Pondré las fuentes completas en el spoiler, para no difuminar la presentación con hojas de código

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


La idea principal de la implementación es usar la clase osgQt :: GraphicsWindow, que crea una ventana gráfica basada en la clase QGLWidget. Para crear esta ventana, use el método

 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 ventana se configura de acuerdo con los parámetros de geometría transmitidos a la entrada y la configuración requerida de la representación 3D OSG. El puntero devuelto es el contexto de gráficos OSG que se debe pasar a la cámara. Por lo tanto, el siguiente paso es inicializar la cámara.

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

La llamada real

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

y pasa el contexto deseado asociado con el widget QGLWidget a la cámara. Ponemos toda la rutina del widget en el constructor de la clase.

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

Aquí configuramos el visor y prestamos especial atención al desafío.

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

Cambiar el visor al modo de subproceso único. Esta es una medida necesaria cuando se integra OSG en Qt, ya que en algunas distribuciones de Linux, el programa se bloqueará al usar la representación de subprocesos múltiples utilizada por OSG de manera predeterminada. Las razones para esto requieren un informe separado, así que adelante y preste atención a este código

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

en el que creamos una capa, interfiriendo con un QGLWidget devuelto desde el contexto gráfico de la cámara, convertido en un puntero osgQt :: GraphicsWindows. Agregamos la capa creada a nuestro widget QViewerWidget llamando

 this->setLayout(layout); 

Para que nuestro widget, y con él, la escena se actualice cuando se actualiza la ventana, debe redefinir el controlador de eventos QPaintEvent

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

en el que iniciamos la representación del marco llamando al método osgViewer :: Viewer :: frame ().

Ok, el código de nuestro widget está listo, ahora lo insertamos en un marco ubicado en el formulario. Para hacer esto, en el constructor de la clase MainWindow escribimos dicho código

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

o más bien, todavía estamos interesados ​​en esta parte

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

donde creamos una capa, creamos nuestro widget, con dimensiones iguales al tamaño del marco, agregamos el widget creado a la capa y adjuntamos la capa al marco. Y, para no molestarnos con el diseño en este ejemplo, estiramos el marco a toda el área del cliente de la ventana, convirtiéndolo en el widget central.

Para renderizar, debe organizar una actualización periódica de la ventana por temporizador. Para hacer esto, cree un temporizador con un intervalo de 40 milisegundos (25 cuadros por segundo) y asocie su señal de tiempo de espera con el intervalo de actualización de la ventana. Lo hago de esta manera usando la sintaxis Qt5

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

habiendo definido previamente la ranura de actualización para la clase de ventana de esta manera

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

Por qué es así, porque puede asociar directamente la señal del temporizador con la ranura QMainWindow :: update de la misma manera que la mayoría de los ejemplos osgQt muestran

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

El hecho es que la sintaxis con las macros SIGNAL () y SLOT () está en desuso, y en previsión de la transición a Qt6, debe abandonarse. Al mismo tiempo, la clase QMainWindow no tiene una sobrecarga de la ranura de actualización () sin parámetros, lo que provocará un error en la llamada de enlace durante la compilación. Para hacer esto, tuve que definir mi ranura de actualización () sin parámetros, llamando a la base QMainWindow :: update () en ella, pasando el área del cliente de la ventana allí.

Agregando a este lugar y ejecutando el programa, obtendremos un cierto resultado



Al presionar "S" podemos activar el monitor de estadísticas OSG y asegurarnos de que nuestro widget funcione como debería, dibujando una escena vacía.

¿Qué tipo de monitor de estadísticas?
Para no sobrecargar el artículo, escribiré sobre él aquí. OSG tiene un monitor incorporado que muestra estadísticas del motor en tiempo real. Para agregarlo al visor, incluimos el archivo de encabezado

 #include <osgViewer/ViewerEventHandlers> 

y agregar un controlador al espectador

 viewer.addEventHandler(new osgViewer::StatsHandler); 

luego en cualquier momento presionando "S" para mostrar mucha información útil.

4. Termine nuestro visor: agregue un menú


En el diseñador de formularios, personalizamos el menú usando programación "orientada al mouse" (a lo que soy indiferente, pero sí, a veces es conveniente). Al final obtendremos algo como esto



Ahora comenzaremos las ranuras de procesador apropiadas, permitiéndole cargar el modelo a lo largo de la ruta seleccionada desde el cuadro de diálogo, borrar la escena y salir de la aplicación

Código de controlador de menú
 //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ 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(); } 


Después de eso, obtendremos un visor muy conveniente de los modelos con formato * .osg.



Una demostración de su trabajo se muestra en el video al comienzo del artículo. El código fuente completo para este ejemplo está disponible aquí.

Conclusión


Como hemos visto, la integración de OSG y Qt no es particularmente difícil ni en la comprensión ni en la implementación. Esta es una gran ayuda para crear aplicaciones multiplataforma para visualización técnica y posiblemente juegos.

Este artículo abre una continuación de la serie OSG, que describirá técnicas complejas de desarrollo. Creo que ella salió exitosa. Gracias y hasta pronto!

Source: https://habr.com/ru/post/438654/


All Articles