Rabid Lion Games

 

Last time we figured out how to use our occlusion map to generate a light map for a point light in our scene. Had we altered the XNA rendering code to show us what a typical light map generated looks like, we would have seen something like this:

If you click on it to see the larger version you’ll see that it’s not particularly attractive is it? Although it fades nicely towards the edge of the light radius, edges caused by shadow casting objects are very sharp (not to mention heavily aliased). Of course what we want is for these edges to also be nice and smooth. Further more, we also want the shadows to be crisper closer to the light, and more indistinct the further from the light the shadow is. As I mentioned in Part 2, this section of the algorithm is heavily based on @CatalinZima’s method for accomplishing the same. So if my explanation of the techniques we use doesn’t quite make sense to you I suggest that you head over to @CatalinZima’s blog (linked to again at the end of this part) for the explanation over there.

Our light maps actually need to be blurred twice, once vertically and once horizontally. The basic idea is that for each pixel we want to replace it’s value with a sort of weighted average of the pixels around it, with those closest to the original pixel having the biggest weighting, and those furthest from it having the lowest weighting. The main considerations in deciding on a blurring algorithm are how many samples to take when creating this average, and how spread out they should be. The more spread out the pixels, the more blurred the final image, whereas the more samples we take, the fewer artefacts we have in the final image.

For now lets just create a simple vertical blur shader, and then we’ll adapt it for our purposes.

Fire up our Visual Studio Solution and add a new Effect file called ‘VerticalBlur.fx’. As usual we delete the contents that XNA gives us by default, and add the following stubb:

 

float4 PixelShaderFunction(float2 texcoord : TEXCOORD0) : COLOR0
{

}

technique Technique1
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

 

Now we create a variable to hold the sum of each of our samples at the top of our Pixel Shader function:

 

float sum = 0;

 

We need to decide how many samples we want our shader to take. I’ve followed @CatalinZima here and have borrowed his table of distances/ weightings. The normal name for this is a sampling kernel. Lets now add it to our shader. At the top of the file add the following:

 

static const int KernelSize = 13;

static const float2 OffsetAndWeight[KernelSize] =
{
    { -6, 0.002216 },
    { -5, 0.008764 },
    { -4, 0.026995 },
    { -3, 0.064759 },
    { -2, 0.120985 },
    { -1, 0.176033 },
    {  0, 0.199471 },
    {  1, 0.176033 },
    {  2, 0.120985 },
    {  3, 0.064759 },
    {  4, 0.026995 },
    {  5, 0.008764 },
    {  6, 0.002216 },
};

 

If you’re wondering where these values come from, they’re just a weighting for each distance that add up to approximately 1, so that by multiplying each sample by it’s weighting and summing them we’ll get a weighted average of the samples (the maths behind what weighting to give to each sample is slightly more complex, so I won’t go into it here).

We now use these to create our weighted average. Let’s add the following code to our PixelShaderFunction(), and then we’ll discuss exactly what it does:

 

for (int i = 0; i < KernelSize; i++)
{
    sum += tex2D(textureSampler, texcoord + OffsetAndWeight[i].x / ScreenDims.y * float2(0, 1)).r * OffsetAndWeight[i].y;
}

 

Before we discuss this let’s quickly add the textureSampler and ScreenDims parameters to the top of the file:

 

sampler textureSampler : register(s0);

float2 ScreenDims;

 

So what is going on in this for loop? Well, essentially we’re sampling a pixel for each entry in our kernel, multiplying it by some value, and adding it to our sum to give us our average. To decide which pixel to sample each time through the loop, we’re adding some offset value to the texture coordinates of the current pixel. That offset is calculated by taking one of the values in our kernel (which are stored as a number of pixels), dividing it by the height of the screen to get it into the texture coordinate scale, and multiplying that by a normal pointing vertically downwards, to give us a float2 that we can add to our texture coordinates. We also only look at the r channel of each sampled pixel. This is because at the moment on our lightmap we only have pixels where the r, g, and b components are all the same, so to simplify matters we will only look at one channel.

Finally, we add the following lines to the bottom of our function in order to return the correct value:

 

float4 result = sum;
result.a = 1;
return result;

 

All we are doing here is creating a return color from our sum value, and setting the alpha channel to 1, as we want every pixel in our light map to be opaque.

So, that’s a simple, uniform, vertical blur.

However, as discussed above, we actually want the amount of blur to depend on the distance of the pixel from the light. Or to be more precise we want our sampling kernel to be more ‘spread out’ the further from the light we get. To do this we need to multiply the offset value of our kernel by some value based on the distance to from the pixel to the light.  The obvious way to do this is to linearly interpolate between some minimum blur value at the light position and a maximum blur value at the radius of the light. Which is in fact exactly what we do! The code for our altered PixelShaderFunction() to achieve this is as follows:

 

float sum = 0;
float2 screenPos = float2(texcoord.x * ScreenDims.x, texcoord.y * ScreenDims.y);
float dist = length(screenPos - LightPos);

for (int i = 0; i < KernelSize; i++)
{
    sum += tex2D(textureSampler, texcoord + OffsetAndWeight[i].x * lerp(minBlur, maxBlur, saturate(dist/ Radius))/ ScreenDims.y * float2(0, 1)).r * OffsetAndWeight[i].y;
}

float4 result = sum;
result.a = 1;
return result;

 

Here we use the inbuilt lerp function rather than manually linearly interpolating ourselves. We also use the inbuilt function ‘saturate’ which just clamps dist/ Radius to the range 0-1. Before we finish with this shader we need to add some parameters to the top of our shader file:

 

float2 LightPos;
float Radius;

float minBlur;
float maxBlur;

 

And we’re done with VerticalBlur.fx. Let’s barrel on to HorizontalBlur.fx. Create a new effect file named HorizontalBlur.fx  in the usual way, and empty it as normal. Now I’m going to tell you to do something that will make some of you shake your head in disgust, but I’m doing it to illustrate a point (ok, and because I’m a bit lazy!).

Copy the contents of your VerticalBlur shader and paste them into the new file. You can type it all out again if you really want to, I’ll wait. The rest of you grab a coffee or something…

Right, now we’re all caught up we only actually need to change 3 characters in the whole file (see why I told you to copy and paste now?).

 

First up, inside our call to tex2D() inside our for loop, look for our reference to ScreenDims.y. Change this to ScreenDims.x.

Secondly (and thirdly) follow a bit further along that line to where we create the normal float(0, 1). Change this to float(1, 0).

 

And that’s it. You’ve just created a Horizontal blur. Everything else is exactly the same, as you should probably expect. If you’re scratching your head wondering why we didn’t have change our sampling kernel, remember that the kernel just holds single values. In order to turn them into coordinates we are multiplying them with a normal either pointing directly down or directly right, to give us vertical or horizontal offsets from our texture coordinates.

Before we finish we need to add some code to our LightRenderer class to set our shader parameters. Open up LightRenderer.cs and add the following to the top of the class:

 

public float minBlur = 0.0f;
public float maxBlur = 5.0f;

 

These are just values that I’ve found to work well, feel free to experiment at your leisure. Next up in prepare resources add the following lines:

 

verticalBlur.Parameters["ScreenDims"].SetValue(screenDims);
verticalBlur.Parameters["minBlur"].SetValue(minBlur);
verticalBlur.Parameters["maxBlur"].SetValue(maxBlur);

horizontalBlur.Parameters["ScreenDims"].SetValue(screenDims);
horizontalBlur.Parameters["minBlur"].SetValue(minBlur);
horizontalBlur.Parameters["maxBlur"].SetValue(maxBlur);

 

Finally we go to the BlurLightMaps() method. Just before the first call to spriteBatch.Begin(), add the following lines:

 

horizontalBlur.Parameters["LightPos"].SetValue(light.Position);
horizontalBlur.Parameters["Radius"].SetValue(light.radius);

 

and then finally, just before the second call to spriteBatch.Begin(), add these lines:

 

verticalBlur.Parameters["LightPos"].SetValue(light.Position);
verticalBlur.Parameters["Radius"].SetValue(light.radius);

 

And we’re done! We now have all the code we need to blur our light maps. If you’d like to see the before and after shots, I’ve added them below:

 

 

If you recall from Part 3, after the light maps are blurred they are additively rendered to a texture that accumulates all of the maps for each light, eventually leading to the final light map that we can use with our shader from Part 2. The final 2 steps in our series before we can write some game code to actually use our system are to do with spot lights. Once again these need to be ‘unwrapped’ before their rays can be used to create an occlusion map, and then we’ll need to create a shader that uses that occlusion map to generate a light map. Its a bit different to what we did with point lights though, so we’ll still have plenty of ground to cover.

Finally, as usual, here is an upload of the solution so far up to this point:

 

Part 6 Solution

 

Until next time!

(Link to @CatalinZima’s blog: http://www.catalinzima.com/2010/07/my-technique-for-the-shader-based-dynamic-2d-shadows/ )

Leave a Reply


seven × = 42

Proudly powered by WordPress. Theme developed with WordPress Theme Generator.
Copyright © Rabid Lion Games. All rights reserved.