Today I spent the whole day doing just one thing: a major restructuring of VectorStorm’s rendering architecture. This is, without a doubt, the biggest change to how rendering works in VectorStorm since I first wrote it.
What you’re looking at in this screenshot is a not-yet-activated starting point, being rendered as a glowing outline, behind a tree. Anyone who’s played with the MS1 build of Tycoon2 will know that one of the big rendering problems that it had was that glowing objects would often glow through opaque objects that were in front of them. As you can see, this is no longer the case. VectorStorm now automatically ensures that objects are drawn in the correct order, so you won’t incorrectly see obscured glowing effects.
To do this, I had to (warning: the rest of this post contains dangerously boring technical details) change how VectorStorm renders.
Originally, VectorStorm was designed for rendering simple 2D vector graphic games. As such, it was designed with a system of “layers”. Your game could configure how many layers it wanted to use, and register sprites or other objects onto any layer you liked. Within each layer, objects were drawn in order, with later objects (or later layers) drawn on top of previous layers. With vector graphics, since you only had additive lines and no solid objects, the whole issue of which line was on top really didn’t matter; the layers were really used just for putting objects under different cameras. For example, in The Muncher’s Labyrinth, the game world was in one layer, and the HUD was in a different layer, just so the two could be rendered using different cameras and I didn’t need to worry about moving the HUD around to match the camera. When I added solid rendering for ThunderStorm, the layers worked really well to ensure that some objects would always draw in front of other objects.
Each renderable object would have a “Draw()” function called on it, being given a pointer to a display list to which it could append its own rendering commands. This was (in my own opinion) simple and elegant, and it was great for a while.
But now that VectorStorm is rendering in 3D, layers don’t really make sense any more. We still have them, but in 3D we have a z buffer which automatically lets the closest object be drawn, regardless of whether it was drawn first or last, so it’s usually no longer important to manage which objects are drawn first or last, except for in special circumstances (glow effects, transparency, etc).
But the real problem is that we now often have objects which have different drawing styles within a single object. For example, the cursor in MT2 (and MT1, for that matter) is made up of two parts; a dark background, and a glowing outline. Under the “just add your commands to this display list” approach, each object has to render all of its geometry at once. This meant that I couldn’t (for example) draw the background portion of the cursor early, and the glowing part later on; they’d both always render at the same time.
Another possible issue is that of rendering partially transparent objects. These see-through objects are always a problem with modern 3D games, since z-buffers can’t be used to render them (z-buffers assume that everything is opaque, and so won’t allow you to draw anything behind a pane of glass, for example, if you draw the glass first). The traditional way to handle these translucent objects is to sort them from back to front, then draw them in that order, after you’ve drawn all the non-translucent objects. But with the simple-and-elegant way that I was handling drawing of objects, there was no way to sort the objects into any different order than they were already in.
Anyhow, enough of the problem statement. :)
What I’ve changed is that renderable objects in VectorStorm are no longer given a pointer to the game’s vsDisplayList when it’s time for them to render. Instead, they’re now given a vsRenderQueue object. On that vsRenderQueue, they can provide their rendering instructions in one of several ways, and the vsRenderQueue can then reorder the drawing as required, to make sure that (for example) the glowing and transparent parts of objects are drawn last, while their opaque portions can still be drawn earlier.
The other change is that vsLayer is now completely gone. Instead, we now have vsScenes, which work very much like vsLayers did; they’re just collections of renderable objects. Like before, your game can have any number of vsScenes, and they’re rendered in order. Right now, they share a single z-buffer and render target between them all, but setting it up so that the different scenes can have separate renders is certainly possible in the future, if I ever need that feature.
You can probably tell by how I’m gushing, but I’m absolutely thrilled and relieved to have this all working at last. Lots of stuff in MT2 (particularly the very old code) is currently working via a “compatibility” mode that I’ve put in the vsRenderQueue, and I do still need to convert those bits of code over to using the new systems instead. But this change has illuminated a whole bunch of unreliable rendering code that was lurking in the GUI systems until now, and the stricter system should keep me from writing terrible code again in the future.
Right now, these changes are only in my MT2 development branch — they haven’t been propagated back into trunk, largely because I’d probably have to make a lot of big adjustments to the testbed apps to make them render under the new systems, and I’m feeling like taking a bit of a rest, after this ten-hour coding session. But I’ll update trunk sometime in the foreseeable future, and will post about it when I do.