Megabyte Softworks
C++, OpenGL, Algorithms

Current series: OpenGL 3.3
(Return to list of OpenGL 3.3 tutorials)

17.) Spotlight

Hello guys! Welcome to 17th tutorial from OpenGL 3.3 and beyong series. In this one, we'll discover another type of light - spotlight. With knowing this type of light, we'll finally finish our arsenal of basic and most simple lights. Programming spotlight is a lot easier than you might think. Let's get into right into it.

What is spotlight? (just in case)

Spotlight is a light that comes from a single source, has some cutoff angle and creates a cone of light, that illuminates objects within it. Objects within cone that are closer to the spotlight gets illuminated more. Typical example of spotlight is flashlight (Screenshot from Doom 3):

In this tutorial, we'll try to program something similar. Not only flashlight, but also the dark scene - we'll set the ambient light intensity to very low.

Spotlight parameters

Try to think of parameters such spotlight must have. And I guess you're right - it's position, direction and cutoff angle. Is it all? Not really. We must define some spotlight attenuation parameters (or hardcode them in the shaders, but we don't want to) - i.e. how will light's strength (or intensity) fade with increasing distance. In this tutorial, I've created only linear attenuation for spotlight - the greater the attenuation, the shorter will our spotlight cone will be and this decrease will depend linearly on distance. And the last important parameter that our spotlight has is color, of course.

We create a spotLight.frag fragment shader, where spotlight definition and calculations are stored:

#version 330

#include_part

struct SpotLight
{
vec3 vColor;
vec3 vPosition;
vec3 vDirection;

int bOn;

float fConeAngle, fConeCosine;
float fLinearAtt;
};

vec4 GetSpotLightColor(const SpotLight spotLight, vec3 vWorldPos);

#definition_part

vec4 GetSpotLightColor(const SpotLight spotLight, vec3 vWorldPos)
{
if(spotLight.bOn == 0)return vec4(0.0, 0.0, 0.0, 0.0);

float fDistance = distance(vWorldPos, spotLight.vPosition);

vec3 vDir = vWorldPos-spotLight.vPosition;
vDir = normalize(vDir);

float fCosine = dot(spotLight.vDirection, vDir);
float fDif = 1.0-spotLight.fConeCosine;
float fFactor = clamp((fCosine-spotLight.fConeCosine)/fDif, 0.0, 1.0);

if(fCosine > spotLight.fConeCosine)
return vec4(spotLight.vColor, 1.0)*fFactor/(fDistance*spotLight.fLinearAtt);

return vec4(0.0, 0.0, 0.0, 0.0);
}

Our scene for this tutorial is renewed - whole scene is now dark, there is a tower with spiral staircase along which you can walk and on top of it, you'll find something lurking there, in the dark... (SpongeBob maybe ? No, he's afraid to go up there. Or something worse? Find it out yourself... ). Such dark scene is good to make spotlight effect clear. Spotlight is positioned around our camera and it's always walking around with us, as we were holding it in our right hand. But how to position it there, as if it was in our right hand? Well, we only need to move it a little down and a little to right. But not just subtract some constant on Y axis and add some constant on X axis - we must take camera's rotation into consideration. On Y axis, we'll just move down and vertical displacement is solved. But to move the flashlight horizontally to right, we'll take the cross product of camera's view and up vector to obtain a new vector that's perpendicular to them both and gives us the right vector to move spotlight horizontally along:

Another solution, that also works is to take vector v=(0.2, -0.2, 0.0, 1.0) and multiply it with view matrix and we'd get the same thing. Either way is fine, I chose the cross product to do so.

Now that he have spotlight position calculated, we must calculate spotlight direction. Practically, we want to point the spotlight to shine in front of us. To do it, we'll take a faraway point from camera's position in camera's view direction (in this tutorial it's 75.0 from camera's position, thus cCamera.vEye + (cCamera.vView-cCamera.vEye)*75.0 ) and then create a directional vector from spotlight's position to our created point.

What about linear attenuation? This will be relatively small constant, that can be changed during runtime using 'R' and 'V' key (starting at 0.017). Color of our spotlight is white. Cutoff angle is 15.0 by default, but you can change it using 'E' and 'C' key. And now, let's get finally into math behind.

Calculating fragment's color

To calculate each fragment's color, we must first know it's world coordinates. This is nothing difficult and you should already know it from previous tutorials (using smooth keyword). Then, we calculate the most important thing - angle between spotlight direction and fragment's position. If calculated angle is lesser than cutoff angle, this fragment will be somehow lit with spotlight. But do we really have to deal with angles? No, we don't. If we calculate the dot product between spotlight direction and vector from spotlight's position to the fragment, we'll get cosine of angle. So instead of transforming that cosine into actual angle using acos function, we'll rather compare it with cosine of cutoff angle. If calculated cosine is greater than cosine of cutoff angle, we'll take the fragment into consideration. If we're directly looking at fragment, angle is 0.0 and its cosine is 1.0. This is maximal value of cosine. If we have cutoff angle 15 degrees, its cosine is 0.96592, so all greater cosines mean that fragment is inside the cone. This is most difficult part of tutorial (and it's not any hardcore math) and I hope I explained it deep enough. Go through this paragraph again if you don't understand before proceeding, this is important to know.

After this we must calculate amount of color that's added to that fragment. Fragments that are closer to the middle of cone are lit more than fragments on the outer border of cone. Somehow we must interpolate color from the inside of cone to the outside from full color to none (or only weak color). And for this purpose, we'll use calculated cosines. Our cosines are in range from (cutoffangle, 1> and we need to map this range to <weak, full spotlight color>, or easier to interval <0, 1> and then multiplying spotlight's color with that value. We don't need necessarily to map to interval <0, 1>, it can be also something like <0.2, 1> so that the color doesn't fade out completely on borders - it's up to you to play with constants. How to do it? All you need is to calculate on "which part of way" calculated cosine is - i.e if calculated cosine is 0.95 and cosine ranges are <0.9, 1>, calculated cosine is on "halfway" and thus the "onway" factor is 0.5. Then we'll use this onway factor on interval to map to. So if we're mapping to interval <0.2, 1> with onway factor 0.5, result is 0.2+(1.0-0.2)*0.5 = 0.6. This concrete example should just trigger appropriate processes in your brain to understand how mapping one interval onto another bijectively (not important term, it just means that for every value in first interval there's exactly one value in second and every value from second interval is covered). But in general, the mapping code to calculate color's intensity factor is:

float fCosine = dot(spotLight.vDirection, vDir);
float fDif = 1.0-spotLight.fConeCosine;
float fFactor = clamp((fCosine-spotLight.fConeCosine)/fDif, 0.0, 1.0);

As you see, we also clamp this factor to 0.0 and 1.0, so that things like negative colors don't occur. But it's still not all. Last thing we'll consider is distance of fragment to spotlight's position. The attenuation factor is just multiplied by distance and final color is then divided by the attenuation, just like we did in Point Lights tutorial. If you want to, you can add exponentional and constant attenuation as well to create a better effect. I just used linear to make a homework for you to add exponentional and constant attenuation (or OK, the truth, I'm lazy ). Play with linear attenuation with 'R' and 'V' keys.

And this is all the math you need to know with spotlights. Now that we have the color from spotlight, we just add it to fragment's final color along with colors from any point lights and directional lights on the scene. I hope you didn't find it difficult and that this tutorial made it all clear.

If you have played games like Doom 3 (or Counter Strike), you'll be familiar with using spotlight. In this tutorial, you can also turn the spotlight on and off with F key. That's why spotlight also has boolean parameter for on and off. Try exploring the scene with your spotlight, even the darkest corners of it . Another new thing in this tutorial, that I won't explain in this article is code for cylinder creation. If you want to, you can have a look at it in static_geometry.cpp function. Or just know, that it creates cylinder and stores it in the VBO.

Result

Here is a traditional screenshot at the end of the article:

I hope you liked this tutorial and if there's something unclear from this thema, let me know via e-mail or comment. In next tutorial, you can expect reflections - I will create some mirrors probably, so don't forget to check it out when it's out. The model of strange creature on the top of tower was downloaded from http://www.turbosquid.com.

Name:

E-mail:
(Optional)
Entry:

Enter the text from image:

Smileys

 Gameengineer (stevemj@cox.net) on 10.08.2016 21:25:15 Great tutorial and yours gives a more realistic spot light result compared to other tutorials. Theirs just draws a perfect circle which appears screen aligned. Small thing but isn't the "smooth" qualifier unnecessary since its the default when no qualifier is specified? I'm no GLSL expert so I was wondering why you explicitly stated it for the "in" variables.
 iQmMabtx (gmq2ke1x@outlook.com) on 15.12.2015 15:51:59 To make the name more intuitive, we can retormulafe it as . It looks similar to integrating over times . Interestingly this forces us to integrate backwards . For the difference is uniform so it does not matter whether we are integrating or . But it does matter for other s. For example when , . Now the larger t is, the smaller becomes. For a symmetric function like a constant step inside a step function, this conforms with our intuition that the integral grows slower at larger values. For a non-symmetric one like the boundary between one step and another, this funny backward integration means the most recent points get the most credits, so the integral suddenly change shapes. I guess the same effect is achieved by the term in the original formula.
 Dmn3BT3t (50p0oveh5uu@hotmail.com) on 11.12.2015 22:00:25 An ineleligtnt answer - no BS - which makes a pleasant change
 kyrq4r7B (mz5mthl7yud@gmail.com) on 11.12.2015 21:17:06 Your answer was just what I nedeed. It's made my day!
Alex on 21.03.2013 12:22:15
Is that flashlight lights through objects?
Great tutorial, thanks.
 Michal Bubnar (michalbb1@gmail.com) on 29.04.2013 17:00:54 Basically yes, but you always see only closest object and that is lit, so it shouldn't happen. But you can possibly see two objects one behind other and both will be lit, and the light will pass through...