Техника рендеринга в текстуру позволяет разработчику создать текстуру, основанную на некоторой трехмерной подсцене или модели и применить её к поверхности на основной сцене. Подобную технологию часто называют “запеканием” текстуры.

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

  1. Создать объект текстуры для рендеринга в неё.
  2. Отрендерить сцену в текстуру.
  3. Использовать полученную текстуру по назначению.

Мы должны создать пустой текстурный объект. OSG позволяет сосздать пустую текстуру заданного размера. Метод setTextureSize() позволяет задавать ширину и высоту текстуры, а так же ещё глубину в качестве дополнительного параметра (для 3D-текстур).

Для выполнения рендеринга в текстуру её следует присоединить к объекту камеры путем вызова метода attach(), принимающего в качестве аргумента объект текстуры. Кроме того данный метод принимает аргрумент, указывающий, какую часть буфера кадра следует рендерить в данную текстуру. Например, для передачи буфера цвета в текстуру следует выполнить следующий код

camera->attach( osg::Camera::COLOR_BUFFER, texture.get() );

К другим, доступным для рендеринга частям кадрового буфера, относятся буфер глубины DEPTH_BUFFER, буфер трафарета STENCIL_BUFFER дополнительные буферы цвета от COLOR_BUFFER0 до COLOR_BUFFER15. Наличие дополнительных буферов цвета и их количество определяется моделью видеокарты.

Кроме того, для камеры, выполняющей рендеринг в текстуру следует установить параметры матрицы проекции и вьюпорта, размер которого соотвествует размеру текстуры. Текстура будет обновлятся в процессе прорисовки каждого кадра. Необходимо учитывать, что основная камера не должна использоваться для рендеринга в текстуру, так как она обеспечивает рендеринг основной сцены и вы просто получите черный экран. Это требование может не выполнятся только тогда, когда вы выполняете внеэкранный рендеринг.

Буфер кадра, пиксельный буфер и FBO

Проблема данной задачи в том, что необходим способ получения визуального представления буфера кадра и передача его в объект текстуры. Прямой подход к её решению заключается в использовании функций glReadPixels() для возврата пикселей из буферов кадра и применение результата в функции glTexImage* (). Это довольно простая операция, но она всегда будет выполнятся крайне медленно.

В плане повышения эффективности лучше использовать функцию glCopyTexSubImage(). Однако и этот способ оставляет ресурс для оптимизации. Хорошей идеей является рендеринг сцены напрямую в текстурый буфер изображения, не прибегая к копированию данных из буфера кадра. Для этого существует два основных решения:

  1. Буфер для хранения данных о пикселях, с дескриптором формата пикселя, эквивалентный окну. Должен быть уничтоден по окончании рендеринга.
  2. Объект буфера кадра (FBO), который иногда лучше пиксельного буфера в том смысле что позволяет добавлять буферы кадра и перенаправлять вывод на себя.

OSG поддерживает разные конечные точки рендеринга: прямой рендеринг в кадровый буфер (FRAME_BUFFER), пиксельный буфер (PIXEL_BUFFER) и FBO (FRAME_BUFFER_OBJECT). Класс osg::Camera предоставляет метод setRenderTargetImplementation() для задания цели рендеринга, например

camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER);

Пример реализации рендеринга в текстуру

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

main.h

#ifndef		MAIN_H
#define		MAIN_H

#include    <osg/Camera>
#include    <osg/Texture2D>
#include    <osg/MatrixTransform>
#include    <osgDB/ReadFile>
#include    <osgGA/TrackballManipulator>
#include    <osgViewer/Viewer>

#endif

main.cpp

#include	"main.h"

//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
osg::Geometry *createQuad(const osg::Vec3 &pos, float w, float h)
{
    osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
    vertices->push_back( pos + osg::Vec3( w / 2, 0.0f, -h / 2) );
    vertices->push_back( pos + osg::Vec3( w / 2, 0.0f,  h / 2) );
    vertices->push_back( pos + osg::Vec3(-w / 2, 0.0f,  h / 2) );
    vertices->push_back( pos + osg::Vec3(-w / 2, 0.0f, -h / 2) );

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

    return quad.release();
}

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

    osg::ref_ptr<osg::Node> sub_model = osgDB::readNodeFile("../data/cessna.osg");

    osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform;
    transform1->setMatrix(osg::Matrix::rotate(0.0, osg::Vec3(0.0f, 0.0f, 1.0f)));
    transform1->addChild(sub_model.get());

    osg::ref_ptr<osg::Geode> model = new osg::Geode;
    model->addChild(createQuad(osg::Vec3(0.0f, 0.0f, 0.0f), 2.0f, 2.0f));

    int tex_widht = 1024;
    int tex_height = 1024;

    osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
    texture->setTextureSize(tex_widht, tex_height);
    texture->setInternalFormat(GL_RGBA);
    texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR);
    texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR);

    model->getOrCreateStateSet()->setTextureAttributeAndModes(0, texture.get());    

    osg::ref_ptr<osg::Camera> camera = new osg::Camera;
    camera->setViewport(0, 0, tex_widht, tex_height);
    camera->setClearColor(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
    camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    camera->setRenderOrder(osg::Camera::PRE_RENDER);
    camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
    camera->attach(osg::Camera::COLOR_BUFFER, texture.get());

    camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);
    camera->addChild(transform1.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());
    viewer.setCameraManipulator(new osgGA::TrackballManipulator);
    viewer.setUpViewOnSingleScreen(0);

    camera->setProjectionMatrixAsPerspective(30.0, static_cast<double>(tex_widht) / static_cast<double>(tex_height), 0.1, 1000.0);

    float dist = 100.0f;
    float alpha = 10.0f * 3.14f / 180.0f;
    
    osg::Vec3 eye(0.0f, -dist * cosf(alpha), dist * sinf(alpha));
    osg::Vec3 center(0.0f, 0.0f, 0.0f);
    osg::Vec3 up(0.0f, 0.0f, -1.0f);
    camera->setViewMatrixAsLookAt(eye, center, up);

    float phi = 0.0f;
    float delta = -0.01f;

    while (!viewer.done())
    {
        transform1->setMatrix(osg::Matrix::rotate(static_cast<double>(phi), osg::Vec3(0.0f, 0.0f, 1.0f)));
        viewer.frame();
        phi += delta;
    }

    return 0;
}

Для создания квадрата напишем отдельную свободную функцию

osg::Geometry *createQuad(const osg::Vec3 &pos, float w, float h)
{
    osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
    vertices->push_back( pos + osg::Vec3( w / 2, 0.0f, -h / 2) );
    vertices->push_back( pos + osg::Vec3( w / 2, 0.0f,  h / 2) );
    vertices->push_back( pos + osg::Vec3(-w / 2, 0.0f,  h / 2) );
    vertices->push_back( pos + osg::Vec3(-w / 2, 0.0f, -h / 2) );

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

    return quad.release();
}

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

В теле основной программы загрузим модельку цессны

osg::ref_ptr<osg::Node> sub_model = osgDB::readNodeFile("../data/cessna.osg");

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

osg::ref_ptr<osg::MatrixTransform> transform1 = new osg::MatrixTransform;
transform1->setMatrix(osg::Matrix::rotate(0.0, osg::Vec3(0.0f, 0.0f, 1.0f)));
transform1->addChild(sub_model.get());

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

osg::ref_ptr<osg::Geode> model = new osg::Geode;
model->addChild(createQuad(osg::Vec3(0.0f, 0.0f, 0.0f), 2.0f, 2.0f));

Создаем пустую текстуру для квадрата размером 1024х1024 пикселя с форматом пикселя RGBA (32-битный трехкомпонентный цвет с альфа-каналом)

int tex_widht = 1024;
int tex_height = 1024;

osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
texture->setTextureSize(tex_widht, tex_height);
texture->setInternalFormat(GL_RGBA);
texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR);
texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR);

Применяем эту текстуру к модели квадрата

model->getOrCreateStateSet()->setTextureAttributeAndModes(0, texture.get()); 

Затем создаем камеру, которая будет запекать текстуру

osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setViewport(0, 0, tex_widht, tex_height);
camera->setClearColor(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

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

camera->setRenderOrder(osg::Camera::PRE_RENDER);
camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
camera->attach(osg::Camera::COLOR_BUFFER, texture.get());

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

camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);
camera->addChild(transform1.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());
viewer.setCameraManipulator(new osgGA::TrackballManipulator);
viewer.setUpViewOnSingleScreen(0);

Натраиваем матрицк проекции для камеры - перспективная проекция через параметры пирамицы отсечения

camera->setProjectionMatrixAsPerspective(30.0, static_cast<double>(tex_widht) / static_cast<double>(tex_height), 0.1, 1000.0);

Настраиваем матрицу вида, задающую положение камеры в пространстве по отношению к началу координат подсцены с цессной

float dist = 100.0f;
float alpha = 10.0f * 3.14f / 180.0f;

osg::Vec3 eye(0.0f, -dist * cosf(alpha), dist * sinf(alpha));
osg::Vec3 center(0.0f, 0.0f, 0.0f);
osg::Vec3 up(0.0f, 0.0f, -1.0f);
camera->setViewMatrixAsLookAt(eye, center, up);

Наконец, анимируем и отображаем сцену, меняя угол поворота самолета вокруг оси Z на каждом кадре

float phi = 0.0f;
float delta = -0.01f;

while (!viewer.done())
{
    transform1->setMatrix(osg::Matrix::rotate(static_cast<double>(phi), osg::Vec3(0.0f, 0.0f, 1.0f)));
    viewer.frame();
    phi += delta;
}

В итоге мы получаем довольно интересную картинку

В данном примере мы реализовали некоторую анимацию сцены, но следует помнить о том, что разворачивание цикла run() и изменение параметров рендеринга перед или после отрисовки кадра является небезопасным занятием с точки зрения организации доступа к данным разных потоках. Поскольку OSG использует многопоточный рендеринг, то существуют и штатные механизмы встраивания собственных дейсвий в процесс рендеринга, обеспечивающие потокобезопасный доступ к данным.

Сохранение результата рендеринга в файл

OSG поддерживает возможность прикрепить к камере объект osg::Image и созранить содержимое буфера кадра в буфер данных изображения. После этого возможно сохранить эти данные на диск используя функцию osg::writeImageFile()

osg::ref_ptr<osg::Image> image = new osg::Image;
image->allocateImage( width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE );
camera->attach( osg::Camera::COLOR_BUFFER, image.get() );
...
osgDB::writeImageFile( *image, "saved_image.bmp" );