Хабрахабр

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

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

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

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

В 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-Data)

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

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

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

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) 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. Запускаем программу со следующими аргументами

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

Пример лога 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;

пользовательское сообщение просто не будет выведено.
Класс 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() позволяющий применить различные функторы.
Проиллюстрируем описанный функционал, на примере сбора информации о треугольных гранях и точках некоторой, определенной нами заранее геометрии.

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

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

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

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

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 имитируются, а вместо них выводятся данные о вершинах треугольника, отрисовка которого имитируется

Для реализации обработки этих данных необходимо переопределить следующие операторы в аргументе шаблона Класс 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 );

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

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

Эти методы имеются у большинства основных типов узлов OSG. Для создания подкласса посетителя, мы должны переопределить один или несколько виртуальных перегружаемых методов apply(), предоставляемых базовым классом osg::NodeVisitor. Разработчик переопределяет метод 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.

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

Пример 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.

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

Важным приемом работы с 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. Компилируем пример и запускаем его

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

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


osgUtil::CullVisitor *cv = dynamic_cast<osgUtil::CullVisitor *>(&nv); if (cv)
{ /// Выполняем что-то тут, характерное для обработки отсечения
}

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

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

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

Чуть выше мы писали пример с переключением двух моделей самолетов. Теперь мы повторим этот пример, но сделаем всё правильно, используя механизм обратных вызовов 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. Работа программы аналогична предыдущему примеру

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть