OpenSceneGraph: Manipulação de Eventos

imagem

1. Introdução


Um dos recursos da linguagem C ++ para os quais é frequentemente criticado é a falta de um mecanismo de processamento de eventos no padrão. Enquanto isso, esse mecanismo é uma das principais formas de interação de alguns componentes de software com outros componentes e hardware de software e é implementado no nível de um sistema operacional específico. Naturalmente, cada plataforma possui suas próprias nuances de implementação do mecanismo descrito.

Em conexão com tudo isso, ao desenvolver em C ++, é necessário implementar o processamento de eventos de uma maneira ou de outra, resolvida usando bibliotecas e estruturas de terceiros. A conhecida estrutura Qt fornece um mecanismo para sinais e slots, que permite organizar a interação de classes herdadas do QObject. A implementação de eventos também está presente na biblioteca de impulso. E, é claro, o mecanismo OpenSceneGraph não poderia prescindir de sua própria “bicicleta”, cuja aplicação será discutida no artigo.

OSG é uma biblioteca gráfica abstrata. Por um lado, ele abstrai da interface de procedimentos do OpenGL, fornecendo ao desenvolvedor um conjunto de classes que encapsula toda a mecânica da API do OpneGL. Por outro lado, ele também abstrai de uma interface gráfica específica do usuário, uma vez que as abordagens para sua implementação são diferentes para diferentes plataformas e possuem recursos mesmo dentro da mesma plataforma (MFC, Qt, .Net for Windows, por exemplo).

Independentemente da plataforma, do ponto de vista do aplicativo, a interação do usuário com a interface gráfica é reduzida para gerar elementos de uma sequência de eventos que são processados ​​dentro do aplicativo. A maioria das estruturas gráficas usa essa abordagem, mas mesmo dentro da mesma plataforma elas, infelizmente, não são compatíveis entre si.

Por esse motivo, o OSG fornece sua própria interface básica para manipular eventos de widget e entrada do usuário com base na classe osgGA :: GUIEventHandler. Esse manipulador pode ser anexado ao visualizador chamando o método addEventHandler () e removido pelo método removeEventHandler (). Naturalmente, a classe de manipulador de concreto deve ser herdada da classe osgGA :: GUIEventHandler, e o método handle () deve ser redefinido nela. Este método aceita dois argumentos: osgGA :: GUIEventAdapter, que contém a fila de eventos da GUI e osg :: GUIActionAdepter, usados ​​para feedback. Típico na definição é esse design

bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdepter &aa) { //        } 

O parâmetro osgGA :: GUIActionAdapter permite que o desenvolvedor solicite à GUI que execute alguma ação em resposta ao evento. Na maioria dos casos, um visualizador é afetado por esse parâmetro, um ponteiro para o qual pode ser obtido por conversão dinâmica de ponteiro

 osgViewer::Viewer* viewer = dynamic_cast<osgViewer::Viewer *>(&aa); 

1. Manipulação de eventos de teclado e mouse


A classe osgGA :: GUIEventAdapter () gerencia todos os tipos de eventos suportados pelo OSG, fornecendo dados para definir e recuperar seus parâmetros. O método getEventType () retorna o evento GUI atual contido na fila de eventos. Toda vez, substituindo o método handle () do manipulador, ao chamar esses métodos, você deve usar esse getter para receber o evento e determinar seu tipo.

A tabela a seguir descreve todos os eventos disponíveis.

Tipo de eventoDescrição do produtoMétodos de recuperação de dados de eventos
PUSH / RELEASE / DOUBLECLICKClique / Solte e clique duas vezes nos botões do mousegetX (), getY () - obtendo a posição do cursor. getButton () - código do botão pressionado (LEFT_MOUSE_BUTTON, RIGHT_MOUSE_BUTTON, MIDDLE_MOUSE_BUTTON
ScrolRoda (s) do mouse com rolagemgetScrollingMotion () - retorna SCROOL_UP, SCROLL_DOWN, SCROLL_LEFT, SCROLL_RIGHT
DRAGArrastar o mousegetX (), getY () - posição do cursor; getButtonMask () - valores semelhantes a getButton ()
MOVEMovimento do mousegetX (), getY () - posição do cursor
KEYDOWN / KEYUPPressionar / soltar uma tecla no tecladogetKey () - código ASCII da tecla pressionada ou o valor do enumerador Key_Symbol (por exemplo, KEY_BackSpace)
QUADROEvento gerado ao renderizar um quadrosem entrada
USEREvento definido pelo usuáriogetUserDataPointer () - retorna um ponteiro para um buffer de dados do usuário (o buffer é controlado por um ponteiro inteligente)

Também existe um método getModKeyMask () para recuperar informações sobre uma tecla modificadora pressionada (retorna valores no formato MODKEY_CTRL, MODKEY_SHIFT, MODKEY_ALT e assim por diante), permitindo processar combinações de teclas que usam modificadores

 if (ea.getModKeyMask() == osgGA::GUIEventAdapter::MODKEY_CTRL) { //    Ctrl } 

Lembre-se de que métodos setter como setX (), setY (), setEventType () etc. não usado no manipulador handle (). Eles são chamados pelo sistema de janelas gráficas de baixo nível OSG para enfileirar o evento.

2. Nós controlamos a cessna pelo teclado


Já sabemos como transformar objetos de cena através das classes osg :: MatrixTransform. Examinamos vários tipos de animações usando as classes osg :: AnimationPath e osg :: Animation. Mas para a interatividade de um aplicativo (por exemplo, um jogo), animação e transformações claramente não são suficientes. O próximo passo é controlar a posição dos objetos no palco a partir dos dispositivos de entrada do usuário. Vamos tentar prender a administração à nossa amada cessna.

Exemplo de teclado
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <osgGA/GUIEventHandler> #include <osgViewer/Viewer> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class ModelController : public osgGA::GUIEventHandler { public: ModelController( osg::MatrixTransform *node ) : _model(node) {} virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: osg::ref_ptr<osg::MatrixTransform> _model; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ bool ModelController::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { (void) aa; if (!_model.valid()) return false; osg::Matrix matrix = _model->getMatrix(); switch (ea.getEventType()) { case osgGA::GUIEventAdapter::KEYDOWN: switch (ea.getKey()) { case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break; case 'd': case 'D': matrix *= osg::Matrix::rotate( 0.1, osg::Z_AXIS); break; case 'w': case 'W': matrix *= osg::Matrix::rotate(-0.1, osg::X_AXIS); break; case 's': case 'S': matrix *= osg::Matrix::rotate( 0.1, osg::X_AXIS); break; default: break; } _model->setMatrix(matrix); break; default: break; } return true; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform; mt->addChild(model.get()); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(mt.get()); osg::ref_ptr<ModelController> mcontrol = new ModelController(mt.get()); osgViewer::Viewer viewer; viewer.addEventHandler(mcontrol.get()); viewer.getCamera()->setViewMatrixAsLookAt( osg::Vec3(0.0f, -100.0f, 0.0f), osg::Vec3(), osg::Z_AXIS ); viewer.getCamera()->setAllowEventFocus(false); viewer.setSceneData(root.get()); return viewer.run(); } 


Para resolver esse problema, escrevemos uma classe de manipulador de eventos de entrada

 class ModelController : public osgGA::GUIEventHandler { public: ModelController( osg::MatrixTransform *node ) : _model(node) {} virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: osg::ref_ptr<osg::MatrixTransform> _model; }; 

Ao construir essa classe, como parâmetro, é passado um ponteiro para o nó de transformação, no qual agiremos no manipulador. O próprio método manipulador handle () é redefinido da seguinte maneira

 bool ModelController::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { (void) aa; if (!_model.valid()) return false; osg::Matrix matrix = _model->getMatrix(); switch (ea.getEventType()) { case osgGA::GUIEventAdapter::KEYDOWN: switch (ea.getKey()) { case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break; case 'd': case 'D': matrix *= osg::Matrix::rotate( 0.1, osg::Z_AXIS); break; case 'w': case 'W': matrix *= osg::Matrix::rotate(-0.1, osg::X_AXIS); break; case 's': case 'S': matrix *= osg::Matrix::rotate( 0.1, osg::X_AXIS); break; default: break; } _model->setMatrix(matrix); break; default: break; } return false; } 

Entre os detalhes essenciais de sua implementação, deve-se notar que devemos primeiro obter a matriz de transformação do nó que controlamos

 osg::Matrix matrix = _model->getMatrix(); 

Em seguida, duas instruções switch aninhadas analisam o tipo de evento (pressionamento de tecla) e o código da tecla pressionada. Dependendo do código da tecla pressionada, a matriz de transformação atual é multiplicada por uma matriz de rotação adicional em torno do eixo correspondente

 case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break; 

- gire o avião em ângulos de -0,1 radianos ao pressionar a tecla "A".

Após o processamento das teclas, não se esqueça de aplicar uma nova matriz de transformação ao nó de transformação

 _model->setMatrix(matrix); 

Na função main (), carregue o modelo do avião e crie um nó de transformação pai para ele, adicionando o subgráfico resultante ao nó raiz da cena

 osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform; mt->addChild(model.get()); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(mt.get()); 

Criar e inicializar manipulador de entrada do usuário

 osg::ref_ptr<ModelController> mcontrol = new ModelController(mt.get()); 

Crie um visualizador adicionando nosso manipulador a ele

 osgViewer::Viewer viewer; viewer.addEventHandler(mcontrol.get()); 

Configurar a matriz de visualização da câmera

 viewer.getCamera()->setViewMatrixAsLookAt( osg::Vec3(0.0f, -100.0f, 0.0f), osg::Vec3(), osg::Z_AXIS ); 

Proibir a câmera de receber eventos de dispositivos de entrada

 viewer.getCamera()->setAllowEventFocus(false); 

Se isso não for feito, o manipulador pendurado na câmera, por padrão, interceptará toda a entrada do usuário e interferirá no nosso manipulador. Definimos os dados da cena para o visualizador e executamos

 viewer.setSceneData(root.get()); return viewer.run(); 

Agora, após o lançamento do programa, poderemos controlar a orientação da aeronave no espaço pressionando as teclas A, D, W e S.



Uma pergunta interessante é o que o método handle () deve retornar ao sair dele. Se true for retornado, indicamos OSG, já processamos eventos de entrada e não é necessário processamento adicional. Na maioria das vezes, esse comportamento não nos convém, portanto, é uma boa prática retornar falso do manipulador para não interromper o processamento de eventos por outros manipuladores se eles estiverem anexados a outros nós na cena.

3. Usando visitantes para manipular eventos


Semelhante à maneira como ele é implementado ao percorrer um gráfico de cena ao atualizá-lo, o OSG suporta retornos de chamada para manipular eventos que podem ser associados a nós e objetos geométricos. Para isso, são usadas chamadas para setEventCallback () e addEventCallback (), que tomam como parâmetro um ponteiro para o filho osg :: NodeCallback. Para receber eventos no operador operator (), podemos converter o ponteiro passado para o visitante do site em um ponteiro para osgGA :: EventVisitor, por exemplo, como este

 #include <osgGA/EventVisitor> ... void operator()( osg::Node *node, osg::NodeVisitor *nv ) { std::list<osg::ref_ptr<osgGA::GUIEventAdapter>> events; osgGA::EventVisitor *ev = dynamic_cast<osgGA::EventVisitor *>(nv); if (ev) { events = ev->getEvents(); //       } } 

4. Criação e processamento de eventos personalizados


O OSG usa uma fila de eventos interna (FIFO). Os eventos no início da fila são processados ​​e excluídos dela. Os eventos gerados recentemente são colocados no final da fila. O método handle () de cada manipulador de eventos será executado quantas vezes houver eventos na fila. A fila de eventos é descrita pela classe osgGA :: EventQueue, que, entre outras coisas, permite colocar um evento na fila a qualquer momento, chamando o método addEvent (). O argumento para esse método é um ponteiro para osgGA :: GUIEventAdapter, que pode ser definido para um comportamento específico usando os métodos setEventType () e assim por diante.

Um dos métodos da classe osgGA :: EventQueue é userEvent (), que define um evento do usuário associando-o aos dados do usuário, um ponteiro ao qual é passado como parâmetro. Esses dados podem ser usados ​​para representar qualquer evento personalizado.

Não foi possível criar a própria instância da fila de eventos. Esta instância já foi criada e anexada à instância do visualizador, portanto, você só pode obter um ponteiro para esse singleton

 viewer.getEventQueue()->userEvent(data); 

Os dados do usuário são um objeto do herdeiro de osg :: Referenced, ou seja, você pode criar um ponteiro inteligente para ele.

Quando um evento personalizado é recebido, o desenvolvedor pode extrair dados dele chamando o método getUserData () e processá-lo como achar melhor.

5. Implementação do timer do usuário


Muitas bibliotecas e estruturas que implementam a GUI fornecem um desenvolvedor de classe para implementar temporizadores que geram um evento após um determinado intervalo de tempo. O OSG não contém meios regulares de implementação de cronômetros; portanto, tentamos implementar algum tipo de cronômetro por conta própria, usando a interface para criar eventos personalizados.

Em que podemos confiar ao resolver esse problema? Para um determinado evento periódico gerado constantemente pela renderização, por exemplo, em FRAME, o evento de desenhar o próximo quadro. Para isso, usamos o mesmo exemplo ao mudar o modelo de cessna de normal para queima.

Exemplo de temporizador
main.h

 #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgGA/GUIEventHandler> #include <osgViewer/Viewer> #include <iostream> #endif 

main.cpp

 #include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ struct TimerInfo : public osg::Referenced { TimerInfo(unsigned int c) : _count(c) {} unsigned int _count; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ class TimerHandler : public osgGA::GUIEventHandler { public: TimerHandler(osg::Switch *sw, unsigned int interval = 1000) : _switch(sw) , _count(0) , _startTime(0.0) , _interval(interval) , _time(0) { } virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: osg::ref_ptr<osg::Switch> _switch; unsigned int _count; double _startTime; unsigned int _interval; unsigned int _time; }; //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ bool TimerHandler::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { switch (ea.getEventType()) { case osgGA::GUIEventAdapter::FRAME: { osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer *>(&aa); if (!viewer) break; double time = viewer->getFrameStamp()->getReferenceTime(); unsigned int delta = static_cast<unsigned int>( (time - _startTime) * 1000.0); _startTime = time; if ( (_count >= _interval) || (_time == 0) ) { viewer->getEventQueue()->userEvent(new TimerInfo(_time)); _count = 0; } _count += delta; _time += delta; break; } case osgGA::GUIEventAdapter::USER: if (_switch.valid()) { const TimerInfo *ti = dynamic_cast<const TimerInfo *>(ea.getUserData()); std::cout << "Timer event at: " << ti->_count << std::endl; _switch->setValue(0, !_switch->getValue(0)); _switch->setValue(1, !_switch->getValue(1)); } break; default: break; } return false; } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg"); osg::ref_ptr<osg::Switch> root = new osg::Switch; root->addChild(model1.get(), true); root->addChild(model2.get(), false); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); viewer.addEventHandler(new TimerHandler(root.get(), 1000)); return viewer.run(); } 


Primeiro, vamos determinar o formato dos dados enviados na mensagem do usuário, definindo-o como uma estrutura

 struct TimerInfo : public osg::Referenced { TimerInfo(unsigned int c) : _count(c) {} unsigned int _count; }; 

O parâmetro _count conterá o número inteiro de milissegundos decorrido desde o momento em que o programa foi iniciado até o próximo evento de timer ser recebido. A estrutura herda da classe osg :: Referenced para que possa ser controlada através de ponteiros inteligentes OSG. Agora crie um manipulador de eventos

 class TimerHandler : public osgGA::GUIEventHandler { public: TimerHandler(osg::Switch *sw, unsigned int interval = 1000) : _switch(sw) , _count(0) , _startTime(0.0) , _interval(interval) , _time(0) { } virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: osg::ref_ptr<osg::Switch> _switch; unsigned int _count; double _startTime; unsigned int _interval; unsigned int _time; }; 

Este manipulador possui vários membros protegidos específicos. A variável _switch indica um nó que alterna os modelos de aeronave; _count - a contagem regressiva relativa do tempo decorrido desde a última geração do evento do timer, serve para contar os intervalos de tempo; _startTime - uma variável temporária para armazenar a contagem regressiva anterior, conduzida pelo visualizador; _time - o tempo total de operação do programa em milissegundos. O construtor da classe aceita um nó do comutador como parâmetro e, opcionalmente, o intervalo de tempo necessário para o temporizador de comutação operar.

Nesta classe, substituímos o método handle ()

 bool TimerHandler::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { switch (ea.getEventType()) { case osgGA::GUIEventAdapter::FRAME: { osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer *>(&aa); if (!viewer) break; double time = viewer->getFrameStamp()->getReferenceTime(); unsigned int delta = static_cast<unsigned int>( (time - _startTime) * 1000.0); _startTime = time; if ( (_count >= _interval) || (_time == 0) ) { viewer->getEventQueue()->userEvent(new TimerInfo(_time)); _count = 0; } _count += delta; _time += delta; break; } case osgGA::GUIEventAdapter::USER: if (_switch.valid()) { const TimerInfo *ti = dynamic_cast<const TimerInfo *>(ea.getUserData()); std::cout << "Timer event at: " << ti->_count << std::endl; _switch->setValue(0, !_switch->getValue(0)); _switch->setValue(1, !_switch->getValue(1)); } break; default: break; } return false; } 

Aqui analisamos o tipo de mensagem recebida. Se for FRAME, as seguintes ações são executadas:
  1. Coloque um ponteiro no visualizador

 osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer *>(&aa); 

  1. Após o recebimento do ponteiro correto, leia o tempo decorrido desde o início do programa

 double time = viewer->getFrameStamp()->getReferenceTime(); 

calcular a quantidade de tempo gasto renderizando um quadro em milissegundos

 unsigned int delta = static_cast<unsigned int>( (time - _startTime) * 1000.0); 

e lembre-se da contagem de tempo atual

 _startTime = time; 

Se o valor do contador _count exceder o intervalo de tempo necessário (ou esta for a primeira chamada quando _time ainda for zero), colocaremos a mensagem do usuário na fila, passando na estrutura acima o tempo do programa em milissegundos. O contador _count é redefinido para zero

 if ( (_count >= _interval) || (_time == 0) ) { viewer->getEventQueue()->userEvent(new TimerInfo(_time)); _count = 0; } 

Independentemente do valor de _count, devemos aumentá-lo e _time pela quantidade de atraso necessária para desenhar um quadro

 _count += delta; _time += delta; 

É assim que o evento do timer será gerado. A manipulação de eventos é implementada da seguinte maneira

 case osgGA::GUIEventAdapter::USER: if (_switch.valid()) { const TimerInfo *ti = dynamic_cast<const TimerInfo *>(ea.getUserData()); std::cout << "Timer event at: " << ti->_count << std::endl; _switch->setValue(0, !_switch->getValue(0)); _switch->setValue(1, !_switch->getValue(1)); } break; 

Aqui, verificamos a validade do ponteiro para o nó de comutação, subtraímos os dados do evento, levando da estrutura TimerInfo, exibimos o conteúdo da estrutura na tela e alteramos o estado do nó.

O código na função main () é semelhante ao código nos dois exemplos de comutação anteriores, com a diferença de que, neste caso, penduramos um manipulador de eventos no visualizador

 viewer.addEventHandler(new TimerHandler(root.get(), 1000)); 

passando o ponteiro para o nó raiz e o intervalo de alternância necessário em milissegundos para o construtor do manipulador. Executando o exemplo, veremos que os modelos mudam em intervalos de segundo e, no console, encontramos a saída dos horários em que a troca ocorreu

 Timer event at: 0 Timer event at: 1000 Timer event at: 2009 Timer event at: 3017 Timer event at: 4025 Timer event at: 5033 

Um evento personalizado pode ser gerado a qualquer momento durante a execução do programa, e não apenas quando o evento FRAME é recebido, e isso fornece um mecanismo muito flexível para a troca de dados entre partes do programa, possibilita o processamento de sinais de dispositivos de entrada não padrão, como joysticks ou luvas de VR, por exemplo.

Para continuar ...

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


All Articles