Hello guys and welcome to the 21st tutorial of my OpenGL4 series! This one will teach you specular lighting, which is the final part of basic light model and makes our objects look even more realstic and interesting. As for article, I have actually re-used texts I have written in my older OpenGL 3 Tutorial Series, because I found out that they still hold true and are actually pretty good . So let's go !
So far, we have been dealing with two components of light - ambient and diffuse. To recap: ambient component is the component of light that is constantly present with specific intensity and represents light that has been scattered all over the place and provides constant illumination. Diffuse component requires a little calculation and depends on the angle between surface normal and direction of light. So far, this is all that we've achieved.
But today, we are going to add yet another part - specular part. This is a part, that is responsible for creating shiny effects on materials like metal, but only if we look at the object from specific place. And that's where another variable comes to play - specular part doesn't depend only on normal, but also on material properties of object and most importantly - position of the viewer!
Let's get into more detail. Camera (the player, you) looks at the object. Because our light has some specified direction, it hits the object and the object reflects light back depending on the normal. The angle between incident vector (vector that comes from light source and hits the object) and normal is same as angle between reflected vector and normal. The following illustration shows this pretty clearly:
I is the vector that is coming from the light source to the surface. However, I used negated vector I, so that I would align it properly with reflected vector R. Original incident vector points to the surface. The angle Thetai and Thetar are identical. Now, the known variables are these - vector I and vector N. Our goal is to find vector R. This will be the first task to calculate specular reflection.
There is an old saying, that my parents used to tell me when I was younger. It was something like this: "Son, wherever you'll be in trouble and you'll need to calculate reflected vector, it's this:"
('*' stands for regular multiplication and '.' stands for dot product). If you really thought, that my parents taught me this, they didn't . This is the equation you can find anywhere around the internet. Now there are two approaches to this - either you just learn it, memorize it and believe it works, or you will try to understand how could someone come with such an equation. In the following lines, I will do my very best to explain this.
The trick here is to complete a parallelogram using the incident and reflected vertex. This parallelogam will help us understand several things, so now a picture is worther than thousands of words written here:
You may notice several things here. First, there is a mysterious vector X, which goes from the point of impact to the center of parallelogram. If we continue further in that direction, we will notice, that at the endpoint of the parallelogram we actually have 2X. But how do we get X? Really easily . X is just vector -I projected on normal vector. This means, that the dot product of -I and N results in a number (a scalar), which basically says how much we need to multiply N to get to the center of the parallelogram. This way, we get vector X (not mysterious anymore ).
Now some really basic vector additions are remaining. We can see, that following equation holds:
From my previous arguments we can conclude, that:
If we replace X in the equation 1, we get:
Finally, doing some basic rearrangements of equation, we will get the formula I mentioned at the beginning (the one from the old saying ):
Now that's really all that I can say to reflection equation and I hope, that now you have an idea how did smart people came up with something like this .
Some notes though: you should always normalize the reflected vector to get unit vector. If you do a reflection with a normalized normal vector, then you're completely fine - the reflected vector will be of SAME LENGTH as incident vector. This fact just holds, if you look at all the previous pictures, it's obvious . If the normal isn't of unit length (which it never should be), the reflected vector won't have proper length of course.
With the reflected vector in our hands, we can finally go to some more theory behind specular highlights on objects.
Specular highlights on objects appear, because the light reflected from the object hits viewer. Sometimes, we are hit directly by the light and the highlight appears to be very intense, and sometimes we are hit so slightly, that we basically don't see the specular highlight. The strength of this highlight depends on cosine of angle between the reflected vector and vector taken from the point at object to the viewer's position:
Now you can see it all clearly. In addition to the reflected vector, we need to calculate vector from the object's point to the viewer and then we can calculate cosine of angle between these two vectors using dot product. You MUST make sure that both vectors are normalized to get correct cosine. The resulting value is called specular factor. However, the specular factor isn't everything that has a role in specular highlight calculation. The highlight depends on yet another factor - material properties. In this world, we have many different materials and not all of them shine. Take wood for example. Can you imagine shining wood? In real world, I cannot, but we can program one of course .
Jokes aside, there are basically shiny materials, like gold, metal, silver, and dim materials, like wood, fabrics and so on. In old good OpenGL, you could set material properties with functions like glMaterialfv. There were several properties to set, but still, it was fixed pipeline and what OpenGL developers gave you, only that was possible. Today, we will implement specular highlight with two most basic material properties (included in every lighting model I've seen so far) - specular power and specular intensity.
Specular power is the number, that we exponentiate specular factor to. It's usually ranging from 1 to 128 at max (I think these were the limits in old OpenGL, but in shaders you can work with arbitrary numbers of course and go beyond these limits). Why would we do this? Without doing this, the specular highlight would be too big, too unrealistic and too wide. Just think about it. Specular factor is a number ranging from 0 to 1 and it's obtained by cosine of the angle. The cosine of 0 degrees is 1 and at 45 degrees it's still 0.707. This means, that the specular highlight would be too powerful looking from even pretty big angles like 45 degrees. By exponentiating this number to a power like 32, we will get significantly lower number. For the record, 0.70732 is already really small number - 0.000015185, which means that the highlight will be practically not visible from this angle.
However, if we aren't too askew from the reflected vector and the cosine of angle is a number close to 1 (let's say 0.97), the resulting value would be 0.9732 = 0.377, which is a reasonable number and that will make the highlight visible! So by playing with power, you can change the size and intensity of specular highlight. Try it and play around with this number. See for yourself, that if you set the power to 1, the highlight is no good, it's just a mess . I also allow you to go to negative values to see what happens if you raise specular factor to negative powers. It's just fun .
Specular intensity is now a really, really simple thing in comparison to specular power. After you raised specular factor to specular power, specular intensity just linearly controls the intensity of calculated highlight in a linear gashion. So if we calculated highlight was for example 0.8 and our specular intensity is 0.5, the very final value will be 0.8 * 0.5 = 0.4. There is really nothing difficult here.
I hope, that now it is clear to you, what specular factor, power and intensity are. With the knowledge you should have now, we can move to actual code in the shaders.
All of the code is inside the specularHighlight.frag, so let's have a look at this shader:
#version 440 core
#include "diffuseLight.frag"
#include_part
struct Material
{
bool isEnabled;
float specularIntensity;
float specularPower;
};
vec3 getSpecularHighlightColor(vec3 worldPosition, vec3 normal, vec3 eyePosition, Material material, DiffuseLight diffuseLight);
#definition_part
vec3 getSpecularHighlightColor(vec3 worldPosition, vec3 normal, vec3 eyePosition, Material material, DiffuseLight diffuseLight)
{
if(!material.isEnabled) {
return vec3(0.0);
}
vec3 reflectedVector = normalize(reflect(diffuseLight.direction, normal));
vec3 worldToEyeVector = normalize(eyePosition - worldPosition);
float specularFactor = dot(worldToEyeVector, reflectedVector);
if (specularFactor > 0)
{
specularFactor = pow(specularFactor, material.specularPower);
return diffuseLight.color * material.specularIntensity * specularFactor;
}
return vec3(0.0);
}
Remember - the #include_part and $definition_part is just my custom shaders extension to be able to combine multiple shaders. First things first - the Material struct holds all the parameters for a material. It holds the specular intensity and power and there is also one boolean to be able to turn the specular highlight on and off in a simple way (in some real graphics engine, I would probably not do it this way though).
The more important part is in the getSpecularHighlightColor method. Let's have a look at its parameters:
First condition just checks, if material is enabled. If not, we quit (we don't do any calculations). Next line calculates the reflected vector using the reflection equation explained sooner. If you're looking for it in the code, the equation is actually hidden in the GLSL built-in function reflect. This function takes exactly two parameters you would expect - incident vector and normal vector. I also normalize the resulting vector.
After that, we have to calculate specular factor and for this we need the worldToEyeVector - which is exactly the point-to-viewer vector. Having those two vectors, we calculate specularFactor using the dot product.
Now notice, that we continue the calculations only in case that specular factor is greater than zero. Why? Because if it's less than zero, this means we are looking at the object from behind, so it shouldn't be lit at all. Moreover, negative number raised to powers would result in either negative or positive number, depending on parity of specular power, which is weird anyway. And finally, you don't want the specular part of the light to be negative - it would just make object darker, which doesn't make any sense at all. If our specularFactor is indeed greater than 0, we just raise specular factor to specular power of provided material and add it to the resulting color, scaled by specular intensity, again provided by material. That's all .
In the C++ code, I have created the corresponding struct for materials.
Implemented specular highlight looks like this:
And I think it's very nice . In the end, if you break down all those equations into smaller pieces, they are actually quite logical and not such a headache. I really hope that my explanations have helped you to understand this technique and see you in the next tutorial!
Download 6.45 MB (918 downloads)