Перенос блога
alex_bobkov
Блог переехал на отдельный сайт http://alexander-bobkov.ru. Избранные посты перенес туда. Здесь уже вряд ли что-то будет.

Быстрое введение в OpenSceneGraph. Часть 4.
alex_bobkov

Часть 4 посвящена ключевой теме – графу сцены.

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

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

В сложных приложениях высокоуровневые концептуальные сущности это: 3д-объекты, аватары, системы частиц, источники света, камеры и другие. Эти сущности используются как строительные кирпичи при разработке приложения. Оптимизация производительности идет уже на уровне объектов, а не отдельных команд.

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

1. Что такое граф сцены, что он может и для чего нужен

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

Для реализации графа сцены используется паттерн ООП “Компоновщик”.

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

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

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

Пример графа сцены:

Геометрия дома и геометрия стола заданы в своих собственных локальных СК, в которых центры СК совпадает с центрами объектов. Узел Table transform хранит матрицу преобразования, которая определяет положение стола в системе координат дома. Например, с помощью матрицы стол можно сдвинуть к северной стене. Узел House transform хранит матрицу преобразования, которая определяет положение объектов, заданных в системе координат дома, в системе координат города (мировой СК). Абсолютное положение стола в мировой СК определяется произведением 2х матриц преобразования.

Группу узлов, образующих подграф внутри графа, можно интерпретировать как один 3д-объект, состоящий из частей. Например, можно воспринимать и дом, и стол внутри него, как единый объект, хотя на нижнем уровне он состоит из нескольких геометрических объектов. Это решает задачу управления положением объектов на сцене. Достаточно изменить матрицу у одного промежуточного узла, как изменяется положение на сцене сразу у всего объекта и его частей. Так, изменяя только House transform, можно двигать дом вместе со всеми вложенными объектами.

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

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

Существуют также глобальные операции, которые могут применяться ко всему графу сцены целиком. Такие операции осуществляются путем обхода графа в глубину (depth-first).

Одна из самых важных таких операций - отсечение невидимых объектов (culling).

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

Древовидная структура графа сцены позволяет легко организовать отсечение невидимых объектов (culling). Для этого для каждого узла графа сцены вычисляется ограничивающая сфера (bounding shpere), т.е. минимальная сфера, которая охватывает сферы всех подузлов данного узла.

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

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

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

Можно подвести некоторый промежуточный итог. Основные функции графа сцены:

  • единообразное представление всех элементов трехмерной сцены;
  • гибкое управление отдельными 3д-объектами и группами 3д-объектов, как единым целым: изменение их положения, ориентации, состояние отображения;
  • отбрасывание невидимые объектов.

2. Группировка объектов в графе сцены

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

Граф сцены - очень гибкая структура данных и позволяет объекты группировать произвольно. Однако необходимо стремиться, чтобы структура графа сцены отражала логические и/или пространственные связи между объектами. Рассмотрим несколько примеров.

2.1. Стол является составной частью дома, поэтому на рисунке выше показано, как они сгруппированы вместе.

2.2. 3д-модель машины содержит кузов и 4 одинаковых колеса. 3д-модель колеса можно не дублировать 4 раза. Колесо можно сделать в локальной СК, а положение относительно корпуса задавать с помощью узла преобразования. Чтобы получить 4 колеса, нужно 4 узла преобразования. Узел Car transform задает положение машины как целого: корпуса и колес вместе.

2.3. Если имеется множество однотипных объектов (например, домов), то их всех можно разместить на одном уровне:

Однако это приведет к тому, что на этапе отсечения будет проверяться видимость всех домов без исключения.

Можно оптимизировать производительность, если сгруппировать объекты в виде квадродерева. В этом случае все пространство рекурсивно разбивается на квадраты. Большой квадрат разбивается на 4 меньших квадрата. Ведется подсчет количества объектов в каждом квадрате. Разбиение прекращается, когда количество объектов в квадрате станет меньше заданного значения Nmax. Пример разбиения пространства из Википедии:

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

Если в область видимости попадают не все объекты, то такая структура графа позволяет более эффективно отсекать невидимые объекты, делая меньшее число проверок.

Если в сцене имеются объекты другого типа, например, деревья, то есть 2 основных варианта:

  • Построить единое квадродерево для объектов всех типов;
  • Для каждого типа объектов строить отдельное квадродерево.

Первый вариант - это группировка по пространственному признаку. Второй вариант - по логическому. В разных ситуациях могут быть полезны разные варианты группировки.

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

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

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

3. Классы OpenSceneGraph

Эталонная реализация графа сцены представлена в графической библиотеке OpenSceneGraph (OSG), которая является надстройкой над OpenGL. Алгоритмы работы графа сцены и примеры его применения удобно проиллюстрировать с помощью классов OSG:

Основные классы OSG, которые формируют каркас графа сцены:

osg::Node - базовый класс, от которого наследуются все другие классы.

osg::Geode - листовой узел графа сцены, содержит один или несколько геометрических объектов.

osg::Group - базовый класс для всех промежуточных узлов, содержит список дочерних узлов.

osg::Transform - базовый класс для всех узлов, которые занимаются преобразованием систем координат.

osg::MatrixTransform - узел, который содержит матрицу преобразования в явном виде.

osg::PositionAttitudeTransform - узел, который содержит матрицу преобразования в виде отдельно вектора перемещения, кватерниона вращения и вектора масштабирования.

Для работы с дочерними узлами класс osg::Group содержит ряд методов, смысл которых понятен из названия:

bool addChild(osg::Node* child);

bool insertChild(unsigned int index, osg::Node *child);

bool removeChild(osg::Node *child);

bool removeChildren(unsigned int pos,unsigned int numChildrenToRemove);

bool replaceChild(osg::Node *origChild, osg::Node* newChild);

unsigned int getNumChildren();

bool setChild(unsigned  int i, osg::Node* node);

osg::Node* getChild(unsigned  int i)

bool containsNode(const osg::Node* node)

unsigned int getChildIndex(const osg::Node* node);

osg::MatrixTransform содержит метод setMatrix для задания матрицы преобразования и соответственно getMatrix для ее получения.

osg::PositionAttitudeTransform содержит методы getPosition/setPosition, getAttitude/setAttitude, getScale/setScale, getPivot, setPivot для получения/задания сдвига, вращения, масштабирования и центра вращения.

При экспорте 3д-объекта из 3д-редактора сразу формируется подграф сцены из составных объектов этого 3д-объекта. Загрузить сразу весь подграф можно с помощью функции:

osg::Node* osgDB::readNodeFile(std::string filename);

4. Глобальные действия над графом сцены

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

Во время подготовки кадра граф сцены обходится как минимум один раз - это обход для отсечения невидимых объектов. Если к узлам прикреплены функции обратного вызова EventCallback или UpdateCallback, то выполняется еще 2 обхода (event traversal и update traversal). Дополнительно при необходимости может выполняться обход графа для поиска пересечения луча с 3д-объектами.

Можно придумать множество других действий над графом сцены, для которых нужно производить обход. Невозможно все эти действия реализовать в виде методов класса osg::Node. Поэтому для универсального выполнения действий над графом применяется паттерн ООП “Посетитель” (Visitor).

Диаграмма классов паттерна:

Для каждого действия над графом создается свой класс ConcreteVisitor, который наследуется от базового абстрактного класса Visitor. В этих классах имеется набор перегруженных методов apply() для выполнения действий над отдельными типами узлов.

В классе osg::Node определен метод accept(), который принимает на вход экземпляр посетителя. Внутри этого метода происходит вызов метода apply() посетителя. После выполнения действия внутри метода apply() вызывается метод узла traverse(), который в случае группы рекурсивно вызывает accept() у всех дочерних узлов.

Диаграмма последовательности:

Вся логика взаимодействия с посетителем заложена в классах osg::Node и osg::Group. Через методы accept() и traverse() они обеспечивают рекурсивный обход графа.

Вся полезная нагрузка заложена в методах apply() посетителя. Благодаря полиморфизму ООП, для разных типов узлов автоматически вызываются разные варианты метода apply().

Если требуется исключить некоторые узлы графа из обхода, то применяется система битовых масок. Маска - целое число размером 4 байта. Каждый узел графа имеет свою маску (по умолчанию 0xffffffff). Посетитель тоже имеет свою маску (по умолчанию 0xffffffff). Можно менять маски как посетителя, так и отдельных узлов. Во время обхода метод apply() вызывается, только если операция “побитовое и” маски посетителя и текущего узла дает значение не 0.

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

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

Пример посетителя, который ищет в графе сцены узел с заданным именем:

struct FindNodeVisitor: public osg::NodeVisitor
{
    FindNodeVisitor(const std::string& name):
        osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN),
        _name(name)
    { }
    virtual void apply(osg::Node& node)
    {
        if (node.getName() == _name)
        {
            _node = &node;
            return;
        }
        traverse(node);
    }
    std::string _name;
    osg::ref_ptr<osg::Node> _node;
};

Пример использования посетителя:

FindNodeVisitor visitor;
root->accept(visitor);
if (visitor._node.valid())
{
   //do something
}

5. Локальные действия узлов графа сцены

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

Есть 2 основных способа добавить функциональность к узлу: через функции обратного вызова и через переопределение метода traverse().

Каждому узлу можно прикрепить функции обратного вызова 3х типов: EventCallback, UpdateCallback, CullCallback. К одному узлу может быть прикреплено любое количество функций одного типа.

Функции EventCallback предназначены для обработки событий ввода (клавиатуры и мыши) и реакции на эти события, специфичные для данного узла.

Функции UpdateCallback предназначен для модификации узла, например, для анимации, перемещения объекта.

Функции CullCallback определяют нужно ли отображать объект, или его нужно отсечь.

Функции обратного вызова реализуются в виде классов с переопределенной операцией (). Пример:

class MyUpdateCallback: public osg::NodeCallback
{
        virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
        {
            std::cout<<"update callback - pre traverse"<<node<<std::endl;
            traverse(node,nv);
            std::cout<<"update callback - post traverse"<<node<<std::endl;
        }
};

Чтобы обход продолжался дальше (вызывались функции обратного вызова дочерних узлов), необходимо вставить метод traverse (как в примере выше).

Пример добавления к узлу функции обратного вызова:

node->setUpdateCallback(new MyUpdateCallback);

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

6. Оптимизация производительности

3д-объекты могут состоять из нескольких частей (деталей, подобъектов). Удобно для каждой детали иметь свою геометрию, свой узел графа сцены (osg::Geode). Это показано на рисунке:

Это позволяет:

  • Изменять положение и ориентацию детали относительно основного объекта (если сверху над каждым геодом вставить MatrixTransform)
  • Изменять состояние отображения (текстуры, шейдеры)
  • Привязывать к детали метаданные, доступные, например, при клике мышкой

Однако, если таких деталей становится очень много, то это приводит к снижению производительности из-за 2 основных причин:

  • проверка видимости детали (этап отсечения)
  • накладные расходы на вызов функции отрисовки OpenGL для каждой детали

Один узел osg::Geode может хранить несколько геометрических объектов (класс osg::Drawable). Первая причина устраняется, если несколько деталей поместить в один узел Geode:

Чтобы устранить накладные расходы на вызов функций отрисовки, необходимо дальнейшее укрупнение геометрии. Близкорасположенные детали с одинаковой текстурой можно объединить в одну геометрию. Дальше можно несколько текстур объединить в текстурный атлас. И так далее.

Но при этом теряется возможность присоединения метаданных и удобной работы с каждым отдельным объектом.

7. Дополнительные узлы графа сцены

Помимо хранения геометрии, группировки геометрии, задания матриц преобразования узлы графа сцены могут выполнять и другие роли. Здесь краткий обзор еще нескольких узлов (на самом деле их в несколько раз больше):

osg::LOD - управляет уровнями детализации. Дочерние узлы хранят версии 3д-объекта разных уровней детализации. В зависимости от расстояния до камеры osg::LOD отображает один из дочерних узлов.

osg::PagedLOD - то же самое, что и osg::LOD, только дочерние узлы не хранятся в памяти, а подгружаются из внешних файлов при необходимости.

osg::Switch - позволяет выборочно отображать дочерние узлы.

osg::AutoTransform - изменяет матрицу преобразования в зависимости от расстояния до камеры так, чтобы видимый размер объекта на экран был постоянным.

osg::Billboard - ориентирует объект так, чтобы он всегда смотрел на камеру (перпендикулярно взгляду).

osg::LightSource - задает положение источника света.

osg::Camera - дополнительная камера. Применяется для следующих задач: отрендерить подграф в текстуру, отобразить подграф в экранных координатах (текст, меню), отрендерить подграф с измененными матрицами проекции и/или вида, отрендерить подграф в отдельный вьюпорт в рамках основного окна.

osgParticle::ParticleSystemUpdater - обновляет положение частиц в системе частиц каждый кадр.

osgParticle::ParticleProcessor - базовый класс для операций над частицами.

osgShadow::ShadowedScene - создает тени на 3д-объектов.

osgVolume::VolumeNode - класс для объемного рендеринга.

Некоторые из этих классов будут рассмотрены подробнее в следующих частях.


Быстрое введение в OpenSceneGraph. Часть 3.
alex_bobkov

Часть 3 посвящена умным указателям (smart pointers).

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

Поэтому для упрощения отслеживания ссылок на объекты были придуманы умные указатели. Они были включены в стандарт С++11. Однако умные указатели могут быть реализованы средствами самого языка. Так умные указатели присутствуют в библиотеке boost. Также своя реализация есть в библиотеке OpenSceneGraph. Имеет смысл посвятить отдельную часть умным указателям, чтобы было проще изучать исходники и примеры ОСГ.

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

Все классы ОСГ наследуются от класса osg::Referenced, который содержит поле _refCount — счетчик ссылок. Для увеличения и уменьшения счетчика применяются методы ref() и unref(), но вручную их вызывать не нужно. Деструктор виртуальный и защищенный. Это означает, что объекты нельзя удалять вручную, а поэтому их нельзя создавать на стеке. Объекты должны находиться всегда только в куче.

Сами умные указатели реализуются с помощью шаблонного класса osg::ref_ptr<>. Объект этого класса хранит обычный указатель на объект osg::Referenced и для доступа к нему реализует паттерн прокси.

Если присвоить объекту osg::ref_ptr<> адрес объекта osg::Referenced, то osg::ref_ptr<> автоматически вызовет метод ref() у этого объекта и увеличит счетчик ссылок:

osg::ref_ptr<osg::Geode> myptr = new osg::Geode; //_refCount++

Изначально osg::ref_ptr<> является пустым. Проверить это можно с помощью метода valid():

osg::ref_ptr<osg::Geode> myptr2:
if (myptr2.valid() == false)
{     
    //do something
}

При обращении к osg::ref_ptr<> используются перегруженные операторы *, –>, ==, что позволяет использовать osg::ref_ptr<> в любых местах вместо реального указателя на osg::Referenced.

osg::Group* gr = new osg::Group;
osg::ref_ptr<osg::Group> myptr3 = gr;
if (myptr3 == gr)
{
    //do something
}
myptr3->addChild(new osg::Geode); //addChild() – метод класса osg::Group
 
osg::ref_ptr<osg::Group> myptr4 = new osg::Group;
myptr4->addChild(myptr3);

Если объекту osg::ref_ptr<> присвоить 0, то будет вызван метод unref() и счетчик уменьшится на 1; Если объект osg::ref_ptr<> выйдет за пределы области видимости, то будет вызван метод unref() и счетчик уменьшится на 1.

void myfunc()
{
    osg::ref_ptr<osg::group> myptr5 = new osg::Group; //_refCount = 1
    osg::ref_ptr<osg::group> myptr6 = myptr5; //_refCount = 2
    //...
    myptr5 = 0; //_refCount = 1
    //...
} //myptr6 уничтожается => _refCount = 0 => объект удаляется

Если нужно вернуть из функции указатель на объект osg::Referenced, то видно, что в предыдущем примере объект будет уничтожен. Чтобы этого не произошло, нужно использовать метод release() у объекта osg::ref_ptr<>. Этот метод уменьшает счетчик ссылок, но не удаляет объект.

osg::Group* myfunc()
{
    osg::ref_ptr<osg::group> myptr7 = new osg::Group; //_refCount = 1
    //...
    return myptr7.release(); _refCount = 0, но объект не удаляется
} //myptr7 уничтожается, но он уже пустой и ни на что не влияет

Советы по использованию

Нужно применять osg::ref_ptr<>, если предполагается длительное хранение ссылки на объект.

Нужно применять osg::ref_ptr<>, если используется аггрегация: один объект хранит указатель на другой объект.

Нужно применять osg::ref_ptr<>, если объект создается внутри функции и наружу возвращается указатель (предыдущий пример). Это нужно на случай возникновения исключения во время работы функции.

Нужно применять osg::ref_ptr<> без фанатизма. Например, если после создания объекта он сразу передается другому объекту:

osg::ref_ptr<osg::Geode> geode = new osg::Geode;
 
osg::Geometry* geom = new osg::Geometry;
geode->addDrawable(geom);

Циклические ссылки

Если 2 объекта хранят ссылки друг на друга, то это называется циклическая ссылка. Если для хранения ссылок используется osg::ref_ptr<>, то эти 2 объекта никогда не будут удалены.

Для разрешения такой ситуации в OpenSceneGraph введен дополнительный класс для хранения указателей: osg::observer_ptr<>.

osg::observer_ptr<> не изменяет счетчик ссылок, но отслеживает факт удаления объекта, указатель на который он хранит. Если объект удален, то внутренний указатель обнуляется. Это означает, что никогда не произойдет обращения к уже удаленному объекту.

В остальных случаях использование osg::observer_ptr<> почти аналогично osg::ref_ptr<> за одним исключением. Для получения реального указателя нужно использовать метод get(). Пример:

osg::observer_ptr<osg::Geometry> geom = new osg::Geometry;
 
osg::Geode* geode= new osg::Geode;
geode->addDrawable(geom.get());

Возвращаясь к циклическим ссылкам. Если 2 объекта должны хранить ссылки друг на друга, то 1 объект является главным и должен хранить ссылку на подчиненный объект с помощью osg::ref_ptr<>. Подчиненный объект должен хранить ссылку на главный с помощью osg::observer_ptr<>. Пример:

class TestEventHandler: public osgGA::GUIEventHandler
{
public:
    TestEventHandler(osgViewer::Viewer* viewer):
    osgGA::GUIEventHandler(),
    _viewer(viewer)
    {}
 
    bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &us) {}
 
private:
    osg::observer_ptr<osgViewer::Viewer> _viewer;
};


Быстрое введение в OpenSceneGraph. Часть 2.
alex_bobkov

Обновил часть 1: добавил описание класса osg::DisplaySettings.

Часть 2 посвящена обработки событий ввода.

1. Класс osgGA::GUIEventAdapter содержит информацию о событии.

Метод getEventType возвращает тип события: события мыши (PUSH, RELEASE, DOUBLECLICK, DRAG, MOVE, SCROLL), события клавиатуры (KEYDOWN, KEYUP) и несколько других (FRAME – конец очереди событий кадра, RESIZE – изменение размеров окна).

getTime – время события

getWindowX, getWindowY, getWindowWidth, getWindowHeight – положение и размер графического окна

getKey – нажатая кнопка клавиатуры

getX, getY – координаты курсора мыши

getXmin, getXmax, getYmin, getYmax – границы значений координат курсора мыши

getButtonMask – нажатые кнопки мыши

getModKeyMask – кнопки модификаторы (shift, ctrl, alt)

getHandled – индикатор, было ли событие уже обработано в другом обработчике

2. Класс osgGA::GUIActionAdapter – интерфейсный класс. Обычно вместе с каждым событием osgGA::GUIEventAdapter дополнительно передается экземпляр osgGA::GUIActionAdapter, который содержит 2 полезных метода:

asView – возвращает экземпляр класса osg::View, связанный с этим событием

requestWarpPointer – перемещает курсор мыши в заданную точку

3. В части 1 уже упоминался метод frame, который занимается отрисовкой одного кадра. Выполнение метода frame распадается на 4 основных этапа: event, update, cull, draw.

Во время этапа event происходит обработка событий ввода, которые произошли в течение предыдущего кадра. В OpenSceneGraph существует 3 основных способа обработки событий ввода:

  • через объект-манипулятор
  • через обработчики событий
  • через функции обратного вызова у узлов графа сцены

3й вариант рассмотрим позже. А сейчас первые 2.

4. С каждым видом связан один объект-манипулятор – экземпляр класса, производного от osgGA::CameraManipulator. Задача этого объекта – перемещение наблюдателя по трехмерной сцене и соответственно расчет матрицы вида для главной камеры.

2 основных метода:

handle – обработка 1го события. Этот метод вызывается для каждого события из очереди

getInverseMatrix – этот метод должен возвращать матрицу вида. Он вызывается 1 раз за кадр после обработки всех событий

3 дополнительных метода:

setHomePosition – установка начального положения с помощью 3х векторов: eye, center, up

home – возврат в начальное положение

init – этот метод вызывается после изменений параметров графического окна

В составе OpenSceneGraph имеется несколько готовых классов-манипуляторов, которые реализуют различные модели движения (osgGA::TrackballManipulator, osgGA::DriveManipulator) и несколько классов-заготовок, которые можно использовать как базовые классы и расширять их функционал в производных классах.

Для тестирования разных манипуляторов и динамического переключения между ними можно использовать специальный манипулятор osgGA::KeySwitchMatrixManipulator, которые перенаправляет все вызовы текущему выбранному манипулятору.

5. Обработчик событий – экземпляр класса osgGA::GUIEventHandler. Прикрепляется к виду с помощью метода addEventHandler. К одному виду может быть прикреплено неограниченное количество обработчиков. В отличие от манипулятора обработчик может применяться для выполнения любых задач, связанных с событиями ввода.

Главный метод handle – обрабатывает 1 событие. Вызывается для каждого события из очереди событий.

В OpenSceneGraph имеется несколько встроенных обработчиков:

osgGA::StateSetManipulator – изменение ряда параметров визуализации: переключение режимов полигонов (‘w’), включение/отключение освещения (‘l’), включение/отключение текстур (‘t’), включение/отключение отбрасывания задних граней полигонов (‘b’). При инициализации нужно указать нужный экземпляр osg::StateSet (что это такое – позже). Пусть слово “манипулятор” в названии класса вас не смущает, это обычный обработчик. Пример подключения к вьюеру:

viewer.addEventHandler( new osgGA::StateSetManipulator(viewer.getCamera()->getOrCreateStateSet()) );

osgViewer::ThreadingHandler – переключение режимов многопоточности (‘m’)

osgViewer::WindowSizeHandler – переход в оконный режим и обратно (“f’)

osgViewer::StatsHandler – вывод статистики (‘s’)

osgViewer::HelpHandler – показ справки, какие кнопки что означают (‘h’)

osgViewer::RecordCameraPathHandler – запись и воспроизведение маршрута движения камеры (‘z’, ‘Z’)

osgViewer::ScreenCaptureHandler – создание скриншота (‘c’)


Быстрое введение в OpenSceneGraph. Часть 1.
alex_bobkov

Это введение предназначено для людей, которые хорошо умеют программировать на C++, у которых есть опыт использования OpenGL, DirectX или игровых движков, и которые понимают базовые вещи в компьютерной графике.

Здесь будут описаны основные классы, связи между ними и кратко примеры использования.

Начнем с полезных утилит, которые входят в состав OSG.

i. osgviewer – просмотр 3д-моделей. Пример использования из командной строки:

osgviewer cow.osg

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

ii. osgconv – конвертация между разными форматами 3д-моделей, которые поддерживает OSG. Пример использования из командной строки:

osgconv inputfile outputfile

Есть полезный аргумент –compressed, который при конвертации сжимает текстуры. Рекомендуется использовать всегда:

osgconv –compressed inputfile outputfile

Аргументы командной строки.

Переходим к служебным классам, которые формируют каркас приложения.

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

Вид реализуется в 2х классах. Класс osg::View содержит ссылку на виртуальную камеру и возможно на камеры-слейвы (о них позже). От него наследуется класс osgViewer::View, который дополнительно содержит ссылки на граф сцены, на объект-манипулятор (для перемещения по сцене) и имеет методы для быстрого создания графического контекста, взаимодействия с ним и несколько вспомогательных методов.

Метод setSceneData устанавливает ссылку на граф сцены.

Методы setUpViewAcrossAllScreens, setUpViewInWindow, setUpViewOnSingleScreen предназначены для быстрого создания графического контекста.

2. Вьюер – главный организационный объект. Должен быть только один в приложении (это не синглтон, но все равно создавать несколько вьюеров не желательно). Запускает цикл рендеринга и контролирует многопоточность.

В OSG имеется 2 класса-вьюера, производные от базового osgViewer::ViewerBase.

osgViewer::Viewer – для простых приложений, где имеется только один вид. Поэтому для удобства использования osgViewer::Viewer дополнительно наследуется от класса osgViewer::View.

osgViewer::CompositeViewer – для приложений, в которых несколько видов.

Метод realize нужен для автоматического создания графического контекста (выбирает один из 3х методов osgViewer::View).

Метод frame рендерит один кадр.

Метод run запускает цикл рендеринга (вызов метода frame в цикле).

Диаграмма классов выглядит так:

3.  Класс osg::ArgumentParser позволяет удобно разбирать аргументы командной строки. Есть куча примеров использования.

Итого простейшая программа на OpenSceneGraph будет выглядеть так:

int main(int argc, char** argv)
{
    osg::ArgumentParser arguments(&argc, argv);

    osgViewer::Viewer viewer(arguments);

    osg::Node* root = …;

    viewer.setSceneData(root);
    viewer.realize();   
    return viewer.run();
}

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

Матрицу вида можно задавать/получать как через osg::Matrix, так и с помощью 3х векторов eye, center, up.

Матрицу проекции можно задавать/получать 4 способами: напрямую через osg::Matrix, а также через asOrtho, asFrustum, AsPerspective.

Метод setGraphicsContext устанавливает графический контекст.

Можно рендерить в текстуру, если задать RenderTargetImplementationи прикрепить к камере нужные текстуры с помощью метода attach.

Можно прикрепить функции обратного вызова для получения результата рендеринга (например, чтобы сделать скриншот).

Нужно отметить, что среди базовых классов osg::Cameraесть класс osg::Node. В целом, в приложении экземпляры класса osg::Cameraмогут встречаться в 3х разных местах:

  • Главная камера вида. Камера прикреплена к osg::View. Об этом говорилось раньше.
  • Подчиненная камера (slave). Также прикреплена к osg::View, но через метод addSlave. Об этом позже.
  • Один из узлов графа сцены (благодаря наследованию от osg::Node). Об этом позже.

5. Графический контекст – экземпляр одного из классов, производных от osg::GraphicsContext. Отвечает за взаимодействие с операционной системой: создание графического окна, контроль размеров окна, выделение памяти под буферы кадра, получение от операционной системы входных данных от клавиатуры и мыши. В разных ОС это осуществляется по разному, поэтому для каждой ОС есть своя реализация и свой производный класс от osg::GraphicsContext. Есть также производный класс для встраивания в интерфейс Qt.

Графический контекст может быть автоматически создан в методе вьюераrealizeлибо с помощью методов класса osgViewer::View. Но иногда его необходимо настраивать вручную. Пример:

osg::ref_ptr<osg::GraphicsContext::Traits> traits = newosg::GraphicsContext::Traits(osg::DisplaySettings::instance().get());
traits->screenNum = 0;
traits->x = 0;
traits->y = 0;
traits->width = 1920;
traits->height = 1080;
traits->windowDecoration = false;
traits->doubleBuffer = true;
traits->sharedContext = 0;

osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits.get());

camera->setGraphicsContext(gc.get());
camera->setViewport(new osg::Viewport(0, 0, traits->width, traits->height));

camera->setDrawBuffer(GL_BACK);
camera->setReadBuffer(GL_BACK);

Здесь через класс osg::GraphicsContext::Traitsустанавливаются свойства контекста: номер экрана, размеры окна в пикселях и т.д. Статический метод createGraphicsContextавтоматически выбирает реализацию контекста в зависимости от операционной системы.

После создания контекста вся работа с ним происходит “под капотом”.

6. Класс osg::DisplaySettings инкапсулирует некоторые настройки приложения. Похож на синглтон в том смысле, что есть один выделенный экземпляр, доступный из любой точки приложения с помощью статического метода instance. Но при этом можно создавать новые экземпляры этого класса для изменения поведения отдельных видов.

Кратко список настроек:

  • Настройки буферов кадра
  • Настройки параметров стерео
  • Настройки количества потоков для подгрузки данных

osg::DisplaySettings может инициализировать параметры стереорежима на основе переменных окружения или аргументов командной строки. После этого параметры доступны на чтение. Можно также их задавать вручную.

Пример включения сглаживания и буфера трафарета. Эти строки нужно поместить в начало функции main:

osg::DisplaySettings::instance()->setNumMultiSamples(4);
osg::DisplaySettings::instance()->setMinimumNumStencilBits(8);


Бэтман Аполло
alex_bobkov

Несмотря на негативный фон в блогосфере, “Бэтман” мне понравился. Это не лучший роман Пелевина, но и не хуже других. По крайней мере я получил то, что и ожидал.

В творчестве Пелевина можно выделить 2 основные линии: социальная сатира и буддистские мотивы (иллюзорность мира, мир – это сон и прочее). Где-то перевешивает одна линия, где-то другая. Навскидку по памяти я бы разбил все романы так:

  • Социальная сатира: “Омон Ра”, “Жизнь насекомых”, “Поколение П”, “Числа”, “Ампир В”, “СНАФФ”
  • Буддизм: “Чапаев и Пустота”, “Священная книга оборотня”, “Шлем ужаса”, “t”, “Бэтман Аполло”

Формально “Бэтман” заявлен как сиквел “Ампир В”, но относится к другой категории. Люди ждали одного, а получили другое.

Роман переполнен рассуждениями о природе сознания. Сначала главному герою объясняет свою позицию Улл, потом Дракула, потом Озирис, потом сам бэтман. Все разными словами, но все по сути одно и тоже.

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

Лично меня особенно порадовали многочисленные отсылки к фильму “Начало” Нолана. Собственно название романа – это тоже отсылка к Нолану. В фильме я в первый раз услышал слово “лимбо”. В “Бэтмане” оно употребляется каждую вторую страницу. Герой Ди Каприо постоянно сомневается, где кончается сон и начинается реальности. Все тоже самое испытывает и Рама Второй.

На момент написания этого поста, судя по гуглу, никто не провел параллелей между “Бэтманом” и “Началом”.


Нил Стивенсон “Анафем”
alex_bobkov

Недавно прочитал “Анафем” Нила Стивенсона. Роман понравился не меньше предыдущих его произведений. Здесь он еще сильнее уходит в сторону философии науки.

Примерно как Пелевин пересказывает в своих романах восточную философию, так же Стивенсон пересказывает западную философию. Примерно пол-романа посвящены рассуждениям о мире эйдосов Платона и о работе сознания. Здесь Стинвенсон использует идею Пенроуза о квантовой природе сознания (“Новый ум короля”, “Тени разума”). Для объяснения квантовых явлений привлекает многомировую интерпретацию Эверетта и Дойча (“Структура реальности”). В послесловии Стивенсон перечисляет и других философов, повлиявших на него: Лейбниц, Кант, Гуссерль и Гёдель. Но их я не читал, поэтому реминисценций в тексте не уловил.

Действие романа разворачивается в параллельном мире на планете Арб. После “Ужасных событий” (вероятно мировая ядерная война) ученые были изолированы от общества в специальных монастырях (“концентах”). Последовательно были запрещены компьютеры (“синтаксические аппараты”) и генетические исследования. Тогда ученые, лишенные всего, начали заниматься исследованием квантовой природы сознания и достижением квантового бессмертия. События романа начинаются спусти 3000 лет после Ужасных событий, когда в небе над Арбом обнаруживают странный космический объект.

Чтобы подчеркнуть отличие от Земли Стивенсон создает собственную историю мира, собственных философов и собственные слова:
актал -> ритуал
теорика -> физика
кноон -> эйдос
концент -> монастырь
синтаксический аппарат -> компьютер
лесперовы координаты -> декартовы координаты
гемново пространство -> фазовое пространство
Протес -> Платон

Стивенсон вводит собственные любопытные аналоги Бритвы Оккама:
Грабли Диакса: нельзя верить во что-то, только потому что хочется, чтобы так было.
Весы Гардана: при сравнении двух гипотез предпочтение должно отдаваться более простой.

Первые страниц сто довольно сложно въехать во всю эту терминологию, но потом привыкаешь. В конце книги даже есть словарик.


Книги 2012
alex_bobkov

Решил вспомнить, какие книги прочитал в прошедшем году. Более-менее удалось восстановить только книги второго полугодия. Ярче всего в памяти отпечатался Барочный цикл Нила Стивенсона. В 2011 году запомнился “Криптономикон” того же автора. А на январь запланировал “Анафем”.

Барочный цикл посвящен эпохе Ньютона и Лейбница, их противостоянию, созданию новой финансовой и научной Системы Мира. Основано на реальных событиях, как говорится. Маст рид.

Ждал также новую книгу Пелевина в декабре по аналогии с предыдущими 4мя годами. Но не судьба.

Итак вот список в приблизительном хронологическом порядке:

  • Нассим Талеб “Черный лебедь” – о роли случайности в нашей жизни
  • Бенуа Мандельброт, Ричард Хадсон “(Не)послушные рынки” – идейно перекликается с книжкой Талеба
  • Чак Паланик “Колыбельная”
  • Джон Фаулз “Женщина французского лейтенанта” - не особо понравилась, особенно по сравнению с “Коллекционером” и “Волхвом”
  • Люк Рейнхард “Трансформация”
  • Нил Стивенсон “Ртуть” - 1й том Барочного цикла
  • Нил Стивенсон “Смешенье” - 2й том Барочного цикла
  • Нил Стивенсон “Система мира” - 3й том Барочного цикла
  • Брайан Грин “Элегантная вселенная” - научно-популярная книжка по теории струн
  • Карлос Кастанеда “Сказки о силе” - 4я книга про дона Хуана

Обзор NASA World Wind
alex_bobkov

NASA World Wind - это виртуальный глобус, разрабатываемый NASA и сообществом открытого программного обеспечения. Он позволяет просматривать спутниковые снимки Земли, Луны, Марса, Юпитера, некоторых его спутников и карту звёздного неба.

Основные отличия от Google Earth:

  • WW полностью бесплатно в том числе для коммерческого использования
  • WW имеет открытый исходный код, который можно использовать для создания своих приложений на базе WW
  • WW использует бесплатные снимки Земли: на мелких масштабах - Blue Marble NG, на крупных масштабах - Landsat 7, но при этом позволяет брать данные и из других источников

Первая версия WW вышла в 2004 году, была написана на C# и работала только под Windows. Разработка этой версии прекращена в 2007 году, и поэтому имеются проблемы совместимости с Windows Vista/Windows 7. Предлагается использовать версию с сайта spoon. Грузится довольно долго и работает нестабильно. Многие фичи недоступны, в частности модуль Scientific Visualization Studio, который позволяет просматривать анимацию многих природных явлений (пожаров, ураганов). Вот описание 2005 года. Всеобъемлющая информация по этой версии и список плагинов доступны на сайте World Wind Central.

Скриншот World Wind 1.4.0 (14.02.2007)

В 2007 году началась разработка новой версии на языке Java. Она называется World Wind Java SDK и работает на любых операционных системах с поддержкой Java и OpenGL. Слово SDK в названии находится неспроста: это не единое цельное приложение, а набор компонентов, которые можно встраивать в сторонние приложения на Java. Вместе с WW Java SDK идёт набор демонстрационных приложений, каждое из которых иллюстрирует отдельные возможности WW.

Коротко их перечислю некоторые из них:

  • загрузка изображений поверхности Земли из разных интернет-источников, доступных по протоколу WMS
  • добавление пользовательских спутниковых снимков
  • сохранение изображений и рельефа отдельных участков поверхности в виде GeoTIFF-файлов
  • просмотр данных из KML-файлов
  • добавление 3д-моделей, фигур, линий и меток на поверхность глобуса

WW позволяет просмотр научных данных NASA с сайта: http://neowms.sci.gsfc.nasa.gov. Здесь собрано множество растровых слоёв данных: температура суши и моря, толщина аэрозольного слоя, концентрация углекислого газа, плотность растительного покрова, альбедо поверхности и другие.

Также WW можно встраивать в браузеры. Вот пример с вулканами в США.

Скриншот World Wind Java SDK (ApplicationTemplate)

Просмотр температуры поверхности Земли


3D Engine Design for Virtual Globes
alex_bobkov

В 2011 годы вышла книга, посвященная разработке виртуальных глобусов: 3D Engine Design for Virtual Globes by Patrick Cozzi and Kevin Ring. Это первый фундаментальный труд по данной теме, которая стала популярной с момента выхода в начале 2000х годов таких приложений, как Google Earth и NASA World Wind.

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

Для иллюстрации идей, описанных в книге, авторы разработали движок OpenGlobe. А сейчас они занимаются портированием движка на WebGL.

Мастхэв для всех, кто занимается этой темой.


?

Log in