I’ve been making slow but remarkably steady progress on this. There hasn’t been much to write about, as most of the code has been under-the-hood stuff, which isn’t really that interesting. So’ I’ll just do a brief summary of what amounts to months of work (though at a quite leisurely pace).
Basically, I’ve been working on the “asset conditioning pipeline” – which is to say, the scripts, tools, utilities and general technology which gets my props, characters and bits of scenery all the way from the 3D program I use and into the game, where they can be rendered to the screen. I really wanted to have an automated process for this, so that I could just set up an object or character the way I want it in Poser (the 3D program I use), and then just save that file, and have my tools pipeline do the rest. And by “the rest”, I mean quite a few things. Here’s a brief summary of the steps that happens after I’ve saved a new Poser file somewhere in the asset directory for my game.
- When I’m done using my machine for the day, I launch the asset conditioning program.
- This scans through the asset directory, and detects any poser file which is new or have been changed.
- If there are new/changed files, the conditioning tool generates a poser script file, which, when called, will load and render the files.
- The conditioner starts Poser, and automatically starts the script (by sending key presses! – it was the only way I could get it working)
- The script loads up each poser file in order, and renders each object from eight different directions – but also, for each direction, it renders a bunch of different images – I’ll explain way later.
- When everything’s been rendered (which could take hours, if there’s a lot of new assets) the conditioning pipeline goes through each rendered image, and processes it into a format that can be loaded by the game. This is a bit tricky, as I want the data to both be as small as possible, and as fast as possible to draw.
- Each image is divided up into 16 by 16 pixel tiles. This leaves us with quite a few empty tiles, and these are just discarded.
- Next, each 16×16 tile is compressed using a custom RLE compression.
- All the images for one object is put into the same file – and what’s more, if I have several animations for an object (as with most characters), all images of all animations are put into the same file. This way, I end up with neatly packaged object files that contains everything there is for that object.
- Finally, I look through all the tiles of an object, looking for duplicate tiles. I then get rid of the duplicates and points them to the one instance of that tile. It is amazing how much redundancy there is, especially when having lots of animations, or objects with flat colour areas.
So what are all those different images I render for each image? Well, here they are, for a simple wooden chest:
From top to bottom, they are:
- Albedo – this is basically the diffuse, unlit texture of the model. Its “colour” if you will.
- Depth – used to get pixel perfect depth sorting in the game
- Normals – the surface normal of each pixel (including displacement map details) for dynamic lighting
- Occlusion – masks off the pixels that should receive no ambient lights. Gives objects more definition
- Specular colour – colour to apply to the highlights of the object
- Specular power – how focused/spread out the highlights should be for each pixel. In this example, it looks similar to specular colour, but for some models it varies significantly
- Height – this is a bit of a special one, in that it is not used for rendering. Instead, it defines the height of each pixel of the object, but stored on a flat plane. The idea is to use this data to determine where the player can walk (imagine that you could walk behind the chest, but not through it)
- Not shown here, but there’s also a whole bunch of renders for shadows – I’m rendering shadows from 8 different direction, for each of the objects 8 directions, to be able to have some sort of dynamic shadowing. It won’t be super-smooth, but should be good enough).
Occlusion and shadows are stored as 4 bits (16 levels) per pixel, to save memory. Albedo and specular colour are stored as 8 bits per pixel – serving as indices into a 16-bit palette (which is shared between both albedo and specular, and also for all images of an object). Normals are stored as two 8 bit values, with the third component being calculated on the fly. Depth, specular power and height are all 8 bits per pixel.
All in all, this data gives me the ability to do full dynamic lighting, and with all the different tricks, the memory overhead is workable, though perhaps not ideal.
I still have a few bits to finish off with the conditioning pipeline, but it feels really good to have the bulk of it done – this should allow me to focus on what I want my assets to look like, without having to waste a lot of time on getting them in the right format for the game.
Keep going with the development. Sounds like an interesting project. Keep us posted with how it’s going. Phil.