OpenSceneGraph: Групповые узлы, узлы трансформации и узлы-переключатели +12


image

Введение


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

Развитие программных инструментов, реализующих трехмерную графику пришло, вне зависимости от того, какой из них вы выбираете, примерно к одинаковой концепции как математического, так и алгоритмического описания вышеупомянутых трансформаций. Идеологически и «чистые» графические API типа OpenGL, и крутые игровые движки типа Unity и Unreal, используют схожие механизмы описания преобразования трехмерной сцены. Не является исключением и OpenSceneGraph.

В этой статье мы сделаем обзор механизмов группировки и трансформации трехмерных объектов в OSG.

1. Матрица модели, матрица вида и матрица проекции


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

Матрица модели служит для описания расположения объекта в 3D-мире. Она осуществляет преобразование вершин из локальной системы координат объекта в мировую систему координат. К слову, все системы координат в OSG являются правовинтовыми.

Следующим шагом является преобразование мировых координат в пространство вида, выполняемое с помощью матрицы вида. Предположим, что мы имеем камеру, расположенную в начале отсчета мировой системы координат. Матрица, обратная матрице преобразования камеры фактически и используется как матрица вида. В правовинтовой системе координат OpenGL, по-умолчанию, всегда определяет камеру расположенной в точке (0, 0, 0) глобальной системы координат и направленной вдоль отрицательного направления оси Z.

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

Ve = V * modelViewMatrix

Следующей важной задачей является определить, как 3D-объекты будут проецироваться в плоскость экрана и вычислить так называемую пирамиду отсечения — область пространства, содержащую объекты, подлежащие отображению на экране. Матрица проекции используется для задания пирамиды отсечения, заданной в мировом пространстве шестью плоскостями: левой, правой, нижней, верхней, ближней и дальней. OpenGL предоставляет функцию gluPerapective(), позволяющую задать пирамиду отсечения и способ проецирования трехмерного мира на плоскость.

Полученная после вышеописанных преобразований система координат называется нормализованной системой координат устройства, имеет по каждой оси диапазон изменения координат от -1 до 1 и является левовинтовой. И, в качестве последнего шага, происходит проецирование полученных данных в порт отображения (вьюпорт) окна, определяемое прямоугольником клиентской области окна. После этого 3D-мир появляется на нашем 2D-экране. Окончательное значение экранных координат вершин Vs можно выразить следующим преобразованием

Vs = V * modelViewMatrix * projectionMatrix * windowMatrix

или

Vs = V * MVPW

где MVPW — эквивалентная матрица преобразования, равная произведению трех матриц: матрицы модель-вид, матрицы проекции и матрицы окна.

Vs в этой ситуации является трехмерным вектором, который определяет положение 2D-пикселя со значением глубины. Обратив операцию преобразования координат мы получим линию в трехмерном пространстве. Поэтому 2D-точку можно рассматривать как две точки — одну на ближней (Zs = 0), другую — на дальней плоскости отсечения (Zs = 1). Координаты этих точек в трехмерном пространстве

V0 = (Xs, Ys, 0) * invMVPW
V1 = (Xs, Ys, 1) * invMVPW

где invMVPW — матрица, обратная MVPW.

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

2. Групповые ноды


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

Класс osg::Group является производным от класса osg::Node, и соответственно наследуется и от класса osg::Referenced. osg::Group содержит список дочерних нод, где каждая дочерняя нода управляется умным указателем. Это гарантирует отсутствие утечек памяти при каскадном удалении ветки дерева сцены. Данный класс предоставляет разработчику ряд публичных методов
  1. addChild() — присоединяет узел в конец списка дочерних узлов. С другой стороны есть метод insertChild(), помещающий дочерний узел в конкретную позицию списка, которая задается целочисленным индексом или указателем на узел, передаваемыми в качестве параметра.
  2. removeChild() и removeChildren() — удаление одного узла или группы узлов.
  3. getChild() — получение указателя на ноду по её индексу в списке
  4. getNumChildren() — получение числа дочерних узлов, прикрепленных к данной группе.

Управление родительскими узлами


Как мы уже знаем, класс osg::Group управляет группами своих дочерних объектов, среди которых могут присутствовать и экземпляры osg::Geode, управляющие геометрией объектов сцены. Оба упомянутых класса имеют интерфейс для управления родительскими узлами.

OSG позволяет узлам сцены иметь несколько родительских узлов (об этом мы поговорим когда-нибудь потом). Пока же мы рассмотрим методы, определенные в osg::Node, используемые для манипуляций над родительскими узлами:
  1. getParent() — возвращает указатель типа osg::Group, содержащий перечень родительских узлов.
  2. getNumParants() — возвращает число родительских узлов.
  3. getParentalNodePath() — возвращает все возможные пути к корневой ноде сцены от текущей ноды. Он возвращает список переменных типа osg::NodePath.

osg::NodePath представляет собой std::vector указателей на узлы сцены.



Например, для сцены, изображенной на рисунке следующий код

osg::NodePath &nodePath = child3->getParentalNodePaths()[0];
for (unsigned int i = 0; i < nodePath.size(); ++i)
{
	osg::Node *node = nodePath[i];
	// Что-нибудь делаем с нодой
}

вернет ноды Root, Child1, Child2.

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

3. Добавление нескольких моделей в дерево сцены


Проиллюстрируем механизм использования групп следующим примером

Полный текст примера group
main.h
#ifndef     MAIN_H
#define     MAIN_H

#include    <osg/Group>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif

main.cpp

#include    "main.h"

int main(int argc, char *argv[])
{
    (void) argc, (void) argv;

    osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg");
    osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cow.osg");

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild(model1.get());
    root->addChild(model2.get());

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());

    return viewer.run();
}


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

osg::ref_ptr<osg::Group> root = new osg::Group;
root->addChild(model1.get());
root->addChild(model2.get());



В итоге мы получаем сцену, состоящую из двух моделей — самолета и смешной зеркальной коровы. Кстати, зеркальная корова не будет зеркальной, если не скопировать её текстуру из OpenSceneGraph-Data/Images/reflect.rgb а каталог data/Images нашего проекта.

Класс osg::Group может принимать в качестве дочерних любые типы узлов, в том числе и узлы своего типа. Напротив, класс osg::Geode не содержит вообще каких-либо дочерних узлов — он является оконечным узлом, содержащим в себе геометрию объекта сцены. Этот факт удобен при выяснении вопроса является ли узел узлом типа osg::Group или другого типа производного от osg::Node. Рассмотрим маленький пример

osg::ref_ptr<osg::Group> model = dynamic_cast<osg::Group *>(osgDB::readNodeFile("../data/cessna.osg"));

Значение, возвращаемое функцией osgDB::readNodeFile() всегда имеет тип osg::Node*, но оно может быть преобразовано к своему наследнику osg::Group*. Если коневой узел модели Cessna это групповой узел, то преобразование будет успешным, в противном случае преобразование вернет NULL.

Можно выполнить так же такой фокус, работающий на большинстве компиляторов

// Загружаем модель в групповой узел
osg::ref_ptr<osg::Group> group = ...;
// Преобразуем его к узлу
osg::Node* node1 = dynamic_cast<osg::Node*>( group.get() );
// Преобразуем группу к узлу неявно
osg::Node* node2 = group.get();

В критических для производительности местах кода лучше использовать специальные методы преобразования

osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cessna.osg");
osg::Group* convModel1 = model->asGroup(); // Работает нормально
osg::Geode* convModel2 = model->asGeode(); // Вернет NULL.

4. Ноды трансформации


Узлы osg::Group не могут делать никаких преобразований, кроме возможности перехода к своим дочерним узлам. Для пространственного перемещения геометрии OSG предоставляет класс osg::Transform. Этот класс является наследником класса osg::Group, но и сам является абстрактным — на практике вместо него применяются его наследники, реализующие различные пространственные преобразования геометрии. При обходе графа сцены узел osg::Transform добавляет свое преобразование в текущую матрицу преобразования OpenGL. Это эквивалентно перемножению матриц преобразования OpenGL, выполняемое командой glMultMatrix()



Этот пример графа сцены можно транслировать в следующий кода на OpenGL

glPushMatrix();
	glMultMatrix( matrixOfTransform1 );
	renderGeode1(); 
	
	glPushMatrix();
		glMultMatrix( matrixOfTransform2 );
		renderGeode2();
	glPopMatrix();

glPopMatrix();

Можно сказать, что положение Geode1 задается в системе координат Transform1, а положение Geode2 задается в системе координат Transform2, смещенной относительно Transform1. При этом в OSG можно включить позиционирование в абсолютных координатах, что приведет к поведению объекта, эквивалентному результату команды glGlobalMatrix() OpenGL

transformNode->setReferenceFrame( osg::Transform::ABSOLUTE_RF );

Можно переключится обратно в режим позиционирования относительными координатами

transformNode->setReferenceFrame( osg::Transform::RELATIVE_RF );

5. Понятие о матрице преобразования координат


Тип osg::Matrix это базовый тип OSG не управляемый умными указателями. Он предоставляет интерфейс к операциями над матрицами размерности 4х4, описывающими преобразование координат, таких как перемещение, поворот, масштабирование и вычисление проекций. Матрица может быть задана явно

// Единичная матрица 4х4
osg::Matrix mat(1.0f, 0.0f, 0.0f, 0.0f,
		0.0f, 1.0f, 0.0f, 0.0f,
		0.0f, 0.0f, 1.0f, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f ); 

Класс osg::Matrix предоставляет следующие публичные методы:

  1. postMult() и operator* () — умножение справа текущей матрицы на матрицу или вектор, переданные в качестве параметра. Метод preMult() выполняет умножение слева.
  2. makeTranslate(), makeRotate() и makeScale() — сбрасывают текущую матрицу и создают матрицу 4х4 описывающую перемещение, вращение и масштабирование. их статические версии translate(), rotate() и scale() могут быть использованы для создания матричного объекта со специфическими параметрами.
  3. invert() — вычисление матрицы обратной текущей. Его статическая версия inverse() принимает в качестве параметра матрицу и возвращает новую матрицу, обратную данной.

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

osg::Matrix mat = …;
osg::Vec3 vec = …;
osg::Vec3 resultVec = vec * mat;

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

osg::Matrix mat1 = osg::Matrix::scale(sx, sy, sz);
osg::Matrix mat2 = osg::Matrix::translate(x, y, z);
osg::Matrix resultMat = mat1 * mat2;

Разработчик должен читать процесс трансформации слева направо. То есть, в описанном фрагменте кода сначала происходит масштабирование вектора, а затем его перемещение.

osg::Matrixf содержит элементы типа float.

6. Применение класса osg::MatrixTransform


Применим полученные теоретические знания на практике, загрузив две модели самолета в разные точки сцены.

Полный текст примера transform
main.h
#ifndef     MAIN_H
#define     MAIN_H

#include    <osg/MatrixTransform>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif

main.cpp

#include    "main.h"

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg");

    osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform;
    transform1->setMatrix(osg::Matrix::translate(-25.0, 0.0, 0.0));
    transform1->addChild(model.get());

    osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform;
    transform2->setMatrix(osg::Matrix::translate(25.0, 0.0, 0.0));
    transform2->addChild(model.get());

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild(transform1.get());
    root->addChild(transform2.get());

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());

    return viewer.run();
}


Пример, на самом деле довольно тривиален. Загружаем модель самолета из файла

osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg");

Создаем ноду трансформации

osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform;

Устанавливаем в качестве матрицы преобразования перемещение модели по оси X на 25 единиц влево

transform1->setMatrix(osg::Matrix::translate(-25.0, 0.0, 0.0));

Задаем для ноды трансформации нашу модель в качестве дочернего узла

transform1->addChild(model.get());

Аналогично поступаем и со второй трансформацией, но в качестве матрица задаем перемещение вправо на 25 единиц

osg::ref_ptr<osg::MatrixTransform> transform2 = new osg::MatrixTransform;
transform2->setMatrix(osg::Matrix::translate(25.0, 0.0, 0.0));
transform2->addChild(model.get());

Создаем корневую ноду и в качестве дочерних узлов для неё задаем трансформационные ноды transform1 и transform2

osg::ref_ptr<osg::Group> root = new osg::Group;
root->addChild(transform1.get());
root->addChild(transform2.get());

Создаем вьювер и в качестве данных сцены передаем ему корневую ноду

osgViewer::Viewer viewer;
viewer.setSceneData(root.get());

Запуск программы дает такую картинку



Структура графа сцены в этом примере такова



Нас не должен смущать тот факт, что ноды трансформации (Child 1.1 и Child 1.2) ссылаются на один и тот же дочерний объект модели самолета (Child 2). Это штатный механизм OSG, когда один дочерний узел графа сцены может иметь несколько родительских узлов. Таким образом нам не обязательно хранить в памяти два экземпляра модели, чтобы получить в сцене два одинаковых самолета. Такой механизм позволяет очень эффективно распределять память в приложении. Модель не будет удалена из памяти, пока на неё ссылается, как на дочернюю, хотя бы одна нода.

По своему действию класс osg::MatrixTransform эквивалентен командам OpenGL glMultMatrix() и glLoadMatrix(), реализует все виды пространственных преобразований, но сложен в использованию из-за необходимости вычислять матрицу преобразования.

Класс osg::PositionAttitudeTransform работает как функции OpenGL glTranslate(), glScale(), glRotate(). Он предоставляет публичные методы для преобразования дочерних узлов:

  1. setPosition() — переместить узел в данную точку пространства, задаваемую параметром osg::Vec3
  2. setScale() — масштабировать объект по осям координат. Коэффициенты масштабирования по соответствующим осям задаются параметром типа osg::Vec3
  3. setAttitude() — задать пространственную ориентацию объекта. В качестве параметра принимает кватернион преобразования поворота osg::Quat, конструктор которого имеет несколько перегрузок, позволяющих задавать кватернион как непосредственно (покомпонентно), так и, например, через углы Эйлера osg::Quat(xAngle, osg::X_AXIS, yAngle, osg::Y_AXIS, zAngle, osg::Z_AXIS) (углы задаются в радианах!)


7. Ноды-переключатели


Рассмотрим еще один класс — osg::Switch, позволяющий отображать или пропускать рендеринг узла сцены, в зависимости от некоего логического условия. Он является наследником класса osg::Group и прикрепляет к каждой своей дочерней ноде некоторое логическое значение. Он имеет несколько полезных публичных методов:
  1. Перегруженный addChild(), в качестве второго параметра принимающий логический ключ, указывающий отображать или нет данный узел.
  2. setValue() — установка ключа видимости/невидимости. Принимает индекс интересующей нас дочерней ноды и желаемое значение ключа. Соответственно getValue() позволяет получить текущее значение ключа по индексу интересующей нас ноды.
  3. setNewChildDefaultValue() — установка значения по-умолчанию для ключа видимости всех новых объектов, добавляемых в качестве дочерних.

Рассмотрим применение данного класса на примере.

Полный текст примера switch
main.h
#ifndef     MAIN_H
#define     MAIN_H

#include    <osg/Switch>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif


main.cpp
#include    "main.h"

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg");
    osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg");

    osg::ref_ptr<osg::Switch> root = new osg::Switch;
    root->addChild(model1.get(), false);
    root->addChild(model2.get(), true);

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());

    return viewer.run();
}


Пример тривиален — мы загружаем две модели: обычную цессну и цессну с эффектом горящего двигателя

osg::ref_ptr<osg::Node> model1 = osgDB::readNodeFile("../data/cessna.osg");
osg::ref_ptr<osg::Node> model2 = osgDB::readNodeFile("../data/cessnafire.osg");

Однако, в качестве корневой ноды создаем osg::Switch, что позволяет нам, при добавлении в неё моделей в качестве дочерних узлов задать ключ видимости для каждой из них

osg::ref_ptr<osg::Switch> root = new osg::Switch;
root->addChild(model1.get(), false);
root->addChild(model2.get(), true);

То есть, model1 не будет рендерится, а model2 будет, что мы и пронаблюдаем, запустив программу



Поменяв местами значения ключей будем видеть противоположную картину

root->addChild(model1.get(), true);
root->addChild(model2.get(), false);



Взведя оба ключа, увидим две модели одновременно

root->addChild(model1.get(), true);
root->addChild(model2.get(), true);



Включать видимость и невидимость ноды, дочерней для osg::Switch можно прямо на ходу, используя метод setValue()

switchNode->setValue(0, false);
switchNode->setValue(0, true);
switchNode->setValue(1, true);
switchNode->setValue(1, false);

Заключение


В этом уроке мы рассмотрели все основные классы промежуточных узлов, используемых в OpenSceeneGraph. Таким образом мы уложили ещё один базовый кирпич в фундамент знаний об устройстве этого несомненно интересного графического движка. Рассмотренные в статье примеры, как и ранее, доступны в моем репозитории на Github. Продолжение следует...




К сожалению, не доступен сервер mySQL