So far on this project I've completed just the bare minimum in graphics and art work. I thought it would be a nice change of pace to finally get around to it. You know, something simple, like say, adding point lights to the game. There's going to be lots of glowing bullets, and explosions, and they'll all need to light the environment. So at the very least, I'll need to be able to render tonnes of point lights. Of course, I also figured I'd need some art to show off some of the lighting. What's the point in writing a blog post about lights, if everything you are showing is debug grey, and has flat spec?
I'm not interested in tackling any Physically Based Rendering (PBR) at this point in development, so I figured I'd just stick with a pretty simple specular model. Besides, the whole point of Physically Based Rendering is to do a whole bunch of little things as accurately as possible. You can't just slap a nice specular model in your game and call it a day. You've got to cover so many different bases:
I've seen a few games call themselves PBR, and yet the lighting falls apart as soon as they move out of the direct sunlight. Sometimes it's because they've missed out on a good set up for indirect specular lighting. Other times, they've done everything right, and it's the complicated data set up required that has let them down. A missing cube map, or a just a texture with the wrong values. The point is, if you don't do all the steps, then you aren't approximating things very well.
Considering I'm only interested in just laying down the framework for rendering lights on the screen, why would I care if at this point if they have the best possible specular model? I will try to address that astounding list of features later on in development. It's going to take a whole lot of baby steps to get there. For now though, lets just get some lights in :)
With that in mind, I sat down to do a first pass on some level assets. I won't go into any detail here on my asset creation pipeline, since I've already touched on how I do some of the texture work here. However, as soon as I started to use Substance Painter, I ran into one major floor in my delayed PBR plan. Nothing I imported into my engine looked anything like what I had on screen in Substance Painter.
I've been in productions where we've asked the art staff to “Follow the PBR RULES!” on blind faith while we concurrently work on the renderer. I've always had an idea of the difficulty with what we were asking, but experiencing it first hand is borderline soul crushing. As specially when the point of creating the assets was so that I could show the internets my lighting. Further more, it fed back into the code work I was doing. Suddenly I'm sitting there with a choice of fudging my data, so that it will look good now, or just making it work. The problem with fudging the data is that I'll be making my code side life just that much harder further down the line when I go to implement PBR.
It's basically at this point where I caved in. I'd rather have good data, and not have my artistic self hate my code self, so I figured, at the very least, I'd implement the same specular model that Substance Painter uses. Additionally, my ambient light at the moment is garbage. So stylistically I chose a scene with very dark ambient. This also had the effect of boosting my point lights (since I currently don't have tone mapping). I'll tackle an outdoor scene when I get around to dealing with indirect lighting.
Now my scene renders as I'd expect it to, even though I've only implemented a small subset of what is required for PBR.
So now that I've gotten the artsy fartsy stuff out of the way, here's a few quick details about what I've smashed out on the code side:
Lighting is expensive, so on the whole, you try calculate as little of it as possible. Deferred shading is a method for saving all the information you need to do your lighting into a set of buffers. You then do the lighting once per a pixel later on. This allows you to avoid redoing the light calculations every time you have overdraw for your objects. It also, depending on your implementation, allows you to efficiently cull your lights to only effect the pixels they cover. On the bright side, it's fast. On the negative side, it's hard to do transparencies, and you are stuck with one lighting model.
I'm expecting the majority of my scenes surfaces to be opaque, and since I'd already had experience building a deferred renderer, I fairly quickly knocked out a basic implementation. Initially I was pretty naive about how I stored my G-Buffer; full position data stored, every buffer was R32G32B32A32_Float, as was my light buffer. It was glorious… Until I tried to run in on my Surface Pro. Twin sticks are not fun at 10 fps.
Ok, fair enough, my bad. I packed the G-Buffer as tight as I could, let any parts that didn't need high precision be R8G8B8A8 textures (so nice that you can now have different formats for each texture in DX11). I also stored linear depth, to be reconstructed as position later. Additionally, I converted my back buffer over to R11G11B10_Float (mainly because it was a format I'd never used before and it looked like it was designed for pretty much this)! With these changes, performance shot up to an acceptable range. I later quickly used my Google Foo to look up R11G11B10_Float, and was pleased to see that it's generally regarded as a decent option for storing hdr data.
One thing I'd never got around to was dealing with unique lighting models, or transparencies. I was pretty determined to make sure that rendering lots of point lights wouldn't be detrimental if I had transparent objects in my scene. To that end, I made sure my scene was composed of a lot of water. May as well break things early right?
A friend of mine, Leon O’Reilly, suggested I look into Forward+ shading. I was pleased to see that lots of lights is now largely considered a solved problem, that is, if you are happy for most of your lights to not have shadows.
At this point I'm ok with that restriction, so I dug in and implemented Forward Plus. It's basically a method of dividing the screen up into tiles of pixels, and pre-calculating which lights are used on those pixels. The results are stored in an index buffer that any shader can look up, and retrieve a list of the used lights. Therefore, you don't have to render more lights than you are using on a given block of the screen. The calculation is done at the start of each frame, and it uses compute shaders, so it's blindingly fast. I'm not going to go into the specifics, but here's a couple of resources that covers it in great detail:
The method worked so well that I decided to see if I could use the same index buffer to do my deferred point lights in a single full screen pass. My main bottleneck on the deferred rendering was still the Gbuffer lookup and this change would mean that I'd only have to hit it once. The results were fantastic, and it's now the implementation I'm sticking with.
The last part of the puzzle was making sure everything drew in the correct order. Leon suggested that I give every draw an integer key (I'd have to decide the precision depending on my needs). The layer should be written into the top bits, any other information I need for sorting can then be written into subsequent bits (like depth, or what shader it's using). The higher bits get priority, and will draw first. The draw call list can then simply be sorted by this key.
I decided to try it out, and it worked really well! I've tried looking up this technique on google, and I've seen many people use it to achieve stateless rendering:
I'm currently not doing anything to reduce the amount of states I'm setting, but it's nice to see that I'll be in a pretty good spot to fix that with my current set-up.
I'm really happy with the current results, and it's amazing the motivation I've gotten just from adding a little art to the game. At this point it would be really easy to go down the rest of the PBR rabbit hole, but I feel like I could ship with what I have now render wise. The plan is to come back and complete the rendering later, but I think for now, it would be wise to get the rest of the game to a shippable state!