Rabid Lion Games

 

Welcome back!

Last time we looked at how to blur a light map to create soft shadows. We were looking at point lights, but actually we use the same method and shaders to blur all of our light maps, not just point lights.
In this part we’re moving away from point lights and instead looking at the other type of light that our lighting system will be able to handle: spot lights.

A very simple model of spot lights (and one often used in simple 2D lighting) is to just model it as a ‘slice’ of a point light. To achieve this all we’d need to do is generate an occlusion map using our point light unwrap shader, and then in the spot light shader (in place of the point light shader), you would just need to add an extra test to determine whether ray that the current pixel is on is one of those covered by the spot light. If it is, continue as normal, if it isn’t then color it black.

If that’s the effect you’re looking for, then feel free to have a crack at writing the spot light shader yourself (and remember to share it with the community when you’re finished 🙂 ). For this series however, we’re going to be looking at a slightly more complex model of a spot light.

We essentially use a 2D version of the model described here: D3DBook

The basic idea is that there is an ‘inner cone’ or inner triangle in 2D, that gets the full power of the light, and an outer cone/ triangle in which the light power decreases the further from the centre of the light it gets.

To keep things simple the rate at which the light decreases in the outer triangle will be linear, however if you want a slightly more realistic effect check out the formulas & code in the link above.

But that’s all a problem for next time. For now we’re only worried about the unwrap shader for spot lights.

Technically there is nothing stopping us using the point light unwrap shader as described above. However, this would be rather wasteful, as the vast majority of the occlusion map would go unused by storing rays that we will ignore because they are not in the arc of the light. Despite the waste, if I were to start over, this is probably the approach I’d use, as it took me quite a while to work out how to code the method described below!

Instead we will use the whole texture to store an occlusion map for just the arc of the light. This has the effect of squeezing more rays into the arc of our light, in theory giving us a bit better accuracy.

So how are we going to do this? Actually it’s surprisingly similar to the unwrap shader for point lights, but with a few major changes:

We want our x texture coordinate to map from 0 – 1 to MinAngle – MaxAngle instead of 0 – PI (where MinAngle is defined as the angle of the leftmost ray if you are looking down the centre ray of the light away from the source, and MaxAngle is the angle of the rightmost ray).

We only store one occlusion map in the texture, rather than one in each of 2 different channels as we did with point lights.

We can’t guarantee which direction any of the rays point in (unlike our left/ right divide for point lights), so we need to check all 4 edges of the screen for collision with each ray to determine it’s length.

 

So let’s try and alter our unwrap shader for point lights to deal to meet these requirements. Create a new Effect file called UnwrapSpotlight.fx, and delete it’s contents as normal.

First up, let’s take the code from Unwrap.fx and paste it in:

 

#define PI 3.14159265

float2 LightPos;

float TextureWidth;

float TextureHeight;

float DiagonalLength;

sampler shadowCastersSampler  : register(s0);

float4 PixelShaderFunction(float2 texcoord : TEXCOORD0) : COLOR0
{
    float rayAngle = texCoord.x * PI;

    float sinTheta, cosTheta;

    sincos(rayAngle, sinTheta, cosTheta);

    float2 norm1 = float2(-sinTheta, cosTheta);

    float2 norm2 = float2(sinTheta, cosTheta);

    float2 LightDist;

    if (cosTheta == 0)
    {
        LightDist = float2(TextureWidth - LightPos.x, LightPos.x);
    }
    else if (sinTheta == 0)
    {
        LightDist = abs((((cosTheta + 1) / 2.0) * TextureHeight) - LightPos.y);
    }
    else
    {
        float4 hit = float4(-LightPos.y / cosTheta, LightPos.x / sinTheta, (TextureHeight - LightPos.y) / cosTheta, (TextureWidth - LightPos.x) / sinTheta);

        hit = (hit < 0) > 2 * TextureWidth : hit;

        LightDist = min(hit.wy, min(hit.x, hit.z));
    }

    LightDist = mul(LightDist, texCoord.y);

    norm1 = mul(norm1, LightDist.y);

    norm2 = mul(norm2, LightDist.x);

    float4 sample1 = tex2D(shadowCastersSampler, float2((LightPos.x + norm1.x) / TextureWidth, (LightPos.y + norm1.y) / TextureHeight));

    float4 sample2 = tex2D(shadowCastersSampler, float2((LightPos.x + norm2.x) / TextureWidth, (LightPos.y + norm2.y) / TextureHeight));

    return float4((sample1.a < 0.01) ? 1 : LightDist.x / DiagonalLength, (sample2.a < 0.01) ? 1 : LightDist.y / DiagonalLength, 0, 1);

}

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

 

 

Next, we change it so that we’re only looking at the ray that pointed left.

 

float4 PixelShaderFunction(float2 texcoord : TEXCOORD0) : COLOR0
{
    float rayAngle = texCoord.x * PI;

    float sinTheta, cosTheta;

    sincos(rayAngle, sinTheta, cosTheta);

    float2 norm = float2(-sinTheta, cosTheta);

    float LightDist;

    if (cosTheta == 0)
    {
        LightDist = float2(TextureWidth - LightPos.x, LightPos.x);
    }
    else if (sinTheta == 0)
    {
        LightDist = abs((((cosTheta + 1) / 2.0) * TextureHeight) - LightPos.y);
    }
    else
    {
        float4 hit = float4(-LightPos.y / cosTheta, LightPos.x / sinTheta, (TextureHeight - LightPos.y) / cosTheta, (TextureWidth - LightPos.x) / sinTheta);

        hit = (hit < 0) > 2 * TextureWidth : hit;

        LightDist = min(hit.wy, min(hit.x, hit.z));
    }

    LightDist = mul(LightDist, texCoord.y);

    norm = mul(norm, LightDist.y);

    float4 sample = tex2D(shadowCastersSampler, float2((LightPos.x + norm.x) / TextureWidth, (LightPos.y + norm.y) / TextureHeight));

    return float4((sample.a < 0.01) ? 1 : LightDist / DiagonalLength, 0, 0, 1);

}

 

 

The reason we’ve chosen this ray is because of the way we defined MinAngle and MaxAngle. Imagine our spot light covers a 90 degree arc, with the MinAngle ray pointing straight down (our zero direction from when we were working with point lights). The rays of the spot light would be exactly those of the left hand side of the equivalent point light, with MinAngle = 0, and MaxAngle = PI.

 

Next we change rayAngle so that is lies in the range MinAngle – MaxAngle:

 

float rayAngle = MinAngle + (texCoord.x * (MaxAngle - MinAngle));

 

 

We’ll also need to add parameters for MinAngle and MaxAngle at the top of the file:

 

float MinAngle;

float MaxAngle;

 

 

Now recall that we had to handle special cases when cosTheta == 0 or sinTheta == 0. Because we only ever had angles in the range 0 – PI we were able to simplify the case when cosTheta == 0. Since our angles might lie outside this range now we have to alter this case to handle all possibilities (it ends up very similar to our sinTheta case):

 

LightDist = abs((((1 - sinTheta) / 2.0) * TextureWidth) - LightPos.x);

 

I won’t go over the trigonometry involved, but essentially if cosTheta == 0 then we are pointing either directly right or directly left. If left then sinTheta == 1, if right then sinTheta == -1. Armed with that information you should be able to work out that the code gives LightDist as LightPos.x if the ray is pointing directly left and TextureWidth – LightPos.x if it’s pointing right, which is exactly what we want.

In the general case we need to test against all 4 sides of the screen when trying to find out which our ray hits first. All that means for our code is that the length we’re looking for is the minimum of all four values in our hit float4, rather than separating out the top/ bottom and sides. We also need to change one of the four values to account for the fact that for our point light rays hitting the right hand side had their angles increasing in the opposite direction to those we are dealing with in our spot light:

 

float4 hit = float4(-LightPos.y / cosTheta, LightPos.x / sinTheta, (TextureHeight - LightPos.y) / cosTheta, (LightPos.x - TextureWidth) / sinTheta);

hit = (hit < 0) ? 2 * TextureWidth : hit;

LightDist = min(min(hit.x, hit.y), min(hit.w, hit.z));

 

Finally we need to change the two lines that transform norm to give us the offset from the light in pixels of our sample. The reason for this is that the intrinsic function mul doesn’t work on single values, so we need to fall back to the good old * symbol:

 

LightDist = LightDist * texCoord.y;

norm = norm * LightDist;

 

And that’s it! Well, sort of. That’s it for our shader, but I’ve left  a rather glaring hole in all of this, which we’ll have to deal with in our LightRenderer code. We’ve already said, in a round about way, that for the rays to the left of the light we’ll be using the same angles as we did for point lights, with down having angle 0, left having angle PI/2, and up having angle PI. But what about the rays on the other side of the light? For point lights we used the same scale, because we dealt with each half separately, but clearly that won’t work with spot lights. We have 2 other options, each of which have their own issues.

We could say that the angles will continue to increase from PI, meaning that for the rays on the right hand side, up will still be PI, right would be (3 * PI) / 4, and down would be 2 * PI. The problem with this, of course, is what happens if the arc of the spot light straddles the zero line. Then we’d have MinAngle somewhere between PI and 2 * PI, and MaxAngle between 0 and PI. Clearly our shader wouldn’t work in this scenario.

The other option is to instead extend the range of angles anti-clockwise from 0, so that for the rays on the right hand side of the light, down remains 0, right becomes (- PI) / 2, and up becomes -PI. However, here again we have a problem, this time if the arc of the light includes the up direction, as we’d have MinAngle somewhere in the range 0 – PI, and MaxAngle in the range (-PI) – 0. Again, our shader wouldn’t work in this scenario.

To solve this problem, we need to use different methods of labelling the angles in different situations. To make this work we need to limit our Spot light to a maximum arc of 90 degrees (which is fine, because it’d look weird otherwise!). Then we can determine which model we want to use based on the sign of the y component of the spot light’s direction (i.e. the ‘centre’ ray). If this ray is pointing ‘up’ at all (i.e. it’s y component is negative, recall that down is positive in texture coordinates) then we can safely use the model that ranges from 0 – 2 * PI. Otherwise we need to use the model that ranges from -PI – PI.

In order to switch models we need to test which quadrant of the circle around the light the spot light’s direction lies in, and add an appropriate bias to it to change the model that we’re using. We’ll also need to take account of which quadrant MinAngle and MaxAngle lie in, in order to determine whether they need to have that bias added to them as well. Originally I wrote this as a mind-numbing pile of  if/else if statements to handle every permutation. However in the end I condensed it into a few lines manipulating the sign of the direction vector’s components and wrapped them in two methods within the SpotLight class. Feel free to dissect them in your own time, or leave a comment if you need more explanation. I know this is far from the best way of achieving this result, but hey, it works….

Open up the SpotLight class and add the following code to the bottom of it:

 

public float GetAngleBias()
{
    float diffAngle = (float)Math.Acos(Vector2.Dot(direction, Vector2.UnitY));
    if (float.IsNaN(diffAngle))
        diffAngle = (float)(((Math.Sign(-direction.Y) + 1) / 2f) * MathHelper.Pi);
    if (diffAngle - (outerAngle / 2f) < 0)
        return 0;
    return MathHelper.Pi * 2f;
}

public float GetBiasedDirAngle()
{
    float lightDirAngle = Vector2.Dot(direction, Vector2.UnitY);
    lightDirAngle = (float)(Math.Acos(lightDirAngle) * Math.Sign(direction.X));

    if (float.IsNaN(lightDirAngle))
        lightDirAngle = ((Math.Sign(-direction.Y) + 1) / 2f) * (float)Math.PI;

    float angleBias = GetAngleBias();
    lightDirAngle += (Math.Abs(Math.Sign(lightDirAngle))) * (angleBias * (1 - ((Math.Sign(lightDirAngle) + 1) / 2f)));
    return lightDirAngle;
}

 

 

So how do we use these methods? Flick back to the LightRenderer class and find the UnwrapShadowCasters(SpotLight sLight) class. If you recall we left a comment here to indicate that there were extra parameters to be added to this method. So, change the name of the method to the following:

 

private void UnwrapShadowCasters(SpotLight sLight, float lightDirAngle, float angleBias)

 

And then within the method itself add the following after the line graphics.GraphicsDevice.Clear(Color.Transparent):

 

unwrapSpotlight.Parameters["LightPos"].SetValue(sLight.Position);
unwrapSpotlight.Parameters["MinAngle"].SetValue(lightDirAngle - (sLight.outerAngle / 2f));
unwrapSpotlight.Parameters["MaxAngle"].SetValue(lightDirAngle + (sLight.outerAngle / 2f));

 

 

Now find where we called this method inside DrawLitScene(), just inside the for loop for looping through the spotlights. At the beginning of the loop add the following code:

 

float lightDirAngle = spotLights[i].GetBiasedDirAngle();

float angleBias = spotLights[i].GetAngleBias();

 

 

And then change the following line:

 

UnwrapShadowCasters(spotLights[i] /*,other params*/);

 

To:

UnwrapShadowCasters(spotLights[i], lightDirAngle, angleBias);

Finally, head to PrepareResources() and add the following code:

 

unwrapSpotlight.Parameters["TextureWidth"].SetValue(screenDims.X);
unwrapSpotlight.Parameters["TextureHeight"].SetValue(screenDims.Y);
unwrapSpotlight.Parameters["DiagonalLength"].SetValue(screenDims.Length());

 

And we’re done! We now have all we need to create an occlusion map for our spot lights.

As usual you can find the solution for the series so far up on codeplex at the link below so that you can compare your code:

 

Part 7 solution

 

Next time we’ll be adding the last of the code we need for our lighting system itself, specifically a shader to use this occlusion map to generate light maps for our spot lights. We’re nearly there!

 

Till then!

 

 

Leave a Reply


three × = 12

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