In the previous article I wrote that using modern OpenGL (i.e. version 3.0 and above) is possible, although the core profile cannot be used yet. I also mentioned this article which briefly describes how to use the core profile, although in fact this example will also work in the default compatibility mode. In this mode we can use both the fixed pipeline and shaders, but I will focus on the "modern" approach.
Qt has a handy class called
QGLShaderProgram which wraps the OpenGL API related to shaders. A big advantage of this class is that it supports all classes related to 3D graphics provided by Qt, such as
QMatrix4x4, as well as basic types like
QColor. This way we don't have to worry about converting those types to OpenGL types. Internally this class is little more than a
GLuint storing the handle of the shader program and most its methods are simple wrappers around functions like
glUniform3fv so it's very lightweight.
Note, however, that shaders work in quite a different way depending on the version of the GLSL specification. By default version 1.20 is assumed, so your shaders can access all information known from the fixed pipeline - vertex position, normal, texture coordinates, transformation matrices, lighting parameters, etc. Things change dramatically when you put the following declaration at the beginning of the shader:
Any attempt to access these built-in uniforms and attributes will result in an error. It means that you have to pass all information using explicitly declared uniforms and attributes. For example, to define the world-to-camera transformation matrix, you could use the following code:
view.translate( 0.0, 0.0, -CameraDistance );
view.rotate( m_angle, 1.0, 0.0, 0.0 );
view.rotate( m_rotation, 0.0, 0.0, 1.0 );
m_program.setUniformValue( "ViewMatrix", view );
This is not only much more elegant than a series of calls to
glRotate etc., but also faster and more flexible. The vector and matrix classes provided by Qt are really handy; the authors of this class even thought about the
normalMatrix method that calculates the transposed inverse (or was it inversed transpose?) for transforming normal vectors.
Similarly, uniforms can be used to pass lighting parameters, materials, blending information and many more things which are not possible to achieve using the fixed pipeline. When it comes to attributes, the
QGLShaderProgram offers a bunch of functions for passing single values to attributes (which are not very useful in most cases) and for passing arrays of various types. However this is not recommended, because OpenGL knows nothing about the contents of these arrays and it cannot assume that they don't change between executions of the shader or between successive frames.
A much better approach is to use the
setAttributeBuffer method in connection with the
QGLBuffer class. Internally this method is a wrapper for
glVertexAttribPointer just like the attribute array methods, but it makes the code much more readable as it explicitly states that vertex buffers are used. In addition there's no need to cast the offset to a pointer because Qt will do that for us.
QGLBuffer class is also a very thin wrapper around a
GLuint representing the vertex buffer object (or index buffer or pixel buffer object). Unlike
QGLShaderProgram it's a value type (it doesn't make sense to copy a program anyway), so we can share buffers without having to worry about tracking and releasing them when they are no longer needed.
In order to use the
QGLBuffer, we need to create it and fill it with data; then we can bind it with the attributes of the shader program. By using appropriate offset and stride, we can easily bind multiple attributes to a single buffer; usually all attributes of a single vertex would be stored together, followed by the remaining vertices. Don't forget about calling
enableAttributeArray for each attribute. We can also use another instance of
QGLBuffer to store the indexes.
When everything is set up like this, the rendering is a matter of binding the program and both buffers to the context and calling
glDrawElements. In more complex scenarios we can use multiple vertex array objects to store the bindings between vertex buffers and attributes. But since we're not using the core profile, OpenGL will create an implicit vertex array object for us.
We can also use uniform buffer objects to simplify passing lots of uniforms to multiple programs. Although Qt doesn't support them at the moment, there is a simple hack which allows us to abuse
QGLBuffer. If you look at the declaration of this class you will notice that the values of the enumeration defining the type of a buffer are the same as the corresponding target constants in OpenGL. So we could simply pass
GL_UNIFORM_BUFFER as the type of the buffer - I haven't tested it yet, but it should work.
Some time ago I stumbled upon a great e-book on OpenGL programming: Learning Modern 3D Graphics Programming. The best thing about it is that it teaches the modern approach to graphics programming, based on OpenGL 3.3 with programmable shaders, and not the "fixed pipeline" known from OpenGL 1.x which is now considered obsolete. I already know a lot about vectors, matrices and all the basics, and I have some general idea about how shaders work, but this book describes everything in a very organized fashion and it allows me to broaden my knowledge.
When I first learned OpenGL over 10 years ago, it was all about a bunch of glBegin/glVertex/glEnd calls and that's how Grape3D, my first 3D graphics program, actually worked. Fraqtive, which also has a 3D mode, used the incredibly advanced technique of glVertexPointer and glDrawElements, which dates back to OpenGL 1.1.
A lot has changed since then. OpenGL 2.0 introduced shaders, but they were still closely tied to the fixed pipeline state objects, such as materials and lights. The idea was that shaders could be used when supported to improve graphical effects, for example by using per-pixel Phong lighting instead of Gouraud lighting provided by the fixed pipeline. Since many graphics cards didn't support shaders at that time, OpenGL would gracefully fall back to the fixed pipeline functionality, and everything would still be rendered correctly.
Nowadays all decent graphics cards support shaders, so in OpenGL 3.x the entire fixed pipeline became obsolete and using shaders is the only "right" way to go. There is even a special mode called the "Core profile" which enforces this by disabling all the old style API. This means that without a proper graphics chipset the program will simply no longer work. I don't consider this a big issue. All modern games require a chipset compatible with DirectX 10, so why should a program dedicated to rendering 3D graphics be any different? Functionally OpenGL 3.3 is more or less the equivalent of DirectX 10, so it seems like a reasonable choice.
I was happy to learn that Qt supports the Core profile, only to discover that it's not actually working because of an unresolved bug. Besides, the article mentions that "some drivers may incur a small performance penalty when using the Core profile". This sounds like a major WTF to me, because the whole idea of the Core profile was to simplify and optimize things, right? Anyway I decided to use OpenGL 3.3 without enforcing the Core profile for now, but to try to implement everything as if I was using that profile.
Another problem that I faced is that my laptop is three years old, and even though its graphics chipset is pretty good for that time (NVIDIA Quadro NVS 140M), I discovered that the OpenGL version was only 2.1. I couldn't find any newer drivers from Lenovo, so I installed the latest generic drivers from NVIDIA and now I have OpenGL 3.3. Yay! So I modified my Descend prototype to use shaders 3.30 and
QGLBuffer objects (which are wrappers for Vertex Buffer Objects and Index Buffer Objects), but I will write more about it in the next post.
When I created this website over six years ago, it was simply a place where I could publish my development projects and components. Over time I started adding photo galleries and posting some personal notes once in a while, but I thought that the whole blogging business was simply for people with too much free time. But things have changed since then. Ex-bloggers are using Facebook nowadays, and modern technical blogs are one of the most important sources of specialist knowledge for us programmers. So it's not a matter of having fans, regular readers, etc. It's rather a matter of feeding Google's spiders with information that someone, someday may find useful.
Writing has always been the most natural form of communication for me, especially about technical things. In the past I've been constantly publishing various open source components (I will return to this topic in a while), but this requires a lot of time. I found it easier to write short technical articles and I can't deny that they actually started forming a blog. So today I tagged all posts and placed a nice "tag cloud" in the sidebar to make the whole thing look a bit more like Web 2.0 (or is it 3.0 already? I'm always lagging behind ;>). And now that I'm slowly starting to work on Descend, you can expect more about 3D graphics and compiler programming in the nearest future. I think it's worth doing it even if it's just for the purpose of archiving and helping my thinking process by writing things down.
Another change that I finally made was adding previous/next links to images in the gallery. I should've done this a long time ago, and it was simply a matter of copying some code from forum module to the image module. All right, I should've upgraded this whole website a long time ago, but I made so many customizations, that manually patching the code here and there became easier than migrating the whole thing.
Talking about legacy code, now I return to the topic of open source components. Those for MFC haven't been updated for years and I'm no longer able to maintain them even if I wanted to. Besides, who uses MFC today? Obviously those who have to maintain legacy code, but no sane person would start a new project using it. So as part of the cleaning process, I moved all those components to a single place. The documentation is still available, obviously, but moved out of this website. I've been also thinking about deleting all the related forums, but I decided to leave them for now for archival purposes. And by the way, even more out of date versions of these articles are still available on CodeGuru.com, where I initially published them.
Finally, I renamed the "articles" section to "components", because that's what they actually are. In a sense my blog posts are more like articles. Anyway... I also have to publish new versions of the Qt
articles components, because the code is finished for a long time and I just have to update the documentation and demo projects.
All components listed below were written by me between 2002 and 2005 when I used the MFC framework for developing various applications. I published them first on the CodeGuru.com portal, then on my personal website. At that time they were very popular.
Today I no longer maintain or support them and I can't even guarantee that they will work with latest versions of MFC. However, they are still moderately popular and can still be useful to someone, so I'm keeping a list of those components for reference. Also an archive of comments is still available.
Each of the components includes a simple demo application and a brief documentation (also included in demo packages). They can be freely used for any purposes, including commercial use.
Simple tree view with columns and horizontal scrolling
Creating multi-threaded SDI applications with multiple windows
IE-style menu bar and toolbar with 32-bit images
A flexible properties control with modern look
User interface with many views, tabs and splitters
The Descend project is now officially reactivated and yesterday I committed the current version of the prototype into SVN repository. The UI is very basic, but it does its job of drawing a 3D surface based on mathematical equations. So far it consist of three important parts:
A parametric surface is described by a function V(p, q), where V is the vector describing the location of a point in 3D space and p and q are parameters (usually in [0, 1] or some other range). Obviously the surface consists of an infinite number of points, so we must calculate a number of samples and join the resulting points into triangles. This process is called tessellation. If the triangles are small enough, the Phong shading will create an illusion that the surface is smooth and curved.
The only difficulty is to determine what does "small enough" mean. Some surfaces are flat or almost flat and need just a few triangles to look good. Other surfaces are very curved and require thousands of triangles. In practice most surfaces are flatter is some areas and more curved in other areas. Take a sphere for example. It's curvature is the same everywhere, but we must remember that our samples are not distributed uniformly on its surface. Imagine a globe: meridians are located much closer to each other near the poles than near the equator. So in practice the distance between two samples located near the equator is greater and the surface needs to be divided into more triangles. This way, the size of all triangles will be more or less the same. Without adaptive tessellation, triangles would be closely packed near the pole and very large near the equator.
The tessellation algorithm works by first calculating four points at the corners of the surface. Wait, where does a sphere have corners? Just unwrap it mentally into a rectangular map, transforming it from (x, y, z) space into the (p, q) space. This gives us a square divided diagonally into two triangles. Then we calculate a point in the middle of the diagonal and divide each triangle into two smaller triangles. This process can be repeated recursively until the desired quality is reached.
How to measure the quality? The simplest method is to calculate the distance between the "new" point and the line that we are attempting to divide. The greater the distance, relatively to the length of the line, the more curved the surface. If this distance is smaller than some threshold value, we simply assume that the point lays on the line and discard it. The smaller the threshold, the more accurate the tessellation and the more triangles we get.
Unfortunately there are situations when this gives wrong results. If the curvature of the surface between two points resembles a sinusoid, then the third point in between appears to be located very near the line drawn between those two points. The tessellation algorithm will assume that the surface is not curved in this area. This produces very ugly artifacts.
So I came up with a method which produces much more accurate results. In order to render the surface with lighting, we need to calculate normal vectors at every point. For the Phong shading to look nice, those normals must be calculated very accurately. So two more points are calculated at a very small distance from the original one and the resulting triangle is used to calculate the normal. Note that the angle between normals is a very accurate measure of the curvature. An algorithm which compares the angle between the normals of two endpoints and the normal of the "new" point with a threshold angle can handle situations like the above much better. It's also more computationally expensive, because we must calculate three samples before we can decide if the point is rejected or not.
Of course this method can also be fooled in some specific cases, but in combination with the first one it works accurately in most cases. Experimentation shows that the threshold angle of 5° gives excellent results for every reasonable surface I was able to come up with.
In practice we also have to introduce the minimum and maximum number of divisions. Up to a certain point we simply keep dividing the grid into smaller triangles without even measuring the curvature, because otherwise the results would be very inaccurate. And since the curvature may be infinite in some points, we also must have some upper limit.
Final notes: Adaptive tessellation of parametric surfaces is the subject of many PhD dissertations and my algorithm is very simplistic, but it's just fast and accurate enough for the purposes of Descend. Also it should not be confused with adaptive tessellation of displacement mapping, which is a different concept.