Использование кадрового буфера в Qt 5

в 13:55, , рубрики: c++, OpenGL, qt, Qt Software, qt5, переводы

Во времена Qt 4 можно было ускорить рисование QPainter с OpenGL используя класс QGLPixelBuffer: он предоставляет удобный и быстрый способ создания поверхности для рисования, рендеринга на неё (с помощью обычных методов QPainter) и захвата конечного результата как QImage.
В Qt 5 QGLPixelBuffer по-прежнему существует, но он считается устаревшим в пользу объектов кадрового буфера, обёрнутых в Qt в класс QOpenGLFramebufferObject. Однако QOpenGLFramebufferObject это не QPaintDevice, поэтому мы не можем использовать QPainter прямо на нём.

Введение QOpenGLPaintDevice

QOpenGLPaintDevice является связующим звеном, которое нам нужно. Создание QOpenGLPaintDevice на активном контексте OpenGL разрешает нам рисовать на нём используя QPainter. Давайте посмотрим на пример его использования:

#include <QtCore>
#include <QtGui>
#include <QtWidgets>
 
QImage createImageWithFBO() {
    QSurfaceFormat format;
    format.setMajorVersion(3);
    format.setMinorVersion(3);
 
    QWindow window;
    window.setSurfaceType(QWindow::OpenGLSurface);
    window.setFormat(format);
    window.create();
 
    QOpenGLContext context;
    context.setFormat(format);
    if (!context.create()) qFatal("Cannot create the requested OpenGL context!");
    context.makeCurrent(&window);
 
    const QRect drawRect(0, 0, 400, 400);
    const QSize drawRectSize = drawRect.size();
 
    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setSamples(16);
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
 
    QOpenGLFramebufferObject fbo(drawRectSize, fboFormat);
    fbo.bind();
 
    QOpenGLPaintDevice device(drawRectSize);
    QPainter painter;
    painter.begin(&device);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::HighQualityAntialiasing);
 
    painter.fillRect(drawRect, Qt::blue);
 
    painter.drawTiledPixmap(drawRect, QPixmap(":/qt-project.org/qmessagebox/images/qtlogo-64.png"));
 
    painter.setPen(QPen(Qt::green, 5));
    painter.setBrush(Qt::red);
    painter.drawEllipse(0, 100, 400, 200);
    painter.drawEllipse(100, 0, 200, 400);
 
    painter.setPen(QPen(Qt::white, 0));
    QFont font;
    font.setPointSize(24);
    painter.setFont(font);
    painter.drawText(drawRect, "Hello FBO", QTextOption(Qt::AlignCenter));
 
    painter.end();
 
    fbo.release();
    return fbo.toImage();
}
 
int main(int argc, char **argv) {
    QApplication app(argc, argv);
 
    QImage targetImage = createImageWithFBO();
 
    QLabel label;
    label.setPixmap(QPixmap::fromImage(targetImage));
    label.show();
    return app.exec();
}

Разбор

Этот небольшой фрагмент кода создаёт QImage, рисуя на временный объект буфера кадров с использованием QPainter, а затем отображает полученное изображение на QLabel. Давайте проанализируем, что делает метод createImageWithFBO.

QSurfaceFormat format;
format.setMajorVersion(3);
format.setMinorVersion(3);
 
QWindow window;
window.setSurfaceType(QWindow::OpenGLSurface);
window.setFormat(format);
window.create();
 
QOpenGLContext context;
context.setFormat(format);
if (!context.create()) qFatal("Cannot create the requested OpenGL context!");
context.makeCurrent(&window);

Вначале мы создаём QWindow (с типом OpenGL) и QOpenGLContext, затем делаем контекст текущим в нашем окне. Они оба пытаются использовать OpenGL 3.3 (учтите, что Mac OS X на данный момент поддерживает OpenGL до 3.2).
Использование QWindow как результирующей поверхности контекста является воркэраундом, потому что на данный момент в Qt 5.0 нет никаких других типов QSurface. Так как нам нужна поверхность для установки текущего контекста, мы просто создаём невидимое окно без определённого размера и этого достаточно, чтобы иметь возможность использовать контекст OpenGL на нём.

const QRect drawRect(0, 0, 400, 400);
const QSize drawRectSize = drawRect.size();

Эти переменные будут содержать размер прямоугольника для рисования.

QOpenGLFramebufferObjectFormat fboFormat;
fboFormat.setSamples(16);
fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
 
QOpenGLFramebufferObject fbo(drawRectSize, fboFormat);
fbo.bind();

Здесь мы создаём QOpenGLFramebufferObject с конечным размером. Прежде чем сдалать это, мы также указываем его формат: мы хотим сделать мультисэмплинг активным на нашем объекте кадрового буфера, таким образом мы получим сглаженные примитивы, поэтому мы задаём 16 сэмплов на пиксель; кроме того, мы просим Qt задействовать также буфер глубины и буфер шаблона. Мы не собираемся использовать их явно, но движок отрисовки Qt может, так как в нём есть вызовы glStencil.
В конце мы привязываем объект QOpenGLFramebufferObject (это переадресовывает все операции рисования на него).

QOpenGLPaintDevice device(drawRectSize);
QPainter painter;
painter.begin(&device);
painter.setRenderHints(QPainter::Antialiasing | QPainter::HighQualityAntialiasing);
 
painter.fillRect(drawRect, Qt::blue);
 
painter.drawTiledPixmap(drawRect, QPixmap(":/qt-project.org/qmessagebox/images/qtlogo-64.png"));
 
painter.setPen(QPen(Qt::green, 5));
painter.setBrush(Qt::red);
painter.drawEllipse(0, 100, 400, 200);
painter.drawEllipse(100, 0, 200, 400);
 
painter.setPen(QPen(Qt::white, 0));
QFont font;
font.setPointSize(24);
painter.setFont(font);
painter.drawText(drawRect, "Hello FBO", QTextOption(Qt::AlignCenter));
 
painter.end();

Далее мы приступаем к созданию QOpenGLPaintDevice и рисованию на нём с помощью обычных вызовов QPainter. Обратите внимание на то, что мы должны явно закончить рисование вызовом end(), после чего наш объект кадрового буфера будет содержать результат.

fbo.release();
return fbo.toImage();

Эти строки завершают магию: мы отвязываем объект кадрового буфера, сбросив таким образом все его настройки к стандартным, и захватываем его содержимое как QImage вызовом метода toImage().
Результат:

Использование кадрового буфера в Qt 5

Встраивание нативных вызовов OpenGL

Мы можем доработать этот простой пример и нарисовать что-нибудь вызовами «чистого» OpenGL во время рисования с QPainter. Достаточно добавить следующие строки в приведённый выше фрагмент, пока painter активен:

painter.beginNativePainting();
nativePainting();
painter.endNativePainting();

Пара вызовов begin/endNativePainting нужна, чтобы сбросить состояние OpenGL на «чистое», прежде чем мы выполним наше рисование. Функция nativePainting():

void nativePainting() {
    static const float vertexPositions[] = {
        -0.8f, -0.8f, 0.0f,
         0.8f, -0.8f, 0.0f,
         0.0f,  0.8f, 0.0f
    };
 
    static const float vertexColors[] = {
        1.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f
    };
 
    QOpenGLBuffer vertexPositionBuffer(QOpenGLBuffer::VertexBuffer);
    vertexPositionBuffer.create();
    vertexPositionBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
    vertexPositionBuffer.bind();
    vertexPositionBuffer.allocate(vertexPositions, 9 * sizeof(float));
 
    QOpenGLBuffer vertexColorBuffer(QOpenGLBuffer::VertexBuffer);
    vertexColorBuffer.create();
    vertexColorBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
    vertexColorBuffer.bind();
    vertexColorBuffer.allocate(vertexColors, 9 * sizeof(float));
 
    QOpenGLShaderProgram program;
    program.addShaderFromSourceCode(QOpenGLShader::Vertex,
                                    "#version 330n"
                                    "in vec3 position;n"
                                    "in vec3 color;n"
                                    "out vec3 fragColor;n"
                                    "void main() {n"
                                    "    fragColor = color;n"
                                    "    gl_Position = vec4(position, 1.0);n"
                                    "}n"
                                    );
    program.addShaderFromSourceCode(QOpenGLShader::Fragment,
                                    "#version 330n"
                                    "in vec3 fragColor;n"
                                    "out vec4 color;n"
                                    "void main() {n"
                                    "    color = vec4(fragColor, 1.0);n"
                                    "}n"
                                    );
    program.link();
    program.bind();
 
    vertexPositionBuffer.bind();
    program.enableAttributeArray("position");
    program.setAttributeBuffer("position", GL_FLOAT, 0, 3);
 
    vertexColorBuffer.bind();
    program.enableAttributeArray("color");
    program.setAttributeBuffer("color", GL_FLOAT, 0, 3);
 
    glDrawArrays(GL_TRIANGLES, 0, 3);

Это чересчур подробный (но современный) способ для рисования трёхцветного треугольника в центре нашего изображения. Я не буду анализировать этот фрагмент кода подробно, так как он вполне читаем: вначале мы создаём два объекта буфера вершин, один для хранения позиции вершин (уже в усечённом пространстве), а другой для хранения цветов вершин (в RGB).
Затем мы создаём необходимые для вершин и фрагментов шейдеры, которые являются тривиальными — вершинный шейдер просто выводит (нормированные) позиции на выходе и передаёт цвета фрагментному шейдеру; фрагментный шейдер выводит цвета, которые он получает на входе.
Потом происходит связывание и буферы, созданные вначале устанавливаются как аттрибутные буферы.
Теперь всё готово для рисования нашего треугольника простым вызовом glDrawArrays. Конечный результат:

Использование кадрового буфера в Qt 5

Полный исходный код: pastebin.com/pkLA42G0

Динамические текстуры

Приложив немного больше усилий мы также можем продублировать возможности создания динамических текстур QGLPixelBuffer: QOpenGLFramebufferObject имеет метод texture(), который возвращает идентификатор текстуры, связанный с буфером цвета. Это означает, что мы можем рисовать текстуры, используя QPainter и накладывать их на любые объекты на нашей сцене OpenGL. Но это потребовало бы написания другой статьи…

Счастливого хакинга!

Автор: epicfailguy93

Источник

Поделиться

* - обязательные к заполнению поля