Главная » Хабрахабр » OpenSceneGraph: Групповые узлы, узлы трансформации и узлы-переключатели

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

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

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

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

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

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

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

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

Ve = V * modelViewMatrix

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

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

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.

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

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

osg::Group содержит список дочерних нод, где каждая дочерняя нода управляется умным указателем. Класс osg::Group является производным от класса osg::Node, и соответственно наследуется и от класса osg::Referenced. Данный класс предоставляет разработчику ряд публичных методов
Это гарантирует отсутствие утечек памяти при каскадном удалении ветки дерева сцены.

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

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

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

Пока же мы рассмотрим методы, определенные в osg::Node, используемые для манипуляций над родительскими узлами:
OSG позволяет узлам сцены иметь несколько родительских узлов (об этом мы поговорим когда-нибудь потом).

  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.

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

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

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

Узлы 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 );

Тип 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.

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

Полный текст примера 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());

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

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

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

По своему действию класс 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) (углы задаются в радианах!)

Рассмотрим еще один класс — 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. Продолжение следует...


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Вышел Rust 2018… но что это такое?

Статья написана Лин Кларк в сотрудничестве с командой разработчиков Rust («мы» в тексте). Можете прочитать также сообщение в официальном блоге Rust. В этом релизе мы сосредоточились на производительности, чтобы разработчики Rust стали работать максимально эффективно. 6 декабря 2018 года вышла ...

50 лет спустя. The Mother of All Demos

«Компьютерная революция еще не случилась.(The computer revolution hasnt happened yet)»— Алан Кей Всем привет. И я стартую проект «Энгельбарт» (чтобы это ни было и что бы это ни значило). Сегодня 50 лет с исторического события, известного как "Мать всех демонстраций" ...