
Введение
Одной из интереснейших задач, решаемых посредством трехмерной графики является создание «больших миров» — протяженных сцен, содержащих большое число объектов с возможностью неограниченного перемещения по сцене. Решение этой задачи упирается в понятные ограничения, присущие аппаратному обеспечению компьютера.
Типичный пример: «большой мир» при визуализации железной дороги на движке OSG. Не хватает только лангольеров, пожирающих мир за поездом...
В этой связи возникает необходимость управления ресурсами приложения, сводящаяся к очевидному решению: загрузке только тех ресурсов (моделей, текстур и так далее), которые необходимы для формирования сцены в текущий момент времени при текущем положении наблюдателя; уменьшении уровней детализации удаленных объектов; выгрузке не нужных более объектов из памяти системы. В большинстве своем графические и игровые движки предоставляют некоторый набор инструментов для решения подобных задач. Сегодня мы рассмотрим, какие из них имеются в OpenSceneGraph.
1. Использование уровней детализации (LOD)
Техника использования уровней детализации позволяет отображать один и тот же объект более или менее детально, в зависимости от расстояния от него до наблюдателя. Использование этой техники основано на том простом соображении, что мелкие детали трехмерной модели неразличимы на большом расстоянии, значит нет необходимости в их прорисовке. С одной стороны этот прием позволяет уменьшить общее количество геометрических примитивов, выводимых в буфер кадра, а с другой — не терять в дальности отображения объектов сцены, что очень полезно при создании больших открытых миров.
OSG предоставляет инструменты для реализации этого приема через класс osg::LOD, наследуемый от всё того же osg::Group. Этот класс позволяет представить один и тот же объект в нескольких уровнях детализации. Каждый уровень детализации характеризуется минимальной и максимальной дистанцией до наблюдателя, при соблюдении которой происходит переключение отображения объекта в этом уровне детализации.
osg::LOD позволяет задавать данный диапазон сразу при задании дочерней ноды, или позже, применением методы setRange()
osg::ref_ptr<osg::LOD> lodNode = new osg::LOD; lodNode->addChild(node2, 500.0f, FLT_MAX); lodNode->addChild(node1); . . . lodNode->setRange(1, 0.0f, 500.0f);
Продолжаем мучать цессну и проиллюстрируем описанную технику примером
Пример lod
main.h
main.h
#ifndef MAIN_H #define MAIN_H #include <osg/LOD> #include <osgDB/ReadFile> #include <osgUtil/Simplifier> #include <osgViewer/Viewer> #endif
main.h
#include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Node> modelL3 = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::Node> modelL2 = dynamic_cast<osg::Node *>(modelL3->clone(osg::CopyOp::DEEP_COPY_ALL)); osg::ref_ptr<osg::Node> modelL1 = dynamic_cast<osg::Node *>(modelL3->clone(osg::CopyOp::DEEP_COPY_ALL)); osgUtil::Simplifier simplifer; simplifer.setSampleRatio(0.5f); modelL2->accept(simplifer); simplifer.setSampleRatio(0.1f); modelL1->accept(simplifer); osg::ref_ptr<osg::LOD> root = new osg::LOD; root->addChild(modelL1.get(), 200.0f, FLT_MAX); root->addChild(modelL2.get(), 50.0f, 200.0f); root->addChild(modelL3.get(), 0.0f, 50.0f); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
Для начала загружаем модель
osg::ref_ptr<osg::Node> modelL3 = osgDB::readNodeFile("../data/cessna.osg");
Теперь необходимо генерировать несколько (ограничимся для примера двумя) моделек, с более низким уровнем детализации. Для этого скопируем загруженную ноду дважды, применяя методику так называемого "глубокого" копирования класса, для ноды реализуемого методом clone()
osg::ref_ptr<osg::Node> modelL2 = dynamic_cast<osg::Node *>(modelL3->clone(osg::CopyOp::DEEP_COPY_ALL)); osg::ref_ptr<osg::Node> modelL1 = dynamic_cast<osg::Node *>(modelL3->clone(osg::CopyOp::DEEP_COPY_ALL));
Теперь редуцируем геометрию этих моделей, используя класс osgUtil::Simplifer. Степень упрощение модели задается методом setSampleRatio() данного класса — чем меньше передаваемый параметр, тем менее детальной будет модель после применения процедуры редукции
osgUtil::Simplifier simplifer; simplifer.setSampleRatio(0.5f); modelL2->accept(simplifer); simplifer.setSampleRatio(0.1f); modelL1->accept(simplifer);
Когда у нас есть модельки разного уровня детализации мы можем зарядить их в корневую ноду, созданную как умный указатель на osg::LOD. Для каждого уровня детализации задаем дистанцию отображения этого уровня
osg::ref_ptr<osg::LOD> root = new osg::LOD; root->addChild(modelL1.get(), 200.0f, FLT_MAX); root->addChild(modelL2.get(), 50.0f, 200.0f); root->addChild(modelL3.get(), 0.0f, 50.0f);
Под FLT_MAX понимается в некотором роде "бесконечно" большое расстояние до наблюдателя. После запуска вьювера получаем следующую картину
Уровень детализации 3

Уровень детализации 2

Уровень детализации 1

Видно, как при отдалении камеры от объекта снижается детальность отображаемой геометрии. Применяя этот прием можно добиться высокой реалистичности сцены при малом расходе ресурсов.
2. Техника фоновой загрузки узлов сцены
В движке OSG представлены классы osg::ProxyNode и osg::PagedLOD, предназначенный для баллансировки нагрузки при рендеринге сцены. Оба класса наследуются от osg::Group.
Узел типа osg::ProxyNode уменьшает время запуска приложения до начала рендеринга, если в сцене огромное количество загружаемых с диска и отображаемых моделей. Он работает как интерфейс к внешним файлам, позволяя выполнять отложенную загрузку моделей. Для добавления дочерних узлов используется метод setFileName() (вместо addChild) чтобы установить имя файла модели на диске и загрузить его динамически.
Узел osg::PagedNode наследует методы osg::LOD и загружает и выгружает уровни детализации таким образом, чтобы избежать перегрузки конвейера OpenGL и обеспечить плавную отрисовку сцены.
3. Динамическая (runtime) загрузка модели
Посмотрим, как происходит процесс загрузки модели с применением osg::ProxyNode.
Пример proxynode
main.h
main.cpp
#ifndef MAIN_H #define MAIN_H #include <osg/ProxyNode> #include <osgViewer/Viewer> #endif
main.cpp
#include "main.h" int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::ProxyNode> root = new osg::ProxyNode; root->setFileName(0, "../data/cow.osg"); osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
Процесс загрузки здесь немного отличается
osg::ref_ptr<osg::ProxyNode> root = new osg::ProxyNode; root->setFileName(0, "../data/cow.osg");
Вместо явной загрузки модели коровы мы указываем корневой ноде имя файла, где содержится модель и индекс дочерней ноды, куда следует поместить эту модель после её загрузки. При выполнении программы мы получи такой результат

Видно, что точка обзора выбрана не лучшим образом — камера упирается прямо в зеркальный бок коровы. Это произошло потому, что модель загрузилась уже после запуска рендера и инициализации камеры, когда нода 0 ещё не была видна. Вьювер просто не смог просчитать необходимые параметры камеры. Однако, модель загрузилась и мы может настроить режим её отображения путем манипуляций мышью

Что происходит в рассмотренном примере? osg::ProxyNode и osg::PagedLOD работают в данном случае как контейнеры. Внутренний менеджер данных OSG будет посылать запросы и загружать данные в граф сцены по мере того, как возникнет необходимость в файлах моделей и уровнях детализации.
Данный механизм работает в нескольких фоновых потоках и управляет загрузкой статических данных, расположенных в файлах на диске и динамических данных, генерируемых и добавляемых в процессе выполнения программы.
Движок автоматически обрабатывает узлы, не отображаемые в текущем вьюпорту и удаляет их из графа сцены когда рендер перегружен. Однако, такое поведение не затрагивает узлы osg::ProxyNode.
Как и прокси-узел, класса osg::PagedLOD также имеет метод setFileName() для задания пути к загружаемой модели, однако для его необходимо установить диапазон дистанции видимости, как для узла osg::LOD. При условии, что у нас имеется файл cessna.osg и низкополигональная модель уровня L1 мы можем организовать выгружаемую ноду следующим образом
osg::ref_ptr<osg::PagedLOD> pagedLOD = new osg::PagedLOD; pagedLOD->addChild(modelL1, 200.0f, FLT_MAX ); pagedLOD->setFileName( 1, "cessna.osg" ); pagedLOD->setRange( 1, 0.0f, 200.0f );
Нужно понимать, что узел modelL1 не может быть выгружен из памяти, так как это обычный дочерний не прокси-узел.
При рендеринге внешне не видна разница между osg::LOD и osg::PagedLOD, если использовать только один уровень детализации модели. Интересной идеей будет организовать громадный кластер моделей Cessna, используя класс osg::MatrixTransform. Для этого можно использовать например такую функцию
osg::Node* createLODNode( const osg::Vec3& pos ) { osg::ref_ptr<osg::PagedLOD> pagedLOD = new osg::PagedLOD; … osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform; mt->setMatrix( osg::Matrix::translate(pos) ); mt->addChild( pagedLOD.get() ); return mt.release(); }
Пример программы реализующей фоновую загрузку 10000 самолетов
main.h
#ifndef MAIN_H #define MAIN_H #include <osg/PagedLOD> #include <osg/MatrixTransform> #include <osgViewer/Viewer> #endif
main.cpp
#include "main.h" //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ osg::Node *createLODNode(const std::string &filepath, const osg::Vec3 &pos) { osg::ref_ptr<osg::PagedLOD> pagedLOD = new osg::PagedLOD; pagedLOD->setFileName(0, filepath); pagedLOD->setRange(0, 0.0f, FLT_MAX); osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform; mt->setMatrix(osg::Matrix::translate(pos)); mt->addChild(pagedLOD.get()); return mt.release(); } //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { (void) argc; (void) argv; osg::ref_ptr<osg::Group> root = new osg::Group; float dist = 50.0f; int N = 100; for (int i = 0; i < N; ++i) { float x = i * dist; for (int j = 0; j < N; ++j) { float y = j * dist; osg::Vec3 pos(x, y, 0.0f); osg::ref_ptr<osg::Node> node = createLODNode("../data/cessna.osg", pos); root->addChild(node.get()); } } osgViewer::Viewer viewer; viewer.setSceneData(root.get()); return viewer.run(); }
Предполагается, что самолеты будут располагаться на плоскости с интервалом в 50 единиц координат. При загрузке мы увидим, что загружаются только те цессны, что попадаю в кадр. Те самолеты, что исчезают из кадра пропадают из дерева сцены.

Заключение
Этот урок в цикле об OpenSceneGraph будет последним, выполненным в формате «How To». В рамках двенадцати статей удалось уместить базовые принципы работы и использования OpenSceneGraph на практике. Очень надеюсь, что данный движок стал более понятен русскоязычному разработчику.
Это не означает, что я закрываю тему OpenSceneGraph на ресурсе, напротив, будущие статьи планируется посвятить более продвинутым техникам и приемам применения OSG в разработке графических приложений. Но для этого следует накопить хороший материал и переработать массу англоязычных источников, а на это требуется время.
Но я не прощаюсь, благодарю за внимание и до новых встреч!
