Based Logs #5 - Lighting Types in OpenGL
May 19, 2024
In the last blog I setup the lighting system in a way where we could support different types of lights. Our current implementation of a light is a basic point light. I’ll be adding support for directional lights, “advanced” point lights, and spot lights.
A directional light can be basically described as the sun. It gives an equal amount of light to each object from the same direction. It also doesn’t really matter how far away an object is from the sun, it has the same amount of light applied.
// We will be creating a new struct and new array for each one of
// the new lighting types we add.
struct DirectionalLight {
vec3 Direction; // Note we want direction instead of position
vec3 Ambient;
vec3 Diffuse;
vec3 Specular;
};
uniform DirectionalLight DirectionalLights[MAX_LIGHTS];
uniform int DirectionalLightCount; // In all cases we loop through all the lights and sum the value
// of the lighting result.
for(int i = 0; i < DirectionalLightCount; ++i) {
lighting += CalculateDirectionalLight(DirectionalLights[i]);
} These parts will all remain the same between each lighting implementation. The important part is the theory and the actual implementation of the function that calculates the light.
This function actually looks really similar to our original basic point light implementation. The only difference is how we calculate the lightDirection which is completely determined by direction that the light is facing.
vec4 CalculateDirectionalLight(DirectionalLight light) {
// ambient
vec3 ambient = light.Ambient * MaterialData.Ambient;
// diffuse
vec3 normal = normalize(Normal);
vec3 lightDirection = normalize(-light.Direction);
float diff = max(dot(normal, lightDirection), 0.0);
vec3 diffuse = light.Diffuse * (diff * MaterialData.Diffuse);
// specular
vec3 viewDirection = normalize(CameraPosition - FragPosition);
vec3 reflectionDirection = reflect(-lightDirection, normal);
float spec = pow(max(dot(viewDirection, reflectionDirection), 0.0), MaterialData.Shininess);
vec3 specular = light.Specular * (spec * MaterialData.Specular);
vec3 result = ambient + diffuse + specular;
return vec4(result, 1.0);
} We can get the direction of the light by extracting the direction from the transform. This is all we care about since for the sun the distance away from the object doesn’t really matter, it’s the direction.
glm::vec3 TransformComponent::GetDirection()
{
return glm::normalize(glm::vec3(GetTransform()[2])); // z direction
}
The lighting still looks pretty off because we aren’t actually casting any shadows. The important part is to consider that we have a light source that is giving off light to each object to our scene like the sun.
As mentioned the initial implementation of our light was a basic point light. One way we can make the point light feel more natural is by adding attenuation. The main idea is that lights don’t diminish linearly over a distance. We factor this into our calculation and the lights will appear to be more realistic. I don’t care to explain the formula details but I’ll give a snippet to my implementation.
vec4 CalculatePointLight(PointLight light) {
// ambient
vec3 ambient = light.Ambient * MaterialData.Ambient;
// diffuse
vec3 normal = normalize(Normal);
vec3 lightDirection = normalize(light.Position - FragPosition);
float diff = max(dot(normal, lightDirection), 0.0);
vec3 diffuse = light.Diffuse * (diff * MaterialData.Diffuse);
// specular
vec3 viewDirection = normalize(CameraPosition - FragPosition);
vec3 reflectionDirection = reflect(-lightDirection, normal);
float spec = pow(max(dot(viewDirection, reflectionDirection), 0.0), MaterialData.Shininess);
vec3 specular = light.Specular * (spec * MaterialData.Specular);
// attenuation
float distance = length(light.Position - FragPosition);
float attenuation = 1.0 / (light.Constant + light.Linear * distance + light.Quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
vec3 result = ambient + diffuse + specular;
return vec4(result, 1.0);
}
The last type of light we will be adding is a spot light. We can think of these as basically a flash light. I’m going to steal another diagram from LearnOpenGL to explain them.
SpotDirwe can think of the direction method we added in directional lightingLightDiris a vector pointing from the fragment to the position of the light sourceϕthe cut off angle where anything outside of this angle is not litθis the angle between theSpotDirandLightDir, it needs to be smaller thanϕto be inside the light
We calculate the dot product between SpotDir and LightDir and compare this with the cut off ϕ. The dot product result gives us a cosine value. So when we pass the cut off value into our shader we also want it to be a cosine value.
// I called it "Radius" instead of "cutoff" but I feel like the term radius doesn't make sense here?
_modelShader.SetFloat(Shader::Format(Shader::SPOT_LIGHTS, Shader::RADIUS, index), glm::cos(glm::radians(light.Radius))); vec3 lightDirection = normalize(light.Position - FragPosition);
float theta = dot(lightDirection, normalize(-light.Direction));
if(theta > light.Radius)
{
// do lighting calculations
}
else
{
// we are not inside of the light
} There is a concept of smooth/soft edges we need to consider. Anything outside of the light doesn’t just instantly go black but instead should have a fall off. The formula is described on the LearnOpenGL page if you want to read up about it.
vec4 CalcualteSpotLight(SpotLight light) {
vec3 lightDirection = normalize(light.Position - FragPosition);
// ambient
vec3 ambient = light.Ambient * MaterialData.Ambient;
// diffuse
vec3 normal = normalize(Normal);
float diff = max(dot(normal, lightDirection), 0.0);
vec3 diffuse = light.Diffuse * (diff * MaterialData.Diffuse);
// specular
vec3 viewDirection = normalize(CameraPosition - FragPosition);
vec3 reflectionDirection = reflect(-lightDirection, normal);
float spec = pow(max(dot(viewDirection, reflectionDirection), 0.0), MaterialData.Shininess);
vec3 specular = light.Specular * (spec * MaterialData.Specular);
// soft edges
float theta = dot(lightDirection, normalize(-light.Direction));
float epsilon = (light.Radius - light.OuterRadius);
float intensity = clamp((theta - light.OuterRadius) / epsilon, 0.0, 1.0);
ambient *= intensity;
diffuse *= intensity;
specular *= intensity;
// attenuation
float distance = length(light.Position - FragPosition);
float attenuation = 1.0 / (light.Constant + light.Linear * distance + light.Quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
vec3 result = ambient + diffuse + specular;
return vec4(result, 1.0);
}
Notice the circular pattern the light gives off
Here is the complete fragment shader file if anyone is interested in checking it out.