Wiki

Accelerate your Widgets with OpenGL

by Samuel Rødal

Qt 4.4 introduced a powerful feature to allow any QWidget subclass to be put into QGraphicsView. It is now possible to embed ordinary widgets in a QGLWidget, a feature that has been missing in Qt's OpenGL support for some time. In this article, we will show how to use QGraphicsView to place widgets in a scene rendered with standard OpenGL commands.

In some applications, such as those used in Computer Aided Design (CAD), or in applications that use OpenGL rendering to improve performance, being able to render widgets inside a QGLWidget is a desirable feature. For example, using semi-transparent widgets on top of an OpenGL-rendered scene can better utilize screen real estate. Also, widgets can be used as tooltips or labels to annotate objects in a 3D scene.

One way to achieve this is to perform OpenGL drawing in a QGraphicsView, as opposed to the traditional way of subclassing QGLWidget and overriding its paintGL() function.

To demonstrate this technique, we will use a model viewer which loads a 3D model stored in the .obj file format and renders it using OpenGL. On top of this model, we will draw a set of widgets to control the rendering parameters and display various bits of information about the model. This is done simply by adding the widgets to the scene.

Openglwidgets2

Turbo Charging Graphics View

In our example, the actual OpenGL scene rendering and widget controls will be handled in a QGraphicsScene subclass, but first we need to correctly set up a QGraphicsView with a QGLWidget viewport.

Since we want our widgets to be positioned relative to the view coordinates, we need to keep the scene rectangle synchronized with the size of the QGraphicsView. To achieve this, we create a simple subclass of QGraphicsView which updates the scene's rectangle whenever the view itself is resized.

The custom GraphicsView class looks like this:

    class GraphicsView : public QGraphicsView
    {
    public:
        GraphicsView()
        {
            setWindowTitle(tr("3D Model Viewer"));
        }
 
    protected:
        void resizeEvent(QResizeEvent *event) {
            if (scene())
                scene()->setSceneRect(
                    QRect(QPoint(0, 0), event->size()));
            QGraphicsView::resizeEvent(event);
        }
    };

In our main function we instantiate this class and set the QGraphicsView parameters that are needed in our case. First of all the viewport needs to be a QGLWidget in order to do OpenGL rendering in our graphics scene. We use the SampleBuffers format specifier to enable multisample anti-aliasing in our rendering code.

    int main(int argc, char **argv)
    {
        QApplication app(argc, argv);
 
        GraphicsView view;
        view.setViewport(new QGLWidget(
            QGLFormat(QGL::SampleBuffers)));
        view.setViewportUpdateMode(
            QGraphicsView::FullViewportUpdate);
        view.setScene(new OpenGLScene);
        view.show();
 
        view.resize(1024, 768);
        return app.exec();
    }

Next, we set the viewport update mode of the QGraphicsView to FullViewportUpdate as a QGLWidget cannot perform partial updates. Thus, we need to redraw everything whenever a part of the scene changes. We set as the scene an instance of our OpenGLScene class, a subclass of QGraphicsScene, and resize the view to a decent size.

Rendering the OpenGL Scene

The actual OpenGL rendering is done by reimplementing QGraphicsScene's drawBackground() function. By rendering the OpenGL scene in drawBackground(), all widgets and other graphics items are drawn on top. It is possible to reimplement drawForeground() to do OpenGL rendering on top of the graphics scene, if that is required.

Unlike when subclassing QGLWidget, it is not sufficient to set up common state in initializeGL() or resizeGL(). A painter is already active on the GL context when drawBackground() is called, and QPainter changes the GL state when begin() is called. For example, the model view and projection matrices will be changed so that we have a coordinate system that maps to the QWidget coordinate system.

Here is how our reimplemented drawBackground() looks:

    void OpenGLScene::drawBackground(QPainter *painter,
                                     const QRectF &)
    {
        if (painter->paintEngine()->type()
                != QPaintEngine::OpenGL) {
            qWarning("OpenGLScene: drawBackground needs a "
                     "QGLWidget to be set as viewport on the "
                     "graphics view");
            return;
        }

First, we ensure that we actually have an OpenGL paint engine, otherwise we issue a warning and return immediately.

Managing State

It is the application developer's responsibility to restore any state changes made when issuing OpenGL commands inside an active painter, like pushing and popping the matrices used. If you change a state and do not restore it, there is a chance that the paint engine's state gets out of sync, causing painting issues when the actual graphics items in the scene are painted.

If you do any painting using QPainter instead of using pure OpenGL commands, you will need to invoke save() and restore() on the painter.

Since the painter passed to drawBackground() is already active we know that we have an active GL context, and we are ready to issue GL commands. We start by clearing the background and performing any setup that is required, such as initializing the matrices.

      glClearColor(m_backgroundColor.redF(),
                     m_backgroundColor.greenF(),
                     m_backgroundColor.blueF(), 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
        if (!m_model)
            return;
 
        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glLoadIdentity();
        gluPerspective(70, width() / height(), 0.01, 1000);
 
        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();
        glLoadIdentity();

Once the matrices have been set up, we position the light and render the model. Afterwards, we restore the matrices and schedule a repaint.

      const float pos[] = { m_lightItem->x() - width() / 2,
                              height() / 2 - m_lightItem->y(),
                              512, 0 };
        glLightfv(GL_LIGHT0, GL_POSITION, pos);
        glColor4f(m_modelColor.redF(), m_modelColor.greenF(),
                  m_modelColor.blueF(), 1.0f);
 
        const int delta = m_time.elapsed() - m_lastTime;
        m_rotation += m_angularMomentum * (delta / 1000.0);
        m_lastTime += delta;
 
        glTranslatef(0, 0, -m_distance);
        glRotatef(m_rotation.x, 1, 0, 0);
        glRotatef(m_rotation.y, 0, 1, 0);
        glRotatef(m_rotation.z, 0, 0, 1);
 
        glEnable(GL_MULTISAMPLE);
        m_model->render(m_wireframeEnabled, m_normalsEnabled);
        glDisable(GL_MULTISAMPLE);
 
        glPopMatrix();
 
        glMatrixMode(GL_PROJECTION);
        glPopMatrix();
 
        QTimer::singleShot(20, this, SLOT(update()));
    }

We will not look further into how the actual 3D model is rendered as it is beyond the scope of this article---more details can be found in the source code available on the Qt Quarterly Web site.

Embedding Widgets in the Scene

In our OpenGLScene constructor, which we won't quote here, we add the widgets and graphics items to control rendering parameters. In order to get a window frame for widgets that are embedded in the graphics scene, we construct them using QDialog, with the CustomizeWindowHint and WindowTitleHint flags set, disabling their close buttons.

The helper function used for this is as follows:

    QDialog *OpenGLScene::createDialog(
             const QString &windowTitle) const
    {
        QDialog *dialog = new QDialog(0,
            Qt::CustomizeWindowHint | Qt::WindowTitleHint);
 
        dialog->setWindowOpacity(0.8);
        dialog->setWindowTitle(windowTitle);
        dialog->setLayout(new <a href="http://www.crossplatform.ru/documentation/qt/4.4.0/qvboxlayout.php">QVBoxLayout</a>);
 
        return dialog;
    }

Setting the window opacity causes the embedded widgets to be slightly transparent, which makes it possible to discern OpenGL scene elements that are partially hidden behind the control widgets. We also use setWindowTitle() to add a title to each embedded widget's window frame.

Subwidgets are added to each embedded widget by creating a layout and adding widgets to it before using QGraphicsScene's addWidget() function to embed the widget in the scene. Here's how the instructions widget is created and added to the scene:

    QWidget *instructions = createDialog(tr("Instructions"));
    instructions->layout()->addWidget(new QLabel(
        tr("Use mouse wheel to zoom model, and click and "
           "drag to rotate model")));
    instructions->layout()->addWidget(new QLabel(
        tr("Move the sun around to change the light "
           "position")));
    ...
    addWidget(instructions);

Once we have embedded all the widgets in the graphics scene, we iterate through the resulting QGraphicsItems and position them beneath each other in the left part of the view. We use the ItemIsMovable flag to allow the user to move the widgets around with the mouse, and we set the cache mode to DeviceCoordinateCache so that widgets are cached in QPixmaps. These pixmaps are then mapped to OpenGL textures, making the widgets very cheap to draw when they are not being updated.

    QPointF pos(10, 10);
    foreach (QGraphicsItem *item, items()) {
        item->setFlag(QGraphicsItem::ItemIsMovable);
        item->setCacheMode(
            QGraphicsItem::DeviceCoordinateCache);
 
        const QRectF rect = item->boundingRect();
        item->setPos(pos.x() - rect.x(), pos.y() - rect.y());
        pos += QPointF(0, 10 + rect.height());
    }

For event handling, we need to reimplement the relevant function in QGraphicsScene. Here is how our mouse move event handling is done:

    void OpenGLScene::mouseMoveEvent(
                      QGraphicsSceneMouseEvent *event)
    {
        QGraphicsScene::mouseMoveEvent(event);
        if (event->isAccepted()) return;
        if (event->buttons() & Qt::LeftButton) {
            const QPointF delta =
                event->scenePos() - event->lastScenePos();
            const Point3d angularImpulse =
                Point3d(delta.y(), delta.x(), 0) * 0.1;
 
            m_rotation += angularImpulse;
            m_accumulatedMomentum += angularImpulse;
 
            event->accept();
            update();
        }
    }

Since we are handling events for widgets in a graphics scene, we use QGraphicsSceneMouseEvent instead of QMouseEvent. We begin by calling the base class implementation to give other items on top of the OpenGL scene the chance to handle events first. If the event has already been handled we return, otherwise we handle the event ourselves and call accept().

Note that calling scenePos() on the QGraphicsSceneMouseEvent will give us the coordinates in the actual view as, earlier, we made sure to set the scene rectangle to match the view rectangle. QGraphicsSceneMouseEvent also provides the convenient lastScenePos() function which simplifies our code a bit. Finally we call update() to redraw the scene and background.

Wrapping Up

With the development of new underlying technologies in Qt, the Graphics View framework is becoming a focal point for new experiments in user interface design and a proving ground for initiatives to improve rendering performance.

The example code outlined in this article is available from the Qt Quarterly Web site, and is part of a Trolltech Labs project:

http://labs.trolltech.com/page/Graphics/About/Dojo

Обсудить на форуме...