Looking at the Code
This demo is getting pretty large, and I don't have time to explain everything I've added to it since the last article. Since the main addition I've made since the last article is impostors, I'll try to explain that piece as well as I can.
Let's start by analyzing the members of the CImpostor class. If you haven't looked at the source code from my previous articles, C3DObject contains position and orientation information about the object itself. It also contains a bounding radius for the object, which I recently added for a number of things like view frustum culling, collision detection, and impostor rendering. For impostors it is used to determine how much screen space the object is taking up, which is then used to determine the resolution of the billboard texture as well as the projection matrix for rendering into that texture. A bounding box or convex hull may easily be used instead of a bounding radius, but for planets a bounding radius gives the best fit.
class CImpostor : public C3DObject
// Static members to hold state information between calls to
// InitImpostorRender() and FinishImpostorRender().
static int m_nViewport;
// These members contain state information regarding the most recent update of
// the billboard texture. They are used to determine how much error is in the
// current billboard texture, and how badly it needs to be updated.
float m_fDistance; // The distance to the camera
CVector m_vOrientation; // The orientation of the camera relative to the impostor's orientation
// These members contain the information used to render the billboard
float m_fBillboardRadius; // The radius of the billboard texture
CVector m_vUp; // The up vector used to generate the texture
CTexture m_tBillboard; // The billboard texture map object
// Other informational members.
short m_nFlags; // A set of bit flags for impostor states
short m_nResolution; // The resolution of the billboard texture
// These are the three main methods.
// To update the billboard, call InitImpostorRender(), render the object, and
// call FinishImpostorRender(). To draw the billboard, call DrawImpostor().
void InitImpostorRender(C3DObject *pCamera);
void DrawImpostor(C3DObject *pCamera);
// Used to determine when the billboard texture needs to be updated
float GetImpostorError(C3DObject *pCamera);
// Helper methods
void GetImpostorViewMatrix(C3DObject *pCamera, CMatrix &mView);
float GetImpostorScreenSpace(float fDistance);
short GetImpostorResolution(float fScreenSpace);
Overall the concept is not too complex, but the math to fit the billboard tightly around the object and to generate the right projection matrix is not trivial. Keep in mind that we need the projection matrix to respect perspective projection or it will not look right, especially when switching between impostor rendering and normal rendering. Here is some pseudo-code with comments for InitImpostorRender(), which should explain the bulk of the math.
void InitImpostorRender(C3DObject *pCamera);
CVector vView = vector from the camera to the center of the object;
m_fDistance = distance between the two (or vView.Magnitude();
m_vOrientation = unit vector pointing from center to camera (relative to object's orientation);
m_vUp = arbitrary up vector (make sure it's not parallel to vView);
CMatrix mModel = model matrix for this object;
CMatrix mView = view matrix for the camera using vView and m_vUp;
if(using bounding box or convex hull)
for(each vertex in bounding volume)
Transform vertex by model/view matrices calculated above;
Divide x and y values by -z (poor man's perspective projection matrix);
Store max and min values for x and y;
// The min and max x and y values define the billboard's rectangle
// For x and y the range -1 to 1 covers a 90 degree field of view
// Use that information to determine screen space and billboard size
// Calculate a projection matrix that tightly encloses the bounding box
else if(using bounding radius)
// See figure 1 below (values d and r are already known)
// h is easy because it is part of a right triangle with d and r
h = sqrt(d*d - r*r); // Pythagorean theorem gives distance to horizon
// a and b are also parts of right triangles, but first we must determine
// the length of the horizon ray up to a and b
cos = h/d; // This is the cosine of angle at camera vertex
ha = (d-r)/cos; // This is the length of the horizon ray to a
hb = d/cos; // This is the length of the horizon ray to b
a = sqrt(ha*ha - (d-r)*(d-r));
b = sqrt(hb*hb - d*d);
glFrustum(-a, a, -a, a, d-r, d+r); // This is fairly straightforward
m_fBillboardRadius = b; // Again, fairly straightforward
// Push impostor projection and model-view matrices onto the stack
// Determine texture resolution and set up viewport
Figure 1 - Impostor view frustum for bounding sphere
Though the bounding sphere requires slightly more complicated geometry, it actually requires less code and is more efficient. Because the view frustum and billboard are perfect squares, they fit nicely into standard square textures. Bounding boxes will have a rectangular view frustum and billboard, and to fit it into a texture efficiently (i.e. without a lot of unused space), the up vector chosen can be much more important. I would not really recommend using bounding boxes because even though they can use the texture more efficiently, they can only help in one of two dimensions, and most textures are square anyway. If you plan to use the rectangular texture extension to try to optimize video memory usage, it may be worth playing around with.
This class is just a simple wrapper class around the WGL_ARB_pbuffer extension. The code is straightforward enough that there's no need to explain it in the article, but some of the concepts behind it need to be explained. First of all, a pbuffer is an off-screen video buffer that you can create with its own rendering context. It's like having an extra back buffer to render to, but it can be any size or pixel format you want. It is not tied to the size and format of the rendering window. When you're finished rendering to this pbuffer, you can copy its contents into a texture object. If your video card supports the WGL_ARB_render_texture extension, you can even bind a texture object directly to the pbuffer (which saves you the overhead of copying it).
Not all video cards support the WGL_ARB_pbuffer extension, and even fewer seem to support the WGL_ARB_render_texture extension. Luckily, it is easy enough to organize your code to use the back buffer when these extensions are not supported. In fact, my demo is written to work this way. Most current video cards should support copying from the back buffer to a texture.
Unfortunately, rendering to the back buffer is usually slower and less convenient than using a pbuffer. This is because you have to call glClear() before you render each impostor. The glClear() call is slower on a large back buffer than on a small pbuffer, and it's more convenient to be able to update an impostor after you've started rendering to the back buffer (which you can't do if you need to clear it for an impostor).
There is one more requirement that might trip you up. Because an impostor texture needs to be partially transparent, your video card must support "destination alpha". This means that it must be able to store an alpha channel in the back buffer (or pbuffer) that you will be rendering into. Without that alpha channel, copying your buffer into a texture with an alpha channel will set all alpha values to 1.0, making your texture completely opaque. When rendering impostor textures, you must also make sure that the alpha component of the clear color is 0.
Although the WGL_ARB_render_texture extension seems like it may offer a decent improvement in performance, it doesn't really work well with impostors. Let's say you create a solar system with 50 planets and moons. This will require up to 50 textures just for the planetary bodies (you may want to use more impostors for clouds, forests, etc.) Since most of these textures will be distant and can be made fairly small, you shouldn't run out of video memory creating that many textures on today's video cards. However, you're not allowed to create that many rendering contexts, which means you can't create that many pbuffers to bind directly to texture objects.
It is possible to store many small textures inside one large one. Many games merge all their smaller textures into one large one to avoid unnecessary context switching in the video card. For impostors, you could do the same in one of two ways. First, you could render impostors into pbuffers of a certain size, then copy the pbuffer to a specific location of a larger texture. Second, you could create a large pbuffer and use glViewport() to render into a small part of the pbuffer. The second idea might allow you to use WGL_ARB_render_texture efficiently. One possible drawback is that glClear() seems to run slower if you're not clearing the full buffer, and it may actually be quicker to copy than to bind for smaller impostor textures. I suppose the only way to find out for sure is to take the time to test it.
We now have a demo capable of rendering an entire solar system fairly efficiently. Planets that aren't close to the camera aren't updated or fully rendered every frame, and inter-planetary distances should not cause any scale or depth problems. Believe it or not, the only depth problem you may have now is when you get too close to a planet to render it as an impostor, but the horizon is still pretty far away. You can alleviate this problem by changing the distance at which you switch to normal rendering, changing the function that scales distance and size, and possibly by dynamically moving the near and far clipping planes based on the horizon distance. The last option will give the best Z-Buffer precision when the camera is close to the ground, which is when you are likely to need it most.
I've also added some classes to organize planets and moons into a solar system, along with code to load the solar system from an INI file. The planets don't move at this point, but it shouldn't be too hard to add orbits and motion. The demo project comes with an INI file defining 4 planets with a single moon orbiting the third planet. For now I divide orbital distances by 10 when loading the planets to make them easier to see from each other.
At this time, I don't have any plans to write another article for a while. If you have any questions, comments, or ideas, feel free to drop me an email (see the Author's Bio link above). If you're working on something similar, or using my code in a project of your own, I'd also be interested in hearing from you.
The Virtual Terrain Project: A one-stop site for all your terrain/world modeling and rendering needs. If you liked my articles, then you need to check this site out.
Delphi3D Impostor Article: A short but informative article on impostors. I couldn't find it, but source code may be available in Delphi using OpenGL.
SkyWorks Cloud Rendering Engine: A real-time volumetric cloud rendering engine using impostors. Check out the publications as well as the source code.
Efficient Impostor Manipulation for Real-Time Visualization of Urban Scenery: A publication explaining the use of impostors to render large urban areas.
Real-Time Tree Rendering: A publication explaining the use of impostors and multi-resolution rendering techniques to render detailed trees.