The new private member functions and variables relate exclusively to the 2D graphics and animation. The animate() slot is called periodically by the animationTimer to update the widget; the createBubbles() function initializes the bubbles list with instances of a helper class used to draw the animation; the drawInstructions() function is responsible for a semi-transparent message that is also overpainted onto the OpenGL scene.
GLWidget Class Implementation
Again, we only show the parts of the GLWidget implementation that are relevant to this example. In the constructor, we initialize a QTimer to control the animation:
GLWidget::GLWidget(QWidget *parent)
: QGLWidget(QGLFormat(QGL::SampleBuffers), parent)
{
QTime midnight(0, 0, 0);
qsrand(midnight.secsTo(QTime::currentTime()));
logo = 0;
xRot = 0;
yRot = 0;
zRot = 0;
qtGreen = QColor::fromCmykF(0.40, 0.0, 1.0, 0.0);
qtPurple = QColor::fromCmykF(0.39, 0.39, 0.0, 0.0);
animationTimer.setSingleShot(false);
connect(&animationTimer, SIGNAL(timeout()), this, SLOT(animate()));
animationTimer.start(25);
setAutoFillBackground(false);
setMinimumSize(200, 200);
setWindowTitle(tr("Overpainting a Scene"));
}
We turn off the widget's autoFillBackground property to instruct OpenGL not to paint a background for the widget when QPainter::begin() is called.
As in the Hello GL example, the destructor is responsible for freeing any OpenGL-related resources:
GLWidget::~GLWidget()
{
}
The initializeGL() function is fairly minimal, only setting up the QtLogo object used in the scene. See the Hello GL example for details of the QtLogo class.
void GLWidget::initializeGL()
{
glEnable(GL_MULTISAMPLE);
logo = new QtLogo(this);
logo->setColor(qtGreen.dark());
}
To cooperate fully with QPainter, we defer matrix stack operations and attribute initialization until the widget needs to be updated.
In this example, we implement paintEvent() rather than paintGL() to render our scene. When drawing on a QGLWidget, the paint engine used by QPainter performs certain operations that change the states of the OpenGL implementation's matrix and property stacks. Therefore, it is necessary to make all the OpenGL calls to display the 3D graphics before we construct a QPainter to draw the 2D overlay.
We render a 3D scene by setting up model and projection transformations and other attributes. We use an OpenGL stack operation to preserve the original matrix state, allowing us to recover it later:
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
We define a color to use for the widget's background, and set up various attributes that define how the scene will be rendered.
qglClearColor(qtPurple.dark());
glShadeModel(GL_SMOOTH);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_MULTISAMPLE);
static GLfloat lightPosition[4] = { 0.5, 5.0, 7.0, 1.0 };
glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);
setupViewport(width(), height());
We call the setupViewport() private function to set up the projection used for the scene. This is unnecessary in OpenGL examples that implement the paintGL() function because the matrix stacks are usually unmodified between calls to resizeGL() and paintGL().
Since the widget's background is not drawn by the system or by Qt, we use an OpenGL call to paint it before positioning the object defined earlier in the scene:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glTranslatef(0.0, 0.0, -10.0);
glRotatef(xRot / 16.0, 1.0, 0.0, 0.0);
glRotatef(yRot / 16.0, 0.0, 1.0, 0.0);
glRotatef(zRot / 16.0, 0.0, 0.0, 1.0);
logo->draw();
Once the QtLogo object's draw method has been executed, the GL states we changed and the matrix stack needs to be restored to its original state at the start of this function before we can begin overpainting:
glShadeModel(GL_FLAT);
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
glDisable(GL_LIGHTING);
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
With the 3D graphics done, we construct a QPainter for use on the widget and simply overpaint the widget with 2D graphics; in this case, using a helper class to draw a number of translucent bubbles onto the widget, and calling drawInstructions() to overlay some instructions:
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
foreach (Bubble *bubble, bubbles) {
if (bubble->rect().intersects(event->rect()))
bubble->drawBubble(&painter);
}
drawInstructions(&painter);
painter.end();
}
When QPainter::end() is called, suitable OpenGL-specific calls are made to write the scene, and its additional contents, onto the widget.
With paintGL() the swapBuffers() call is done for us. But an explicit call to swapBuffers() is still not required because in the paintEvent() method the QPainter on the OpenGL widget takes care of this for us.
The implementation of the resizeGL() function sets up the dimensions of the viewport and defines a projection transformation:
void GLWidget::resizeGL(int width, int height)
{
setupViewport(width, height);
}
Ideally, we want to arrange the 2D graphics to suit the widget's dimensions. To achieve this, we implement the showEvent() handler, creating new graphic elements (bubbles) if necessary at appropriate positions in the widget.
void GLWidget::showEvent(QShowEvent *event)
{
Q_UNUSED(event);
createBubbles(20 - bubbles.count());
}
This function only has an effect if less than 20 bubbles have already been created.
The animate() slot is called every time the widget's animationTimer emits the timeout() signal. This keeps the bubbles moving around.
void GLWidget::animate()
{
QMutableListIterator<Bubble*> iter(bubbles);
while (iter.hasNext()) {
Bubble *bubble = iter.next();
bubble->move(rect());
}
update();
}
We simply iterate over the bubbles in the bubbles list, updating the widget before and after each of them is moved.
The setupViewport() function is called from paintEvent() and resizeGL().
void GLWidget::setupViewport(int width, int height)
{
int side = qMin(width, height);
glViewport((width - side) / 2, (height - side) / 2, side, side);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
#ifdef QT_OPENGL_ES
glOrthof(-0.5, +0.5, -0.5, 0.5, 4.0, 15.0);
#else
glOrtho(-0.5, +0.5, -0.5, 0.5, 4.0, 15.0);
#endif
glMatrixMode(GL_MODELVIEW);
}
The drawInstructions() function is used to prepare some basic instructions that will be painted with the other 2D graphics over the 3D scene.
void GLWidget::drawInstructions(QPainter *painter)
{
QString text = tr("Click and drag with the left mouse button "
"to rotate the Qt logo.");
QFontMetrics metrics = QFontMetrics(font());
int border = qMax(4, metrics.leading());
QRect rect = metrics.boundingRect(0, 0, width() - 2*border, int(height()*0.125),
Qt::AlignCenter | Qt::TextWordWrap, text);
painter->setRenderHint(QPainter::TextAntialiasing);
painter->fillRect(QRect(0, 0, width(), rect.height() + 2*border),
QColor(0, 0, 0, 127));
painter->setPen(Qt::white);
painter->fillRect(QRect(0, 0, width(), rect.height() + 2*border),
QColor(0, 0, 0, 127));
painter->drawText((width() - rect.width())/2, border,
rect.width(), rect.height(),
Qt::AlignCenter | Qt::TextWordWrap, text);
}
Summary
When overpainting 2D content onto 3D content, we need to use a QPainter and make OpenGL calls to achieve the desired effect. Since QPainter itself uses OpenGL calls when used on a QGLWidget subclass, we need to preserve the state of various OpenGL stacks when we perform our own calls, using the following approach:
- Reimplement QGLWidget::initializeGL(), but only perform minimal initialization. QPainter will perform its own initialization routines, modifying the matrix and property stacks, so it is better to defer certain initialization tasks until just before you render the 3D scene.
- Reimplement QGLWidget::resizeGL() as in the pure 3D case.
- Reimplement QWidget::paintEvent() to draw both 2D and 3D graphics.
The paintEvent() implementation performs the following tasks:
- Push the current OpenGL modelview matrix onto a stack.
- Perform initialization tasks usually done in the initializeGL() function.
- Perform code that would normally be located in the widget's resizeGL() function to set the correct perspective transformation and set up the viewport.
- Render the scene using OpenGL calls.
- Pop the OpenGL modelview matrix off the stack.
- Construct a QPainter object.
- Initialize it for use on the widget with the QPainter::begin() function.
- Draw primitives using QPainter's member functions.
- Call QPainter::end() to finish painting.