Magic in-depth

Finally, after way to much delay, I got around to making that video showing the magic shader I mentioned before.

As the video in it self isn’t very spectacular, I thought I would go a little bit more in depth with how the rendering pipeline works.

First of, lets have a look at what the effect looks like in action. If you read the original pose you may notice I have tweaked it a bit since then, added a simple cell-shading and a background.

I’ve mentioned the customizable rendering pipeline in a previous post, so let’s begin with looking at the setup.

This will get very technical soon and quite possibly a bit boring. If you are not interested in the implementation details I suggest you check out my previous post for a quick overview.

So lets get into the code. This way of defining the pipeline is heavily inspired by the Bitsquid game engine. I intend to move these settings away from C# and into a settings file but, for now, they are hard coded. These are all the render-targets the scene uses.

The first argument is simply the name of the target, which layers and resource generators use to refer to it below. Second is the format, duh. The last two are related to each other. If the fourth argument is null, or not supplied, the third argument is the absolute size of the render-target. If the fourth argument is another render target, the third argument is the scale relative to that target. magic_fourth0 is therefore ¼ the size of magic which is the same size as the back-buffer, and so on. The back_buffer is automatically created by the engine, and is therefore not defined here.

albedo and normal, together with the depth and depth_stencil targets, form the g-buffer for deferred shading. magic is used to draw the silhouette of any objects that should have the magic effect and magic_fourth0 and magic_fourth1 are used to downscale and blur the silhouette to create a distance field. light is a HDR target for the deferred shading. There’s no bloom effect implemented yet so it could have rendered directly to the back-buffer.

Next up in the configuration are the layers.

Again, the first argument is the name of the layer, used in shaders to define when in the frame an object should render. Next is an array of all the render targets the layer will use, not including the depth-stencil-buffer. These are all the outputs available to a shader rendering in that layer. The third argument is a depth-stencil-buffer to use for depth and stencil writing/testing. The rest of the arguments are optional. Fourth is a resource generator to run in the layer, more on those later. Fifth is a color to set the render-target(s) to before rendering begins, this can be a single color to set all render-targets to or an array to have separate colors for all targets. Sixth is a value [0, 1] to clear the depth-buffer to. Finally, seventh is a value to clear the stencil buffer to [0..255].

The first layer, default, render to the g-buffer plus the magic target. It also clears the color and depth of the targets. linearize_depth is used to recalculate the depth from depth_stencil into a linear representation stored in depth. magic0, magic1 and magic2 scale down and blur the silhouette from magic. overlay copies the color from light and overlays the magic effect, this layer is also used for drawing GUI elements.

Last but not least in the configuration are the resource generators. These are used to populate resources such as render-targets. There is only one implemented so far though.

These have a slightly different syntax than the render-targets and layers. I intend to fix that when I move it to a settings file. A number of collections of resource generators are added to the ResourceGenerators collection here. FullscreenPassDescription is basically what it says on the tin. A fullscreen pass using a shader and a set of inputs (render-targets). Effect is a shader and a set of preprocessor definitions, so "blur:HORIZONTAL" is the blur shader with the option HORIZONTAL enabled. The idea is to extend this so that a fullscreen pass can also define an output, different from that of the layer it is running in. If that were possible, magic0-2 could be combined into a single resource generator set, the same goes for deferred_shading and linearize_depth. For now, they always render to the layer they are run in.

Now that the setup is done, lets go into the shaders. The deferred shading still works the same as before so I won’t go into any details there. The only difference is in the shader writing to the g-buffer.

If RENDER_MAGIC is defined, simply write to that render-target. This could be improved so that it writes 1.0 only to red, green, blue or alpha. That way, with some modification to how the blur is performed, four different magic effects could be used on screen at the same time without interfering with each other.

The blur is pretty self explanatory. Scale down to ¼ resolution (for speed) and run a gaussian blur, first horizontal, then vertical. I won’t keep you here to look at my implementation. Once you’ve seen one blur shader you’ve pretty much seen them all. If you want to know more about this step, I recommend this site.

Now for the final step; actually overlaying the aura.

I tried to make the comment as clear as possible. There is a lot of room for improvement in this shader. First of all, everything is hard coded, even the color of the magic aura. The offset is calculated in texels to make it independent of resolution. It would be better to only compensate for different aspect ratios, and offset with the same amount in uv-space regardless of whether the resolution is 1920×1080 or 1280×720.

So there it is. This post sort of got out of hand and took way to long to write, and I’ve only gone over the basics of the rendering pipeline. I’m going to sleep now but and any feedback is greatly appreciated. If anything is unclear, please post a comment and I’ll do my best to fix it, or at least reply.