A Qt Scenegraph

Source code can be found here: http://qt.gitorious.org/qt-labs/scenegraph

A few months ago, I wrote a series of blogs about the graphics stack in Qt and how to make the most of it. In the results I posted there, we could often see that our OpenGL graphics system did not perform as well as one would expect from a hardware accelerated API. When we look at what games can get out of the same hardware it illustrates the fact even more. It doesn't mean that it is impossible to get really smooth apps with our current graphics stack. There are enough really nice looking QML demos to prove that, but we just believe that we should be able to do something even better.

After many attempts at improving the OpenGL performance of the imperative drawing model we have today, we've decided to take a step back and see what can be achieved with a different model; a scene graph. There already exist a few scene graph systems out there and while we have looked at some of them, we have not based ourselves on any,
because the problem they try to solve are different. For instance, it is rare to draw meshes larger than 50 vertices in a UI, while in games that number is so low as to be almost unheard of. Another reason is that we want to try out things and learn what works and what doesn't from the Qt perspective.

Before going into details of our scene graph, here is an example that shows some of what we can do with it:

Photos example, showing our scene graph running on a Nokia N900, shot with another N900. There are about 120 unique image files. It starts out in the Wii-inspired thumb-nail view, then we browse single images with a page turn effect. Finally we zoom in on an image and then return to the main view.

To get optimal performance from hardware rendering, we must minimize the number of state changes; that is, don't change textures, matrices, colours, render targets, shaders, vertex and index buffers etc. unless you really need to. Big chunks of data should also be uploaded to video memory once and stay there for as long as the data are needed, rather
than being sent from system memory each frame. To achieve this, we need to know as much as possible about what is going to be rendered.

Our scene graph is a tree composed of a small set of predefined node types. The most significant is the GeometryNode which is used for all visible content. It contains a mesh and a material. The mesh is rendered to screen using the shaders and states defined by the material. The GeometryNode is in theory capable of representing any graphics primitive we have in QPainter.

In addition, we have TransformNode which will transforms its sub-tree using a full 4x4 matrix, meaning it is 3D capable. Finally, we have the ClipNode, which clips its children to the clip it defines.

The tree is rendered using a Renderer subclass which will traverse the tree and render it. Since the tree is composed of predefined types, the renderer can make full scene optimizations to minimize state changes and make use of the Z buffer to avoid overdrawing, etc. The renderer will typically not render the scene in a depth-first or breadth-first manner, but rather in the order that it decides is the most optimal. As this can vary from hardware to
hardware, the renderer can be reimplemented to do things differently, as required on a per-case base.

The filebrowser example from our source tree. In the example above, I end up scrolling through a directory with 600 files.

In the example above, we are using the default renderer implementation which will reorder the scene based on materials. All the background gradients will be drawn together, then all the text, etc. This minimizes the state changes, resulting in better performance. The default renderer will also use the Z-buffer, and draw all opaque objects front to back followed by drawing all transparent objects back to front. This minimzes overdraw, especially when we have
opaque pixels "on top", and again helps to improve performance.

The graph is written to target OpenGL 2.0 or OpenGL ES 2.0 exclusively. We did this because OpenGL (ES) 2.0 is available pretty much everywhere and it provides the right set of features.

In addition to reordering and Z-buffer based overdraw reduction, we also have the benefit that geometry persists from frame to frame. Text is supported as an array of textured quads that are created once, then just drawn with each call. This compares favourably with the QPainter::drawText() function where text is laid out and converted to textured quads for every call.

Since we are targeting OpenGL (ES) 2.0 we can expose shaders as a core part of the API. This opens up a number of features that are difficult to make fast in our current QPainter stack.

The particles example, illustrating how it is possible to do a particle system that runs primarily on the GPU.

The metaballs example, illustrating how its possible to do custom fragment processing.

As we're only interested in the graphics component, our example code implements animations and input in and ad-hoc manner, as these are not within the our scope.

One might question were QPainter fits with this. The answer is that it doesn't - not directly anyway. There is an example, paint2D, which shows one way of integrating the two. Another way would be to paint directly into the context of the scene graph before or after the scene graph renders.

So if you find this interesting, feel free to check out the code and play around with it. It's rather sparse on documentation for now, but we'll improve on that as time passes.

Enjoy!


Blog Topics:

Comments