This has been a productive week, and I’m now between 80 and 90 percent done with the render engine (by feature). I’m looking forward to having it off my plate, and getting back to gameplay!
- Materials are really important to the game (every tile has a material, which gets carried into items and just about everything else, and affects just about everything), so I was determined to have a render engine that reflects this. I sought out some PBR (Physically Based Rendering) textures to get me started; I’ll probably get some better ones, but FreePBR.com has a good start. So I downloaded quite a few of them, and started figuring out what to do with them.
- Adjusted the raws system to let me specify materials has having a constructed and natural nexture, and linked the loader to it – it loads textures based on the Lua specifications. That makes it very easy to add textures (no recompile). The loader is smart; it combines various single-channel items of data into channels inside textures to save space. (A material texture has RGB per pixel, normal map per pixel, roughness/smoothness per pixel, metalness per-pixel and ambient occlusion per-pixel; that bakes down to three textures per material). I’m still experimenting with how large textures need to be; 2048×2048 seems to be standard for people providing Unreal Engine textures, but that uses a lot of video memory and the in-game views don’t need huge textures because I’m not making an FPS!
- I implemented texture arrays under the hood to let me use lots of textures in the scene without needing to do an (expensive) OpenGL context switch. (I’d use “bindless textures”, but that’s only just become a core OpenGL feature and not everything works with them.). A simple texture renderer using them was easy and very fast.
- Since lighting is handled in the game systems (rather than just being eye candy; it affects determining what entities can see, and there are hooks in place for creatures that prefer the dark…), I needed a way to get the game lighting into the render engine, without requiring that geometry be re-created just because someone turned the lights on/off (so not as part of the vertex data). I settled on creating a 3D texture that (very quickly!) copies the in-game volumetric lighting information, as well as flags such as “above ground”. That fixed my #1 slow-down from previous iterations.
- Got the deferred render pipeline going again. Stage 1 pre-renders everything to the depth-buffer, to speed up later stages (and then does some calculations to give the video card time to finish). Stage 2 renders all visible terrain geometry to a gbuffer consisting of a color/albedo (with gamma removed so we’re in linear color space), normal, world position, combined rough/metal/AO data. Stage 3 renders models onto the gbuffer (see below). Stage 4 goes over the gbuffer, looks up lighting information for each pixel (we’re in screen space now, so it’s a constant-time calculation based on resolution rather than amount of geometry – so no penalty for big/complex scenes from now on!); see below. It outputs in HDR-range color space. Stage 5 will add in transparent items (not done yet). Stage 6 goes over the output buffer and applies a 10x Gaussian blur to over-bright areas (Bloom). Stage 6 tone-maps (currently Reinhart) the HDR back down to colors that a regular video card can render. It currently does all of this in about 1ms per frame. 🙂
- Lighting (stage 4 from above). A tile generally has two direct light sources, as well as ambient light. The engine looks up the world position for a pixel in the 3D textures I mentioned. If a tile is above ground, it gets sunlight (a directional light that rotates around the world). If it has a game entity lighting it, that is added to it (so you don’t get weird darkening effects from outdoor lights in the daytime). I’d like to implement some sort of environment-map or sphere for ambient eventually, but that’s a wish-list item. For now, a slightly simplified PBR pipeline is giving great results. So we start with a simple ambient light, texture color of the material at this point – scaled down to 0.3 (quite dark). This is then multipled by the Ambient Occlusion texture, which prevents materials from looking flat (the color texture can be flat, but the normal might show details – the AO texture bakes in the normal details). Then we do a basic Lambert diffuse calculation for each light (taking into account per-pixel normals), which looks pretty good on its own. Specular color is calculated from the material’s “metalness”; conductive metals reflect the color of the metal, other materials reflect a mix of the light/base color. Roughness/smoothness serves to simulate the “microsurface aberrations” – basically, the rougher a surface, the less specular light it reflects. These are fed into the Cook-Torrance lighting equation, which in turn uses the Fresnel calculation, GGX and Schlick equations to determine a pretty realistic looking specular component per-pixel.
- Voxel Models. I love MagicaVoxel. I was really starting to enjoy making pixel-art, but never felt that it fitted with the 3D world – and this tool lets me make 3D pixel art without having to fight Blender. I’ve failed utterly on using voxel models twice before, but this time was different. I employed the same Greedy Voxels algorithms I talked about last week, and 32x32x32 voxel models are now fast enough for general use! So now beds, lamps, cabinetry, etc. all have lovely 3D representations – without extreme pain on my end.
So, how does it all come together?
- The render loop as a flow chart. This has to run in <2ms on average!
- MagicaVoxel lets me make voxel art like this in 10 minutes
- The anatomy of a texture. All the channels that make up a single texture.
- Parts of the G-Buffer and a small rendered result.
- A full-size in-game rendered map
The remaining items on my renderer to-do list are small: finish adding models, texture trees, add transparency for windows/water, and a cursor/selection system that doesn’t suck. There’s always things to improve, but it’s coming along nicely! I’ll be on vacation next weekend, so no Sharing Saturday for me – but it should be in really good shape soon.