📜 ⬆️ ⬇️

OpenSceneGraph: Event Handling

image

Introduction


One of the features of C ++, for which it is often criticized, is the absence of an event-handling mechanism in the standard. Meanwhile, this mechanism is one of the main ways of interaction of some software components with other software components and hardware, and it is implemented at the level of a specific OS. Naturally, each of the platforms has its own nuances of the implementation of the described mechanism.

In connection with all of the above, when developing in C ++, there is a need to implement event handling in one way or another, solved by using third-party libraries and frameworks. The well-known Qt framework provides a mechanism for signals and slots that allows you to organize the interaction of classes inherited from QObject. The implementation of events is also present in the boost library. And of course, the OpenSceneGraph engine was not without its own “bicycle”, the application of which will be discussed in the article.

OSG is an abstract graphical library. On the one hand, it abstracts from the procedural OpenGL interface, providing the developer with a set of classes that encapsulate the entire OpneGL API mechanics. On the other hand, it abstracts from a specific graphical user interface, since approaches to its implementation are different for different platforms and have features even within the same platform (MFC, Qt, .Net for Windows, for example).

Regardless of the platform, from the point of view of the application, the user’s interaction with the graphical interface is reduced to its generation by the elements of a sequence of events that are then processed within the application. Most graphic frameworks use this approach, but even within the same platform they, unfortunately, are incompatible with each other.

For this reason, OSG provides its own basic interface for handling GUI widgets and user input based on the class osgGA :: GUIEventHandler. This handler can be attached to the viewer by calling the addEventHandler () method and removed by the removeEventHandler () method. Naturally, the concrete handler class must be inherited from the osgGA :: GUIEventHandler class, and the handle () method must be redefined in it. This method accepts two arguments as input: osgGA :: GUIEventAdapter, which contains the event queue from the GUI and osg :: GUIActionAdepter, which is used for feedback. Typical in the definition is such a construction

bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdepter &aa) { // Здесь выполняются конкретные операции по обработке событий } 

The osgGA :: GUIActionAdapter parameter allows the developer to ask the GUI to perform certain actions in response to an event. In most cases, this parameter affects the viewer, a pointer to which can be obtained by a dynamic pointer conversion.

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

1. Handling keyboard and mouse events


The osgGA :: GUIEventAdapter () class manages all event types supported by OSG, providing data for setting and retrieving its parameters. The getEventType () method returns the current GUI event contained in the event queue. Each time, overriding the handler's handle () method, when calling this method, use this getter to receive the event and determine its type.

The following table describes all available events.

Event typeDescriptionMethods for retrieving event data
PUSH / RELEASE / DOUBLECLICKPressing / Release and double-clicking the mouse buttonsgetX (), getY () - getting the cursor position. getButton () - code of the pressed button (LEFT_MOUSE_BUTTON, RIGHT_MOUSE_BUTTON, MIDDLE_MOUSE_BUTTON
SCROLLScrolling the mouse wheel (s)getScrollingMotion () - returns values ​​SCROOL_UP, SCROLL_DOWN, SCROLL_LEFT, SCROLL_RIGHT
DRAGMouse dragginggetX (), getY () - cursor position; getButtonMask () - values ​​similar to getButton ()
MOVEMove the mousegetX (), getY () - cursor position
KEYDOWN / KEYUPPressing / Releasing a key on the keyboardgetKey () - the ASCII code of the key pressed or the enumerator Key_Symbol value (for example, KEY_BackSpace)
FRAMEEvent generated by frame drawingno input data
USERUser Defined EventgetUserDataPointer () - returns a pointer to a user data buffer (the buffer is controlled by a smart pointer)

There is also a getModKeyMask () method for getting information about a pressed modifier key (returns values ​​like MODKEY_CTRL, MODKEY_SHIFT, MODKEY_ALT, and so on), allowing you to handle shortcuts that use modifiers

 if (ea.getModKeyMask() == osgGA::GUIEventAdapter::MODKEY_CTRL) { // Обработка нажатия клавиши Ctrl } 

It should be borne in mind that the setters of the type setX (), setY (), setEventType (), etc. are not used in the handle () handler. They are called by the OSG low-level graphics windowing system to put an event in a queue.

2. Manage process with keyboard


We are already well able to transform scene objects through the osg :: MatrixTransform classes. We looked at various kinds of animation using the classes osg :: AnimationPath and osg :: Animation. But for the interactivity of the application (for example, gaming), animation and transformations are not enough. The next step is to control the position of objects on the scene from user input devices. Let's try to fasten control to our beloved procession.

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


To solve this problem, we write a class of input event handlers.

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

When constructing this class, as a parameter, it is passed a pointer to the transformation node, which we will affect in the handler. The handle () handler method itself is redefined as follows.

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

Among the essential details of its implementation, it should be noted that, first of all, we need to obtain a transformation matrix from the node we manage.

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

Next, two nested switch () statements analyze the type of event (keystroke) and the key code. Depending on the key code pressed, the current transformation matrix is ​​multiplied by an additional rotation matrix around the corresponding axis.

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

- we turn the plane along the yaw angles by -0.1 radians when pressing the "A" key.

After processing the keystrokes, do not forget to apply a new transformation matrix to the transformation node.

 _model->setMatrix(matrix); 

In the main () function, load the model of the aircraft and create a parent transformation node for it, adding the resulting subgraph to the root node of the scene

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

Create and initialize user input handler.

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

Create a viewer by adding our handler to it.

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

Customize the camera view matrix

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

We prohibit the camera from receiving events from input devices.

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

If this is not done, then the handler hanging on the camera by default will intercept all user input and interfere with our handler. Set the viewer scene data and run it

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

Now, by running the program, we will be able to control the orientation of the aircraft in space by pressing the A, D, W and S keys.



An interesting question is what the handle () method should return when leaving it. If true is returned, then we specify OSG, input events are already processed by us and no further processing is necessary. Most often, we will not be satisfied with this behavior, so it will be good practice to return false from the handler, in order not to interrupt event processing by other handlers, if they are attached to other nodes in the scene.

3. Visitor use in event processing


Similar to how this is implemented when traversing a scene graph when it is updated, OSG supports callbacks for handling events that can be associated with nodes and geometric objects. To do this, use the setEventCallback () and addEventCallback () calls, which take as a parameter a pointer to the child osg :: NodeCallback. To get events in the operator () operator, we can convert the pointer to the visitor of the node passed to it into an osgGA :: EventVisitor pointer, for example:

 #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. Creating and processing custom events


OSG uses an internal event queue (FIFO). Events at the beginning of the queue are processed and removed from it. Newly generated events are placed at the end of the queue. The handle () method of each event handler will be executed as many times as there are events in the queue. The event queue is described by the class osgGA :: EventQueue, among other things, which allows to place an event in a queue at any time by calling the addEvent () method. The argument of this method is a pointer to the osgGA :: GUIEventAdapter, which can be configured for specific behavior using the methods setEventType () and so on.

One of the methods of the osgGA :: EventQueue class is userEvent (), which sets a user event, associating it with user data, a pointer to which is passed to it as a parameter. This data can be used to represent any custom event.

Unable to create your own event queue instance. This instance is already created and attached to the viewer instance, so you can only get a pointer to this singleton

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

User data is a successor object from osg :: Referenced, that is, you can create a smart pointer to it.

When a user event is received, the developer can extract the data from it by calling the getUserData () method and process it at his own discretion.

5. Implementing a custom timer


Many libraries and frameworks that implement GUIs provide a class developer to implement timers that generate an event after a certain time interval. OSG does not contain regular means of implementing timers, so we will try to implement some kind of timer on our own, using the interface to create custom events.

What can we rely on when solving this problem? On a certain periodic event that is constantly generated by the render, for example, on FRAME - the event of drawing the next frame. Let us use for this the same example with switching the model of Cessna from normal to burning.

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


To begin with, we will define the format of the data sent in the user message, defining it as a structure

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

The _count parameter will contain an integer number of milliseconds that has elapsed since the program started at the time of receiving the next timer event. The structure is inherited from the osg :: Referenced class so that it can be managed through smart OSG pointers. Now create an event handler.

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

This handler has several specific protected members. The _switch variable points to a node that switches airplane models; _count - the relative countdown of the time elapsed since the last generation of the timer event, serves to count the time intervals; _startTime - a temporary variable for storing the previous time reference maintained by the viewer; _time - the total time of the program in milliseconds. The class constructor accepts the switch node as a parameter and, optionally, the required time interval for switching timer operation.

In this class, we override the handle () method.

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

Here we analyze the type of message received. If it is FRAME, then the following actions are performed:
  1. We get a pointer to the viewer

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

  1. Upon receipt of the correct pointer, read the time elapsed since the launch of the program

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

calculate the time taken to render a frame in milliseconds

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

and remember the current time count

 _startTime = time; 

If the value of the _count counter has exceeded the required time interval (or this is the first call when _time is still zero), we will place the user message in the queue, passing the program operation time in milliseconds in the structure defined above. Counter _count reset to zero

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

Regardless of the _count value, we have to increase it and _time by the amount of delay required to draw a frame.

 _count += delta; _time += delta; 

This is how the generation of the timer event will be arranged. Event handling is implemented as

 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; 

Here we check the validity of the pointer to the switching node, read data from the event, resulting in the TimerInfo structure, display the contents of the structure on the screen, and switch the status of the node.

The code in the main () function is similar to the code in the previous two examples with switching, with the difference that in this case we hang an event handler on the viewer

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

passing the pointer to the root node and the required switching interval in milliseconds to the handler's constructor. By running the example, we will see that the models switch at intervals of a second, and in the console we find the output of the moments in time at which the switching occurred

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

A custom event can be generated at any time during program execution, and not only when receiving a FRAME event, and this provides a very flexible mechanism for exchanging data between program parts, allows for processing signals from non-standard input devices, such as joysticks or VR gloves, for example.

To be continued...

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