31 January 2019

OpenSceneGraph: Обработка событий

ProgrammingWorking with 3D-graphicsDevelopment for LinuxCGIDevelopment for Windows
Tutorial
image

Введение


Одной из особенностей языка C++, за которую его часто критикуют — отсутствие в стандарте механизма обработки событий. Между тем данный механизм это один из основных путей взаимодействия одних программных компонентов с другими программными компонентами и аппаратным обеспечением, и реализуется он на уровне конкретной ОС. Естественно, что каждая из платформ имеет свои нюансы реализации описанного механизма.

В связи со всем вышеперечисленным, при разработке на C++, возникает потребность в реализации обработки событий тем или иным способом, решаемая за счет использования сторонних библиотек и фреймворков. Всем известный фреймворк Qt предоставляет механизм сигналов и слотов, позволяющий организовать взаимодействие классов, наследуемых от QObject. Реализация событий присутствует и в библиотеке boost. И конечно же в движке OpenSceneGraph не обошлось без собственного «велосипеда», о применении которого и пойдет речь в статье.

OSG является абстрактной графической библиотекой. С одной стороны она абстрагируется от процедурного интерфейса OpenGL, предоставляя разработчику набор классов, инкапсулирующих всю механику OpneGL API. С другой стороны, она абстрагируется и от конкретного графического интерфейса пользователя, так как подходы к его реализации различны для разных платформ и имеют особенности даже в рамках одной и той же платформы (MFC, Qt, .Net для Windows, например).

Не зависимо от платформы, с точки зрения приложения, взаимодействие пользователя с графическим интерфейсом сводится к генерации его элементами последовательности событий, обрабатываемых затем внутри приложения. Большинство графических фреймворков используют такой подход, однако даже в пределах одной платформы они, к сожалению, несовместимы между собой.

По этой причине OSG предоставляет свой собственный базовый интерфейс для обработки событий виджетов графического интерфейса и пользовательского ввода на основе класса osgGA::GUIEventHandler. Этот обработчик может быть прикреплен к вьюверу вызовом метода addEventHandler() и удален методом removeEventHandler(). Естественно, конкретный класс-обработчик должен быть унаследован от класса osgGA::GUIEventHandler, и в нем должен быть переопределен метод handle(). Этот метод принимает на вход два аргумента: osgGA::GUIEventAdapter, содержащий очередь событий от GUI и osg::GUIActionAdepter, используемый для обратной связи. Типичным, при определении является такая конструкция

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

Параметр osgGA::GUIActionAdapter позволяет разработчику попросить GUI выполнить некоторые действия, в ответ на событие. В большинстве случаев через этот параметр воздействуют на вьювер, указатель на который может быть получен динамическим преобразованием указателя

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

1. Обработка событий клавиатуры и мыши


Класс osgGA::GUIEventAdapter() управляет всеми типами событий, поддерживаемых OSG, предоставляя данные для установки и получения их параметров. Метод getEventType() возвращает текущее событие GUI, содержащееся в очереди событий. Каждый раз, переопределяя метод handle() обработчика, при вызове этого методы следует использовать данный геттер для получения события и определения его типа.

Нижеследующая таблица описывает все доступные события

Тип события Описание Методы получения данных события
PUSH/RELEASE/DOUBLECLICK Нажатие/Отпускание и двойной клик кнопок мыши getX(), getY() — получение позиции курсора. getButton() — код нажатой кнопки (LEFT_MOUSE_BUTTON, RIGHT_MOUSE_BUTTON, MIDDLE_MOUSE_BUTTON
SCROLL Скроллинг колесом(ами) мыши getScrollingMotion() — возвращает значения SCROOL_UP, SCROLL_DOWN, SCROLL_LEFT, SCROLL_RIGHT
DRAG Перетаскивание мышью getX(), getY() — позиция курсора; getButtonMask() — значения аналогичные getButton()
MOVE Перемещение мыши getX(), getY() — позиция курсора
KEYDOWN/KEYUP Нажатие/Отпускание клавиши на клавиатуре getKey() — ASCII-код нажатой клавиши или значение перечислителя Key_Symbol (например KEY_BackSpace)
FRAME Событие, генерируемой при отрисовке кадра нет входных данных
USER Событие, определяемое пользователем getUserDataPointer() — возвращает указатель на буфер пользовательских данных (буфер управляется умным указателем)

Существует также метод getModKeyMask() для получения информации о нажатой клавише-модификаторе (возвращает значения вида MODKEY_CTRL, MODKEY_SHIFT, MODKEY_ALT и так далее), позволяя обрабатывать комбинации клавиш, в которых используются модификаторы

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

Следует иметь ввиду, что методы-сеттеры типа setX(), setY(), setEventType() и т.п. не используются в обработчике handle(). Они вызываются низкоуровневой графической оконной системой OSG для помещения события в очередь.

2. Управляем цессной с клавиатуры


Мы уже хорошо умеем трансформировать объекты сцены через классы osg::MatrixTransform. Мы рассмотрели различного рода анимации с помощью классов osg::AnimationPath и osg::Animation. Но для интерактивности приложения (например игрового) анимации и трансформаций явно недостаточно. Следующим шагом будет управление положением объектов на сцене с устройств пользовательского ввода. Попробуем прикрутить к нашей любимой цессне управление.

Пример keyboard
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();
}


Для решения этой задачи пишем класс-обработчик событий ввода

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

При конструировании данного класса, в качестве параметра ему передается указатель на узел трансформации, на который мы будем воздействовать в обработчике. Сам метод-обработчик handle() переопределяем следующим образом

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

Среди существенных деталей его реализации следует отметить, что мы, прежде всего должны получить матрицу трансформации из управляемого нами узла

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

Далее, два вложенных оператора switch() анализируют тип события (нажатие клавиши) и код нажатой клавиши. В зависимости от кода нажатой клавиши происходит умножение текущей матрицы трансформации на дополнительную матрицу поворота вокруг соответствующей оси

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

– поворачиваем самолет по углы рыскания на -0.1 радиана при нажатии на клавишу "A".

После обработки нажатия клавиш не забываем применить к узлу трансформации новую матрицу преобразования

_model->setMatrix(matrix);

В функции main() загружаем модель самолета и создаем для него родительский узел трансформации, добавляя полученный субграф в корневую ноду сцены

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

Теперь, запустив программу мы сможем управлять ориентацией самолета в пространстве нажатием клавиш A, D, W и S.



Интересным является вопрос, что должен возвращать метод handle() при выходе из него. Если будет возвращено true, то тем самым мы указываем OSG, то события ввода нами уже обработаны и дальнейшая их обработка не нужна. Чаще всего нас не устроит такое поведение, поэтому хорошей практикой будет возвращать из обработчика false, дабы не прерывать обработку событий другими обработчиками, если таковые будут прикреплены к другим узлам сцены.

3. Использование посетителей при обработке событий


Аналогично тому, как это реализовано при обходе графа сцены при его обновлении, OSG поддерживает обратные вызовы для обработки событий, которые могут быть связаны с узлами и геометрическими объектами. Для этого применяются вызовы setEventCallback() и addEventCallback(), принимающие в качестве параметра указатель на потомка osg::NodeCallback. Чтобы получить события в операторе operator(), мы можем преобразовать переданный в него указатель на посетителя узла в указатель на osgGA::EventVisitor, например так

#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. Создание и обрабтка пользовательских событий


OSG использует внутреннюю очередь событий (FIFO). События находящиеся в начале очереди обрабатываются и удаляются из неё. Вновь генерируемые события помещаются в конец очереди. Метод handle() каждого обработчика события будет выполнятся столько раз, сколько событий находится в очереди. Очередь событий описывается классом osgGA::EventQueue, среди прочего позволяющий поместить событие в очередь в любой момент времени, вызовом метода addEvent(). Аргументом этого метода является указатель на osgGA::GUIEventAdapter, который можно настроить на определенное поведение методами setEventType() и так далее.

Одним из методов класса osgGA::EventQueue является userEvent(), который задает пользовательское событие, связывая его с пользовательскими данными, указатель на которые передается ему в качестве параметра. Эти данные могут быть использованы для представления любого кастомного события.

Невозможно создать собственный экземпляр очереди событий. Этот экземпляр уже создан и прикреплен к экземпляру вьювера, так что можно только получить указатель на данный синглтон

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

Пользовательские данные — объект наследника от osg::Referenced, то есть на него можно создать умный указатель.

При получении пользовательского события разработчик может извлечь из него данные вызовом метода getUserData() и обрабатывать их по своему усмотрению.

5. Реализация пользовательского таймера


Многие библиотеки и фреймворки, реализующие GUI предоставляют разработчику класса для реализации таймеров, генерирующих событие по истечении определенного временного интервала. OSG не содержит штатных средств реализации таймеров, поэтому попробуем реализовать некое подобие таймера самостоятельно, используя интерфейс для создания пользовательских событий.

На что мы можем опереться при решении данной задачи? На некое периодическое событие, постоянно генерируемое рендером, например на FRAME — событие отрисовки очередного кадра. Воспользуемся для этого все тем же примером с переключением модельки цессны с нормальной на горящую.

Пример timer
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();
}


Для начала определимся с форматом данных, пересылаемых в пользовательском сообщении, определив его как структуру

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

Параметр _count будет содержать целое число миллисекунд, прошедшее с момента запуска программы на момент получения очередного события таймера. Структура наследуется от класса osg::Referenced, чтобы ей можно было управлять через умные указатели OSG. Теперь создаем обработчик событий

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

Этот обработчик имеет несколько специфических защищенных членов. Переменная _switch указывает на узел, переключающий модели самолета; _count — относительный отсчет времени, прошедшего с последней генерации события таймера, служит для отсчета временных интервалов; _startTime — временная переменная для хранения предыдущего отсчета времени, ведущегося вьювером; _time — общее время работы программы в миллисекундах. Конструктор класса принимает в качестве параметра узел-переключатель а так же, опционально, требуемый временной интервал срабатывания таймера переключения.

В этом классе мы переопределяем метод 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;
}

Здесь мы анализируем тип принятого сообщения. Если это FRAME, то выполняются следующие действия:
  1. Получаем указатель на вьювер

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

  1. При получении корректного указателя, считываем время, прошедшее с момента запуска программы

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

вычисляем промежуток времени, затраченный на рендеринг кадра в миллисекундах

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

и запоминаем текущий временной отсчет

_startTime = time;

Если значение счетчика _count превысило требуемый временной интервал (или это первый вызов когда _time ещё равно нулю), мы помещаем пользовательской сообщение в очередь, передав в определенной выше структуре время работы программы в миллисекундах. Счетчик _count сбрасываем в ноль

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

Вне зависимости от значения _count мы должны нарастить его и _time на величину задержки, потребовавшейся на рисование кадра

_count += delta;
_time += delta;

Так будет устроена генерация события таймера. Обработка события реализована так

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;

Здесь мы проверяем на валидность указатель на переключающий узел, вычитываем данные из события, приводя из к структуре TimerInfo, выводим содержимое структуры на экран и переключаем состояние узла.

Код в функции main() аналогичен коду в предыдущих двух примерах с переключением, с той разницей что в данном случае мы вешаем на вьювер обработчик событий

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

передавая конструктору обработчика указатель на узел root и требуемый интервал переключения в миллисекундах. Запустив пример мы увидим, что модели переключаются с интервалом в секунду, а в консоли обнаружим вывод моментов времени, в которые произошло переключение

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

Пользовательское событие может быть сгенерировано в любой момент выполнения программы, а не только при приеме события FRAME, и это дает очень гибкий механизм обмена данными между частями программы, дает возможность обработки сигналов с нестандартных устройств ввода, таких как джойстики или перчатки VR, например.

Продолжение следует...
Tags:3d-графикаграфический движокopenscenegraphc++
Hubs: Programming Working with 3D-graphics Development for Linux CGI Development for Windows
+8
1.1k 19
Comments 5