In a previous post (link), I talked about why surface shaders are a great way to write shaders for Unity’s Deferred Lighting rendering path, and they are.
But given the choice, I’d rather write pixel shaders. Surface shaders always felt a little too much like magic for me, and I’ll trade writing more lines of code for more control over what my gpu is doing any day of the week.
For forward rendering, writing pixel shaders is virtually no different from writing shaders for any other engine, however not much information is out there about writing pixel shaders that work with Unity’s deferred lighting system (and more maddeningly, there is no information in Unity’s docs), so this post is going to talk about that.
Note that this article will not cover how to write custom lighting models for deferred lighting, but rather how to write shaders which use whatever lighting calculations are generated by the deferred lighting system.
I’m also on a mac, and the shader compiler for opengl is way less picky than for directX, so if you hit any snags on windows, send me a message on twitter and we’ll get things sorted out.
If you’re unfamiliar with what Deferred Lighting is, I recommend checking out my earlier post (link), where I go over the differences between deferred and forward rendering. As a quick refresher, Unity’s Deferred lighting system in a 3 step process:
Step 1: Initial data buffers are constructed. These buffers consist of a depth buffer (Z-Buffer), and a buffer containing the specular power and normals of the objects visible to the camera (G-Buffer).
Step 2: the previously built buffers are combined to compute the lighting for each pixel on the screen.
Step 3: all of the objects are drawn again. This time, they are shaded with a combination of the computed lighting from step 2 and their surface properties (texture, colour, lighting function, etc).
When you write surface shaders, you don’t really need to worry about the nuts and bolts of this process, but since we’re using pixel shaders we’re directly responsible for the first and last steps. As such all pixel shaders that work with Deferred Lighting are 2 pass shaders (3 pass if you want to cast shadows).
We’ll start building our shader with an empty pixel shader skeleton:
Since we want our object to properly cast shadows, we need to write three passes. Note that if you only need your object to receive shadows, you can omit the third pass, since the shadow attenuation will already be factored into the light buffer that Unity builds as during Step 2.
The first thing we need to do is make sure that the G-Buffer knows about our object’s shape and specularity so that it can properly calculate lighting for the scene. To do this, we need a pass that outputs the normals and specular values for our object.
To start, we need to let Unity know which pass to use to get this information. Just like with forward rendering, we’re going to use the LightMode tag to assign our passes different roles. In this case, we’ll use “PrePassBase.”
A lot of this pass is very straightforward, since for the most part all we’re doing is outputting normals, but since we also want our objects to be shiny, we need to set the alpha of our fragment shader’s output to our object’s shininess, like so:
If you’re a bit confused as to why this pass is necessary, think of Deferred Lighting as though it’s a shader replacement technique. In order to build the GBuffer, the camera renders all the objects in the scene using the PrePassBase pass, and saves this to a render texture. It then uses this data as part of the process of building the lighting buffer.
The line in the fragment function which halves the normal and then adds 0.5 to it just takes each component in the normal and re maps it from the range -1 to +1, to the range 0 to 1 so that it can be stored in a texture.
Once that lighting buffer is created, our job changes from putting data into it, to getting data out of it.
Just like forward rendering, our second pass uses a different tag, PrePassFinal, to let Unity know to use this pass for the last step of the deferred rendering process. Otherwise, the first few lines of this pass are unremarkable.
The vert function is where things start getting interesting. The fragment function will sample the Light buffer using tex2Dproj, which takes a 4 component vector and divides the xy components by the w component.
Since what we need to do is sample the light buffer at the exact point on the screen that our fragment will be drawn, we have to set our 4 component uv vector to our fragment’s position in clip space. This will let tex2Dproj perform the perspective divide for us, letting us get at exactly the point on the light buffer that we need.
Or rather, that’s the easy way of looking at it. In truth it’s a bit messier than that. Let’s look at what our vertex function ends up being:
To start with, we’re halving the clip space coordinates for our projected uvs. I’m assuming this is because the light buffer isn’t actually a screen size texture, but since theres no information available about how Unity’s implementation works under the hood, it’s hard to know for sure.
You could try setting up a deferred lighting scene on an ios device and using Xcode’s gpu capture frame to get at that data, but I don’t have any ios devices in my apartment so I’ll leave that to you (send me a message on twitter if you actually try this :D ).
The Unity docs have this to say about _ProjectionParams:
“x is 1.0 (or –1.0 if currently rendering with a flipped projection matrix), y is the camera’s near plane, z is the camera’s far plane and w is 1/FarPlane.”
So it looks like all that multiplication is doing is making sure that we’re right side up on platforms where rendering to texture flips the image.
I have no idea why we end up adding the halved w component back to our xy components. Again, twitter me if you have any idea.
But now that that’s covered, our fragment function is really straightforward:
Notice how unlike forward rendering, we don’t have to do any per light calculations, because they’ve already been done for us, and had the resulting value stored in the light buffer. All we have to do is read from that buffer and multiply our fragment colour accordingly. Just like in the first pass, specular values are stored on the alpha channel of the light buffer.
The logTex calculation feels to me like an implementation specific detail that we don’t really need to worry about, except to know that we have to do it to get values that make sense from Unity. I haven’t built any deferred lighting systems from scratch, so I’m not sure if this is a standard way of storing data in a light buffer or not.
But nevertheless, you should now have a working pixel shader with deferred lighting. How exciting! Only one pass left to go.
One of the cooler parts of Deferred Rendering is getting to have point lights cast shadows, but to do that we’ll need another pass. Luckily, this pass is the same as any other shadow caster pass in Unity. In theory could let a fallback handle this for you, but for the sake of having a fully standalone shader, let’s add it here as well.
If everything has gone according to plan, you should now have a pixel shader that works with deferred lighting and shadows! If you don’t see your object at all, make sure that you’ve actually switched your camera over to the deferred path (I made that mistake when writing this post).
It’s worth noting that all I did to figure this out was to write surface shaders that only used the deferred lighting path, have them compile down to glsl and figure out what was going on from the compiled shaders.
If you want to learn more (like how to add spherical harmonic lights, or use lightmaps), all you need to do in order to do this yourself is add “exclude_path:forward” to your surface pragma, and add an additional pragma below that, like this:
If you’re on desktop, you’ll need to click the “Show All” button in the inspector to get at the gles code, since gles shaders are meant for mobile devices. If you can read ARB or DirectX assembly, you can do that too, but I find glsl much more readable.
If you have any questions about anything, spot a mistake, or just want to say hi, send me a message on twitter. Happy Shading!