manatttta
manatttta

Reputation: 3124

Render huge image in Qt

I have to render a huge image (e.g. 30.000 x 30.000 pixels) in a Qt based app.

I can do this via OpenGL, but I load it as a single texture. Therefore, I am limited by my graphics card max texture size (in this case, 16.368 pixels). I need to implement something like tiling or similar, while maintaining a good rendering performance.

Is there any example to accomplish this, preferably with good Qt integration? (not necessarily OpenGL). Or is there any other starting point?

Thank you

Upvotes: 3

Views: 3004

Answers (1)

Bertrand
Bertrand

Reputation: 289

You can do it with the QOpenGLxxx classes. Here a complete functionnal example, with very good perfs and using modern OpenGL techniques. Each tile is an independant texture. In this example I use the same image for all the textures, but in your case you can create a slice of your original image and use it as the tile texture.

QOpenGLWidget is used as a base class for displaying the tiles. QOpenGLBuffer to manage the openGL vertex buffer. QOpenGLShaderProgram to manage the shaders.

tilewidget.cpp:

#include "tiledwidget.h"

#include <QOpenGLFunctions>
#include <QPainter>
#include <QOpenGLTexture>
#include <QTransform>
#include <QOpenGLBuffer>
#include <QVector2D>

// a small class to manage vertices in the vertex buffer
class Vertex2D
{
public:
    Vertex2D(){}
    Vertex2D(const QPointF &p, const QPointF &c) :
        position(p)
      , coords(c)
    {
    }
    QVector2D position; // position of the vertex
    QVector2D coords; // texture coordinates of the vertex
};


TiledWidget::TiledWidget(QWidget *parent) :
    QOpenGLWidget(parent)
  , m_rows(5)
  , m_cols(5)
  , m_vertexBuffer(new QOpenGLBuffer)
  , m_program(new QOpenGLShaderProgram(this))
{
}
TiledWidget::~TiledWidget()
{
    qDeleteAll(m_tiles);
    delete m_vertexBuffer;
    delete m_program;
}
void TiledWidget::initializeGL()
{
    // tiles creation based on a 256x256 image
    QImage image(":/lenna.png");
    if (image.format() != QImage::Format_ARGB32_Premultiplied)
        image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);

    for (int row = 0; row < m_rows; row++)
    {
        for (int col = 0; col < m_cols; col++)
        {
            QOpenGLTexture* tile = new QOpenGLTexture(QOpenGLTexture::Target2D);
            if (!tile)
            {
                qDebug() << "Ooops!";
                break;
            }
            if (!tile->create())
            {
                qDebug() << "Oooops again!";
                break;
            }

            tile->setSize(256, 256);
            tile->setFormat(QOpenGLTexture::RGBA8_UNorm);
            // you can manage the number of mimap you desire...
            // by default 256x256 => 9 mipmap levels will be allocated:
            // 256, 128, 64, 32, 16, 8, 4, 2 and 1px
            // to modify this use tile->setMipLevels(n);
            tile->setMinificationFilter(QOpenGLTexture::Nearest);
            tile->setMagnificationFilter(QOpenGLTexture::Nearest);
            tile->setData(image, QOpenGLTexture::GenerateMipMaps);
            m_tiles << tile;
        }
    }
    // vertex buffer initialisation
    if (!m_vertexBuffer->create())
    {
        qDebug() << "Ooops!";
        return;
    }
    m_vertexBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);
    m_vertexBuffer->bind();
    // room for 2 triangles of 3 vertices
    m_vertexBuffer->allocate(2 * 3 * sizeof(Vertex2D));
    m_vertexBuffer->release();

    // shader program initialisation
    if (!m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/basic_vert.glsl"))
    {
        qDebug() << "Ooops!";
        return;
    }
    if (!m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/basic_frag.glsl"))
    {
        qDebug() << "Ooops!";
        return;
    }
    if (!m_program->link())
    {
        qDebug() << "Ooops!";
        return;
    }

    // ok, we are still alive at this point...
}
// this slot is called at windows close before the widget is destroyed
// use this to cleanup opengl
void TiledWidget::shutDown()
{
    // don't forget makeCurrent, OpenGL is a state machine!
    makeCurrent();
    foreach(QOpenGLTexture* tile, m_tiles)
    {
        if (tile->isCreated())
            tile->destroy();
    }
    if (m_vertexBuffer)
        m_vertexBuffer->destroy();
}
void TiledWidget::resizeGL(int width, int height)
{
    Q_UNUSED(width);
    Q_UNUSED(height);
    // ...
}
// you can alternatively override QOpenGLWidget::paintGL if you don't need
// to draw things with classic QPainter
void TiledWidget::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event)

    QPainter painter(this);
    // native draw
    painter.beginNativePainting();
    drawGL();
    painter.endNativePainting();

    // draw overlays if needed
    // ...draw something with painter...
}
void TiledWidget::drawGL()
{
    // always a good thing to make current
    makeCurrent();
    // enable texturing
    context()->functions()->glEnable(GL_TEXTURE_2D);
    // enable blending
    context()->functions()->glEnable(GL_BLEND);
    // blending equation (remember OpenGL textures are premultiplied)
    context()->functions()->glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    // clear
    context()->functions()->glClearColor(0.8, 0.8, 0.8, 1);
    context()->functions()->glClear(GL_COLOR_BUFFER_BIT);
    context()->functions()->glClear(GL_DEPTH_BUFFER_BIT);

    // viewport and matrices setup for a 2D tile system
    context()->functions()->glViewport(0, 0, width(), height());
    QMatrix4x4 projectionMatrix;
    projectionMatrix.setToIdentity();
    projectionMatrix.ortho(0, width(), height(), 0, -1, 1);
    QMatrix4x4 viewProjectionMatrix;
    // use a QTransform to scale, translate, rotate your view
    viewProjectionMatrix = projectionMatrix * QMatrix4x4(m_transform);

    // program setup
    m_program->bind();
    // a good practice if you have to manage multiple shared context
    // with shared resources: the link is context dependant.
    if (!m_program->isLinked())
        m_program->link();

    // binding the buffer
    m_vertexBuffer->bind();

    // setup of the program attributes
    int pos = 0, count;
    // positions : 2 floats
    count = 2;
    m_program->enableAttributeArray("vertexPosition");
    m_program->setAttributeBuffer("vertexPosition", GL_FLOAT, pos, count, sizeof(Vertex2D));
    pos += count * sizeof(float);

    // texture coordinates : 2 floats
    count = 2;
    m_program->enableAttributeArray("textureCoordinates");
    m_program->setAttributeBuffer("textureCoordinates", GL_FLOAT, pos, count, sizeof(Vertex2D));
    pos += count * sizeof(float);

    m_program->setUniformValue("viewProjectionMatrix", viewProjectionMatrix);
    m_program->setUniformValue("f_opacity", (float) 0.5);


    // draw each tile
    for (int row = 0; row < m_rows; row++)
    {
        for (int col = 0; col < m_cols; col++)
        {
            QRect rect = tileRect(row, col);
            // write vertices in the buffer
            // note : better perf if you precreate this buffer

            Vertex2D v0;
            v0.position = QVector2D(rect.bottomLeft());
            v0.coords = QVector2D(0, 1);

            Vertex2D v1;
            v1.position = QVector2D(rect.topLeft());
            v1.coords = QVector2D(0, 0);

            Vertex2D v2;
            v2.position = QVector2D(rect.bottomRight());
            v2.coords = QVector2D(1, 1);

            Vertex2D v3;
            v3.position = QVector2D(rect.topRight());
            v3.coords = QVector2D(1, 0);

            int vCount = 0;
            // first triangle v0, v1, v2
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v0, sizeof(Vertex2D)); vCount++;
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v1, sizeof(Vertex2D)); vCount++;
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v2, sizeof(Vertex2D)); vCount++;

            // second triangle v1, v3, v2
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v1, sizeof(Vertex2D)); vCount++;
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v3, sizeof(Vertex2D)); vCount++;
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v2, sizeof(Vertex2D)); vCount++;

            // bind the tile texture on texture unit 0
            // you can add other textures binding them in texture units 1, 2...
            QOpenGLTexture* tile = m_tiles.at(tileIndex(row, col));
            // activate texture unit 0
            context()->functions()->glActiveTexture(GL_TEXTURE0);
            // setup texture options here if needed...
            // set sampler2D on texture unit 0
            m_program->setUniformValue("f_tileTexture", 0);
            // bind texture
            tile->bind();
            // draw 2 triangles = 6 vertices starting at offset 0 in the buffer
            context()->functions()->glDrawArrays(GL_TRIANGLES, 0, 6);
            // release texture
            tile->release();
        }
    }
    m_vertexBuffer->release();
    m_program->release();
}
// compute the tile index
int TiledWidget::tileIndex(int row, int col)
{
    return row * m_cols + col;
}

// compute the tile rectangle given a row and a col.
// Note : You will have to manage the opengl texture border effect
// to get correct results. To do this you must overlap textures when you draw them.
QRect TiledWidget::tileRect(int row, int col)
{
    int x = row * 256;
    int y = col * 256;
    return QRect(x, y, 256, 256);
}

tilewidget.h:

#ifndef TILEDWIDGET_H
#define TILEDWIDGET_H

#include <QOpenGLWidget>

#include <QOpenGLFramebufferObjectFormat>
#include <QOpenGLShaderProgram>

#include <QTransform>

class QOpenGLTexture;
class QOpenGLBuffer;
class QOpenGLShaderProgram;

class TiledWidget : public QOpenGLWidget
{
public:
    TiledWidget(QWidget *parent = 0);
    ~TiledWidget();
public slots:
    void shutDown();
private:
    QTransform m_transform;
    int m_rows;
    int m_cols;
    QVector<QOpenGLTexture*> m_tiles;
    QOpenGLBuffer *m_vertexBuffer;
    QOpenGLShaderProgram* m_program;

    void resizeGL(int width, int height);
    void initializeGL();
    void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
    void drawGL();
    int tileIndex(int row, int col);
    QRect tileRect(int row, int col);
};

#endif // TILEDWIDGET_H

mainwindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "tiledwidget.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    m_tiledWidget = new TiledWidget(this);
    setCentralWidget(m_tiledWidget);
}
MainWindow::~MainWindow()
{
    delete ui;
}
void MainWindow::closeEvent(QCloseEvent *event)
{
    Q_UNUSED(event);
    // destroy textures before widget desctruction
    m_tiledWidget->shutDown();
}

and the shaders:

// vertex shader    
#version 330 core

in vec2 vertexPosition;
in vec2 textureCoordinates;

uniform mat4 viewProjectionMatrix;

out vec2 v_textureCoordinates;

void main()
{
    v_textureCoordinates = vec2(textureCoordinates);
    gl_Position = viewProjectionMatrix * vec4(vertexPosition, 0.0, 1.0);
}

// fragment shader
#version 330 core

// vertices datas
in vec2 v_textureCoordinates;

// uniforms
uniform sampler2D f_tileTexture; // tile texture
uniform float f_opacity = 1; // tile opacity

out vec4 f_fragColor; // shader output color

void main()
{
    // get the fragment color from the tile texture
    vec4 color = texture(f_tileTexture, v_textureCoordinates.st);
    // premultiplied output color
    f_fragColor = vec4(color * f_opacity);
}

You get this result:

enter image description here

Upvotes: 5

Related Questions