OpenSceneGraph: Event Handling

  • Tutorial
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

boolhandle(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 the 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"//------------------------------------------------------------------------------////------------------------------------------------------------------------------classModelController :public osgGA::GUIEventHandler
{
public:
    ModelController( osg::MatrixTransform *node ) : _model(node) {}
    virtualboolhandle(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())
        returnfalse;
    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;
    }
    returntrue;
}
//------------------------------------------------------------------------------////------------------------------------------------------------------------------intmain(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.

classModelController :public osgGA::GUIEventHandler
{
public:
    ModelController( osg::MatrixTransform *node ) : _model(node) {}
    virtualboolhandle(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())
        returnfalse;
    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;
    }
    returnfalse;
}

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 for it the parent transformation node, 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 exiting 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>
...
voidoperator()( 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 the event to be queued 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 a specific behavior using the setEventType () method 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 retrieve data from it by calling the getUserData () method and process it as it wishes.

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"//------------------------------------------------------------------------------////------------------------------------------------------------------------------structTimerInfo :public osg::Referenced
{
    TimerInfo(unsignedint c) : _count(c) {}
    unsignedint _count;
};
//------------------------------------------------------------------------------////------------------------------------------------------------------------------classTimerHandler :public osgGA::GUIEventHandler
{
public:
    TimerHandler(osg::Switch *sw, unsignedint interval = 1000)
        : _switch(sw)
        , _count(0)
        , _startTime(0.0)
        , _interval(interval)
        , _time(0)
    {
    }
    virtualboolhandle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa);
protected:
    osg::ref_ptr<osg::Switch> _switch;
    unsignedint _count;
    double _startTime;
    unsignedint _interval;
    unsignedint _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();
        unsignedint delta = static_cast<unsignedint>( (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;
    }
    returnfalse;
}
//------------------------------------------------------------------------------////------------------------------------------------------------------------------intmain(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

structTimerInfo :public osg::Referenced
{
    TimerInfo(unsignedint c) : _count(c) {}
    unsignedint _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.

classTimerHandler :public osgGA::GUIEventHandler
{
public:
    TimerHandler(osg::Switch *sw, unsignedint interval = 1000)
        : _switch(sw)
        , _count(0)
        , _startTime(0.0)
        , _interval(interval)
        , _time(0)
    {
    }
    virtualboolhandle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa);
protected:
    osg::ref_ptr<osg::Switch> _switch;
    unsignedint _count;
    double _startTime;
    unsignedint _interval;
    unsignedint _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();
        unsignedint delta = static_cast<unsignedint>( (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;
    }
    returnfalse;
}

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

unsignedint delta = static_cast<unsignedint>( (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 user 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 signal processing from non-standard input devices, such as joysticks or VR gloves, for example.

To be continued...

Also popular now: