Розробка

OpenSceneGraph: Основи роботи з геометрією сцени

Введення

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.0 f, 0.0 f, 0.0 f), f 10.0, f 10.0, 5.0 f));

створює прямокутний паралелепіпед з геометричним центром в точці (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.0 f, 0.0 f, 0.0 f), 2.0 f, 2.0 f, 1.0 f));

 osg::ref_ptr<osg::ShapeDrawable> shape2 = new osg::ShapeDrawable;
 shape2->setShape(new osg::Cone(osg::Vec3(0.0 f, 0.0 f, 0.0 f), 1.0 f, 1.0 f));
 shape2->setColor(osg::Vec4(0.0 f, 1.0 f, 0.0 f, 1.0 f));

 osg::ref_ptr<osg::ShapeDrawable> shape3 = new osg::ShapeDrawable;
 shape3->setShape(new osg::Sphere(osg::Vec3(3.0 f, 0.0 f, 0.0 f), 1.0 f));
 shape3->setColor(osg::Vec4(0.0 f, 0.0 f, 1.0 f, 1.0 f));

 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.0 f, 0.0 f, 0.0 f));

Масиви 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. Приклад — малюємо квадрат розфарбований

Реалізуємо все вищеописане у вигляді простого прикладу

Повний вихідний код прикладу quadmain.h

#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.0 f, 0.0 f, 0.0 f));
 vertices->push_back(osg::Vec3(1.0 f, 0.0 f, 0.0 f));
 vertices->push_back(osg::Vec3(1.0 f, 0.0 f, 1.0 f));
 vertices->push_back(osg::Vec3(0.0 f, 0.0 f, 1.0 f));

 osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
 normals->push_back(osg::Vec3(0.0 f, -1.0 f, 0.0 f));

 osg::ref_ptr<osg::Vec4Array> colors = new osg::Vec4Array;
 colors->push_back(osg::Vec4(1.0 f, 0.0 f, 0.0 f, 1.0 f));
 colors->push_back(osg::Vec4(0.0 f, 1.0 f, 0.0 f, 1.0 f));
 colors->push_back(osg::Vec4(0.0 f, 0.0 f, 1.0 f, 1.0 f));
 colors->push_back(osg::Vec4(1.0 f, 1.0 f, 1.0 f, 1.0 f));

 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.0 f, 0.0 f, 0.0 f));
vertices->push_back(osg::Vec3(1.0 f, 0.0 f, 0.0 f));
vertices->push_back(osg::Vec3(1.0 f, 0.0 f, 1.0 f));
vertices->push_back(osg::Vec3(0.0 f, 0.0 f, 1.0 f));

Далі задаємо масив нормалей. У нашому простому випадку нам не потрібно створювати нормаль для кожної вершини — досить описати один одиничний вектор, направлений перпендикулярно до площини квадрата

osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
normals->push_back(osg::Vec3(0.0 f, -1.0 f, 0.0 f));

Задамо колір для кожної з вершин

osg::ref_ptr<osg::Vec4Array> colors = new osg::Vec4Array;
colors->push_back(osg::Vec4(1.0 f, 0.0 f, 0.0 f, 1.0 f));
colors->push_back(osg::Vec4(0.0 f, 1.0 f, 0.0 f, 1.0 f));
colors->push_back(osg::Vec4(0.0 f, 0.0 f, 1.0 f, 1.0 f));
colors->push_back(osg::Vec4(1.0 f, 1.0 f, 1.0 f, 1.0 f));

Тепер створюємо об’єкт геометрії, де зберігатиметься опис нашого квадрата, яке буде оброблено рендером. Передаємо в цю геометрію масив вершин

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.

Повний вихідний текст прикладу octahedronmain.h

#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.0 f, 0.0 f, 1.0 f);
 (*vertices)[1].set(-0.5 f, -0.5 f, 0.0 f);
 (*vertices)[2].set( 0.5 f, -0.5 f, 0.0 f);
 (*vertices)[3].set( 0.5 f, 0.5 f, 0.0 f);
 (*vertices)[4].set(-0.5 f, 0.5 f, 0.0 f);
 (*vertices)[5].set( 0.0 f, 0.0 f, -1.0 f);

 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.0 f, 0.0 f, 1.0 f);
(*vertices)[1].set(-0.5 f, -0.5 f, 0.0 f);
(*vertices)[2].set( 0.5 f, -0.5 f, 0.0 f);
(*vertices)[3].set( 0.5 f, 0.5 f, 0.0 f);
(*vertices)[4].set(-0.5 f, 0.5 f, 0.0 f);
(*vertices)[5].set( 0.0 f, 0.0 f, -1.0 f);

Ініціалізуємо кожну вершину безпосередньо, звертаючись вектору її координат з використанням операції розіменування вказівника і оператора 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 ми додамо вказівку компоновщику збирати нашу програму і з цією бібліотекою

octahedron.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 підтримує різні техніки обробки полігональної сітки геометрії об’єктів сцени. Ці методи препроцессинга, такі як редукція полігонів і тесселяція, часто використовуються для створення і оптимізації полігональних моделей. Вони мають простий інтерфейс, але в процесі роботу виконують масу складних обчислень і не дуже підходять для виконання “на льоту”.

До описаних технік відносяться:

  1. osgUtil::Simplifier — зменшення числа трикутники в геометрії. Публічний метод simplify() використовується для спрощення геометрії моделей.
  2. osgUtil::SmootingVisitor — обчислення нормалей. Метод smooth() може бути використаний для генерації згладжених нормалей для моделі, замість самостійного їх розрахунку і явного завдання через масив нормалей.
  3. osgUtil::TangentSpaceGenerator — генерація дотичних базисних векторів для вершин моделі. Запускається викликом методу generate() і зберігає результат, що повертається методами getTangentArray(), getNormalArray() і getBinormalArray(). Ці результати можуть бути використані для різний атрибутів вершин при написанні шейдерів на GLSL.
  4. osgUtil::Tesselator — виконує тесселяцію полігональної сітки — розбиття складних примітивів на послідовність простих (метод retesselatePolygons())
  5. 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.

Спробуємо на практиці техніку тесселяції.

Повний код прикладу tesselatormain.h

#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.0 f, 0.0 f, 0.0 f) ); // 0
 vertices->push_back( osg::Vec3(2.0 f, 0.0 f, 0.0 f) ); // 1
 vertices->push_back( osg::Vec3(2.0 f, 0.0 f, 1.0 f) ); // 2
 vertices->push_back( osg::Vec3(1.0 f, 0.0 f, 1.0 f) ); // 3
 vertices->push_back( osg::Vec3(1.0 f, 0.0 f, 2.0 f) ); // 4
 vertices->push_back( osg::Vec3(2.0 f, 0.0 f, 2.0 f) ); // 5
 vertices->push_back( osg::Vec3(2.0 f, 0.0 f, 3.0 f) ); // 6
 vertices->push_back( osg::Vec3(0.0 f, 0.0 f, 3.0 f) ); // 7

 osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
 normals->push_back( osg::Vec3(0.0 f, -1.0 f, 0.0 f) );

 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. Всі приклади перевірено мною особисто, і їх вихідні коди доступні тут. Продовження слідує…

Related Articles

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Close