Как стать автором
Обновить

OpenSceneGraph: Процедурная анимация геометрии и атрибутов состояния

Время на прочтение22 мин
Количество просмотров3.6K
image

Введение


Говоря о приемах программирования, специфичных для OSG в прошлый раз мы говорили о механизме обратных вызовов (Callback) и его реализации в движке. Настало время посмотреть на то, какие возможности дает нам применение этого механизма для управления содержимым трехмерной сцены.

Если говорить об анимации объектов, то OSG предоставляет разработчику две возможности её реализации:

  1. Процедурная анимация, реализуемая программным способом через трансформацию объектов и их атрибутов
  2. Экспорт анимации из 3D-редактора и управление ею из кода приложения

Для начала рассмотрим первую возможность, как наиболее очевидную. О второй мы обязательно поговорим чуть позже.

1. Процедурная морфинг-анимация


При обходе графа сцены OSG выполняет передачу данных в конвейер OpenGL, который выполняется в отдельном потоке. Этот поток должен быть синхронизирован с другими потоками обработки в каждом кадре. Невыполнение этого требования может привести к тому, что метод frame() завершится раньше обработки данных геометрии. Это приведет к непредсказуемому поведению программы и сбоям. OSG предлагает решение этой проблемы в виде метода setDataVariance() класса osg::Object, являющегося базовым для вех объектов сцены. Можно установить три режима обработки объектов

  1. UNSPECIFIED (по-умолчанию) — OSG самостоятельно определяет порядок обработки объекта.
  2. STATIC — объект неизменяем и порядок его обработки не важен. Существенно ускоряет рендеринг.
  3. DYNAMIC — объект должен быть обработан до начала отрисовки.

Эту настройку можно задать в любой момент вызовом

node->setDataVariance( osg::Object::DYNAMIC );

Общепринятой является практика модификации геометрии "на лету", то есть изменение координат вершин, нормалей цветов и текстур динамически в каждом кадре, получая видоизменяемую геометрию. Такой прием называется морфинг-анимацией. В данном случае решающим является порядок обработки геометрии — все её изменения должны быть пересчитаны до того, как начнется отрисовка. Для иллюстрации этого приема немного изменим пример с цветным квадратом, заставив одну из его вершин вращаться вокруг оси X.

Пример animquad
main.h

#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/Geometry>
#include    <osg/Geode>
#include    <osgViewer/Viewer>

#endif

main.cpp

#include	"main.h"

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
osg::Geometry *createQuad()
{
    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));

    return quad.release();
}

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
class DynamicQuadCallback : public osg::Drawable::UpdateCallback
{
public:

    virtual void update(osg::NodeVisitor *, osg::Drawable *drawable);
};

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void DynamicQuadCallback::update(osg::NodeVisitor *, osg::Drawable *drawable)
{
    osg::Geometry *quad = static_cast<osg::Geometry *>(drawable);

    if (!quad)
        return;

    osg::Vec3Array *vertices = static_cast<osg::Vec3Array *>(quad->getVertexArray());

    if (!vertices)
        return;

    osg::Quat quat(osg::PI * 0.01, osg::X_AXIS);
    vertices->back() = quat * vertices->back();

    quad->dirtyDisplayList();
    quad->dirtyBound();
}

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

    osg::Geometry *quad = createQuad();
    quad->setDataVariance(osg::Object::DYNAMIC);
    quad->setUpdateCallback(new DynamicQuadCallback);

    osg::ref_ptr<osg::Geode> root = new osg::Geode;
    root->addDrawable(quad);

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());
    
    return viewer.run();
}


Создание квадрата вынесем в отдельную функцию

osg::Geometry *createQuad()
{
    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));

    return quad.release();
}

описание которой, в принципе не требуется, так как подобные действия мы проделывали многократно. Для модификации вершин этого квадрата пишем класс DynamicQuadCallback, наследуя его от osg::Drawable::UpdateCallback

class DynamicQuadCallback : public osg::Drawable::UpdateCallback
{
public:

    virtual void update(osg::NodeVisitor *, osg::Drawable *drawable);
};

переопределяя в нем метод update()

void DynamicQuadCallback::update(osg::NodeVisitor *, osg::Drawable *drawable)
{
    osg::Geometry *quad = static_cast<osg::Geometry *>(drawable);

    if (!quad)
        return;

    osg::Vec3Array *vertices = static_cast<osg::Vec3Array *>(quad->getVertexArray());

    if (!vertices)
        return;

    osg::Quat quat(osg::PI * 0.01, osg::X_AXIS);
    vertices->back() = quat * vertices->back();

    quad->dirtyDisplayList();
    quad->dirtyBound();
}

Здесь мы получаем указатель на объект геометрии

osg::Geometry *quad = static_cast<osg::Geometry *>(drawable);

читаем из геометрии список вершин (вернее указатель на него)

osg::Vec3Array *vertices = static_cast<osg::Vec3Array *>(quad->getVertexArray());

Для получения последнего элемента (последней вершины) в массиве класс osg::Array предоставляем метод back(). Для выполнения поворота вершины относительно оси X вводим кватернион

osg::Quat quat(osg::PI * 0.01, osg::X_AXIS);

то есть мы задали кватернион, реализующий поворот вокруг оси X на угол 0.01 * Pi. Поворачиваем вершину умножением кватерниона на вектор, задающий координаты вершины

vertices->back() = quat * vertices->back();

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

quad->dirtyDisplayList();
quad->dirtyBound();

В теле функции main() мы создаем квадрат, устанавливаем для него динамический режим отрисовки и добавляем обратный вызов, модифицирующий геометрию

osg::Geometry *quad = createQuad();
quad->setDataVariance(osg::Object::DYNAMIC);
quad->setUpdateCallback(new DynamicQuadCallback);

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



А теперь попробуйте убрать (комментировать) вызов setDataVariance(). Возможно мы и не увидим ничего криминального в этом случае — по-умолчанию OSG пытается автоматически определить когда следует обновлять данные о геометрии, пытаясь синхронизироваться с отрисовкой. Тогда попробуйте изменить режим с DYNAMIC на STATIC и будет видно, что изображение рендерится не плавно, с заметными рывками, в консоль сыпятся ошибки и предупреждения типа этого

Warning: detected OpenGL error 'invalid value' at after RenderBin::draw(..)

Если не выполнить метод dirtyDisplayList(), то OpenGL проигнорирует все изменения геометрии и для отрисовки будет использовать дисплейный список, созданный в самом начале, при создании квадрата. Удалите этот вызов, и увидите, что никакой анимации нет.

Без вызова метода dirtyBound() не будет произведен пересчет ограничивающего параллелепипеда и OSG будет неверно выполнять отсечение невидимых граней.

2. Понятие об интерполяции движения


Предположим, что некий поезд, идущий от станции A к станции B затрачивает на это перемещение 15 минут. Как можно смоделировать эту ситуацию, изменяя положение поезда в обратном вызове? Самый простой способ — соотнести положение станции A с моментом времени 0, а станции B — с моментом 15 минут и равномерно перемещать поезд между этими моментами времени. Такой простейший подход называется линейной интерполяцией. При линейной интерполяции вектор, задающий положение промежуточной точки описывается формулой

p = (1 - t) * p0 + t * p1

где p0 — начальная точка; p1 — конечная точка; t — параметр, изменяющийся равномерно от 0 до 1. Однако, движение поезда намного сложнее: выйдя со станции A он разгоняется, потом движется с постоянной скоростью, а затем замедляется, останавливаясь на станции B. Линейная интерполяция такой процесс уже не в состоянии описать и выглядит неестественно.

OSG предоставляет разработчику библиотеку osgAnimation, содержащую ряд стандартный алгоритмов интерполяции, применяемых для плавной анимации перемещения объектов сцены. Каждая из этих функций имеет обычно два аргумента: начальное значение параметра (обычно 0) и конечное значение параметра (обычно 1). Эти функции могут быть применены к начальному участку движения (InMotion), к конечному участку (OutMotion) или к начальному и конечному участку движения (InOutMotion)

Тип движения in-класс out-класс in/out-класс
Линейная интерполяция LinearMotion
Квадратичная интерполяция InQuadMotion OutQuadMotion InOutQuadMotion
Кубическая интерполяция InCubicMotion OutCubicMotion InOutCubicMotion
Интерполяция 4-порядка InQuartMotion OutQuartMotion InOutQuartMotion
Интерполяция с эффектом отскока InBounceMotion OutBounceMotion InOutBounceMotion
Интерполяция с упругим отскоком InElasticMotion OutElasticMotion InOutElasticMotion
Синусоидальная интерполяция InSineMotion OutSineMotion InOutSineMotion
Интерполяция обратной функцией InBackMotion OutBackMotion InOutBackMotion
Круговая интерполяция InCircMotion OutCircMotion InOutCircMotion
Экспоненциальная интерполяция InExpoMotion OutExpoMotion InOutExpoMotion

Для создания линейной интерполяции движения объекта пишем такой код

osg::ref_ptr<osgAnimation::LinearMotion> motion = new osgAnimation::LinearMotion(0.0f, 1.0f);

3. Анимация узлов трансформации


Анимация движения по траектории — наиболее распространенный вид анимации в графических приложениях. Этот прием может быть использован при анимировании движения автомобиля, полета самолета или движения камеры. Траектория задается предварительно, со всеми положениями, поворотами и изменениями масштаба в ключевые моменты времени. При запуске цикла симуляции состояние объекта пересчитывается в каждом кадре, с применением линейной интерполяции для положения и масштабирования и сферической линейной интерполяции для кватернионов вращения. Для этого используется внутренний метод slerp() класса osg::Quat.

OSG предоставляет класс osg::AnimationPath для описания изменяющейся во времени траектории. Для добавления в траекторию контрольных точек, соответствующих определенным моментам времени используется метод этого класса insert(). Контрольная точка описывается классом osg::AnimationPath::ControlPoint, конструктор которого принимает в качестве параметров позицию, и, опционально, параметры поворота объекта и масштабирование. Например

osg::ref_ptr<osg::AnimationPath> path = new osg::AnimationPath;
path->insert(t1, osg::AnimationPath::ControlPoint(pos1, rot1, scale1));
path->insert(t2, ...);

Здесь t1, t2 — моменты времени в секундах; rot1 — параметр поворота в момент времени t1, описываемый кватернионом osg::Quat.

Возможно управление зацикливанием анимации через метод setLoopMode(). По-умолчанию включен режим LOOP — анимация будет непрерывно повторятся. Другие возможные значения: NO_LOOPING — проигрывание анимации один раз и SWING — циклическое проигрывание движения в прямом и обратном направлениях.

После выполнения всех инициализации мы присоединяем объект osg::AnimationPath к встроенному объекту osg::AnimationPathCallback, являющемуся производным от класса osg::NodeCallback.

4. Пример анимации движения по траектории


Теперь заставим нашу цессну двигаться по окружности с центром в точке (0,0,0). Положение самолета на траектории будет рассчитываться путем линейной интерполяции положения и ориентации между ключевыми кадрами.

Пример animcessna
main.h


#ifndef		MAIN_H
#define		MAIN_H

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

#endif

main.cpp

#include	"main.h"

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
osg::AnimationPath *createAnimationPath(double radius, double time)
{
    osg::ref_ptr<osg::AnimationPath> path = new osg::AnimationPath;
    path->setLoopMode(osg::AnimationPath::LOOP);

    unsigned int numSamples = 32;
    double delta_yaw = 2.0 * osg::PI / (static_cast<double>(numSamples) - 1.0);
    double delta_time = time / static_cast<double>(numSamples);

    for (unsigned int i = 0; i < numSamples; ++i)
    {
        double yaw = delta_yaw * i;
        osg::Vec3d pos(radius * sin(yaw), radius * cos(yaw), 0.0);
        osg::Quat rot(-yaw, osg::Z_AXIS);

        path->insert(delta_time * i, osg::AnimationPath::ControlPoint(pos, rot));
    }

    return path.release();
}

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

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

    osg::ref_ptr<osg::MatrixTransform> root = new osg::MatrixTransform;
    root->addChild(model.get());

    osg::ref_ptr<osg::AnimationPathCallback> apcb = new osg::AnimationPathCallback;
    apcb->setAnimationPath(createAnimationPath(50.0, 6.0));

    root->setUpdateCallback(apcb.get());

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());
    
    return viewer.run();
}


Начинаем с создания траектории самолета, вынося этот код в отдельную функцию

osg::AnimationPath *createAnimationPath(double radius, double time)
{
    osg::ref_ptr<osg::AnimationPath> path = new osg::AnimationPath;
    path->setLoopMode(osg::AnimationPath::LOOP);

    unsigned int numSamples = 32;
    double delta_yaw = 2.0 * osg::PI / (static_cast<double>(numSamples) - 1.0);
    double delta_time = time / static_cast<double>(numSamples);

    for (unsigned int i = 0; i < numSamples; ++i)
    {
        double yaw = delta_yaw * i;
        osg::Vec3d pos(radius * sin(yaw), radius * cos(yaw), 0.0);
        osg::Quat rot(-yaw, osg::Z_AXIS);

        path->insert(delta_time * i, osg::AnimationPath::ControlPoint(pos, rot));
    }

    return path.release();
}

В качестве параметров функция принимает радиус окружности, по которой движется самолет и время, за которое он совершит один оборот. Внутри функции создаем объект траектории и включаем режим циклического повторения анимации

osg::ref_ptr<osg::AnimationPath> path = new osg::AnimationPath;
path->setLoopMode(osg::AnimationPath::LOOP);

Следующий код

unsigned int numSamples = 32;
double delta_yaw = 2.0 * osg::PI / (static_cast<double>(numSamples) - 1.0);
double delta_time = time / static_cast<double>(numSamples);

вычисляет параметры аппроксимации траектории. Мы разбиваем всю траекторию на numSamples прямолинейных участков, и вычисляем изменение угла поворота самолета вокруг вертикальной оси (рыскания) delta_yaw и изменение времени delta_time при переходит от участка к учаcтку. Теперь создаем необходимые контрольные точки

for (unsigned int i = 0; i < numSamples; ++i)
{
    double yaw = delta_yaw * i;
    osg::Vec3d pos(radius * sin(yaw), radius * cos(yaw), 0.0);
    osg::Quat rot(-yaw, osg::Z_AXIS);

    path->insert(delta_time * i, osg::AnimationPath::ControlPoint(pos, rot));
}

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

double yaw = delta_yaw * i;

положением центра масс самолета в пространстве

osg::Vec3d pos(radius * sin(yaw), radius * cos(yaw), 0.0);

Поворот самолета на требуемый угол рыскания (относительно вертикальной оси) задаем кватернионом

osg::Quat rot(-yaw, osg::Z_AXIS);

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

path->insert(delta_time * i, osg::AnimationPath::ControlPoint(pos, rot));

В основной программа обращаем внимание на нюанс в указании имени файла модели самолета при загрузке

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

– к имени файла добавился некий суффикс ".0,0,90.rot". Механизм загрузки геометрии из файла, используемый в OSG позволяет таким образом указать начальное положение и ориентацию модели после загрузки. В данном случае мы хотим, чтобы загрузившись, модель была повернута на 90 градусов вокруг оси Z.

Далее создается корневой узел, являющийся узлом трансформации, и объект модели добавляется к нему в качестве дочернего узла

osg::ref_ptr<osg::MatrixTransform> root = new osg::MatrixTransform;
root->addChild(model.get());

Теперь создаем обратный вызов анимации траектории, добавляя в него путь, создаваемый функцией createAnimationPath()

osg::ref_ptr<osg::AnimationPathCallback> apcb = new osg::AnimationPathCallback;
apcb->setAnimationPath(createAnimationPath(50.0, 6.0));

Прикрепляем этот callback к узлу трансформации

root->setUpdateCallback(apcb.get());

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

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

return viewer.run();

Получаем анимацию движения самолета



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

Секрет в том, что эту работу выполняет специальный класс osg::AnimationPathCallback. В соответствии с текущим положением объекта на траектории он вычисляет матрицу трансформации и автоматически применяет её к тому узлу трансформации, к которому он прикреплен, избавляя разработчика от кучи рутинных операций.

Тут следует отметить, что прикрепление osg::AnimationPathCallback к другим типам узлов, не только не даст эффекта, но и может привести к неопределенному поведению программы. Важно помнить, что данный обратный вызов воздействует исключительно на узлы трансформации.

5. Программное управление анимацией


Класс osg::AnimationPathCallback предоставляет методы для управления анимацией в процессе выполнения программы

  1. reset() – сброс анимации и проигрывание её сначала.
  2. setPause() – постановка анимации на паузу. В качестве параметра принимает логическое значение
  3. setTimeOffset() – задает смещение во времени до начала анимации.
  4. setTimeMultiplier() — задает временной множитель для ускорения/замедления анимации.

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

apcb->setPause(false);
apcb->reset();

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

apcb->setTimeOffset(4.0f);
apcb->setTimeMultiplier(2.0f);

6. Порядок рендеринга примитивов в OpenGL


OpenGL хранит данные вершин и примитивов в различных буферах, таких как буфер цвета (color buffer), буфер глубины (depth buffer), буфер трафарета (stencil buffer) и так далее. Кроме того, он не перезаписывает вершины и треугольные грани уже отправленные в его конвейер. Это означает, что OpenGL создает новую геометрию вне зависимости от того, каким образом создавалась уже существующая геометрия. Это означает, что порядок в котором примитивы посылаются в конвейер рендеринга существенно влияет на конечный результат, который мы видим на экране.

Опираясь на данные буфера глубины OpenGL правильно отрисует непрозрачные объекты, сортируя пиксели по степени их удаленности от наблюдателя. Тем не менее, при использования техники смешивания цветов, например при реализации прозрачных и полупрозрачных объектов, будет выполнятся специальная операция обновления буфера цвета. Новые и старые пиксели изображения смешиваются, с учетом значения альфа-канала (четвертый компонент цвета). Это приводит к тому, что порядок рендеринга полупрозрачных (translucent) и непрозрачных (opaque) граней влияет на конечный результат



на рисунке, в ситуации слева в конвейер были отправлены сначала непрозрачные, а потом прозрачные объекты, что привело к корректному смещению в буфере цвета и корректному отображению граней. В правой ситуации сначала были отрисованы прозрачные объекты, а затем непрозрачные, что привело к неверному отображению.

Метод setRenderingHint() класса osg::StateSet указывает OSG требуемый порядок рендеринга узлов и геометрических объектов, если это необходимо выполнить явно. Этот метод просто указывает, следует или не следует учитывать полупрозрачные грани при рендеринге, тем самым гарантируя, что в случае наличия в сцене полупрозрачных граней сначала будут отрисованы непрозрачные, а затем прозрачные грани, с учетом удаленности граней от наблюдателя. Чтобы сообщать движку, что данный узел непрозрачный используем такой код

node->getOrCreateStateSet()->setRenderingHint(osg::StateSet::OPAQUE_BIN);

или содержит прозрачные грани

node->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);

7. Пример реализации полупрозрачных объектов


Попробуем проиллюстрировать всё вышеописанное теоретическое введение конкретным примером реализации полупрозрачного объекта.

Пример transparency
main.h

#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/BlendFunc>
#include    <osg/Texture2D>
#include    <osg/Geometry>
#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::Vec3Array> vertices = new osg::Vec3Array;
    vertices->push_back( osg::Vec3(-0.5f, 0.0f, -0.5f) );
    vertices->push_back( osg::Vec3( 0.5f, 0.0f, -0.5f) );
    vertices->push_back( osg::Vec3( 0.5f, 0.0f,  0.5f) );
    vertices->push_back( osg::Vec3(-0.5f, 0.0f,  0.5f) );

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

    osg::ref_ptr<osg::Vec2Array> texcoords = new osg::Vec2Array;
    texcoords->push_back( osg::Vec2(0.0f, 0.0f) );
    texcoords->push_back( osg::Vec2(0.0f, 1.0f) );
    texcoords->push_back( osg::Vec2(1.0f, 1.0f) );
    texcoords->push_back( osg::Vec2(1.0f, 0.0f) );

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

    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_OVERALL);
    quad->setTexCoordArray(0, texcoords.get());
    quad->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4));

    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable(quad.get());

    osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
    osg::ref_ptr<osg::Image> image = osgDB::readImageFile("../data/Images/lz.rgb");
    texture->setImage(image.get());

    osg::ref_ptr<osg::BlendFunc> blendFunc = new osg::BlendFunc;
    blendFunc->setFunction(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    osg::StateSet *stateset = geode->getOrCreateStateSet();
    stateset->setTextureAttributeAndModes(0, texture.get());
    stateset->setAttributeAndModes(blendFunc);    

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild(geode.get());
    root->addChild(osgDB::readNodeFile("../data/glider.osg"));

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());
    
    return viewer.run();
}


В большинстве своем приведенный здесь код не содержит ничего нового: создаются два геометрических объекта — текстурированный квадрат и дельтаплан, модель которого загружается из файла. Однако, ко всем вершинам квадрата мы применяем белый полупрозрачный цвет

colors->push_back( osg::Vec4(1.0f, 1.0f, 1.0f, 0.5f) );

– значение альфа-канала равно 0.5, что в смешении с цветами текстуры должно дать эффект полупрозрачного объекта. Кроме того, для обработки прозрачности следует задать функцию смешивания цветов

osg::ref_ptr<osg::BlendFunc> blendFunc = new osg::BlendFunc;
blendFunc->setFunction(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

передав её машине состояний OpenGL

stateset->setAttributeAndModes(blendFunc);

При компиляции и запуске этой программы мы получим следующий результат



Стоп! А где же прозрачность? Всё дело в том, что мы забыли указать движку, что следует обрабатывать прозрачные грани, что решается легко вызовом

stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);

после чего мы получим нужный нам результат – крыло дельтаплана просвечивается через полупрозрачный текстурированный квадрат



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

R = srcR * srcA + dstR * (1 - srcA)
G = srcG * srcA + dstG * (1 - srcA)
B = srcB * srcA + dstB * (1 - srcA)

где [srcR, srcG, srcB] — компоненты цвета текстуры квадрата; [dstR, dstG, dstB] — компоненты цвета каждого пикселя того участка на который накладывается полупрозрачная грань, полученные с учетом того, что на этом месте уже отрисован фон и непрозрачные грани крыла дельтаплана. Под srcA понимаю альфа-компоненту цвета квадрата.

Метод seRenderingHint() отлично упорядочивает отрисовку примитивов, но использовать его не слишком эффективно, так как сортировка прозрачных объектов по глубине при рендеринге кадра довольно ресурсоемкая операция. Поэтому разработчик должен позаботится о порядке отрисовки граней самостоятельно, если это возможно на предварительных этапах подготовки сцены.

8. Анимация атрибутов состояния


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

Классы стандартных интерполяций так же мотут быть использованы для задания функции изменения параметров атрибутов.

У нас уже имеется опыт создания полупрозрачных объектов. Мы знаем, что если альфа-компонента цвета равна нулю, мы получаем полностью прозрачный объект, при значении 1 — полностью непрозрачный. Ясно, что варьируя этот параметр от 0 до 1 во времени можно получить эффект постепенного появления или исчезания объекта. Проиллюстрируем это на конкретном примере

Пример fading-in
main.h

#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/Geode>
#include    <osg/Geometry>
#include    <osg/BlendFunc>
#include    <osg/Material>
#include    <osgAnimation/EaseMotion>
#include    <osgDB/ReadFile>
#include    <osgViewer/Viewer>

#endif

main.cpp

#include	"main.h"

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
class AlphaFadingCallback : public osg::StateAttributeCallback
{
public:

    AlphaFadingCallback()
    {
        _motion = new osgAnimation::InOutCubicMotion(0.0f, 1.0f);
    }

    virtual void operator() (osg::StateAttribute* , osg::NodeVisitor*);

protected:

    osg::ref_ptr<osgAnimation::InOutCubicMotion> _motion;
};

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void AlphaFadingCallback::operator()(osg::StateAttribute *sa, osg::NodeVisitor *nv)
{
    (void) nv;

    osg::Material *material = static_cast<osg::Material *>(sa);

    if (material)
    {
        _motion->update(0.0005f);

        float alpha = _motion->getValue();

        material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 1.0f, 1.0f, alpha));
    }
}

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

    osg::ref_ptr<osg::Drawable> quad = osg::createTexturedQuadGeometry(
                osg::Vec3(-0.5f, 0.0f, -0.5f),
                osg::Vec3(1.0f, 0.0f, 0.0f),
                osg::Vec3(0.0f, 0.0f, 1.0f));

    osg::ref_ptr<osg::Geode> geode = new osg::Geode;
    geode->addDrawable(quad.get());

    osg::ref_ptr<osg::Material> material = new osg::Material;
    material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f));
    material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 1.0f, 1.0f, 0.5f));
    material->setUpdateCallback(new AlphaFadingCallback);

    geode->getOrCreateStateSet()->setAttributeAndModes(material.get());
    geode->getOrCreateStateSet()->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
    geode->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);

    osg::ref_ptr<osg::Group> root = new osg::Group;
    root->addChild(geode.get());
    root->addChild(osgDB::readNodeFile("../data/glider.osg"));

    osgViewer::Viewer viewer;
    viewer.setSceneData(root.get());
    
    return viewer.run();
}


Начинаем с создания обратного вызова-обработчика изменения значения альфа-канала во времени

class AlphaFadingCallback : public osg::StateAttributeCallback
{
public:

    AlphaFadingCallback()
    {
        _motion = new osgAnimation::InOutCubicMotion(0.0f, 1.0f);
    }

    virtual void operator() (osg::StateAttribute* , osg::NodeVisitor*);

protected:

    osg::ref_ptr<osgAnimation::InOutCubicMotion> _motion;
};

Защищенный параметр _motion будет определять ту функцию, по которой будет изменятся значение альфы во времени. Для данного примера выберем аппроксимацию кубическим сплайном, задавая её сразу же, в конструкторе класса

AlphaFadingCallback()
{
    _motion = new osgAnimation::InOutCubicMotion(0.0f, 1.0f);
}

Эта зависимость может быть проиллюстрирована вот такой кривой



В конструкторе объекта InOutCubicMotion определяем пределы изменения аппроксимируемой величины от 0 до 1. Далее переопределяем operator() для данного класса таким образом

void AlphaFadingCallback::operator()(osg::StateAttribute *sa, osg::NodeVisitor *nv)
{
    (void) nv;

    osg::Material *material = static_cast<osg::Material *>(sa);

    if (material)
    {
        _motion->update(0.0005f);

        float alpha = _motion->getValue();

        material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 1.0f, 1.0f, alpha));
    }
}

Получаем указатель на материал

osg::Material *material = static_cast<osg::Material *>(sa);

В callback приходит абстрактное значение атрибута, однако мы прикрепим данный обработчик к материалу, поэтому придет именно указатель на материал, поэтому мы смело преобразуем атрибут состояния к указателю на материал. Далее мы задаем интервал времени обновления аппроксимирующей функции — чем он больше, тем быстрее будет происходить изменение параметра в пределах заданного диапазона

_motion->update(0.0005f);

Читаем значение аппроксимирующей функции

float alpha = _motion->getValue();

и присваиваем материалу новое значение диффузного цвета

material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 1.0f, 1.0f, alpha));

Теперь сформируем сцену в функции main(). Я думаю вы уже устали каждый раз строить квадрат по вершинам, поэтому упростим задачу — генерируем квадратный полигон стандартной функцией OSG

osg::ref_ptr<osg::Drawable> quad = osg::createTexturedQuadGeometry(
                osg::Vec3(-0.5f, 0.0f, -0.5f),
                osg::Vec3(1.0f, 0.0f, 0.0f),
                osg::Vec3(0.0f, 0.0f, 1.0f));

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

osg::ref_ptr<osg::Material> material = new osg::Material;
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f));
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(0.0f, 1.0f, 1.0f, 0.5f));

Мы указываем параметры цвета материала. Ambient color — это параметр, характеризующий цвет материала в затененной области, недоступной для источников цвета. Diffuse color — собственный цвет материала, характеризующий способность поверхности рассеивать падающий на неё цвет, то есть то, что мы привыкли называть цветом в быту. Параметр FRONT_AND_BACK указывает, что данный атрибут цвета присваивается как лицевой, так и обратной стороне граней геометрии.

Назначаем материалу созданный нами ранее обработчик

material->setUpdateCallback(new AlphaFadingCallback);

Назначаем созданный материал квадрату

geode->getOrCreateStateSet()->setAttributeAndModes(material.get());

и задаем другие атрибуты — функцию смешивания цветов и указываем, что данный объект имеет прозрачные грани

geode->getOrCreateStateSet()->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
geode->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);

Завершаем формирование сцены и запускаем вьювер

osg::ref_ptr<osg::Group> root = new osg::Group;
root->addChild(geode.get());
root->addChild(osgDB::readNodeFile("../data/glider.osg"));

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

return viewer.run();

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



Вместо заключения: небольшая ремарка о зависимостях


Наверняка ваш пример не компилируется, выдавая ошибку на этапе компоновки. Это не случайно – обратите внимание на строчку в заголовочном файле main.h

#include    <osgAnimation/EaseMotion>

Каталог заголовков OSG, из которого берется заголовочный файл, обычно указывает на ту библиотеку, в которой содержится реализация функций и классов, описанных в заголовке. Поэтому появление каталога osgAnimation/ должно наводить на мысль о том, что в список линковки сценария сборки проекта следует добавить одноименную библиотеку, примерно так (с учетом путей к библиотекам и версии сборки)

LIBS += -losgAnimation

Продолжение следует...
Теги:
Хабы:
+13
Комментарии0

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн