Введение в OpenSceneGraph: Применение паттерна посетитель
Паттерн посетитель испльзуется для доступа к операциям по изменению элементов графа сцены без изменения классов этих элементов. Класс-посетитель реализует все соответствующие виртуальные функции для применения их к различным типам элементам через механизм двойной диспетчеризации. Используя этот механизм разработчик может создать свой экземпляр посетителя, реализовав нужный ему функционал с помощью специальных операторов и привязать посетителя к различным типам элементов графа сцены “на лету”, не меняя функционала самих элементов. Это отличный способ расширить функциональность элемента без определения подклассов этих элементов.
Для реализации данного механизма в 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 );
Режим обхода задается несколькими перечеслителями
- TRAVERSE_ALL_CHILDREN - перемещение по вем дочерним узлам.
- TRAVERSE_PARENTS - проход назад от текущего узла, не доходя до корневого узла
- TRAVERSE_ACTIVE_CHILDREN - обход исключительно активных узлов, то есть тех, видимость которых активирована через узел osg::Switch.
Анализ сруктуры горящей цессны
Разработчик всегда может проанализировать ту часть графа сцены, что порождается моделью, загруженной из файла.
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.
Это простой пример применения механики посетителей. На самом деле внутри посетителей можно выполнять массу операций по модификации узлов при выполнении программы.