Accelerate your Widgets with OpenGL

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск

__NOTOC__


Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Документация


Содержание

Accelerate your Widgets with OpenGL

by Samuel Rødal

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

In some applications, such as those used in Computer Aided Design (CAD), or inapplications that use OpenGL rendering to improve performance, being able torender widgets inside a QGLWidget is a desirable feature. For example,using semi-transparent widgets on top of an OpenGL-rendered scene can betterutilize screen real estate. Also, widgets can be used as tooltips or labels toannotate 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 overridingits paintGL() function.

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

center

Turbo Charging Graphics View

In our example, the actual OpenGL scene rendering and widget controls will behandled in a QGraphicsScene subclass, but first we need to correctly setup 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 itselfis 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 QGraphicsViewparameters that are needed in our case. First of all the viewport needs to bea QGLWidget in order to do OpenGL rendering in our graphics scene. We usethe SampleBuffers format specifier to enable multisample anti-aliasing inour 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 toFullViewportUpdate 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'sdrawBackground() function. By rendering the OpenGL scene indrawBackground(), all widgets and other graphics items are drawn on top.It is possible to reimplement drawForeground() to do OpenGL renderingon top of the graphics scene, if that is required.

Unlike when subclassing QGLWidget, it is not sufficient to set up commonstate in initializeGL() or resizeGL(). A painter is already active on the GL context when drawBackground() is called, and QPainterchanges the GL state when begin() is called. For example, the model viewand projection matrices will be changed so that we have a coordinate systemthat 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 weissue a warning and return immediately.

Since the painter passed to drawBackground() is already active we knowthat we have an active GL context, and we are ready to issue GL commands. Westart 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 itis beyond the scope of this article---more details can be found in the sourcecode available on the Qt Quarterly Web site.

Embedding Widgets in the Scene

In our OpenGLScene constructor, which we won't quote here, we add thewidgets and graphics items to control rendering parameters. In order to geta window frame for widgets that are embedded in the graphics scene, weconstruct them using QDialog, with the CustomizeWindowHintand 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 QVBoxLayout);
 
        return dialog;
    }

Setting the window opacity causes the embedded widgets to be slightlytransparent, which makes it possible to discern OpenGL scene elements that arepartially hidden behind the control widgets. We also usesetWindowTitle() to add a title to each embedded widget's window frame.

Subwidgets are added to each embedded widget by creating a layout and addingwidgets to it before using QGraphicsScene's addWidget() function toembed the widget in the scene. Here's how the instructions widget is createdand 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 throughthe resulting QGraphicsItems and position them beneath each other in theleft part of the view. We use the ItemIsMovable flag to allow the user tomove the widgets around with the mouse, and we set the cache mode toDeviceCoordinateCache so that widgets are cached in QPixmaps. Thesepixmaps are then mapped to OpenGL textures, making the widgets very cheap todraw 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 callingthe base class implementation to give other items on top of the OpenGL scenethe chance to handle events first. If the event has already been handled wereturn, otherwise we handle the event ourselves and call accept().

Note that calling scenePos() on the QGraphicsSceneMouseEventwill give us the coordinates in the actual view as, earlier, we made sure to set thescene rectangle to match the view rectangle. QGraphicsSceneMouseEventalso provides the convenient lastScenePos() function which simplifies ourcode 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 Viewframework is becoming a focal point for new experiments in user interfacedesign and a proving ground for initiatives to improve rendering performance.

The example code outlined in this article is part of a Qt Labs project:

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

A snapshot of the code from this project is available from the Qt Quarterly Web site.

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