Введение в OpenSceneGraph

OpenSceneGraph — это кроссплатформенная библиотека с открытыми исходниками для разработки высокопроизводительных 3D-приложений. Это не игровой движок, связывающий пользователя по рукам и ногам заложенными в него ограничениями, а именно библиотека — набор полезных модулей, которые отлично работают как поодиночке, так и в сборке.



Ядро OpenSceneGraph, собственно граф сцены, — довольно тонкая обёртка вокруг OpenGL, позволяющая задавать иерархию объектов и выполнять над ними любые желаемые преобразования:
  • изменять характеристики узлов (двигать объекты в пространстве, назначать им материалы, свойства освещённости, шейдеры и прочие режимы и атрибуты OpenGL);
  • перестраивать дерево любым желаемым образом (создавать и удалять объекты, перелинковывать их к другим узлам графа);
  • делать обход графа, выполняя для каждого их узлов какие-либо действия;
  • ну и конечно рендерить сцену при помощи OpenGL.


Граф сцены в OpenSceneGraph


Каждый узел графа сцены — это экземпляр какого-то из потомков класса Node. Стрелочки — это отношения «родитель-ребёнок»:



Узлы, имеющие детей, называются группами (Group). Обычный Group ничего не делает со своими детьми — все они будут отрендерены, как есть. Однако, наследники класса Group могут иметь дополнительное поведение. Например, MatrixTransform наследует класс Group, и позволяет применить матрицу преобразования ко всем детям разом. Например, если изменить матрицу, отвечающую за башню танка, то башня будет крутиться вместе со стволом:



Примитивы рисования OpenSceneGraph называются Drawables. Каждый Drawable соответствует какому-либо примитиву рисования OpenGL: сфере, кубу, произвольному mesh, чайнику OpenGL и т. д. Сами Drawables попадают на сцену только в контейнере Geode (сокращение от «geometry node»). Geode может содержать в себе любое количество Drawables:



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



Если мы хотим сделать полноценную модель танка, который умеет крутить колёсами и гусеницами, поворачивать башню и управлять стволом, то получится граф типа такого:



Чтобы крутить колёсами, достаточно изменить texture mapping на левых или правых колёсах. И пользователь увидит, что соответствующие колёса все разом крутятся. Аналогично с гусеницами — изменением текстуры можно добиться видимого эффекта движения.

Два слова про управление памятью


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

{
    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
}

В этом примере создаётся новый экземпляр osg::Geode, инициализируется умный указатель, затем указатель разрушается, и вместе с ним osg::Geode, поскольку больше на него не осталось ни одной ссылки. При добавлении детей в группу, Drawables в Geode и всех прочих перекрёстных ссылок между объектами графа, используются умные указатели. Это гарантирует, что при удалении ссылки на корневой узел графа все объекты будут корректно уничтожены.

Привет, мир


Вот минимальное приложение OpenSceneGraph:

#include <osgViewer/Viewer>
int main()
{
    osgViewer::Viewer viewer;
    return viewer.run();
}

Здесь используется модуль osgViewer, который берёт на себя открытие графического окна, инициализацию OpenGL, создание камеры по умолчанию, инициализацию обработчика клавиши Escape и контроллера мыши, чтобы можно было двигать камеру с её помощью. При запуске программы мы увидим пустую сцену, которая по Escape закроется.
Следующий шаг — создание корневого узла. Например, поместим перед viewer.run() код создания сферы:

    // Создание Drawable
    osg::Sphere *shape = new osg::Sphere(
            osg::Vec3(0.0f, 0.0f, 0.0f), 1.0f);
    osg::ShapeDrawable *drawable = new osg::ShapeDrawable(shape);

    // Создание Geode
    osg::Geode *geode = new osg::Geode;
    geode->addDrawable(drawable);

    // Регистрация корневого узла сцены
    viewer.setSceneData(geode);

После запуска этого приложения мы увидим сферу:



Теперь можно перейти к загрузке шрифта и выводу текста. Нам понадобится библиотека osgText, которая отвечает за работу с текстом:

    osgText::Font *font = osgText::readFontFile(
            "/usr/share/fonts/truetype/msttcorefonts/arial.ttf");

    osgText::Text *text = new osgText::Text;
    text->setFont(font);
    text->setAxisAlignment(osgText::Text::XZ_PLANE);
    text->setText("Привет, хабр!", osgText::String::ENCODING_UTF8);

    osg::Geode *geode = new osg::Geode;
    geode->addDrawable(text);

Функция readFontFile загружает из файла шрифт, затем мы создаём объект Text, который является наследником Drawable. А значит, его можно при помощи метода addDrawable добавить в Geode.
После запуска программы появится текст:



Главный цикл приложения


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



Изменение сцены — это те операции, которые делает приложение. Пока приложение не закончит свои обновления, начинать рендерить следующий кадр невозможно — иначе в процессе обхода дерева системой рендеринга граф может оказаться в несогласованном состоянии, и приложение повредит себе память или просто упадёт. Это значит, что чем быстрее будут выполнены обновления, тем больший FPS будет на выходе.
При создании реальных приложений имеет смысл выполнять тяжёлые вычисления одновременно с фазой рендеринга в другом потоке. Там же можно создавать новые объекты сцены. А когда главный цикл дойдёт до фазы изменения сцены, можно будет быстро применить результаты расчётов к объектам сцены, прилинковать созданные поддеревья к графу сцены. Аналогично и с удалением большого числа объектов. Во время фазы изменения сцены можно их просто отлинковать от дерева и поместить в очередь удаления, а фактическое разрушение объектов выполнять в другом потоке.

Как изменять сцену


Для примера сделаем, чтобы надпись «Привет, хабр» крутилась на экране. Для этого мы сначала обернём Geode в MatrixTransform:

    osg::MatrixTransform *mat = new osg::MatrixTransform;
    mat->addChild(geode);
    viewer.setSceneData(mat);

Затем попросим Viewer зарегистрировать наш обработчик событий:

    RotationHandler *handler = new RotationHandler(mat);
    viewer.addEventHandler(handler);

Каждый обработчик событий — это объект, наследующий класс osgGA::GUIEventHandler. Нас сейчас интересует, как обработать событие FRAME, которое вызывается перед каждым кадром:

class RotationHandler: public osgGA::GUIEventHandler {
public:
    RotationHandler(osg::MatrixTransform *mat):
        m_mat(mat)
    {
    }

    virtual bool handle(const osgGA::GUIEventAdapter& ea,
            osgGA::GUIActionAdapter &adapter)
    {
        osg::Matrix mat;
        switch (ea.getEventType()) {
        case osgGA::GUIEventAdapter::FRAME:
            mat.makeRotate(ea.getTime(), osg::Vec3(0.0f, 0.0f, 1.0f));
            m_mat->setMatrix(mat);
        }
    }

private:
    osg::ref_ptr<osg::MatrixTransform> m_mat;
};

При запуске программы текст начнёт вращаться вокруг оси (0, 0, 1).

Какие ещё возможности есть у OpenSceneGraph


Мы рассмотрели базовые принципы построения сцены, оставив за кадром обход графа, назначение атрибутов узлам (материалов, освещённости), управление камерой, обработку мыши и клавиатуры и многое другое. Просто упомяну некоторые интересные возможности:
  • реализация паттерна Visitor позволяет написать свой класс и попросить OpenSceneGraph обойти граф, вызывая для каждого подходящего объекта сцены ваш код;
  • узел Switch (который является наследником Group) позволяет включать и выключать детей, исключая их из обхода графа;
  • узел LOD (тоже наследник Group) позволяет указать, на каком расстоянии от камеры какой из детей должен рендериться. Позволяет на большом удалении от камеры использовать более простые модели;
  • поддержка шейдеров (fragment и geometry);
  • для достижения высокой производительности и масштабируемости есть поддержка многопоточного рендеринга изображений с нескольких камер. В том числе, и multi-GPU;
  • возможен рендеринг в текстуру;
  • многослойные текстуры, анизотропное освещение, bump-mapping, specular highlights;
  • выбор объектов сцены, в которые пользователь указывает мышью (точнее, преобразование экранных координат в длинный и острый многогранник, который как бы втыкается в экран, а затем поиск пересечений этого многогранника с объектами сцены и сортировка найденных объектов по расстоянию от камеры);
  • много математических примитивов для работы с матрицами, кватернионами, многогранниками (вычисление пересечений, объединений и т. д.), 3d-морфинга;
  • объекты сцены можно сериализовывать и десериализовывать. Новые форматы экспорта и импорта легко подключаются при помощи системы плагинов. Родной формат (osg) сохраняет все атрибуты объектов в человекочитаемом текстовом формате и позволяет в точности восстановить граф после десериализации;
  • специальные узлы графа HUD (head up display) позволяют разворачивать детей так, чтобы они всегда были ориентированы лицом к монитору;
    системы частиц позволяют создавать различные спецэффекты типа огня, дыма, искр, программируя частоту создания каждой частицы, траекторию её полёта, время жизни, текстуру и т.д.;
  • поддержка полупрозрачных объектов и теней;
  • библиотека многопоточности OpenThreads позволяет абстрагироваться от операционной системы и писать единый код для всех платформ;
  • модуль osgDB, кроме простой загрузки и выгрузки объектов на диск, имеет в своём арсенале модуль базы данных объектов, позволяющий подгружать их в фоновом потоке (режим paging);
  • интеграция с Qt позволяет рендерить GUI в текстуру, которая, в свою очередь, может быть натянута на какой-то объект сцены. В частности, можно взять Qt-компонент веб-браузера, поместить его на текстуру, отобразить на сцене, в браузере открыть YouTube и посмотреть какой-нибудь ролик. И это работает;
  • есть поддержка визуализации поверхности земли (terrain) и неба (skybox);
  • возможность работы на мобильных платформах Android и iOS;
  • отличная лицензия (relaxed LGPL), позволяющая даже статически линковать библиотеку с закрытыми проектами.

Где взять


Официальный сайт — www.openscenegraph.org
Лучшая документация — книги от авторов.
Лучшая документация, доступная бесплатно — это огромное множество примеров, поставляемых с библиотекой, и отличный код, который читать легко и приятно.
Поделиться публикацией
Комментарии 14
    0
    Спасибо. Наглядная статья. Принцип работы приблизительно общий с другими графическими движками.
      0
      Спасибо. По описанию очень интересная библиотека. Как раз скоро понадобится нечто подобное.
        +1
        Использовали примерно 8 лет назад в одной игре. Но на финальном этапе выкинули из-за плохой производительности. Хотя с точки зрения удобства использования — мне нравился OSG.
          0
          То есть для обучения и написания прототипов он очень хорошо подходит?
            +2
            О, этот вопрос уже задали. В копилку, вопросы к xanep:

            1. У вас были свои абстракции для работы с ним? (связано со следующим вопросом)
            2. Насколько затратно вышло перепилить на чистый OpenGL?
            3. Насколько лучше стала производительность после перехода?
              0
              Надо же, вот так неосторожную фразу кинешь, поймут неправильно :)
              С самим OSG все нормально, он хорошо написан, хорошая документация. Просто не каждой игре подходит Scene Graph. Есть и другие способы деления сцены на части. К примеру, есть BSP деревья, которые часто более производительны. Но повторюсь еще раз — я не хотел обидеть OSG, просто он не подошел нашей игре. Хотя я подозреваю, что для игр он в принципе хуже подходит, чем для какого-то 3D редактора, например.
              0
              Для обучения подходит все ))
              Для написания прототипов, пожалуй лучше взять какой-то игровой движок. OSG — это не игровой движок, а графический. В игровом движке есть такие вещи как физика, звук, контроллеры персонажей и прочее, чего нет в графическом.
              0
              Все там нормально с производительностью, но как говорится, на движок надейся, а сам не плошай. Некоторую оптимизацию производительности нужно и вручную делать. Движок же не знает особенностей вашей игры.
                +1
                А точно в движке было дело? OSG очень тонкий ведь. Это почти голый OpenGL, только с агрессивным кэшированием всего, чего только можно.
                0
                Интересно, кто то использовал osgText на iOS и Android как хорошо оно иероглифы рисует в плане использования памяти? Unity3D декларировало, что у них реализованы шрифты очень эффективно. Т.е. можно ли, что бы пользователь ввел свое имя и положится в на osgText в отрисовке пары иероглифов или букв Ё

                Немогу найти, внятно про поддерку анимированый 3Д моделей в OpenGL ES, Дело в том, что анимацию моделей часто пытаются перенести на шейдер или на второе ядро. Анимация модели может сильно жрать ресурсы.

                Если бинарный формат моделей, который можно быстро загрузить. Родной формат текстовый. Вряд ли бинарный формат 3DS это само оптимальное для загрузки моделей в игру.
                  0
                  Бинарный формат есть (osgb). Так же как текстовый (osgt) и XML (osgx). Про мобильную версию ничего не скажу — ни разу не пользовался.
                  0
                  не очень понятно, что с тенями на OpenGL ES
                  trac.openscenegraph.org/projects/osg//wiki/Support/ProgrammingGuide/osgShadow

                  Вероятно, разработчики о OpenGL ES вообще не задумываются. Кое как, что скомпелировали и основная разработка идет на OpenGL

                  Думаю тени на OpenGL ES не реализованы.
                    0
                    Документация на сайте несколько устарела. Этой страничке про тени уже лет 5.
                    В последние годы работа над поддержкой OpenGL ES велась очень активно, судя по их форуму.
                      0
                      Я так понял, что ни автор ни кто либо еще не использовал этот OSG в реальном проекте. В первую очередь меня интересуют возможность использовать OSG в разработке мобильных игр.

                      Вот тут перечисленно как его использовали
                      www.openscenegraph.org/index.php/gallery/use-cases?limitstart=0

                      Статья я бы сказал очень оптимистичная. Либо OSG используют повсеместно, но код воруют.

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

                  Самое читаемое