Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
825 views
in Technique[技术] by (71.8m points)

qt - Easiest way for offscreen rendering with QOpenGLWidget

I have a hidden QOpenGLWidget (Qt 5.4.2, NOT QGLWidget) and I want to basically continually do grab() or grabFramebuffer() to get its content (and write it to disk). The widget renders fine when visible, but does not when hidden. If I do a show() followed by a hide() call it works. This seems strange because QOpenGLWidget does internally already render to a framebuffer according to the docs. What is the easiest way to achieve this (if possible without creating another framebuffer)?
Bonus points for being able to capture an offscreen QGraphicsView using an QOpenGLWidget as its viewport with custom OpenGL-painted QGraphicsItems in it...

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

UPDATE 2: The corresponding bug in QOpenGLWidget seems to be fixed in Qt 5.10, so I suggest to simply use the class again. Although you might want to wait for this bug to also get fixed...
UPDATE 1: Added 3, best solution using a custom QWindow-derived class

1 - QOpenGLWidget
If a hidden QOpenGLWidget does allocate a framebuffer (not sure if this happens), there is still no way to bind it manually, because you can not get the buffer id. Additionally none of the necessary functions initializeGL(), resizeGL() and paintGL are called and none of the functions grab(), grabFramebuffer and render() are working correctly. Here is (imo) a workaround to draw the widget offscreen. You call paintGL directly after setting up all the necessary stuff:

class GLWidget: public QOpenGLWidget
{
public:
    GLWidget(QWidget * parent = nullptr);
private:
    bool m_isInitialized = false;
    QOpenGLFramebufferObject m_fbo = nullptr;
};

void GLWidget::drawOffscreen()
{
        //the context should be valid. make sure it is current for painting
    makeCurrent();
    if (!m_isInitialized)
    {
        initializeGL();
        resizeGL(width(), height());
    }
    if (!m_fbo || m_fbo->width() != width() || m_fbo->height() != height())
    {
        //allocate additional? FBO for rendering or resize it if widget size changed
        delete m_fbo;
        QOpenGLFramebufferObjectFormat format;
        format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
        m_fbo = new QOpenGLFramebufferObject(width(), height(), format);
        resizeGL(width(), height());
    }

    //#1 DOES NOT WORK: bind FBO and render() widget
    m_fbo->bind();
    QOpenGLPaintDevice fboPaintDev(width(), height());
    QPainter painter(&fboPaintDev);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
    render(&painter);
    painter.end();
    //You could now grab the content of the framebuffer we've rendered to
    QImage image1 = m_fbo->toImage();
    image1.save(QString("fb1.png"));
    m_fbo->release();
    //#1 --------------------------------------------------------------

    //#2 WORKS: bind FBO and render stuff with paintGL() call
    m_fbo->bind();
    paintGL();
    //You could now grab the content of the framebuffer we've rendered to
    QImage image2 = m_fbo->toImage();
    image2.save(QString("fb2.png"));
    m_fbo->release();
    //#2 --------------------------------------------------------------

    //bind default framebuffer again. not sure if this necessary
    //and isn't supposed to use defaultFramebuffer()...
    m_fbo->bindDefault();
    doneCurrent();
}

void GLWidget::paintGL()
{
    //When doing mixed QPainter/OpenGL rendering make sure to use a QOpenGLPaintDevice, otherwise only OpenGL content is visible!
    //I'm not sure why, because according to the docs (http://doc.qt.io/qt-5/topics-graphics.html) this is supposed to be the same...
    QOpenGLPaintDevice fboPaintDev(width(), height());
    QPainter painter(&fboPaintDev);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
    //This is what you'd use (and what would work) if the widget was visible
    //QPainter painter;
    //painter.begin(this);

    //now start OpenGL painting
    painter.beginNativePainting();
    glClearColor(0.5f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    ...
    painter.endNativePainting();
    //draw non-OpenGL stuff with QPainter
    painter.drawText(20, 40, "Foo");
    ...
    painter.end();
}

2 - QGraphicsView with QOpenGLWidget viewport
Here render() works as expected when you provide it with an QOpenGLPaintDevice:

MainWindow::MainWindow()
{
    scene = new QGraphicsScene;
    hiddenView = new QGraphicsView(scene);
    hiddenGLWidget = new QOpenGLWidget;
    hiddenView->setViewport(hiddenGLWidget);
    //hiddenView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
    //hiddenView->show();
}

void MainWindow::screenshot()
{
    //try regular grab functions
    QPixmap pixmap1 = hiddenView->grab(); //image with scrollbars, no OpenGL content
    pixmap1.save("bla1.png");
    QPixmap pixmap2 = hiddenGLWidget->grab(); //produces an empty image
    pixmap2.save("bla2.png");
    //try grabbing only the QOpenGLWidget framebuffer
    QImage image1 = hiddenGLWidget->grabFramebuffer(); //null image
    image1.save("bla3.png");

    //WORKS: render via FBO
    hiddenGLWidget->makeCurrent();
    QOpenGLFramebufferObjectFormat format;
    format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
    QOpenGLFramebufferObject * fbo = new QOpenGLFramebufferObject(hiddenView->width(), hiddenView->height(), format);
    fbo->bind();
    QOpenGLPaintDevice fboPaintDev(hiddenView->width(), hiddenView->height());
    QPainter painter(&fboPaintDev);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
    hiddenView->render(&painter); //WORKS and captures mixed OpenGL and non-OpenGL QGraphicsitems
    //hiddenView->repaint(); //does not work
    //hiddenView->scene()->render(&painter); //does not work
    //hiddenGLWidget->paintGL(); //might work. can not call, protected
    //hiddenGLWidget->render(&painter); //does not work
    //hiddenGLWidget->repaint(); //does not work
    painter.end();
    QImage image2 = fbo->toImage();
    image2.save("bla4.png");
    fbo->release();
    delete fbo;
}

3 - How to render to and grab an image from a hidden QOpenGLWidget
A better overall solution is to use a custom QWindow with a QSurface::OpenGLSurface type. Create an extra QOpenGLContext, an extra background QOpenGLFramebufferObject you will draw to, and a QOpenGLShaderProgram to blit the framebuffer to the backbuffer. If you want multisampling, you might need a resolve QOpenGLFramebufferObject too, to convert the multisampled framebuffer to a non-multisampled one. The class interface can be similar to QOpenGLWidget (virtual initializeGL(), resizeGL(), paintGL() for users). Reimplement exposeEvent(), resizeEvent() and event() (you might need to reimplement metric() too).
A semi-complete implementation:

Header:

#pragma once

#include <QtCore/QObject>
#include <QtGui/QScreen>
#include <QtGui/QWindow>
#include <QtGui/QPaintEvent>
#include <QtGui/QResizeEvent>
#include <QtGui/QOpenGLPaintDevice>
#include <QtGui/QOpenGLFunctions>
#include <QtGui/QOpenGLFunctions_3_0>
#include <QtGui/QOpenGLFramebufferObject>
#include <QtGui/QSurfaceFormat>
#include <QtWidgets/QWidget>

#include <atomic>
#include <mutex>

class MyGLWindow : public QWindow
{
    Q_OBJECT

public:
    /// @brief Constructor. Creates a render window.
    /// @param targetScreen Target screen.
    /// this is because before the FBO and off-screen surface haven't been created.
    /// By default this uses the QWindow::requestedFormat() for OpenGL context and off-screen surface.
    explicit MyGLWindow(QScreen * targetScreen = nullptr);

    /// @brief Constructor. Creates a render window.
    /// @param parent Parent window.
    /// this is because before the FBO and off-screen surface haven't been created.
    /// By default this uses the QWindow::requestedFormat() for OpenGL context and off-screen surface.
    explicit MyGLWindow(QWindow * parent);

    /// @brief Destructor.
    virtual ~MyGLWindow();

    /// @brief Create a container widget for this window.
    /// @param parent Parent widget.
    /// @return Returns a container widget for the window.
    QWidget * createWidget(QWidget * parent = nullptr);

    /// @brief Check if the window is initialized and can be used for rendering.
    /// @return Returns true if context, surface and FBO have been set up to start rendering.
    bool isValid() const;

    /// @brief Return the context used in this window.
    /// @return The context used in this window or nullptr if it hasn't been created yet.
    QOpenGLContext * context() const;

    /// @brief Return the OpenGL function object that can be used the issue OpenGL commands.
    /// @return The functions for the context or nullptr if it the context hasn't been created yet.
    QOpenGLFunctions * functions() const;

    /// @brief Return the OpenGL off-screen frame buffer object identifier.
    /// @return The OpenGL off-screen frame buffer object identifier or 0 if no FBO has been created yet.
    /// @note This changes on every resize!
    GLuint framebufferObjectHandle() const;

    /// @brief Return the OpenGL off-screen frame buffer object.
    /// @return The OpenGL off-screen frame buffer object or nullptr if no FBO has been created yet.
    /// @note This changes on every resize!
    const QOpenGLFramebufferObject * getFramebufferObject() const;

    /// @brief Return the OpenGL off-screen frame buffer object identifier.
    /// @return The OpenGL off-screen frame buffer object identifier or 0 if no FBO has been created yet.
    void bindFramebufferObject();

    /// @brief Return the current contents of the FBO.
    /// @return FBO content as 32bit QImage. You might need to swap RGBA to BGRA or vice-versa.
    QImage grabFramebuffer();

    /// @brief Makes the OpenGL context current for rendering.
    /// @note Make sure to bindFramebufferObject() if you want to render to this widgets FBO.
    void makeCurrent();

    /// @brief Release the OpenGL context.
    void doneCurrent();

    /// @brief Copy content of framebuffer to back buffer and swap buffers if the surface is double-buffered.
    /// If the surface is not double-buffered, the frame buffer content is blitted to the front buffer.
    /// If the window is not exposed, only the OpenGL pipeline is glFlush()ed so the framebuffer can be read back.
    void swapBuffers();

    public slots:
    /// @brief Lazy update routine like QWidget::update().
    void update();
    /// @brief Immediately render the widget contents to framebuffer.
    void render();

signals:
    /// @brief Emitted when swapBuffers() was called and bufferswapping is done.
    void frameSwapped();
    /// @brief Emitted after a resizeEvent().
    void resized();

protected:
    virtual void exposeEvent(QExposeEvent *e) override;
    virtual void resizeEvent(QResizeEvent *e) override;
    virtual bool event(QEvent *e) override;
    //      virtual int metric(QPaintDevice::PaintDeviceMetric metric) const override;

    /// @brief Called exactly once when the window is first exposed OR render() is called when the widget is invisible.
    /// @note After this the off-screen surface and FBO are available.
    virtual void initializeGL() = 0;
    /// @brief Called whenever the window size changes.
    /// @param width New window width.
    /// @param height New window height.
    virtual void resizeGL(int width, int height) = 0;
    /// @brief Called whenever the window needs to repaint itself. Override to draw OpenGL content.
    /// When this function is called, the context is already current and the correct framebuffer is bound.
    virtual void paintGL() = 0;
    //      /// @brief Called whenever the window needs to repaint itself. Override to draw QPainter content.
    //      /// @brief This is called AFTER paintGL()! Only needed when p

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...