OpenSceneGraph: Integração com o Qt Framework



1. Introdução


Por um lado, o próprio mecanismo OpenSceneGraph possui um subsistema avançado para gerenciar janelas, processar eventos de entrada do usuário, enviar e receber mensagens do usuário. Falamos sobre isso com mais detalhes nos artigos anteriores desta série. Em geral, juntamente com os recursos do C ++ / STL, isso é suficiente para o desenvolvimento de aplicativos arbitrariamente complexos.

Um exemplo de integração do OSG em um aplicativo desenvolvido pelo QtDesigner. Este exemplo será discutido em detalhes abaixo.


Por outro lado, para acelerar o desenvolvimento em C ++, as duas bibliotecas de terceiros são usadas para expandir os recursos dessa linguagem (como boost), e estruturas inteiras permitem que você desenvolva fácil e naturalmente aplicativos de plataforma cruzada com amplo objetivo funcional. Uma dessas estruturas é o ultra popular Qt. Não importa como eles criticaram o Qt por seu compilador de meta-objetos e outras falhas e inconvenientes, a força do Qt está em uma extensa biblioteca de classes que resolve todas as tarefas concebíveis do desenvolvimento de plataforma cruzada, bem como no conceito de "sinais - slots" que implementa o subsistema de mensagens entre as classes. Os sinais e slots também são baseados nos métodos de interação entre o aplicativo e o sistema operacional, bem como na comunicação entre processos.

E diabos, seria muito interessante combinar duas tecnologias: Qt e OSG. Minha equipe teve que resolver um problema semelhante, sobre o qual eu já escrevi em uma de minhas publicações . No entanto, gostaria de abrir essa questão um pouco mais e este artigo abordará esse tópico.

Existem duas opções para integrar o OSG e o Qt:

  1. Usando sinais e slots de Qt para interagir objetos em um aplicativo OSG
  2. Integração do visualizador OSG em uma interface gráfica desenvolvida em C ++ / Qt, incluindo o uso do designer de formulários QtDesigner

A primeira opção é aplicável quando você não precisa usar os elementos da GUI fornecidos pelo Qt, mas deseja garantir a interação dos componentes do aplicativo através de sinais e slots. Por exemplo, surgiu a necessidade de integrar um aplicativo OSG a uma biblioteca de comunicação entre processos através de soquetes TCP usando Qt.

A segunda opção é necessária quando a integração do mecanismo OSG e um aplicativo gráfico desenvolvido usando o Qt são necessários. Sinais e slots ficam disponíveis para nós e, além deles, toda a gama de elementos GUI padronizados fornecidos pelo Qt.

1. Sinais Qt em um sistema de janelas OSG


O primeiro exemplo será um pouco sintético: escrevemos um aplicativo OSG simples com uma cena primitiva; crie duas classes, uma das quais manipulará pressionamentos de teclas e a outra para exibir uma mensagem no console sobre a tecla pressionada. Nesse caso, o manipulador gerará um sinal Qt, com uma mensagem sobre a tecla pressionada como parâmetro.

Para integrar com o Qt, basta atender às três condições a seguir

  1. Herdar classes interagindo de QObject
  2. Organizar um loop de processamento de sinal
  3. Crie uma instância da classe QApplication (ou QCoreApplication) que existe na memória durante a operação do aplicativo

O código de exemplo completo pode ser visto aqui no meu repositório OSG-aulas , que contém todas as lições deste ciclo.

Primeiro, crie uma classe que "torcerá" a fila de processamento de sinal.

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

Tudo é extremamente simples - essa classe é um manipulador de eventos OSG padrão que, ao renderizar um quadro, inicia o processamento da fila de sinal Qt chamando

 QCoreApplication::processEvents(QEventLoop::AllEvents); 

Agora vamos criar uma classe que processa o teclado, novamente, usando o mecanismo embutido no OSG, mas ao mesmo tempo capaz de enviar um sinal Qt. Para fazer isso, usaremos a herança múltipla recentemente anatematizada

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 

A turma processará uma mensagem sobre uma tecla pressionada e enviará um sinal com uma mensagem contendo o código da tecla pressionada. Esse sinal será recebido por uma classe que não está conectada de maneira alguma ao OSG, que é descendente do QObject e contém um único slot que imprime uma mensagem na saída padrão

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 

Agora, junte tudo escrevendo um aplicativo 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(); } 

Primeiro, criamos uma instância da classe QCoreApplication.

 QCoreApplication app(argc, argv); 

Isso é necessário para que a tecnologia descrita funcione. No entanto, não chamaremos o método QCoreApplication :: exec () ! Em vez disso, o loop de processamento de sinal girará dentro do nosso loop osgViewer :: Viewer :: run (), para o qual criamos e registramos o manipulador correspondente

 viewer.addEventHandler(new QtEventsHandler); 

Criamos instâncias de classes que irão interagir através de sinais Qt, conectando o sinal de um com o slot do outro

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

Registrar o manipulador de teclado

 viewer.addEventHandler(keyboardHandler); 

Todos executam o visualizador

 return viewer.run(); 

e ver essa foto


Sim, o exemplo é um tanto artificial, mas ilustra os principais princípios de integração de código usando mecanismos Qt em um aplicativo usando OSG. Essa ideia, adquirida no OpenSceneGraph 3. Cookbook , economizou muito tempo e nervosismo para mim e minha equipe de desenvolvimento, permitindo que eu usasse um módulo baseado em Qt que foi depurado e padronizado dentro de nossa base de código em nossa base de código.

Mas e se ainda quisermos usar o OSG dentro de um aplicativo Qt GUI?

2. biblioteca osgQt


o osgQt é uma biblioteca de integração projetada para:

  1. Incorporando uma cena 3D implementada no OSG na interface gráfica de um aplicativo desenvolvido no Qt
  2. Incorporando widgets Qt em uma superfície de geometria 3D dentro de uma cena OSG. Sim, você ouviu direito - os widgets Qt podem funcionar silenciosamente dentro do mundo virtual. Algum dia eu definitivamente demonstrarei

Houve alguns problemas com esta biblioteca, que conseguimos superar estudando cuidadosamente os exemplos anexados e lendo o já mencionado OpenSceneGraph 3. Cookbook

A biblioteca deve ser montada e esse processo é semelhante à montagem do próprio mecanismo, descrito em detalhes no primeiro artigo do ciclo . A única observação é que -DCMAKE_INSTALL_PREFIX deve ser selecionado com o mesmo que foi especificado ao construir o mecanismo - portanto, o osgQt será instalado próximo ao mecanismo e será conveniente usá-lo durante o desenvolvimento.

3. Integração do osgViewer :: Viewer na Qt GUI


O exemplo a seguir será bastante útil. Escreveremos um visualizador que permite carregar modelos no formato * .osg usando os controles Qt padrão. Além disso, para desenvolver uma interface gráfica, usamos o QtDeisgner.

Vamos criar um novo projeto como "Qt Widgets Application"



Nesse caso, a janela principal do aplicativo será gerada com uma barra de menus, barra de ferramentas e barra de status. No QtDesigner, adicione um componente QFrame a esta janela.



Colocaremos o visualizador OSG nesse quadro. O visualizador OSG será essencialmente um widget Qt. Para sua implementação, escreveremos a classe QViewerWidget. Colocarei todas as fontes no spoiler, para não desfocar a apresentação com folhas 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(); } 


A idéia principal da implementação é usar a classe osgQt :: GraphicsWindow, que cria uma janela gráfica baseada na classe QGLWidget. Para criar esta janela, use o 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()); } 

A janela é configurada de acordo com os parâmetros geométricos transmitidos à entrada e as configurações necessárias da renderização 3D do OSG. O ponteiro retornado é o contexto gráfico OSG que deve ser passado para a câmera. Portanto, o próximo passo é inicializar a câmera

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

A chamada real

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

e passa o contexto desejado associado ao widget QGLWidget para a câmera. Colocamos toda a rotina do widget no construtor da 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); } } 

Aqui configuramos o visualizador e prestamos atenção especial ao desafio

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

Alternando o visualizador para o modo de thread único. Essa é uma medida necessária ao integrar o OSG ao Qt, pois em algumas distribuições Linux o programa falha ao usar a renderização multithread usada por OSG por padrão. Os motivos para isso exigem uma análise separada; portanto, preste atenção ao 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); } 

na qual criamos uma camada, interferindo com um QGLWidget retornado do contexto gráfico da câmera, convertido em um ponteiro osgQt :: GraphicsWindows. Adicionamos a camada criada ao nosso widget QViewerWidget chamando

 this->setLayout(layout); 

Para que nosso widget e, com ele, a cena seja atualizada quando a janela for atualizada, é necessário redefinir o manipulador de eventos QPaintEvent

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

em que iniciamos a renderização do quadro chamando o método osgViewer :: Viewer :: frame ().

Ok, o código do nosso widget está pronto, agora o incorporamos em um quadro localizado no formulário. Para fazer isso, no construtor da classe MainWindow, escrevemos esse 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"); } 

ou melhor, ainda estamos interessados ​​nessa 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); 

onde criamos uma camada, criamos nosso widget, com dimensões iguais ao tamanho do quadro, adicionamos o widget criado à camada e anexamos a camada ao quadro. E, para não se preocupar com o layout deste exemplo, estendemos o quadro para toda a área do cliente da janela, tornando-o o widget central.

Para renderizar, você deve providenciar uma atualização periódica da janela por timer. Para fazer isso, crie um timer com um intervalo de 40 milissegundos (25 quadros por segundo) e associe seu sinal de tempo limite ao slot de atualização da janela. Eu faço dessa maneira usando a sintaxe Qt5

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

ter definido anteriormente o slot de atualização para a classe window dessa maneira

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

Por que isso, porque você pode associar diretamente o sinal do timer com o slot QMainWindow :: update da mesma maneira que a maioria dos exemplos osgQt mostra

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

O fato é que a sintaxe com as macros SIGNAL () e SLOT () está obsoleta e, em antecipação à transição para o Qt6, ela deve ser abandonada. Ao mesmo tempo, a classe QMainWindow não possui uma sobrecarga do slot update () sem parâmetros, o que causará um erro na chamada de ligação durante a compilação. Para fazer isso, tive que definir meu slot update () sem parâmetros, chamando o QMainWindow :: update () base nele, passando a área do cliente da janela para lá.

Adicionando a este local e executando o programa, obteremos um certo resultado



Ao pressionar "S", podemos ativar o monitor de estatísticas OSG e garantir que nosso widget esteja funcionando como deveria, desenhando uma cena vazia.

Que tipo de estatísticas monitoram?
Para não sobrecarregar o artigo, escreverei sobre isso aqui. O OSG possui um monitor embutido que exibe estatísticas do mecanismo em tempo real. Para adicioná-lo ao visualizador, incluímos o arquivo de cabeçalho

 #include <osgViewer/ViewerEventHandlers> 

e adicione um manipulador ao visualizador

 viewer.addEventHandler(new osgViewer::StatsHandler); 

depois, a qualquer momento, pressionando "S" para exibir muitas informações úteis.

4. Finalize o visualizador: adicione um menu


No designer de formulários, personalizamos o menu usando a programação "orientada ao mouse" (para a qual sou indiferente, mas sim, às vezes é conveniente). No final, teremos algo assim



Agora iniciaremos os slots de processador apropriados, permitindo carregar o modelo ao longo do caminho selecionado na caixa de diálogo, limpar a cena e sair do aplicativo

Código do manipulador de menus
 //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ 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(); } 


Depois disso, teremos um visualizador muito conveniente de modelos no formato * .osg.



Uma demonstração de seu trabalho é mostrada no vídeo no início do artigo. O código fonte completo para este exemplo está disponível aqui.

Conclusão


Como vimos, a integração do OSG e do Qt não é particularmente difícil de entender ou de implementar. Essa é uma grande ajuda para criar aplicativos de plataforma cruzada para visualização técnica e, possivelmente, jogos.

Este artigo abre uma continuação da série OSG, que descreverá técnicas complexas de desenvolvimento. Eu acho que ela saiu com sucesso. Obrigado e até breve!

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


All Articles