(Return to list of OpenGL 3.3 tutorials)
Download (2.16 MB)
4728 downloads. 3 comments
Hello, welcome to the 13th tutorial of OpenGL 3.3 series, which explains point lights. This article is written 2 months after tutorial's release, instead of announced 2 weeks . I'm sorry about it, but I had to finalize my Bachelor's study. But it's here now, in this very moment, so enjoy it .
Point light is a type of light that shines from a single spot (point) equally in all directions, so in contradiction to directional light, it doesn't have any specific direction. The typical example of point light is a light bulb. You shouldn't have a difficulty to imagine that, but here is a picture of light bulb, so that there are some pictures in this article :
As you can see on the picture, light from the bulb is scattered everywhere. So what will the programming of point light be about? As your intuition might tell you, we'll calculate distance of every fragment on scene from point light, and depending on this distance and point light parameters, we will add appropriate amount of color to that fragment. So what parameters, other than position, can a point light have? Let's have a look at struct in GLSL, in file pointShader.frag:
#include_part
struct PointLight
{
vec3 vColor; // Color of that point light
vec3 vPosition;
float fAmbient;
float fConstantAtt;
float fLinearAtt;
float fExpAtt;
};
vec4 getPointLightColor(const PointLight ptLight, vec3 vWorldPos, vec3 vNormal);
#definition_part
//. . .
Apart from the point light structure itself, you may have noticed a few new keywords - first is #include_part. Well, it's not an official GLSL keyword, but from this tutorial on, it will be used in my shaders and will indicate, that next lines of code contain struct and function DECLARATIONS (just like C++ header files), and when another keyword, #definition_part is found, the declaration part ends. Of course, GLSL shaders won't compile with these keywords, so when loading shaders using loadShader function, I had to change loading with keeping an eye onto these keywords. Third and last new custom keyword is #include. What it does is that it includes the content from include part of another shader, so put even simpler, it copies the declarations from another shader file (absolutely same concept as C++ header files). Separating effects into multiple shader files makes the shader code more readable and structurized and now with these new keywords, it's really easy to combine shaders together. You can have a look in shaders.cpp to see how the parsing of these keywords is implemented - I used C++ STL stringstream, as I found it as most comfortable way to do this kind of stuff.
But now let's get back to point light structure. As we can see, we have in addition to position a color parameter, that represents the color of that light, respectively and 3 attenuation variables - constant, linear and exponential. What's the attenutation? If english is not your native language and you translate the word into your own language, you'll get what it means - how the light's participation in final color of each fragment attenuates (or weakens) according to distance. The greater the distance from fragment to point light is, the less point light color is added to the fragment color (we'll get into more detail later). This is kind of intuitive, that the thread in a bulb is so shiny, that you cannot look at it directly (because it's closest to light's position, or we can tell that it's light's position), but illuminated places aren't that extremely shiny, because they are further from light's position.
Now let's get to the main topic - the math behind point lights. So what should we do exactly? For each fragment on the scene, we have to calculate its distance from point light and then do something with it. OK, that's correct, but first we have to assure that we're working in same coordinate-space with fragments and with point lights. The most natural way to store point light's position is in world coordinates. So in order to calculate the distance correctly, we'll create a smooth type variable, that will give us world positions of each fragment. That's the only tricky part with point lights (and it's not even that tricky ). Or?
Here is the shader code, that calculates the color, that point light will participate on fragment's color with:
#include_part
struct PointLight
{
vec3 vColor; // Color of that point light
vec3 vPosition;
float fAmbient;
float fConstantAtt;
float fLinearAtt;
float fExpAtt;
};
vec4 getPointLightColor(const PointLight ptLight, vec3 vWorldPos, vec3 vNormal);
#definition_part
vec4 getPointLightColor(const PointLight ptLight, vec3 vWorldPos, vec3 vNormal)
{
vec3 vPosToLight = vWorldPos-ptLight.vPosition;
float fDist = length(vPosToLight);
vPosToLight = normalize(vPosToLight);
float fDiffuse = max(0.0, dot(vNormal, -vPosToLight));
float fAttTotal = ptLight.fConstantAtt + ptLight.fLinearAtt*fDist + ptLight.fExpAtt*fDist*fDist;
return vec4(ptLight.vColor, 1.0)*(ptLight.fAmbient+fDiffuse)/fAttTotal;
}
Let's go through the getPointLightColor function line by line. It takes following parameters: PointLight structure (with const modifier to tell compiler that we won't change its value inside the function, so that it can make some optimizations), world position (of fragment), and normal vector in that fragment. Now, I realize I haven't mentioned it before - so this is another parameter we need to take into consideration - the same concept as in directional light applies. As you can see, we have a fragment-to-pointlight vector stored in variable vPosToLight. And we need to calculate diffuse part of light contribution depending on dot product of normal in the fragment and that vector (explained in more detail in Tutorial 8 - Simple Lighting). So how do we get normal for each fragment? Answer is easy - the same way we get a world position in each fragment, using smooth modifier. However, don't forget to transform normals properly using normal matrix and then interpolate them (this is important when you rotate objects). And that may be another tricky part of point lights, but you and normal transformation should be great friends already .
When we have a vector to point light, distance of fragment to point light (length of that vector, respectively), and difusse part calculated, we can calculate final attenuation factor - the greater this number is, the less will light participate on fragment's color with its color (so instead of multiplying with this number, we divide with this number). This is how the attenuation factor is calculated:
Somewhere you may see this as well:
which is the same thing, but it is its inverse value, so you'll multiply by this number, instead of division. I choose to calculate attenuation factor as shown in first equation, and then divide final color with this number. This equation is not difficult - every point light has a CONSTANT attenuation, that always contributes a little, not depending on the distance, LINEAR, which depends on distance linearly, and QUADRATIC (or EXPONENTIAL, even though quadratic is more precise), which takes square of distance, and so this value should be very small. Thanks to constant attenuation, we won't divide by 0, because all factors should be greater than 0 and distance is always positive. However, you can change values to negative if you want, but your graphics card will probably explode... just kidding, try it, it creates interesting effects . After we have it, we simply divide the calculated color with this factor. You can experiment with Constant, Linear, and Exponential factor in this tutorial using keys O, P, K, L, N, and M. And that's it!
This is what the scene looks like:
Even though some people, like me, are scared when they hear LIGHTING USING SHADERS for the first time, if you look into it, it's not really that difficult. Or am I the only one who felt like that ? Either way or another, now you should have a basic understanding of point lights. If you want to play with the code, try to add 2 point lights to scene. If you still have time, try to add 3 .
Next tutorial will finally introduce geometry shaders and I will also create a class for each effect (directional light, point light and fog so far), because the rendering code is starting to get messy.
Download (2.16 MB)
4728 downloads. 3 comments
JOno Green (JonoGreen@spank.com) on 14.12.2014 05:01:16 |
Loved it, thx! |
Problem on 21.08.2014 21:37:44 |
I have two spheres in the scene and the another should be the light source but when I run the program, both of the spheres are fully lit even if the other sphere should be dark in the opposite side where the light source sphere is...Any idea what's wrong? |
jeefo on 13.07.2013 23:27:11 |
but your graphics card will probably explode... nice joke :D I almost believed haha. I know CPU detect divide by zero, but I don't know GPU can detect or not :P Finally reduce renderScene. "rendering code is starting to get messy" Thank you very much :) |
1