Введение
OpenGL, являющийся бэкэндом для OpenSceneGraph, использует геометрические примитивы (такие как точки, линии, треугольники и полигональные грани) для построения всех объектов трехмерного мира.
Эти примитивы задаются данными об их вершинах, в которые входят координаты вершин, компоненты нормалей, данные о цвете и текстурные координаты. Эти данные хранятся в специальных массивах. Примитивы могут быть сформированы, например, путем указания для объектов, их описывающих, списка индексов вершин. Этот метод называется методом массива вершин, он позволяет исключить хранение в памяти избыточных вершин и обладает хорошим быстродействием.
Кроме того, OpenGL может использовать механизм так называемых дисплейных списков, когда однажды подготовленные в видеопамяти примитивы могут использоваться повторно, что существенно ускоряет отображение статических объектов.
По-умолчанию OSG использует метод массивов вершин и метод дисплейных списков для рендеринга геометрии. Однако, стратегия рендеринга может быть изменена, в зависимости от того, каким образом представлены данные о геометрии. В этой статье мы рассмотрим базовые приемы работы с геометрией в OSG.
1. Классы Geode и Drawable
Класс osg::Geode представляет собой оконечный, так называемый "листовой" узел дерева сцены. Он не может иметь дочерних узлов, но при этом содержит всю необходимую информацию для рендеринга геометрии. Его имя — Geode есть сокращение от слов geometry node.
Геометрические данные, подлежащие обработке движком запоминаются в наборе объектов класса osg::Drawable, управляемых классом osg::Geode. Класс osg::Drawable является чисто виртуальным классом. От него наследуются ряд подклассов, представляющих собой трехмерные модели, изображения и текст, обрабатываемые конвейером OpenGL. Под drawable в OSG понимаются все элементы, которые могут быть отрисованы движком.
Класс osg::Geode предоставляет ряд методов для присоединения и отсоединения drawables:
- Публичный метод addDrawable() — передает указатель на drawable элемент в экземпляр класса osg::Geode. Все эти элементы управляются посредством умных указателей osg::ref_ptr<>.
- Публичный метод removeDrawable() и removeDrawables() удаляет объект из osg::Geode и уменьшает счетчик ссылок на него. Метод removeDrawable() принимает в качестве единственного параметра указатель на интересующий элемент, а метод removeDrawables() принимает два параметра: начальный индекс и число элементов, подлежащих удалению из массива объектов osg::Geode.
- Метод getDrawable() возвращает указатель на элемент по передаваемому в качестве параметра индексу.
- Метод getNumDrawables() возвращает общее число элементов, прикрепленных к osg::Geode. Например, для удаления всех элементов из osg::Geode можно использовать такой код
geode->removeDrawables(0, geode->getNumDrawables());
2. Рисование простейших фигур
OSG предоставляет класс osg::ShapeDrawable, являющийся наследником класса osg::Drawable, и предназначенный для создания простейших трехмерных примитивов. Этот класс включает в себя объект osg::Shape, хранящий информацию о специфической геометрии и ещё параметрах. Генерация примитивов осуществляется с помощью метода setShape(), например
shapeDrawable->setShape(new osg::Box(osg::Vec3(1.0f, 0.0f, 0.0f), 10.0f, 10.0f, 5.0f));
создает прямоугольный параллелепипед с геометрическим центром в точке (1.0, 0.0, 0.0) c шириной и высотой 10 и глубиной 5 единиц. Класс osg::Vec3 определяет вектор в трехмерном пространстве (кроме того, представлены и классы osg::Vec2 и osg::Vec4 описывающие векторы соответствующей размерности).
Наиболее популярные примитивы представлены в OSG классами osg::Box, osg::Capsule, osg::Cone, osg::Cylinder и osg::Sphere.
Рассмотрим пример применения данного механизма.
main.h
#ifndef MAIN_H
#define MAIN_H
#include <osg/ShapeDrawable>
#include <osg/Geode>
#include <osgViewer/Viewer>
#endif // MAIN_H
main.cpp
#include "main.h"
int main(int argc, char *argv[])
{
(void) argc;
(void) argv;
osg::ref_ptr<osg::ShapeDrawable> shape1 = new osg::ShapeDrawable;
shape1->setShape(new osg::Box(osg::Vec3(-3.0f, 0.0f, 0.0f), 2.0f, 2.0f, 1.0f));
osg::ref_ptr<osg::ShapeDrawable> shape2 = new osg::ShapeDrawable;
shape2->setShape(new osg::Cone(osg::Vec3(0.0f, 0.0f, 0.0f), 1.0f, 1.0f));
shape2->setColor(osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f));
osg::ref_ptr<osg::ShapeDrawable> shape3 = new osg::ShapeDrawable;
shape3->setShape(new osg::Sphere(osg::Vec3(3.0f, 0.0f, 0.0f), 1.0f));
shape3->setColor(osg::Vec4(0.0f, 0.0f, 1.0f, 1.0f));
osg::ref_ptr<osg::Geode> root = new osg::Geode;
root->addDrawable(shape1.get());
root->addDrawable(shape2.get());
root->addDrawable(shape3.get());
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());
return viewer.run();
}
Данный пример особенно не нуждается в комментариях: в программе создаются три простейшие фигуры, после компиляции и запуска мы увидим такой результат
Механизм, приведенный в примере прост и понятен, однако не является самым эффективным способом создания геометрии и может использоваться исключительно для тестов. Для создания геометрии в высокопроизводительных приложениях на базе OSG используется класс osg::Geometry.
3. Хранение данных геометрии: классы osg::Array и osg::Geometry
Класс osg::Array является базовым абстрактным классом, от которого наследуются несколько потомков, предназначенных для хранения данных, передаваемых в функции OpenGL. Работа с данным классом аналогична работе с std::vector из стандартной библиотеки C++. Следующий код иллюстрирует добавление вектора в массив вершин методом push_back()
vertices->push_back(osg::Vec3(1.0f, 0.0f, 0.0f));
Массивы OSG выделяются в куче и управляются посредством умных указателей. Однако это не касается элементов массивов, таких как osg::Vec3 или osg::Vec2, которые могут быть созданы и на стеке.
Класс osg::Geometry является оберткой над функциями OpenGL, работающими с массивами вершин. Он является производным от класса osg::Drawable и может быть без проблем добавлен в список объектов osg::Geode. Этот класс принимает вышеописанные массивы в качестве входных данных и использует их для генерации геометрии средствами OpenGL.
4. Вершины и их атрибуты
Вершина является атомарной единицей примитивов геометрии. Она обладает рядом атрибутов, описывающих точку двух- или трехмерного пространства. К атрибутам относятся: положение, цвет, вектор-нормаль, текстурные координаты, координаты тумана и так далее. Вершина всегда должна иметь положение в пространстве, что касается других атрибутов, они могут присутствовать опционально. OpenGL поддерживает 16 базовых атрибутов вершины и может использовать разные массивы для их хранения. Все массивы атрибутов поддерживаются классом osg::Geometry и могут быть заданы методами вида set*Array().
Атрибуты вершин в OpenSceneGraph
Атрибут | Тип данных | Метод osg::Geometry | Эквивалентный вызов OpenGL |
---|---|---|---|
Положение | 3-вектор | setVertexArray() | glVertexPointer() |
Нормаль | 3-вектор | setNormalArray() | glNormalPointer() |
Цвет | 4-вектор | setColorArray() | glColorPointer() |
Вторичный цвет | 4-вектор | setSecondaryColorArray() | glSecondaryColorPointerEXT() |
Координаты тумана | float | setFogCoordArray() | glFogCoordPointerEXT() |
Текстурные координаты | 2- или 3-вектор | setTexCoordArray() | glTexCoordPointer() |
Прочие атрибуты | Определен пользователем | setVertexArribArray() | glVertexAttribPointerARB() |
В принципе, устанавливать свои атрибуты необходимо для каждой из вершин, что приводит к образованию нескольких массивов атрибутов одинакового размера — в противном случае несовпадение размеров массивов может привести к неопределенному поведению движка. OSG поддерживает различные методы связывания между собой атрибутов вершин, например
geom->setColorBinding(osg::Geometry::BIND_PER_VERTEX);
означает, что каждая вершина и каждый цвет вершины взаимно-однозначно соотносятся друг с другом. Однако, если взглянуть на такой код
geom->setColorBinding(osg::Geometry::BIND_OVERALL);
то он применяет один цвет ко всей геометрии. Аналогично могут быть настроены взаимосвязи между другими атрибутами путем вызова методов setNormalBinding(), setSecondaryColorBinding(), setFogCoordBinding() и setVertexAttribBinding().
5. Наборы примитивов геометрии
Следующим шагом после определения массивов атрибутов вершин является описание того, как данные вершины будут обработаны рендером. Виртуальный класс osg::PrimitiveSet используется для управления геометрическими примитивами, генерируемыми рендером из набора вершин. Класс osg::Geometry предоставляет несколько методов для работы с наборами примитивов геометрии:
- addPrimitiveSet() — передает указатель на набор примитивов в объект osg::Geometry.
- removePrimitiveSet() — удаление набора примитивов. В качестве параметров принимает начальный индекс наборов и число наборов, которое следует удалить.
- getPrimitiveSet() — возвращает набор примитивов по индексу, переданному в качестве параметра.
- getNumPrimitiveSets() — возвращает общее число наборов примитивов, связанных с данной геометрией.
Класс osg::PrimitiveSet является абстрактным и не инстанцируется, но от него наследуются несколько производных классов, инкапсулирующих наборы примитивов, которыми оперирует OpenGL, такие как osg::DrawArrays и osg::DrawElementsUInt.
Класс osg::DrawArrays использует несколько последовательных элементов массива вершин для конструирования геометрического примитива. Он может быть создан и прикреплен к геометрии вызовом метода
geom->addPrimitiveSet(new osg::DrawArrays(mode, first, count));
Первый параметр mode задает тип примитива, аналогичный соответствующим типам примитивов OpenGL: GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_QUAD_STRIP, GL_QUADS и GL_POLYGON.
Первый и второй параметр задают первый индекс в массиве вершин и число вершин, из которых следует генерировать геометрию. Причем OSG не проверяет, достаточно ли указанного числа вершин для построения заданной режимом геометрии, что может приводить к краху приложения!
6. Пример — рисуем раскрашенный квадрат
Реализуем всё вышеописанное в виде простого примера
#ifndef MAIN_H
#define MAIN_H
#include <osg/Geometry>
#include <osg/Geode>
#include <osgViewer/Viewer>
#endif // MAIN_H
main.cpp
#include "main.h"
int main(int argc, char *argv[])
{
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
vertices->push_back(osg::Vec3(0.0f, 0.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, 0.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, 0.0f, 1.0f));
vertices->push_back(osg::Vec3(0.0f, 0.0f, 1.0f));
osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
normals->push_back(osg::Vec3(0.0f, -1.0f, 0.0f));
osg::ref_ptr<osg::Vec4Array> colors = new osg::Vec4Array;
colors->push_back(osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f));
colors->push_back(osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f));
colors->push_back(osg::Vec4(0.0f, 0.0f, 1.0f, 1.0f));
colors->push_back(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
osg::ref_ptr<osg::Geometry> quad = new osg::Geometry;
quad->setVertexArray(vertices.get());
quad->setNormalArray(normals.get());
quad->setNormalBinding(osg::Geometry::BIND_OVERALL);
quad->setColorArray(colors.get());
quad->setColorBinding(osg::Geometry::BIND_PER_VERTEX);
quad->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4));
osg::ref_ptr<osg::Geode> root = new osg::Geode;
root->addDrawable(quad.get());
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());
return viewer.run();
}
После компиляции и выполнения получим результат, подобный этому
Данный пример нуждается в пояснении. Итак, первым делом мы создаем массив вершин квадрата, в котором хранятся их координаты
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
vertices->push_back(osg::Vec3(0.0f, 0.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, 0.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, 0.0f, 1.0f));
vertices->push_back(osg::Vec3(0.0f, 0.0f, 1.0f));
Далее задаем массив нормалей. В нашем простом случае нам не требуется создавать нормаль для каждой вершины — достаточно описать один единичный вектор, направленный перпендикулярно плоскости квадрата
osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
normals->push_back(osg::Vec3(0.0f, -1.0f, 0.0f));
Зададим цвет для каждой из вершин
osg::ref_ptr<osg::Vec4Array> colors = new osg::Vec4Array;
colors->push_back(osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f));
colors->push_back(osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f));
colors->push_back(osg::Vec4(0.0f, 0.0f, 1.0f, 1.0f));
colors->push_back(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
Теперь создаем объект геометрии, где будет хранится описание нашего квадрата, которое будет обработано рендером. Передаем в эту геометрию массив вершин
osg::ref_ptr<osg::Geometry> quad = new osg::Geometry;
quad->setVertexArray(vertices.get());
Передавая массив нормалей, сообщаем движку, что будет использована одна единственная нормаль для всех вершин, указанием метода связывания ("биндинга") нормалей BIND_OVAERALL
quad->setNormalArray(normals.get());
quad->setNormalBinding(osg::Geometry::BIND_OVERALL);
Передавая цвета вершин, напротив, указываем, что каждой вершине будет соответствовать собственный цвет
quad->setColorArray(colors.get());
quad->setColorBinding(osg::Geometry::BIND_PER_VERTEX);
Теперь создаем набор примитивов для геометрии. Указываем, что из массива вершин следует генерировать квадратные (GL_QUADS) грани, взяв в качестве первой вершины вершину с индексом 0, а общее число вершин будет равно 4
quad->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4));
Ну а передачу геометрии и запуск рендера пояснять, думаю, не стоит
osg::ref_ptr<osg::Geode> root = new osg::Geode;
root->addDrawable(quad.get());
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());
return viewer.run();
Приведенный код эквивалентен следующей конструкции на чистом OpenGL
static const GLfloat vertices[][3] = { … };
glEnableClientState( GL_VERTEX_ARRAY );
glVertexPointer( 4, GL_FLOAT, 0, vertices );
glDrawArrays( GL_QUADS, 0, 4 );
7. Индексирование вершин в примитивах
Класс osg::DrawArrays работает хорошо, когда чтение данных вершин происходит напрямую из массивов, без пропусков. Однако, это не столь эффективно, когда одна и та же вершина может принадлежать нескольким граням объекта. Рассмотрим пример
Куб имеет восемь вершин. Однако, как видно из рисунка (смотрим на развертку куба на плоскость) некоторые вершины принадлежат более чем одной грани. Если строить куб из 12-ти треугольных граней, то эти вершины будут повторятся, и вместо массива на 8 вершин мы получим массив на 36 вершин, большинство из которых на деле являются одной и той же вершиной!
В OSG существуют классы osg::DrawElementsUInt, osg::DrawElementsUByte и osg::DrawElementsUShort, которые используют в качестве данных массивы индексов вершин, призваны решить описанную проблему. Массивы индексов хранят индексы вершин примитивов, описывающих грани и другие элементы геометрии. При применении этих классов для куба достаточно хранить массив из восьми вершин, которые ассоциируются с гранями через массивы индексов.
Классы типа osg::DrawElements* устроены так же как и стандартный класс std::vector. Для добавления индексов может быть использован такой код
osg::ref_ptr<osg::DrawElementsUInt> de = new osg::DrawElementsUInt(GL_TRIANGLES);
de->push_back(0); de->push_back(1); de->push_back(2);
de->push_back(3); de->push_back(0); de->push_back(2);
Данный код определяет переднюю грань куба, изображенного на рисунке.
Рассмотрим еще один показательный пример — октаэдр
Интересен он потому, что содержит всего шесть вершин, но каждая вершина входит аж в четыре треугольных грани! Мы можем создать массив на 24 вершины для отображения всех восьми граней с помощью osg::DrawArrays. Однако мы поступим иначе — вершины будем хранить в массиве из шести элементов, а грани генерируем используя класс osg::DrawElementsUInt.
#ifndef MAIN_H
#define MAIN_H
#include <osg/Geometry>
#include <osg/Geode>
#include <osgUtil/SmoothingVisitor>
#include <osgViewer/Viewer>
#endif
main.cpp
#include "main.h"
int main(int argc, char *argv[])
{
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array(6);
(*vertices)[0].set( 0.0f, 0.0f, 1.0f);
(*vertices)[1].set(-0.5f, -0.5f, 0.0f);
(*vertices)[2].set( 0.5f, -0.5f, 0.0f);
(*vertices)[3].set( 0.5f, 0.5f, 0.0f);
(*vertices)[4].set(-0.5f, 0.5f, 0.0f);
(*vertices)[5].set( 0.0f, 0.0f, -1.0f);
osg::ref_ptr<osg::DrawElementsUInt> indices = new osg::DrawElementsUInt(GL_TRIANGLES, 24);
(*indices)[ 0] = 0; (*indices)[ 1] = 1; (*indices)[ 2] = 2;
(*indices)[ 3] = 0; (*indices)[ 4] = 4; (*indices)[ 5] = 1;
(*indices)[ 6] = 4; (*indices)[ 7] = 5; (*indices)[ 8] = 1;
(*indices)[ 9] = 4; (*indices)[10] = 3; (*indices)[11] = 5;
(*indices)[12] = 3; (*indices)[13] = 2; (*indices)[14] = 5;
(*indices)[15] = 1; (*indices)[16] = 5; (*indices)[17] = 2;
(*indices)[18] = 3; (*indices)[19] = 0; (*indices)[20] = 2;
(*indices)[21] = 0; (*indices)[22] = 3; (*indices)[23] = 4;
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
geom->setVertexArray(vertices.get());
geom->addPrimitiveSet(indices.get());
osgUtil::SmoothingVisitor::smooth(*geom);
osg::ref_ptr<osg::Geode> root = new osg::Geode;
root->addDrawable(geom.get());
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());
return viewer.run();
}
Разберем этот код подробнее. Разумеется, первым делом, мы создаем массив из шести вершин
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array(6);
(*vertices)[0].set( 0.0f, 0.0f, 1.0f);
(*vertices)[1].set(-0.5f, -0.5f, 0.0f);
(*vertices)[2].set( 0.5f, -0.5f, 0.0f);
(*vertices)[3].set( 0.5f, 0.5f, 0.0f);
(*vertices)[4].set(-0.5f, 0.5f, 0.0f);
(*vertices)[5].set( 0.0f, 0.0f, -1.0f);
Инициализируем каждую вершину непосредственно, обращаясь вектору её координат с использованием операции разыменования указателя и оператора operator[] (мы помним, что osg::Array аналогичен по своему устройству std::vector).
Теперь создаем грани в виде списка индексов вершин
osg::ref_ptr<osg::DrawElementsUInt> indices = new osg::DrawElementsUInt(GL_TRIANGLES, 24);
(*indices)[ 0] = 0; (*indices)[ 1] = 1; (*indices)[ 2] = 2; // Грань 0
(*indices)[ 3] = 0; (*indices)[ 4] = 4; (*indices)[ 5] = 1; // Грань 1
(*indices)[ 6] = 4; (*indices)[ 7] = 5; (*indices)[ 8] = 1; // Грань 2
(*indices)[ 9] = 4; (*indices)[10] = 3; (*indices)[11] = 5; // Грань 3
(*indices)[12] = 3; (*indices)[13] = 2; (*indices)[14] = 5; // Грань 4
(*indices)[15] = 1; (*indices)[16] = 5; (*indices)[17] = 2; // Грань 5
(*indices)[18] = 3; (*indices)[19] = 0; (*indices)[20] = 2; // Грань 6
(*indices)[21] = 0; (*indices)[22] = 3; (*indices)[23] = 4; // Грань 7
Грани будут треугольными, их будет 8, а значит список индексов должен содержать 24 элемента. Индексы граней идут в этом массиве последовательно: например грань 0 образована вершинами 0, 1 и 2; грань 1 — вершинами 0, 4 и 1; грань 2 — вершинами 4, 5 и 1 и так далее. Вершины перечисляются в порядке следования против часовой стрелки, если смотреть на лицевую сторону грани (смотрим рисунок выше).
Дальнейшие шаги по созданию геометрии мы выполняли в предыдущих примерах. Единственное чего мы не делали — автоматическая генерация сглаженных (усредненных) нормалей, которую мы выполняем в данном примере вызовом
osgUtil::SmoothingVisitor::smooth(*geom);
Действительно, если заданы вершины грани, то легко рассчитать нормаль к ней. В вершинах, в которых сходятся несколько граней рассчитывается некая усредненная нормаль — нормали сходящихся граней складываются и полученная сумма снова нормируется. Эти операции (а так же многое другое!) может выполнить сам движок с помощью классов из библиотеки osgUtil. Поэтому в нашем примере в файл *.pro мы добавим указание компоновщику собирать нашу программу и с этой библиотекой
CONFIG(debug, debug|release) {
TARGET = $$join(TARGET,,,_d)
.
.
.
LIBS += -L$$OSG_LIB_DIRECTORY -losgUtild
} else {
.
.
.
LIBS += -L$$OSG_LIB_DIRECTORY -losgUtil
}
В итоге мы получаем следующий результат
Чтобы понять как это работает, рассмотрим конвейер OpenGL
Механизм массивов вершин уменьшает число вызовов OpenGL. Он сохраняет данные о вершинах в памяти приложения, которая используется на стороне клиента. Конвейер OpenGL на серверной стороне получает доступ к различным массивам вершин. Как показано на схеме, OpenGL получает данные из буфера вершин на стороне клиента и упорядоченным образом, выполняет сборку примитивов. Так происходит обработка данных при использовании методов set*Array() класса osg::Geometry. Класс osg::DrawArrays проходит по этим массивам непосредственно и отображает их.
При использовании osg::DrawElements* обеспечивается понижение размерности массивов вершин и уменьшается число вершин, передаваемых в конвейер. Массив индексов позволяет сформировать на стороне сервера кэш вершин. OpenGL читает данные о вершинах из кэша, вместо того, чтобы читать из из буфера вершин на стороне клиента. Это существенно увеличивает общую производительность рендеринга.
8. Техники обработки полигональной сетки
OpenSceneGraph поддерживает различные техники обработки полигональной сетки объектов геометрии сцены. Эти методы препроцессинга, такие как редукция полигонов и тесселяция, часто используются для создания и оптимизации полигональных моделей. Они имеют простой интерфейс, но в процессе работу выполняют массу сложных вычислений и не очень подходят для выполнения "на лету".
К описанным техникам относятся:
- osgUtil::Simplifier — уменьшение числа треугольников в геометрии. Публичный метод simplify() используется для упрощения геометрии моделей.
- osgUtil::SmootingVisitor — вычисление нормалей. Метод smooth() может быть использован для генерации сглаженных нормалей для модели, вместо самостоятельного их расчета и явного задания через массив нормалей.
- osgUtil::TangentSpaceGenerator — генерация касательных базисных векторов для вершин модели. Запускается вызовом метода generate() и сохраняет результат, возвращаемый методами getTangentArray(), getNormalArray() и getBinormalArray(). Эти результаты могут быть использованы для различный атрибутов вершин при написании шейдеров на GLSL.
- osgUtil::Tesselator — выполняет тесселяцию полигональной сетки — разбиение сложных примитивов на последовательность простых (метод retesselatePolygons())
- osgUtil::TriStripVisitor — конвертирует геометрическую поверхность в набор полос треугольных граней, что позволяет выполнять рендеринг с эффективным расходом памяти. Метод stripify() конвертирует набор примитивов модели в геометрию на базе набора GL_TRIANGLE_STRIP.
Все методы принимают геометрию объекта в качестве параметра, передаваемого по ссылке osg::Geometry&, например так
osgUtil::TriStripVisitor tsv;
tsv.stripify(*geom);
где под geom понимается экземпляр геометрии, описываемый умным указателем.
Классы osg::Simplifier, osg::SmoothingVisitor и osg::TriStripVisitor могут работать непосредственно с узлами графа сцены, например
osgUtil::TriStripVisitor tsv;
node->accept(tsv);
Метод accept() обрабатывает все дочерние узлы, до тех пор, пока указанная операция не будет применена ко всем оконечным нодам этой части дерева сцены, хранящимся в нодах типа osg::Geode.
Попробуем на практике технику тесселяции.
#ifndef MAIN_H
#define MAIN_H
#include <osg/Geometry>
#include <osg/Geode>
#include <osgUtil/Tessellator>
#include <osgViewer/Viewer>
#endif
main.cpp
#include "main.h"
int main(int argc, char *argv[])
{
/*
Создаем фигуру вида
-----
| _|
| |_
| |
-----
*/
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
vertices->push_back( osg::Vec3(0.0f, 0.0f, 0.0f) ); // 0
vertices->push_back( osg::Vec3(2.0f, 0.0f, 0.0f) ); // 1
vertices->push_back( osg::Vec3(2.0f, 0.0f, 1.0f) ); // 2
vertices->push_back( osg::Vec3(1.0f, 0.0f, 1.0f) ); // 3
vertices->push_back( osg::Vec3(1.0f, 0.0f, 2.0f) ); // 4
vertices->push_back( osg::Vec3(2.0f, 0.0f, 2.0f) ); // 5
vertices->push_back( osg::Vec3(2.0f, 0.0f, 3.0f) ); // 6
vertices->push_back( osg::Vec3(0.0f, 0.0f, 3.0f) ); // 7
osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
normals->push_back( osg::Vec3(0.0f, -1.0f, 0.0f) );
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
geom->setVertexArray(vertices.get());
geom->setNormalArray(normals.get());
geom->setNormalBinding(osg::Geometry::BIND_OVERALL);
geom->addPrimitiveSet(new osg::DrawArrays(GL_POLYGON, 0, 8));
osg::ref_ptr<osg::Geode> root = new osg::Geode;
root->addDrawable(geom.get());
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());
return viewer.run();
}
Исходя из пространственного положения вершин в данном примере, видно, что мы пытаемся создать невыпуклый многоугольник из восьми вершин, применяя генерацию одной грани типа GL_POLYGON. Сборка и выполнение этого примера показывают, что того результата, что мы ожидаем, не получается — пример отображается некорректно
Для исправления этой проблемы, построенную геометрию, перед передачей её во вьювер, следует подвергнуть тесселяции
osgUtil::Tessellator ts;
ts.retessellatePolygons(*geom);
после которой мы получим корректный результат
Как это работает? Невыпуклый многоугольник, без применения корректной тесселяции, не будет отображаться так, как мы этого ожидаем, так как OpenGL, стремясь к оптимизации производительности будет рассматривать его как простой, выпуклый полигон или просто игнорировать, что может давать совершенно неожиданные результаты.
Класс osgUtil::Tessellator использует алгоритмы для трансформацию выпуклого многоугольника в серию невыпуклых — в нашем случае он трансформирует геометрию в GL_TRIANGLE_STRIP.
Этот класс может обрабатывать многоугольники с отверстиями и самопересекающиеся многоугольники. Через публичный метод setWindingType() можно определить различные правила обработки, такие как GLU_TESS_WINDING_ODD или GLU_TESS_WINDING_NONZERO, которые задают внутреннюю и внешнюю области сложного многоугольника.
Заключение
В этой статье мы получили базовые представления от том, каким образом геометрия трехмерных объектов хранится и обрабатывается в движке OSG. Не стоит думать, что те простейшие и не слишком впечатляющие примеры, что рассмотрены в статье — предел возможностей движка. Просто данные примеры могут помочь разработчику понять механику OpenSceneGraph, а без этого понимания сложно представлять себе работу более сложных вещей.
Данная статья основана на переводе и переработке текста соответствующих глав книги OpenSceneGraph 3.0. Beginner’s Guide. Все примеры проверены мной лично, и их исходники доступны здесь. Продолжение следует...
Автор: Дмитрий Притыкин