Support me!

023.) Point Lights

Hello again and I welcome you to my 23rd tutorial of my OpenGL4 tutorial series! In this one we will explore point lights - simply a light with some position and some strength, like a light bulb. How to program it and what we need - you can find all this out if you keep reading this article .

Point light is a type of light that shines from a single spot (point) equally in all directions, so unlike directional light, it doesn't have any specific direction. The typical example of point light is a light bulb. Here is a picture of light bulb, so that there are some pictures in this article :

As you can see, the bulb emits light to all directions. And the common sense tells us, that the further any point from the light is, the less it will be illuminated. Sounds easy, right? And that's exactly what we're going to program! Let's have a look at the point light shader structure, that holds all its data:

struct PointLight

{

vec3 position;

vec3 color;

float ambientFactor;

float constantAttenuation;

float linearAttenuation;

float exponentialAttenuation;

bool isOn;

};

{

vec3 position;

vec3 color;

float ambientFactor;

float constantAttenuation;

float linearAttenuation;

float exponentialAttenuation;

bool isOn;

};

Let's examine what they're about:

- position - represents position of the point light
- color - color of the point light
- ambientFactor - even though it's a point light, its light might be already scattered in the whole world, so this is exactly like a constant contribution to the global illumination. It's not obligatory to have this attribute, it's just my implementation of point light (in fact, there is no "one and only correct" implementation, you can customize your code as much as you want as long as you achieve the desired effect )
- constantAttenuation - represents constant
**attenuation**of the light (attenuation is simply how fast light weakens with rising distance), but because this is constant attenuation, distance has no effect on it - linearAttenuation - represents linear attenuation of the light, so it rises in a linear fashion with rising distance
- exponentialAttenuation - represents exponential attenuation of the light so it rises in an exponential fashion with rising distance
- isOn - simple boolean saying if light is on or off (this is again not something completely necessary, but sometimes it might be easier to just turn light off rather than remove it completely)

I really hope you got the point of those attributes, now let's have a look at math behind the point lights .

Looking at the parameters described above, we will need two things for the equation - directional vector of how light shines on the fragment and the distance from the illuminated fragment to the light. With those two values, we can calculate diffuse factor and the total attenuation of the light depending on the distance. Below is the shader code fragment, that does the whole calculation:

vec3 getPointLightColor(const PointLight pointLight, const vec3 worldPosition, const vec3 normal)

{

if(!pointLight.isOn) {

return vec3(0.0);

}

vec3 positionToLightVector = worldPosition - pointLight.position;

float distance = length(positionToLightVector);

positionToLightVector = normalize(positionToLightVector);

float diffuseFactor = max(0.0, dot(normal, -positionToLightVector));

float totalAttenuation = pointLight.constantAttenuation

+ pointLight.linearAttenuation * distance

+ pointLight.exponentialAttenuation * pow(distance, 2.0);

return pointLight.color * (pointLight.ambientFactor + diffuseFactor) / totalAttenuation;

}

{

if(!pointLight.isOn) {

return vec3(0.0);

}

vec3 positionToLightVector = worldPosition - pointLight.position;

float distance = length(positionToLightVector);

positionToLightVector = normalize(positionToLightVector);

float diffuseFactor = max(0.0, dot(normal, -positionToLightVector));

float totalAttenuation = pointLight.constantAttenuation

+ pointLight.linearAttenuation * distance

+ pointLight.exponentialAttenuation * pow(distance, 2.0);

return pointLight.color * (pointLight.ambientFactor + diffuseFactor) / totalAttenuation;

}

As you can see, first if condition is just checking if the light is on. If it's not, simply return completely black color (so this light does not contribute to the final illumination at all). Second step is calculation of the position to the light vector. This vector serves two purposes - first, we can get the distance of the fragment to the light, second we can calculate the **diffuse factor** of the point light (here applies same logic as explained in the tutorial 14.) Normals and Diffuse Lighting - the more directly the light shines on the fragment, the more it gets illuminated).

The last step is to calculate the total accumulated attenuation of the light depending on its distance from the fragment. The equation for the attenuation is following:

aC is constant attenuation, aL is linear attenuation, aQ is exponential attenuation (quadratic) and d is our calculated distance of the fragment to the point light. This equation corresponds to the variable totalAttenuation in the shader fragment code. Now that we have all the needed values, the final color of the fragment is simply the point light color multiplied by the ambient and diffuse factor summed up and divided by the total attenuation .

I was actually thinking if the ambientFactor should be divided by attenuation too, but I came to conclusion that it's just fine. Because even if point light contributes to the ambient illumination, I think it should fade with the distance. One night lamp in a city simply won't illuminate the whole city, just the particular street it is on .

The final step is to use the getPointLightColor function in the main shader - here is the most important excerpt of the code:

// ...

uniform AmbientLight ambientLight;

uniform DiffuseLight diffuseLight;

uniform Material material;

uniform PointLight pointLightA;

uniform PointLight pointLightB;

uniform vec3 eyePosition;

void main()

{

vec3 normal = normalize(ioVertexNormal);

vec4 textureColor = texture(sampler, ioVertexTexCoord);

vec4 objectColor = textureColor*color;

vec3 ambientColor = getAmbientLightColor(ambientLight);

vec3 diffuseColor = getDiffuseLightColor(diffuseLight, normal);

vec3 specularHighlightColor = getSpecularHighlightColor(ioWorldPosition.xyz, normal, eyePosition, material, diffuseLight);

vec3 pointLightColorA = getPointLightColor(pointLightA, ioWorldPosition.xyz, normal);

vec3 pointLightColorB = getPointLightColor(pointLightB, ioWorldPosition.xyz, normal);

vec3 lightColor = ambientColor + diffuseColor + specularHighlightColor + pointLightColorA + pointLightColorB;

outputColor = objectColor * vec4(lightColor, 1.0);

}

uniform AmbientLight ambientLight;

uniform DiffuseLight diffuseLight;

uniform Material material;

uniform PointLight pointLightA;

uniform PointLight pointLightB;

uniform vec3 eyePosition;

void main()

{

vec3 normal = normalize(ioVertexNormal);

vec4 textureColor = texture(sampler, ioVertexTexCoord);

vec4 objectColor = textureColor*color;

vec3 ambientColor = getAmbientLightColor(ambientLight);

vec3 diffuseColor = getDiffuseLightColor(diffuseLight, normal);

vec3 specularHighlightColor = getSpecularHighlightColor(ioWorldPosition.xyz, normal, eyePosition, material, diffuseLight);

vec3 pointLightColorA = getPointLightColor(pointLightA, ioWorldPosition.xyz, normal);

vec3 pointLightColorB = getPointLightColor(pointLightB, ioWorldPosition.xyz, normal);

vec3 lightColor = ambientColor + diffuseColor + specularHighlightColor + pointLightColorA + pointLightColorB;

outputColor = objectColor * vec4(lightColor, 1.0);

}

In this tutorial, we have two point lights - point light A and point light B. Of course this is not very dynamic, but it's sufficient to demonstrate the effect . As you can see, we simply calculate color contribution of the both point lights and add them to the final color, that's it.

In the C++ code, I have created new shader struct that holds the point light data for easy access. Check the renderScene method to see the whole setup of the point lights.

This is the result that we have achieved with all that I've explained above:

I think it looks very nice . It's not perfect nor photo-realistic, but it demonstrates the basics of point light illuminations. Or as a classic would say:

So that's it for today! I really hope that you've enjoyed this tutorial and that you have learned a thing or two today .