Well that took a while. I finally acknowledged the lumbering, drooling simpleton lurking in the corner of my code. He goes by the name of The Renderer.
This entry is a little technical and has no nice pictures. Boo. Well, I am a programmer.
Simple is generally pretty good and will get you a long way. My 'architecture' is almost non-existent: I have entities (a single structure representing most things in the game), particles (a simpler version) and tile chunks (ranging from full level-size tilemaps to 2 or 3 tile moving platforms).
Previously the approach to rendering was crude. Entities were stored in buckets, each representing a different depth. The rendering loop rendered things in a fixed order - background tilemaps, followed by back entities, foreground tiles, foreground entities, particles, etc.
This was ... ok ... but obviously led to much tediousness in other areas of the code as well as being somewhat inflexible.
The rendering itself was reasonably efficient. I would render tilemaps using a single call with a prebuilt VBO mesh representing enough tiles to fill the screen; just updating vertex uv's each frame depending on the section of the tilemap being shown on screen.
All sprites would be batched up and rendered with one or two calls, as would electrical sparks, which needed the blending mode to be changed.
Water shaders require two passes - take the scene rendered so far and render it with a texture that has had the liquid area masks rendered to it using the water shader.
So ... finally I'd had enough of the fixed hard-coded nature of this and knuckled down to clean it up.
The system now is as follows:
* All entities are stored in a single list
* At render time the entity render functions are called
* This populates an array of 'draw calls' - each represents either a tile map, a sprite, an electrical spark or a liquid mask (the last three are all variations of a sprite) and has an associated depth value
* The draw call array is sorted using a simple implementation of a radix sort - the sort key is formed from the depth and the call type / material
* The draw calls are iterated over to assemble sprite batches which are flushed to OpenGL calls as material / types change, or alternatively to render complete tile maps
It's simpler than it sounds, really
Things like rendering liquid layers and other effect compositions are much easier now. If an entity wants to render a sprite that contains liquid (like the organ jars or blood vials) it simply adds a draw call for the sprite and a draw call for the sprite's liquid mask (just another sprite) at a slightly lower depth than the main sprite.
The liquid sprites for a given depth all get collected up and rendered into a frame-buffer in one go, then composed with everything rendered so far using a liquid shader.
I also use this same approach to render the normal water areas - previously I was using a nasty hardcoded function that replicated lots of other functionality.
Anyway, the upshot is that it's all nicely flexible now and means that I can mix and match materials and depths etc. Should mean that I can add more composition effects much more simply than before.
Oh, enough of all this pseudo-technical rambling. I'll stick some screenshots up next time.