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
Лучшая документация — книги от авторов.
Лучшая документация, доступная бесплатно — это огромное множество примеров, поставляемых с библиотекой, и отличный код, который читать легко и приятно.
Автор: nereati