
Introduccion
Una de las características del lenguaje C ++ por el que a menudo es criticado es la falta de un mecanismo de procesamiento de eventos en el estándar. Mientras tanto, este mecanismo es una de las principales formas de interacción de algunos componentes de software con otros componentes de software y hardware, y se implementa a nivel de un sistema operativo específico. Naturalmente, cada plataforma tiene sus propios matices de implementación del mecanismo descrito.
En relación con todo lo anterior, cuando se desarrolla en C ++, existe la necesidad de implementar el procesamiento de eventos de una forma u otra, resuelto mediante el uso de bibliotecas y marcos de terceros. El conocido marco Qt proporciona un mecanismo para señales y ranuras, que permite organizar la interacción de clases heredadas de QObject. La implementación de eventos también está presente en la biblioteca boost. Y, por supuesto, el motor OpenSceneGraph no podría prescindir de su propia "bicicleta", cuya aplicación se discutirá en el artículo.
OSG es una biblioteca gráfica abstracta. Por un lado, hace un resumen de la interfaz de procedimiento de OpenGL, proporcionando al desarrollador un conjunto de clases que encapsulan toda la mecánica de la API de OpneGL. Por otro lado, también se extrae de una interfaz gráfica de usuario específica, ya que los enfoques para su implementación son diferentes para diferentes plataformas y tienen características incluso dentro de la misma plataforma (MFC, Qt, .Net para Windows, por ejemplo).
Independientemente de la plataforma, desde el punto de vista de la aplicación, la interacción del usuario con la interfaz gráfica se reduce a generar elementos de una secuencia de eventos que luego se procesan dentro de la aplicación. La mayoría de los marcos gráficos utilizan este enfoque, pero incluso dentro de la misma plataforma, desafortunadamente, no son compatibles entre sí.
Por esta razón, OSG proporciona su propia interfaz básica para manejar eventos de widgets de widgets y entradas del usuario basadas en la clase osgGA :: GUIEventHandler. Este controlador se puede adjuntar al visor llamando al método addEventHandler () y eliminado por el método removeEventHandler (). Naturalmente, la clase de controlador concreto debe heredarse de la clase osgGA :: GUIEventHandler, y el método handle () debe redefinirse en ella. Este método acepta dos argumentos: osgGA :: GUIEventAdapter, que contiene la cola de eventos de la GUI y osg :: GUIActionAdepter, utilizado para comentarios. Típico en la definición es tal diseño
bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdepter &aa) {
El parámetro osgGA :: GUIActionAdapter le permite al desarrollador pedirle a la GUI que tome alguna acción en respuesta al evento. En la mayoría de los casos, un espectador se ve afectado a través de este parámetro, un puntero al que se puede obtener mediante la conversión dinámica del puntero
osgViewer::Viewer* viewer = dynamic_cast<osgViewer::Viewer *>(&aa);
1. Manejo de eventos con teclado y mouse
La clase osgGA :: GUIEventAdapter () gestiona todos los tipos de eventos compatibles con OSG, proporcionando datos para establecer y recuperar sus parámetros. El método getEventType () devuelve el evento GUI actual contenido en la cola de eventos. Cada vez, anulando el método handle () del controlador, al llamar a estos métodos, debe usar este captador para recibir el evento y determinar su tipo.
La siguiente tabla describe todos los eventos disponibles.
Tipo de evento | Descripción | Métodos de adquisición de eventos. |
---|
PULSAR / LIBERAR / DOBLECLICK | Haga clic / suelte y haga doble clic con el mouse | getX (), getY (): obtiene la posición del cursor. getButton (): código del botón presionado (LEFT_MOUSE_BUTTON, RIGHT_MOUSE_BUTTON, MIDDLE_MOUSE_BUTTON |
Scrol | Ruedas de desplazamiento del mouse | getScrollingMotion (): devuelve SCROOL_UP, SCROLL_DOWN, SCROLL_LEFT, SCROLL_RIGHT |
Arrastre | Arrastre del mouse | getX (), getY () - posición del cursor; getButtonMask () - valores similares a getButton () |
MOVER | Movimiento del mouse | getX (), getY () - posición del cursor |
KEYDOWN / KEYUP | Presionando / soltando una tecla en un teclado | getKey (): código ASCII de la tecla presionada o el valor del enumerador Key_Symbol (por ejemplo, KEY_BackSpace) |
MARCO | Evento generado al representar un marco | sin entrada |
Usuario | Evento definido por el usuario | getUserDataPointer (): devuelve un puntero a un búfer de datos de usuario (el búfer está controlado por un puntero inteligente) |
También hay un método getModKeyMask () para obtener información sobre la tecla modificadora presionada (devuelve valores de la forma MODKEY_CTRL, MODKEY_SHIFT, MODKEY_ALT, etc.), que le permite procesar combinaciones de teclas que usan modificadores
if (ea.getModKeyMask() == osgGA::GUIEventAdapter::MODKEY_CTRL) {
Tenga en cuenta que los métodos de establecimiento como setX (), setY (), setEventType (), etc. no se utiliza en el controlador handle (). El sistema de ventanas gráficas de bajo nivel OSG las llama para poner en cola el evento.
2. Controlamos cessna desde el teclado
Ya sabemos cómo transformar objetos de escena a través de las clases osg :: MatrixTransform. Examinamos varios tipos de animaciones usando las clases osg :: AnimationPath y osg :: Animation. Pero para la interactividad de una aplicación (por ejemplo, un juego), la animación y las transformaciones claramente no son suficientes. El siguiente paso es controlar la posición de los objetos en el escenario desde los dispositivos de entrada del usuario. Tratemos de fijar la gestión a nuestra querida cessna.
Ejemplo de tecladomain.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"
Para resolver este problema, escribimos una clase de controlador 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; };
Al construir esta clase, como parámetro, se pasa un puntero al nodo de transformación, sobre el cual actuaremos en el controlador. El método del controlador handle () en sí mismo se redefine de la siguiente manera
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 los detalles esenciales de su implementación, debe tenerse en cuenta que primero debemos obtener la matriz de transformación del nodo que controlamos
osg::Matrix matrix = _model->getMatrix();
A continuación, dos instrucciones switch () anidadas analizan el tipo de evento (pulsación de tecla) y el código de la tecla presionada. Dependiendo del código de la tecla presionada, la matriz de transformación actual se multiplica por una matriz de rotación adicional alrededor del eje correspondiente
case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break;
- Gire el avión en ángulos de guiñada de -0.1 radianes cuando presione la tecla "A".
Después de procesar las pulsaciones del teclado, no olvide aplicar una nueva matriz de transformación al nodo de transformación.
_model->setMatrix(matrix);
En la función main (), cargue el modelo de avión y cree un nodo de transformación principal para él, agregando el subgrafo resultante al nodo raíz de la escena
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());
Crear e inicializar manejador de entrada de usuario
osg::ref_ptr<ModelController> mcontrol = new ModelController(mt.get());
Cree un visor agregando nuestro controlador
osgViewer::Viewer viewer; viewer.addEventHandler(mcontrol.get());
Configurar la matriz de vista de la cámara
viewer.getCamera()->setViewMatrixAsLookAt( osg::Vec3(0.0f, -100.0f, 0.0f), osg::Vec3(), osg::Z_AXIS );
Prohibir que la cámara reciba eventos de dispositivos de entrada
viewer.getCamera()->setAllowEventFocus(false);
Si esto no se hace, entonces el controlador que cuelga de la cámara interceptará de forma predeterminada todas las entradas del usuario e interferirá con nuestro controlador. Configuramos los datos de la escena para el espectador y los ejecutamos
viewer.setSceneData(root.get()); return viewer.run();
Ahora, después de lanzar el programa, podremos controlar la orientación de la aeronave en el espacio presionando las teclas A, D, W y S.

Una pregunta interesante es qué debe devolver el método handle () al salir de él. Si se devuelve verdadero, entonces indicamos OSG, entonces ya hemos procesado eventos de entrada y no se necesita más procesamiento. La mayoría de las veces, este comportamiento no nos conviene, por lo que es una buena práctica devolver falso del controlador para no interrumpir el procesamiento de eventos por otros controladores si están conectados a otros nodos en la escena.
3. Uso de visitantes en el procesamiento de eventos.
De manera similar a cómo se implementa al atravesar un gráfico de escena al actualizarlo, OSG admite devoluciones de llamada para manejar eventos que pueden asociarse con nodos y objetos geométricos. Para esto, se utilizan las llamadas a setEventCallback () y addEventCallback (), que toman como parámetro un puntero al elemento secundario osg :: NodeCallback. Para recibir eventos en el operador operator (), podemos convertir el puntero que se le pasó al visitante del sitio en un puntero a osgGA :: EventVisitor, por ejemplo, de esta manera
#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. Creación y procesamiento de eventos personalizados.
OSG utiliza una cola de eventos internos (FIFO). Los eventos al comienzo de la cola se procesan y eliminan de ella. Los eventos recién generados se colocan al final de la cola. El método handle () de cada controlador de eventos se ejecutará tantas veces como haya eventos en la cola. La cola de eventos se describe mediante la clase osgGA :: EventQueue, que, entre otras cosas, le permite colocar un evento en la cola en cualquier momento llamando al método addEvent (). El argumento de este método es un puntero a osgGA :: GUIEventAdapter, que se puede establecer en un comportamiento específico utilizando los métodos setEventType (), etc.
Uno de los métodos de la clase osgGA :: EventQueue es userEvent (), que establece un evento personalizado al asociarlo con los datos del usuario, un puntero al que se le pasa como parámetro. Estos datos se pueden usar para representar cualquier evento personalizado.
No se puede crear una instancia propia de la cola de eventos. Esta instancia ya se ha creado y adjuntado a la instancia del espectador, por lo que solo puede obtener un puntero a este singleton
viewer.getEventQueue()->userEvent(data);
Los datos de usuario son un objeto del heredero de osg :: Referenced, es decir, puede crear un puntero inteligente para ellos.
Cuando se recibe un evento personalizado, el desarrollador puede extraer datos de él llamando al método getUserData () y procesarlo como mejor le parezca.
5. Implementación del temporizador de usuario
Muchas bibliotecas y marcos que implementan la GUI proporcionan un desarrollador de clase para implementar temporizadores que generan un evento después de un cierto intervalo de tiempo. OSG no contiene medios regulares para implementar temporizadores, así que intentemos implementar algún tipo de temporizador por nuestra cuenta, utilizando la interfaz para crear eventos personalizados.
¿En qué podemos confiar al resolver este problema? Para un determinado evento periódico que el render genera constantemente, por ejemplo, en FRAME, el evento de dibujar el siguiente fotograma. Para esto usamos el mismo ejemplo al cambiar el modelo de cessna de normal a quemado.
Ejemplo de temporizadormain.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"
Primero, determinemos el formato de los datos enviados en el mensaje del usuario, definiéndolo como una estructura
struct TimerInfo : public osg::Referenced { TimerInfo(unsigned int c) : _count(c) {} unsigned int _count; };
El parámetro _count contendrá el número entero de milisegundos que transcurrieron desde el momento en que se inició el programa hasta que se recibió el siguiente evento del temporizador. La estructura hereda de la clase osg :: Referenced para poder controlarla mediante punteros inteligentes OSG. Ahora crea un controlador 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 controlador tiene varios miembros protegidos específicos. La variable _switch indica un nodo que cambia los modelos de aeronave; _count: la cuenta regresiva relativa del tiempo transcurrido desde la última generación del evento del temporizador, sirve para contar los intervalos de tiempo; _startTime: una variable temporal para almacenar la cuenta regresiva anterior, realizada por el espectador; _time: el tiempo total de funcionamiento del programa en milisegundos. El constructor de la clase acepta un nodo de conmutación como parámetro y, opcionalmente, el intervalo de tiempo requerido para que funcione el temporizador de conmutación.
En esta clase, anulamos el 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; }
Aquí analizamos el tipo de mensaje recibido. Si es FRAME, se realizan las siguientes acciones:
- Consigue un puntero para el espectador
osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer *>(&aa);
- Al recibir el puntero correcto, lea el tiempo transcurrido desde que comenzó el programa
double time = viewer->getFrameStamp()->getReferenceTime();
calcular la cantidad de tiempo dedicado a renderizar un marco en milisegundos
unsigned int delta = static_cast<unsigned int>( (time - _startTime) * 1000.0);
y recuerda el recuento de tiempo actual
_startTime = time;
Si el valor del contador _count excedió el intervalo de tiempo requerido (o esta es la primera llamada cuando _time aún es cero), colocamos el mensaje de usuario en la cola, pasando la estructura anterior el tiempo del programa en milisegundos. El contador _count se restablece a cero
if ( (_count >= _interval) || (_time == 0) ) { viewer->getEventQueue()->userEvent(new TimerInfo(_time)); _count = 0; }
Independientemente del valor de _count, debemos aumentarlo y _time por la cantidad de retraso requerido para dibujar un marco
_count += delta; _time += delta;
Así es como se generará el evento del temporizador. El manejo de eventos se implementa de la siguiente manera
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;
Aquí verificamos la validez del puntero al nodo de conmutación, restamos los datos del evento, desde la estructura TimerInfo, mostramos el contenido de la estructura en la pantalla y cambiamos el estado del nodo.
El código en la función main () es similar al código en los dos ejemplos de cambio anteriores, con la diferencia de que en este caso colgamos un controlador de eventos en el visor
viewer.addEventHandler(new TimerHandler(root.get(), 1000));
pasar el puntero al nodo raíz y el intervalo de conmutación requerido en milisegundos al constructor del controlador. Ejecutando el ejemplo, veremos que los modelos cambian a intervalos de un segundo, y en la consola encontramos la salida de los tiempos en que ocurrió el cambio
Timer event at: 0 Timer event at: 1000 Timer event at: 2009 Timer event at: 3017 Timer event at: 4025 Timer event at: 5033
Se puede generar un evento personalizado en cualquier momento durante la ejecución del programa, y no solo cuando se recibe el evento FRAME, y esto proporciona un mecanismo muy flexible para el intercambio de datos entre partes del programa, permite procesar señales de dispositivos de entrada no estándar, como joysticks o guantes VR, por ejemplo.
Continuará ...