Around about Christmas of 2014, I was feeling a little restless. Working on the book in my spare time had meant that I hadn’t done any real coding, bar physics demos for about a year. I was feeling a bit burned out writing the book and needed a small distraction, something of a programming challenge to help me relax.
I came up with the idea of writing a quick and dirty 3d engine in straight C that implemented deferred lighting. It would be something that I could use to quickly test out lots of little idea here and here, maybe even prototype a game idea or two. One of the problems is that, although I have written various bit and bobs to do with rendering in various games, I had never really had a “proper go” at an entire rendering engine, especially once using modern multi-pass rendering techniques.
I chose deferred lighting over deferred shading as I wanted to easily support a varying number of material types, as well as something that would scale down well to hardware where frame buffer/texture bandwidth may be limited (for example; my Mac Book Pro with it’s Intel GPU or mobile platforms).
The deferred lighting implementation consists of three main passes:-
- Pre-light Pass – Geometry is rendered, outputting only the depth and surface normal information.
- Light-pass – The depth and normal information from the pre-light pass is used to calculate lighting information. This pass consists of two separate stages; the ambient stage which renders a full-screen quad for a global ambient light-source and the actual finite light sources, which are rendered as 3d-geometry. The resulting albedo and specular values are rendered into the light accumulation buffer. Additive alpha blending is used so that pixels affected by more than one light source are lit accordingly.
- Material pass – The geometry is rendered again, with the added difference in that the depth-buffer is not written to. The depth test is also different from the Pre-Light pass. the “Depth Equals” test is used so that it passes only on pixels that match the values stored in the depth buffer. Each pixel that passes the depth test has its material evaluated to determine the diffuse and specular components which are combined with the light accumulation buffer to determine how bight the pixel is.
And so started my “Muse3d” side-project. Progress was really quick and a lot of fun, too! I had forgotten how fun working in good ol’ C was. How much closer “to the metal” it felt. But as much fun as it was, working in C was also a limiting factor. Writing abstracted interfaces in straight C was becoming a pain, and the lack of templates and inheritance complicated matters somewhat. After a few days I decided to move the whole thing to C++ so I could use language features such as templates and encapsulation and inheritance.
After a week or so, I had something that was rendering models using deferred lighting. It was far from complete, but it had a lot of “under the hood features” such as :-
- Custom memory allocation – I wrote a nice little memory manager that pre-allocated chunks of memory from the underlying OS and used TLSF to manage the heap within each chunk.I did this for no real reason, other than to help me track memory usage at a later stage.
- Shader caching – The engine uses one big “uber-shader” for all of the rendering. The shaders are compiled at run-time as needed for each model/material/pass combination. However, these shader variations are only compiled once and cached for re-use.
- Render batching – To avoid shader and material switches happening too often, draw calls are batched by relevance, in the following order
- Auto insertion into relevant render passes – Not such a big deal, but how models are rendered by each pass is a completely opaque process. I guess this is a standard thing to do for any modern multi-pass renderer, but it was amazing to see how one call to render a model instance would then feed into all of the passes and batching.
I left the project alone for a while as I concentrated on my book again. I’m still working furiously on the book, trying to get the dammed thing finished but now and again, when I’ve hit a block, working on Muse3d helped relax me a little.
Once of the issues with the engine was that in order to save time, I hadn’t abstracted the graphics code, the bit that actually calls platform specific functions to tell the GPU to do things. I had just written some basic wrappers for things like buffers and textures, and the rest was raw OpenGL calls. However, this was going to present me with a few issues in the future and I decided to fix that. It took a couple of weeks of working on the odd bit here-and-there to achieve this.
I cursed my lack of foresight during this process. But at the same time, having a working 3d-engine (even if it wasn’t feature complete) meant that I could do the work in small steps, and verify that I hadn’t screwed anything up as I went a long. Now the render code and graphics code are completely separate. It was a horrible task to have to do, but well worth the effort.
There’s still a bit of work to be done though. Although I have a working light pass, it doesn’t deal with some things properly such as light sources occluded by objects (the occluding objects appear lit when they shouldn’t be). My “to do” list looks a little like this:-
- Implement stenciling for light sources during the light pass so that only objects within light volumes are lit (currently 60% done)
- Implement light volumes for:-
- Directional lights
- Spot lights
- Area lights
- Custom directional & area lights
- Implement LOD models for spherical lights
- Calculate positions of pixels in camera-space, rather than using a texture to hold them. This means that the entire render system will only ever use a single colour render target.
- Implement a shadow pass using stencil shadows
- Add support for custom post-processing passes
- Skinned mesh support using CPU and GPU skinning via Transform Feedback/Streamout
- Implement support for uniform/constant blocks
- Skeletal animation system
It’s a big list, but nothing that I can’t deal with given a bit of time. Right now my focus is on the book and once that’s done, assuming I’m not working on another book straight away, I can devote some time to the additional tasks. As it is, I’ll fit in work on certain features whenever I need an hour or two respite from writing pages and pages of text.