OpenSceneGraph: основные приемы программирования

в 12:46, , рубрики: c++, openscenegraph, графический движок, паттерны проектирования, Программирование, Работа с 3D-графикой, разработка игр, Разработка под Linux, разработка под windows

image

Введение

В этой статье речь пойдет не столько о графике, сколько о том, каким образом должно быть организовано приложение, её использующее, учитывая специфику движка OpenSceneGraph и предоставляемые им программные средства.

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

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

1. Разбор параметров командной строки

В C/C++ Параметры командной строки передаются через аргументы функции main(). В прошлых примерах мы тщательно помечали эти параметры как неиспользуемые, теперь же воспользуемся ими, чтобы сообщить нашей программе некоторые данные при её запуске.

В OSG есть встроенные средства разбора командной строки.

Создадим следующий пример

Пример command-line

main.h
#ifndef     MAIN_H
#define     MAIN_H

#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif // MAIN_H

main.cpp

#include    "main.h"

int main(int argc, char *argv[])
{
    osg::ArgumentParser args(&argc, argv);
    std::string filename;
    args.read("--model", filename);

    osg::ref_ptr<osg::Node> root = osgDB::readNodeFile(filename);
    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());

    return viewer.run();
}

Задаем параметры запуска программы в QtCreator

OpenSceneGraph: основные приемы программирования - 2

Запустив программу на выполнение получаем результат (моделька грузовика взята из того же OpenSceneGraph-Data)

OpenSceneGraph: основные приемы программирования - 3

Теперь разберем пример построчно

osg::ArgumentParser args(&argc, argv);

создает экземпляр класса парсера командной строки osg::ArgumentParser. При создании конструктору класса передаются аргументы, принимаемые функцией main() от операционной системы.

std::string filename;
args.read("--model", filename);

выполняем разбор аргументов, а именно ищем среди них ключ "–model", помещая его значение в строку filename. Таким образом, посредством этого ключа мы передаем в программу имя файла с трехмерной моделью. Далее мы загружаем эту модель и отображаем её

osg::ref_ptr<osg::Node> root = osgDB::readNodeFile(filename);
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());

return viewer.run();

Метод read() класса osg::ArgumentParser имеет массу перегрузок, позволяющих читать из командной строки не только строковые значения, но и целые числа, числа с плавающей запятой, векторы и т.д. Например, можно прочитать некий параметр типа float

float	size = 0.0f;
args.read("--size", size);

Если в командной строке не окажется данного параметра, то его значение останется таким, каким было после инициализации переменной size.

2. Механизм уведомлений и логирования

OpenSceneGraph имеет механизм уведомлений, позволяющий выводить отладочные сообщения в процессе выполнения рендеринга, а так же инициированные разработчиком. Это серьезное подспорье при трассировке и отладке программы. Система уведомлений OSG поддерживает вывод диагностической информации (ошибки, предупреждения, уведомления) на уровне ядра движка и плагинов к нему. Разработчик может вывести диагностическое сообщение в процессе работы программы, воспользовавшись функцией osg::notify().

Данная функция работает как стандартный поток вывода стандартной библиотеки C++ через перегрузку оператора <<. В качестве аргумента она принимает уровень сообщения: ALWAYS, FATAL, WARN, NOTICE, INFO, DEBUG_INFO и DEBUG_FP. Например

osg::notify(osg::WARN) << "Some warning message" << std::endl;

выводит предупреждение с определенным пользователем текстом.

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

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

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

Пример notify

main.h


#ifndef     MAIN_H
#define     MAIN_H

#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>
#include    <fstream>

#endif  //  MAIN_H

main.cpp

#include    "main.h"

class LogFileHandler : public osg::NotifyHandler
{
public:

    LogFileHandler(const std::string &file)
    {
        _log.open(file.c_str());
    }

    virtual ~LogFileHandler()
    {
        _log.close();
    }

    virtual void notify(osg::NotifySeverity severity, const char *msg)
    {
        _log << msg;
    }

protected:

    std::ofstream   _log;
};

int main(int argc, char *argv[])
{
    osg::setNotifyLevel(osg::INFO);
    osg::setNotifyHandler(new LogFileHandler("../logs/log.txt"));

    osg::ArgumentParser args(&argc, argv);
    osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args);

    if (!root)
    {
        OSG_FATAL << args.getApplicationName() << ": No data loaded." << std::endl;
        return -1;
    }

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());

    return viewer.run();
}

Для перенаправления вывода напишем класс LogFileHandler, являющийся наследником osg::NotifyHandler. Конструктор и деструктор этого класса управляют открытием и закрытием потока вывода _log, с которым связывается текстовый файл. Метод notify() есть аналогичный метод базового класса, переопределенный нами для вывода в файл уведомлений, передаваемых OSG в процессе работы через параметр msg.

Класс LogFileHandler

class LogFileHandler : public osg::NotifyHandler
{
public:

    LogFileHandler(const std::string &file)
    {
        _log.open(file.c_str());
    }

    virtual ~LogFileHandler()
    {
        _log.close();
    }

    virtual void notify(osg::NotifySeverity severity, const char *msg)
    {
        _log << msg;
    }

protected:

    std::ofstream   _log;
};

Далее, в основной программе выполняем необходимые настройки

osg::setNotifyLevel(osg::INFO);

устанавливаем уровень уведомлений INFO, то есть вывод в лог всей информации о работе движка, включая текущие уведомления о нормальной работе.

osg::setNotifyHandler(new LogFileHandler("../logs/log.txt"));

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

osg::ArgumentParser args(&argc, argv);
osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args);

if (!root)
{
	OSG_FATAL << args.getApplicationName() << ": No data loaded." << std::endl;
	return -1;
}

При этом обрабатываем ситуацию отсутствия данных в командной строке, выводя сообщение в лог в ручном режиме посредством макроса OSG_FATAL. Запускаем программу со следующими аргументами

OpenSceneGraph: основные приемы программирования - 4

получая вывод в файл лога наподобие этого

Пример лога OSG

Opened DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_osgd.dll
CullSettings::readEnvironmentalVariables()
CullSettings::readEnvironmentalVariables()
Opened DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_deprecated_osgd.dll
OSGReaderWriter wrappers loaded OK
CullSettings::readEnvironmentalVariables()
void StateSet::setGlobalDefaults()
void StateSet::setGlobalDefaults() ShaderPipeline disabled.
   StateSet::setGlobalDefaults() Setting up GL2 compatible shaders
CullSettings::readEnvironmentalVariables()
CullSettings::readEnvironmentalVariables()
CullSettings::readEnvironmentalVariables()
CullSettings::readEnvironmentalVariables()
ShaderComposer::ShaderComposer() 0xa5ce8f0
CullSettings::readEnvironmentalVariables()
ShaderComposer::ShaderComposer() 0xa5ce330
View::setSceneData() Reusing existing scene0xa514220
 CameraManipulator::computeHomePosition(0, 0)
    boundingSphere.center() = (-6.40034 1.96225 0.000795364)
    boundingSphere.radius() = 16.6002
 CameraManipulator::computeHomePosition(0xa52f138, 0)
    boundingSphere.center() = (-6.40034 1.96225 0.000795364)
    boundingSphere.radius() = 16.6002
Viewer::realize() - No valid contexts found, setting up view across all screens.
Applying osgViewer::ViewConfig : AcrossAllScreens
.
.
.
.
ShaderComposer::~ShaderComposer() 0xa5ce330
ShaderComposer::~ShaderComposer() 0xa5ce8f0
ShaderComposer::~ShaderComposer() 0xa5d6228
close(0x1)0xa5d3e50
close(0)0xa5d3e50
ContextData::unregisterGraphicsContext 0xa5d3e50
DatabasePager::RequestQueue::~RequestQueue() Destructing queue.
DatabasePager::RequestQueue::~RequestQueue() Destructing queue.
DatabasePager::RequestQueue::~RequestQueue() Destructing queue.
DatabasePager::RequestQueue::~RequestQueue() Destructing queue.
ShaderComposer::~ShaderComposer() 0xa5de4e0
close(0x1)0xa5ddba0
close(0)0xa5ddba0
ContextData::unregisterGraphicsContext 0xa5ddba0
Done destructing osg::View
DatabasePager::RequestQueue::~RequestQueue() Destructing queue.
DatabasePager::RequestQueue::~RequestQueue() Destructing queue.
DatabasePager::RequestQueue::~RequestQueue() Destructing queue.
DatabasePager::RequestQueue::~RequestQueue() Destructing queue.
Closing DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_osgd.dll
Closing DynamicLibrary osgPlugins-3.7.0/mingw_osgdb_deprecated_osgd.dll

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

По-умолчанию OSG посылает сообщения в стандартный вывод std::cout и сообщения об ошибках в поток std::cerr. Однако, посредством переопределения обработчика уведомлений, как было показано в примере, этот вывод можно перенаправить в любой поток вывода, в том числе и в элементы графического интерфейса.

Следует помнить, что при установке высокого уровня уведомлений (например FATAL) система игнорирует все уведомления более низкого уровня. Например, в подобном случае

osg::setNotifyLevel(osg::FATAL);
.
.
.
osg::notify(osg::WARN) << "Some message." << std::endl;

пользовательское сообщение просто не будет выведено.

3. Перехват геометрических атрибутов

Класс osg::Geometry управляет множеством данных, описывающих вершины и отображает полигональную сетку с использованием упорядоченного набора примитивов. Однако, данный класс не имеет представления о таких элементах топологии модели как грани, ребра и соотношения между ними. Этот нюанс мешает реализации таких вещей как перемещение определенных граней, например при анимировании моделей. В настоящее время OSG не поддерживает такую функциональность.

Однако в движке реализован ряд функторов, позволяющих перечитать атрибуты геометрии любого объекта и использовать их в целях моделирования топологии полигональной сетки. В C++ функтором называется конструкция, позволяющая использовать объект как функцию.

Класс osg::Drawable предоставляет разработчику четыре типа функторов:

  1. osg::Drawable::AttributeFunctor — читает атрибуты вершин как массив указателей. Он имеет ряд виртуальных методов для применения атрибутов вершин разных типов данных. Для использования этого функтора необходимо описать класс и переопределить один или более его методов, внутри которых выполняются требуемые разработчику действия
virtual void apply( osg::Drawable::AttributeType type, 
					unsigned int size, osg::Vec3* ptr )
{
	// Читаем 3-векторы в буфер с указателем ptr.
	// Первый параметр определяет тип атрибута
}

  1. osg::Drawable::ConstAttributeFunctor — read-only версия предыдущего функтора: указатель на массив векторов передается как константный параметр
  2. osg::PrimitiveFunctor — имитирует процесс рендеринга объектов OpenGL. Под видом рендеринга объекта производится вызов переопределенных разработчиком методов функтора. Этот функтор имеет два важных шаблонных подкласса: osg::TemplatePrimitiveFunctor<> и osg::TriangleFunctor<>. Эти классы получают в качестве параметров вершины примитива и передают их в пользовательские методы с применением оператора operator().
  3. osg::PrimitiveIndexFunctor — выполняет те же действия, что и предыдущий функтор, но в качестве параметра принимает индексы вершин примитива.

Классы, производные от osg::Drawable, такие как osg::ShapeDrawable и osg::Geometry имеют метод accept() позволяющий применить различные функторы.

4. Пример использования функтора примитивов

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

Пример functor

main.h


#ifndef     MAIN_H
#define     MAIN_H

#include    <osg/Geode>
#include    <osg/Geometry>
#include    <osg/TriangleFunctor>
#include    <osgViewer/Viewer>

#include    <iostream>

#endif

main.cpp

#include    "main.h"

std::string vec2str(const osg::Vec3 &v)
{
    std::string tmp = std::to_string(v.x());
    tmp += " ";
    tmp += std::to_string(v.y());
    tmp += " ";
    tmp += std::to_string(v.z());

    return tmp;
}

struct FaceCollector
{
    void operator()(const osg::Vec3 &v1,
                    const osg::Vec3 &v2,
                    const osg::Vec3 &v3)
    {
        std::cout << "Face vertices: "
                  << vec2str(v1)
                  << "; " << vec2str(v2)
                  << "; " << vec2str(v3) << std::endl;
    }
};

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
    vertices->push_back( osg::Vec3(0.0f, 0.0f, 0.0f) );
    vertices->push_back( osg::Vec3(0.0f, 0.0f, 1.0f) );
    vertices->push_back( osg::Vec3(1.0f, 0.0f, 0.0f) );
    vertices->push_back( osg::Vec3(1.0f, 0.0f, 1.5f) );
    vertices->push_back( osg::Vec3(2.0f, 0.0f, 0.0f) );
    vertices->push_back( osg::Vec3(2.0f, 0.0f, 1.0f) );
    vertices->push_back( osg::Vec3(3.0f, 0.0f, 0.0f) );
    vertices->push_back( osg::Vec3(3.0f, 0.0f, 1.5f) );
    vertices->push_back( osg::Vec3(4.0f, 0.0f, 0.0f) );
    vertices->push_back( osg::Vec3(4.0f, 0.0f, 1.0f) );

    osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
    normals->push_back( osg::Vec3(0.0f, -1.0f, 0.0f) );

    osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
    geom->setVertexArray(vertices.get());
    geom->setNormalArray(normals.get());
    geom->setNormalBinding(osg::Geometry::BIND_OVERALL);
    geom->addPrimitiveSet(new osg::DrawArrays(GL_QUAD_STRIP, 0, 10));

    osg::ref_ptr<osg::Geode> root = new osg::Geode;
    root->addDrawable(geom.get());

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());

    osg::TriangleFunctor<FaceCollector> functor;
    geom->accept(functor);

    return viewer.run();
}

Опуская рассмотренный нами многократно процесс создания геометрии обратим внимание на следующее. Мы определяем структуру FaceCollector, для которой переопределяем оператор operator() следующим образом

struct FaceCollector
{
    void operator()(const osg::Vec3 &v1,
                    const osg::Vec3 &v2,
                    const osg::Vec3 &v3)
    {
        std::cout << "Face vertices: "
                  << vec2str(v1)
                  << "; " << vec2str(v2)
                  << "; " << vec2str(v3) << std::endl;
    }
};

Данный оператор, при вызове будет выводить на экран координаты трех вершин, передаваемых ему движком. Функция vec2str необходима для перевода компонент вектора osg::Vec3 в std::string. Для вызова функтора создадим его экземпляр и передадим его объекту геометрии через метод accept()

osg::TriangleFunctor<FaceCollector> functor;
geom->accept(functor);

Данный вызов, как говорилось выше, имитирует отрисовку геометрии, подменяя саму отрисовку вызовом переопределенного метода функтора. В данном случае он будет вызываться при "отрисовке" каждого из треугольников, из которых составлена геометрия примера.

На экране мы получим такую геометрию

OpenSceneGraph: основные приемы программирования - 5

и такой выхлоп в консоль

Face vertices: 0.000000 0.000000 0.000000; 0.000000 0.000000 1.000000; 1.000000 0.000000 0.000000
Face vertices: 0.000000 0.000000 1.000000; 1.000000 0.000000 1.500000; 1.000000 0.000000 0.000000
Face vertices: 1.000000 0.000000 0.000000; 1.000000 0.000000 1.500000; 2.000000 0.000000 0.000000
Face vertices: 1.000000 0.000000 1.500000; 2.000000 0.000000 1.000000; 2.000000 0.000000 0.000000
Face vertices: 2.000000 0.000000 0.000000; 2.000000 0.000000 1.000000; 3.000000 0.000000 0.000000
Face vertices: 2.000000 0.000000 1.000000; 3.000000 0.000000 1.500000; 3.000000 0.000000 0.000000
Face vertices: 3.000000 0.000000 0.000000; 3.000000 0.000000 1.500000; 4.000000 0.000000 0.000000
Face vertices: 3.000000 0.000000 1.500000; 4.000000 0.000000 1.000000; 4.000000 0.000000 0.000000

Фактически, при вызове geom->accept(…) отрисовки треугольников не происходит, вызовы OpenGL имитируются, а вместо них выводятся данные о вершинах треугольника, отрисовка которого имитируется

OpenSceneGraph: основные приемы программирования - 6

Класс osg::TemplatePrimitiveFunctor собирает данные не только о треугольниках, но и о любых других примитивах OpenGL. Для реализации обработки этих данных необходимо переопределить следующие операторы в аргументе шаблона

// Для точек
void operator()( const osg::Vec3&, bool );
// Для линий
void operator()( const osg::Vec3&, const osg::Vec3&, bool );
// Для треугольников
void operator()( const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, bool );
// Для четырехугольников
void operator()( const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, const osg::Vec3&, bool );

5. Паттерн "Посетитель"

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

Для реализации данного механизма в OSG определен класс osg::NodeVisitor. Класс, унаследованный от osg::NodeVisitor перемещается по графу сцены, посещает каждый узел и применяет к нему определенные разработчиком операции. Это основной класс, используемый для вмешательство в процесс обновления узлов и отсечения невидимых узлов, а так же применения некоторых других операций, связанных с модификацией геометрии узлов сцены, таких как osgUtil::SmoothingVisitor, osgUtil::Simplifier и osgUtil::TriStripVisitor.

Для создания подкласса посетителя, мы должны переопределить один или несколько виртуальных перегружаемых методов apply(), предоставляемых базовым классом osg::NodeVisitor. Эти методы имеются у большинства основных типов узлов OSG. Посетитель автоматически вызовет метод apply() для каждого из посещенный при обходе графа сцены узлов. Разработчик переопределяет метод apply() для каждого из необходимых ему типов узлов.

В реализации метода apply() разработчик, в соответствующий момент, должен вызвать метод traverse() базового класса osg::NodeVisitor. Это инициирует переход посетителя к следующему узлу, либо дочернему, либо соседнему по уровню иерархии, если текущий узел не имеет дочерних узлов, на которые можно осуществить переход. Отсутствие вызова traverse() означает остановку обхода графа сцены и оставшаяся часть графа сцены игнорируется.

Перегрузки метода apply() имеют унифицированные форматы

virtual void apply( osg::Node& );
virtual void apply( osg::Geode& );
virtual void apply( osg::Group& );
virtual void apply( osg::Transform& );

Чтобы обойти подграф текущего узла, для объекта-посетителя, необходимо задать режим обхода, например так

ExampleVisitor visitor;
visitor->setTraversalMode( osg::NodeVisitor::TRAVERSE_ALL_CHILDREN );
node->accept( visitor );

Режим обхода задается несколькими перечислителями

  1. TRAVERSE_ALL_CHILDREN — перемещение по всем дочерним узлам.
  2. TRAVERSE_PARENTS — проход назад от текущего узла, не доходя до корневого узла
  3. TRAVERSE_ACTIVE_CHILDREN — обход исключительно активных узлов, то есть тех, видимость которых активирована через узел osg::Switch.

6. Анализ структуры горящей цессны

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

Пример functor

main.h


#ifndef		MAIN_H
#define		MAIN_H

#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>
#include    <iostream>

#endif

main.cpp

#include	"main.h"

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
class InfoVisitor : public osg::NodeVisitor
{
public:

    InfoVisitor() : _level(0)
    {
        setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN);
    }

    std::string spaces()
    {
        return std::string(_level * 2, ' ');
    }

    virtual void apply(osg::Node &node);

    virtual void apply(osg::Geode &geode);

protected:

    unsigned int _level;
};

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void InfoVisitor::apply(osg::Node &node)
{
    std::cout << spaces() << node.libraryName() << "::"
              << node.className() << std::endl;

    _level++;
    traverse(node);
    _level--;
}

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void InfoVisitor::apply(osg::Geode &geode)
{
    std::cout << spaces() << geode.libraryName() << "::"
              << geode.className() << std::endl;

    _level++;

    for (unsigned int i = 0; i < geode.getNumDrawables(); ++i)
    {
        osg::Drawable *drawable = geode.getDrawable(i);

        std::cout << spaces() << drawable->libraryName() << "::"
                  << drawable->className() << std::endl;
    }

    traverse(geode);
    _level--;
}

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
int main(int argc, char *argv[])
{
    osg::ArgumentParser args(&argc, argv);
    osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args);

    if (!root.valid())
    {
        OSG_FATAL << args.getApplicationName() << ": No data leaded. " << std::endl;
        return -1;
    }
    
    InfoVisitor infoVisitor;
    root->accept(infoVisitor);

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());

    return viewer.run();
}

Создаем класс InfoVisitor, наследуя его от osg::NodeVisitor

class InfoVisitor : public osg::NodeVisitor
{
public:

    InfoVisitor() : _level(0)
    {
        setTraversalMode(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN);
    }

    std::string spaces()
    {
        return std::string(_level * 2, ' ');
    }

    virtual void apply(osg::Node &node);

    virtual void apply(osg::Geode &geode);

protected:

    unsigned int _level;
};

Защищенное свойство _level будет указывать на тот уровень графа сцены, на котором в данный момент находится наш класс-посетитель. В конструкторе инициализируем счетчик уровня и задаем режим обхода узлов — обходить все дочерние узлы.

Теперь переопределяем методы apply() для узлов

void InfoVisitor::apply(osg::Node &node)
{
    std::cout << spaces() << node.libraryName() << "::"
              << node.className() << std::endl;

    _level++;
    traverse(node);
    _level--;
}

Здесь мы будем выводить тип текущего узла. Метод libraryName() для узла выводит имя библиотеки OSG, где реализован данный узел, а метод className — имя класса узла. Эти методы реализованы за счет применения макросов в коде библиотек OSG.

std::cout << spaces() << node.libraryName() << "::"
              << node.className() << std::endl;

После этого мы наращиваем счетчик уровней графа и вызываем метод traverse() инициируя переход на уровень выше, к дочерней ноде. После возврата из traverse() мы снова уменьшаем значение счетчика. Нетрудно догадаться, что traverse() инициирует повторный вызов метода apply() повторный traverse() уже для подграфа, начинающегося с текущего узла. Мы получаем рекурсивное выполнение посетителя, пока не упремся в оконечные узлы графа сцены.

Для оконечного узла типа osg::Geode переопределяется своя перегрузка метода apply()

void InfoVisitor::apply(osg::Geode &geode)
{
    std::cout << spaces() << geode.libraryName() << "::"
              << geode.className() << std::endl;

    _level++;

    for (unsigned int i = 0; i < geode.getNumDrawables(); ++i)
    {
        osg::Drawable *drawable = geode.getDrawable(i);

        std::cout << spaces() << drawable->libraryName() << "::"
                  << drawable->className() << std::endl;
    }

    traverse(geode);
    _level--;
}

c аналогично работающим кодом, за исключением того, что мы выводим на экран данные о всех геометрических объектах, прикрепленных к текущему геометрическому узлу

for (unsigned int i = 0; i < geode.getNumDrawables(); ++i)
{
    osg::Drawable *drawable = geode.getDrawable(i);

    std::cout << spaces() << drawable->libraryName() << "::"
              << drawable->className() << std::endl;
}

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

osg::ArgumentParser args(&argc, argv);
osg::ref_ptr<osg::Node> root = osgDB::readNodeFiles(args);

if (!root.valid())
{
    OSG_FATAL << args.getApplicationName() << ": No data leaded. " << std::endl;
    return -1;
}

При этом мы обрабатываем ошибки, связанные с отсутствием имен фалов моделей в командной строке. Теперь мы создаем класс-посетитель и передаем его в граф сцены для выполнения

InfoVisitor infoVisitor;
root->accept(infoVisitor);

Далее идут действия по запуску вьювера, которые мы уже проделывали множество раз. После запуска программы с параметрами

$ visitor ../data/cessnafire.osg

мы увидим следующий вывод в консоль

osg::Group
  osg::MatrixTransform
    osg::Geode
      osg::Geometry
      osg::Geometry
    osg::MatrixTransform
      osgParticle::ModularEmitter
      osgParticle::ModularEmitter
  osgParticle::ParticleSystemUpdater
  osg::Geode
    osgParticle::ParticleSystem
    osgParticle::ParticleSystem
    osgParticle::ParticleSystem
    osgParticle::ParticleSystem

По сути мы получили полное дерево загруженной сцены. Позвольте, откуда столько узлов? Всё очень просто — модели формата *.osg сами по себе являются контейнерами, в которых хранятся не только данные о геометрии модели, но и прочая информация о её структуре в виде подграфа сцены OSG. Геометрия модели, трансформации, эффекты частиц, которыми реализованы дым и пламя — всё это узлы графа сцены OSG. Любая сцена может быть как загружена из *.osg, так и выгружена из вьювера в формат *.osg.

Это простой пример применения механики посетителей. На самом деле внутри посетителей можно выполнять массу операций по модификации узлов при выполнении программы.

7. Управление поведением узлов графа сцены через переопределение метода traverse()

Важным приемом работы с OSG является переопределение метода traverse(). Этот метод вызывается каждый раз, когда происходит отрисовка кадра. Они принимает параметр типа osg::NodeVisitor& который сообщает, какой проход графа сцены выполняется в данный момент (обновление, обработка событий или отсечение). Большинство узлов OSG переопределяют этот метод для реализации своего функционала.

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

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

Пример animswitch

main.h


#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/Switch>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif

main.cpp

#include	"main.h"

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
class AnimatingSwitch : public osg::Switch
{
public:

    AnimatingSwitch() : osg::Switch(), _count(0) {}

    AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) :
        osg::Switch(copy, copyop), _count(copy._count) {}

    META_Node(osg, AnimatingSwitch);

    virtual void traverse(osg::NodeVisitor &nv);

protected:

    unsigned int _count;
};

void AnimatingSwitch::traverse(osg::NodeVisitor &nv)
{
    if (!((++_count) % 60) )
    {
        setValue(0, !getValue(0));
        setValue(1, !getValue(1));
    }

    osg::Switch::traverse(nv);
}

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
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<AnimatingSwitch> root = new AnimatingSwitch;
    root->addChild(model1.get(), true);
    root->addChild(model2.get(), false);

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());

    return viewer.run();
}

Разберем этот пример по полочкам. Мы создаем новый класс AnimatingSwitch, который наследует от osg::Switch.

class AnimatingSwitch : public osg::Switch
{
public:

    AnimatingSwitch() : osg::Switch(), _count(0) {}

    AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) :
        osg::Switch(copy, copyop), _count(copy._count) {}

    META_Node(osg, AnimatingSwitch);

    virtual void traverse(osg::NodeVisitor &nv);

protected:

    unsigned int _count;
};

void AnimatingSwitch::traverse(osg::NodeVisitor &nv)
{
    if (!((++_count) % 60) )
    {
        setValue(0, !getValue(0));
        setValue(1, !getValue(1));
    }

    osg::Switch::traverse(nv);
}

Этот класс содержит конструктор по-умолчанию

AnimatingSwitch() : osg::Switch(), _count(0) {}

и конструктор для копирования, созданный в соответствии с требованиями OSG

AnimatingSwitch(const AnimatingSwitch &copy, const osg::CopyOp &copyop = osg::CopyOp::SHALLOW_COPY) :
        osg::Switch(copy, copyop), _count(copy._count) {}

Конструктор для копирования должен содержать в качестве параметров: константную ссылку на экземпляр класса, подлежащий копированию и параметра osg::CopyOp, задающий настройки копирования класса. Далее следуют довольно странные письмена

META_Node(osg, AnimatingSwitch);

Это макрос, формирующий необходимую для наследника класса, производного от osg::Node структуру. Пока не придаем значения этому макросу — важно что он должен присутствовать при наследовании от osg::Switch при определении всех классов-потомков. Класс содержит защищенное поле _count — тот самый счетчик, на основе которого мы выполняем переключение. Переключение реализуем при переопределении метода traverse()

void AnimatingSwitch::traverse(osg::NodeVisitor &nv)
{
    if (!((++_count) % 60) )
    {
        setValue(0, !getValue(0));
        setValue(1, !getValue(1));
    }

    osg::Switch::traverse(nv);
}

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

OpenSceneGraph: основные приемы программирования - 7

Поскольку метод traverse() постоянно переопределяется для различных типов узлов, он должен предоставлять механизм для получения матриц преобразования и состояния рендера для дальнейшего использования их реализуемом перегрузкой алгоритме. Входной параметра osg::NodeVisitor является ключом к различным операциям с узлами. Он, в частности, указывает на тип текущего обхода графа сцены, таких как обновление, обработка событий и отсечение невидимых граней. Первые два связаны с обратными вызовами узлов и будут рассмотрены при изучении анимации.

Проход отсечения может быть идентифицирован путем преобразования объекта osg::NodeVisitor к osg::CullVisitor


osgUtil::CullVisitor *cv = dynamic_cast<osgUtil::CullVisitor *>(&nv);

if (cv)
{
	/// Выполняем что-то тут, характерное для обработки отсечения
}

8. Механизм обратных вызовов (Callback)

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

В движке существует несколько типов обратных вызовов. Обратные вызовы реализуются специальными классами, среди которых osg::NodeCallback предназначен для обработки процесса обновления узлов сцены, а osg::Drawable::UpdateCallback, osg::Drawable::EventCallback и osg::Drawable:CullCallback — выполняют те же функции, но для объектов геометрии.

Класс osg::NodeCallback имеет переопределяемый виртуальный оператор operator(), предоставляемый разработчку для реализации собственного функционала. Чтобы обратный вызов срабатывал, необходимо прикрепить экземпляр класса вызова к тому узлу, для который будет обрабатываться вызовом метода setUpdateCallback() или addUpdateCallback(). Оператор operator() автоматически вызывается во время обновления узлов графа сцены при рендеринге каждого кадра.

Нижеследующая таблица представляет перечень обратных вызовов, доступных разработчику в OSG

Имя Функтор обратного вызова Виртуальный метод Метод для присоединения к объекту
Оновление узла osg::NodeCallback operator() osg::Node::setUpdateCallback()
Событие узла osg::NodeCallback operator() osg::Node::setEventCallback()
Отсечение узла osg::NodeCallback operator() osg::Node::setCullCallback()
Обновление геометрии osg::Drawable::UpdateCallback update() osg::Drawable::setUpdateCallback()
Событие геометрии osg::Drawable::EventCallback event() osg::Drawable::setEventCallback()
Отсечение геометрии osg::Drawable::CullCallback cull() osg::Drawable::setCullCallback()
Обновление атрибутов osg::StateAttributeCallback operator() osg::StateAttribute::setUpdateCallback()
Событие атрибутов osg::StateAttributeCallback operator() osg::StateAttribute::setEventCallback()
Общее обновление osg::Uniform::Callback operator() osg::Uniform::setUpdateCallback()
Общее событие osg::Uniform::Callback operator() osg::Uniform::setEvevtCallback()
Обратный вызов для камеры перед отрисовкой osg::Camera::DrawCallback operator() osg::Camera::PreDrawCallback()
Обратный вызов для камеры после отрисовки osg::Camera::DrawCallback operator() osg::Camera::PostDrawCallback()

9. Переключение osg::Switch при обновлении дерева сцены

Чуть выше мы писали пример с переключением двух моделей самолетов. Теперь мы повторим этот пример, но сделаем всё правильно, используя механизм обратных вызовов OSG.

Пример callbackswith

main.h

#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/Switch>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif

main.cpp

#include	"main.h"

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
class SwitchingCallback : public osg::NodeCallback
{
public:

    SwitchingCallback() : _count(0) {}

    virtual void operator()(osg::Node *node, osg::NodeVisitor *nv);

protected:

    unsigned int _count;
};

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void SwitchingCallback::operator()(osg::Node *node, osg::NodeVisitor *nv)
{
    osg::Switch *switchNode = static_cast<osg::Switch *>(node);

    if ( !((++_count) % 60) && switchNode )
    {
        switchNode->setValue(0, !switchNode->getValue(0));
        switchNode->setValue(1, !switchNode->getValue(0));
    }

    traverse(node, nv);
}

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
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, true);
    root->addChild(model2, false);

    root->setUpdateCallback( new SwitchingCallback );

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());
    
    return viewer.run();
}

Необходимо создать класс, унаследовав его от osg::NodeCallback, управляющий узлом osg::Switch

class SwitchingCallback : public osg::NodeCallback
{
public:

    SwitchingCallback() : _count(0) {}

    virtual void operator()(osg::Node *node, osg::NodeVisitor *nv);

protected:

    unsigned int _count;
};

Счетчик _count будет управлять переключением узла osg::Switch с отображения одной дочерней ноды на другую, в зависимости от своего значения. В конструкторе мы инициализируем счетчик, а виртуальный метод operator() переопределяем

void SwitchingCallback::operator()(osg::Node *node, osg::NodeVisitor *nv)
{
    osg::Switch *switchNode = static_cast<osg::Switch *>(node);

    if ( !((++_count) % 60) && switchNode )
    {
        switchNode->setValue(0, !switchNode->getValue(0));
        switchNode->setValue(1, !switchNode->getValue(0));
    }

    traverse(node, nv);
}

Узел, на котором сработал вызов передается в него через параметр node. Поскольку мы точно знаем, что это будет узел типа osg::Switch, мы выполняем статическое приведение указателя на node к указателю на узел-переключатель

osg::Switch *switchNode = static_cast<osg::Switch *>(node);

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

if ( !((++_count) % 60) && switchNode )
{
    switchNode->setValue(0, !switchNode->getValue(0));
    switchNode->setValue(1, !switchNode->getValue(0));
}

Не забываем вызвать метод traverse() для продолжение рекурсивного обхода графа сцены

traverse(node, nv);

Остальной код программы тривиален, за исключением строчки

root->setUpdateCallback( new SwitchingCallback );

где мы назначаем созданный нами обратный вызов узлу root с типом osg::Switch. Работа программы аналогична предыдущему примеру

OpenSceneGraph: основные приемы программирования - 8

До сих пор мы использовали таинственный метод traverse() для двух целей: переопределение этого метода в классах-наследниках и вызов этого метода в у класса osg::NodeVisitor, чтобы продолжить обход графа сцены.

В только что рассмотренном примере мы применяем третий вариант вызова traverse(), с передачей в качестве параметров указателя на узел и указателя на экземпляр посетителя. Как и в первых двух случаях, при отсутствии вызова traverse() на данном узле будет прекращен обход графа сцены.

Для добавления обратного вызова к узлу служит также методы addUpdateCallback(). В отличие от setUpdateCallback() он применяется для добавления ещё одного обратного вызова к уже существующим. Таким образом может существовать несколько обратных вызовов для одного и того же узла.

Заключение

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

Продолжение следует...

Автор: Дмитрий Притыкин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js