
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:
- Usando sinais e slots de Qt para interagir objetos em um aplicativo OSG
- 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
- Herdar classes interagindo de QObject
- Organizar um loop de processamento de sinal
- 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"
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"
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:
- Incorporando uma cena 3D implementada no OSG na interface gráfica de um aplicativo desenvolvido no Qt
- 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. CookbookA 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 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!