Введение в OpenSceneGraph: От трехмерного мира к изображению на экране
Когда происходит рисование точки, линии или сложного полигона в трехмерном мире, финальный результат, в конечном итоге, будет изображен на плоском, двухмерном экране. Соотвественно, трехмерные объекты проходят неуий путь преобразования, превращаясь в набор пикселей, выводимых в двумерное окно. В математическое преобразование коордниат вовлечены три основных матрицы, осуществляющие трансформацию между различными системами координат. Частно, в терминах 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.
Класс osg::Camera
Разработчики, использующие OpenGL, очень часто любят применять glTranslate() и glRotate() для перемещения сцены, а gluLookAt для перемещения камеры, хотя эти операции могут быть заменены на glMultMatrix(). Фактически, описанные операции сводятся к преобразованиям данных из мирового пространства в пространство вида.
Аналогичным образом OSG предосталвет класс osg::Transform для перемещения модели при её размещении в графе сцены, но мы всегда стараемся использовать osg::MatrixTransform и osg::PositionAttitudeTransform для работы с матрицей модели и класс osg::Camera для работы с матрицей вида.
Класс osg::Camera является одним из наиболее важных классов в ядре OSG. Он может использоваться как групповой узел графа сцены, но функционально являет собой нечто большее, чем просто узел. Его функции можно разделить на несколько категорий.
Во-первых, класс камеры ссылается на видовую матрицу, матрицу проектции и вьюпорт, которые применяются ко всем дочерним нодам камеры и проецируются её на экран. Связанные с этими функциями методы:
- setViewMatrix() и setViewMatrixAsLookAt() - задают видовую матрицу, передаваемую как параметр osg::Matrix или стандартные переменные eye/center/up
- setProjectionMatrix() - принимает параметр osg::MAtrix, определяющий матрицу проекции.
- setProjectionMatrixAsFrustum(), setProjectionMatrixAsOrtho(), setProjectionMatrixAsOrtho2D() и setProjectionMatrixAsPerspective() - используются для задания матрицы перспективной или ортогональной проекции с различными параметрами пирамиды отсечения. Они работают как соотвествующие функции OpenGL (glOrtho(), gluPerspective() и т.д.)
- setViewport() - задает прямоугольую область отрисовки в окне приложения через параметр типа osg::Viewport.
Следующий код демонстрирует процесс задания матрицы проекции и вьпорта для области с диагональю (x, y) - (x + w, x + h)
camera->setViewMatrix(viewMAtrix);
camera->setProjectionMatrix(projectionMatrix);
camera->setViewport(new osg::Viewport(x, y, w, h));
Всегда можно получить текщуие видовую матрицу, матрицу проекции и вьпорт из объекта камеры используя методы get*(), например
osg::Matrix viewMAtrix = camera->getViewMatrix();
Для получения позиции и ориентации видовой матрицы можно использовать такой код
osg::Vec3 eye, center, up;
camera->getViewMatrixAsLookAt(eye, center, up);
Во-вторых, класс камеры инкапсулирует такие функции OpenGL как glClear(), glClearColor() и glClearDepth(), очищает кадровые буферы и устанавлявает их параметры при перерисовке сцены на каждом кадре. С эими функциями связаны методы
-
setClearMask() - устанавлявает механизм очистки буфера. По-умочанию установлена маска GL_COLOR_BUFFER_BIT GL_DEPTH_BUFFER_BIT - setClearColor() - устанавливает цевет в формате RGBA, которым будет залит фуфер кадра.
Аналогично, представлены методы setClearDepth(), setClearStencil() и setClearAccum() а так же соответсвующие методы get*() для получения текущих параметров с камеры.
Третья группа методов включает в себя управление графическим контекстом openGL, о чем мы поговорим, когда будем рассматривать интегрцию OSG в сторонний графический интерфейс.
Наконец, к камере можно прикрепить текстурный объект, связанный с внутренними буферами (буверы цвета, гоубины и т.д.) и рендерить подграф сцены в текстуру. Полученная текстура может быть натянута на поверхность в другой сцене. Эта техника называется рендерингом в текстуру или запеканием текстуры.
Управление порядком рендеринга
Существует как минимум одна основная камера в любом графе сцены. Она создается и управляется классом osgViewer::Viewer и можетбыть прочитана методом getCamera(). Она автоматически добавляет корневую ноду и все её дочерние ноды при запуске вьювера. По-умолчанию, любые другие камеры, явно и неявно добавляемые к корневому узлу, разделяют графический контекст с основной камерой, и рисуют свои подсцены в то же самое графическое окно, что и основная камера.
Коасс osg::Camera предоставляет метод setRenderOrder() для управления порядком рендеринга камерами. Это задается перечислителями, задающими порядок рендеринга и опциональным числовым параметром. Первый параметр-перечислитель может принимать значения PRE_RENDER и POST_RENDER, задающий общий порядок отрисовки. Второй параметр сортирует камеры одного типа в порядке возрастания своего значения. По-умолчанию он равен нулю.
Например
camera1->setRenderOrder(osg::Camera::PRE_RENDER);
camera2->setRenderOrder(osg::Camera::PRE_RENDER, 5);
camera3->setRenderOrder(osg::Camera::POST_RENDER);
Если задан порядок PRE_RENDER, то результат её работы будет очищаться и перекрываться результатами рендеринга следующей камеры. Это особенно полезно при рендеринге в текстуру, так как мы хотим, чтобы данная подсцена была скрыта, не мешая обновлению основной сцены.
Если задан порядок POST_RENDER, то камера может затереть текущий буфер кадра. Мы можем избежать этого, задав соответсвующие параметры для setClearMask(). Типичным примером может служить реализация HUD-дисплея поверх основной сцены.
Создание HUD-дисплея
HUD-дисплей - прием отображения данных, когда эти данные всегда отображаются в порте вывода. Они спользуется для вывода в кадр 3D-сцены важной двухмерной информации типа статистической информации в игре. Попробуем создать такой дисплей.
main.h
#ifndef MAIN_H
#define MAIN_H
#include <osg/Camera>
#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/lz.osg");
osg::ref_ptr<osg::Node> hud_model = osgDB::readNodeFile("../data/glider.osg");
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setClearMask(GL_DEPTH_BUFFER_BIT);
camera->setRenderOrder(osg::Camera::POST_RENDER);
camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);
camera->setViewMatrixAsLookAt(osg::Vec3(0.0f, -5.0f, 5.0f),
osg::Vec3(),
osg::Vec3(0.0f, 1.0f, 1.0f));
camera->addChild(hud_model.get());
osg::ref_ptr<osg::Group> root = new osg::Group;
root->addChild(model.get());
root->addChild(camera.get());
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());
return viewer.run();
}
Загружаем две модели с диска - тестовый ландшафт lz.osg и модель дельтаплана glider.osg, которая будет рисваться поверх всей сцены HUD-камерой
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/lz.osg");
osg::ref_ptr<osg::Node> hud_model = osgDB::readNodeFile("../data/glider.osg");
Создаем HUD-камеру. Эта камера должна рендерить изображение поверх основной сцены. Она перекрывает все ранее отрисованные данные, независимо от их расположения и глубины. Мы используем маску GL_DEPTH_BUFFER_BIT для очистки буфера глубины. GL_COLOR_BUFFER_BIT мы не устанавливаем, чтобы гарантировать сохрание данных буфера цвета
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setClearMask(GL_DEPTH_BUFFER_BIT);
camera->setRenderOrder(osg::Camera::POST_RENDER);
Камера HUD не должна воздейсвовать на вьювер и ноды основной сцены, поэтому её нужно настроить на асолютную систему координат и установить фиксированную матрицу вида. Дельтаплан добавляется в качестве дочернего узла камеры и используется в качестве отображаемого объекта для неё
camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);
camera->setViewMatrixAsLookAt(osg::Vec3(0.0f, -5.0f, 5.0f),
osg::Vec3(),
osg::Vec3(0.0f, 1.0f, 1.0f));
camera->addChild(hud_model.get());
Создаем корневую ноду и добавляем в неё в качестве дочерней камеру HUD и основную сцену в виде ландшафта.
osg::ref_ptr<osg::Group> root = new osg::Group;
root->addChild(model.get());
root->addChild(camera.get());
Как обычно запускаем сцену
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());
return viewer.run();
При запуске программы манипуляции камеры влияют на модель ландшафта, однако дельтаплан не меняет свое положение вне зависимости от пользовательского ввода