Support me!
If you enjoy these webpages and you want to show your gratitude, feel free to support me in anyway!
Like Me On Facebook! Megabyte Softworks Facebook
Like Me On Facebook! Megabyte Softworks Patreon
Donate $1
Donate $2
Donate $5
Donate $10
Donate Custom Amount
22.) Specular Lighting
<< back to OpenGL 3 series
Changelog

28.7.2013 - Improved specular lighting calculation shader a little, that should improve performance by not doing some unnecessary calculations.

</Changelog>

Hello guys! This is the 22nd tutorial from my series, this one teaches specular lighting, which is the final part of basic light model and also how to display normals of an object using geometry shader. Normal displaying can be good for many purposes, for example some debugging of rendering stuff. So let's get down to business.

Specular Lightning

So far, we have been dealing with two parts of light - ambient and diffuse. To recap: ambient part is the part 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 part requires a little calculation and depends on the angle between surface normal and direction of light. 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 some deeper details. Camera (the player, you, whatever ) 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 betweem 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.

Reflection equation

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 think, 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 ):

So we passed the point, where we all should understand the reflection equation and how it was developed. Some notes though: you should always normalize the resulting vector to get unit vector. The vector that is being reflected will be reflected by a vector with SAME LENGTH (supposing that the normal vector is unit vector, if the normal isn't of unit length, reflected vector won't have proper length), so if you normalize your incident vector before applying reflection equation, you should be fine even without normalizing after. But never forget to normalize a normal ok? This fact just holds, if you look at all the previous pictures, it's obvious. I have even brute-force checked it, i.e. I generated ten thousands of random vectors and normalized normals to see, if after applying reflection equation they would have same length, and they all did . I bet, there is some really easy way to prove this mathematically, but I don't really know at this very moment .

With the reflected vector in our hands, we can finally go to some more theory behind specular highlights on objects.

Specular Highlights

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 oject 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. Now, 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 all that is playing role in specular highlight. The highlight depends on 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 it of course .

Material properties

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 we will only play with two, that are really basic and 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 the 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. So if we calculated highlight was for example 0.377 and our specular intensity is 0.5, the very final value will be 0.377 * 0.5 = 0.1885. There is really nothing difficult here.

I hope, that now it is clear to you, what Specular Factor, Specular Power and Specular Intensity are. With the knowledge you should have now, we can move to actual code in the shaders.

Shader Code

All of the code is inside the dirLight.frag, because the light we work with in this tutorial is only classic directional light. This direction of light is going to be incident vector in our case. We need to define material class (structure), which will hold two data only - specular intensity and specular power. And then, we will create a function, that will calculate resulting specular color for each fragment:

#version 330

#include_part

// ...Directional light class definition here...

struct Material
{
   float fSpecularIntensity;
   float fSpecularPower;
};

vec4 GetSpecularColor(vec3 vWorldPos, vec3 vEyePos, Material mat, DirectionalLight dLight, vec3 vNormal);

#definition_part

// ...Directional light function definition here...

vec4 GetSpecularColor(vec3 vWorldPos, vec3 vEyePos, Material mat, DirectionalLight dLight, vec3 vNormal)
{
   vec4 vResult = vec4(0.0, 0.0, 0.0, 0.0);
   
   vec3 vReflectedVector = normalize(reflect(dLight.vDirection, vNormal));
   vec3 vVertexToEyeVector = normalize(vEyePos-vWorldPos);
   float fSpecularFactor = dot(vVertexToEyeVector, vReflectedVector);
   
   if (fSpecularFactor > 0)
   {
      fSpecularFactor = pow(fSpecularFactor, mat.fSpecularPower);
      vResult = vec4(dLight.vColor, 1.0) * mat.fSpecularIntensity * fSpecularFactor;
   }
   
   return vResult;
}

Let's go through the GetSpecularColor line by line. First of all, let's go through parameters:

  • vec3 vWorldPos - position of fragment in world coordinates
  • vec3 vEyePos - position of viewer, i.e. position of eye
  • vec3 vNormal - Normal at this point
  • Material mat - material used at this fragment
  • DirectionalLight dLight - directional light used to lit up the scene

In first line, we just create the result variable with black color. Next line calculates the reflected vector. You may now wonder - where is the reflection equation taking place? The correct answer is - in this line , however it is hidden in GLSL built-in function reflect. This function takes exactly two parameters you would expect - incident vector and normal vector. After doing this, I normalize the result in case that the incident vector wasn't normalized before. After this we must calculate vector from the object to the position of viewer. This is done in next line and the vector is normalized as well.

We have all the vectors needed, now we can finally calculate specular factor using reflected vector and point-to-viewer vector. Now notice, that we continue 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. After all checks, 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 created separate class for materials. In shader, materials are treated as uniform variables, so don't forget to set them before rendering!

That does it, all of the important stuff has been explained. Now let's move onto completely different part of the tutorial - rendering object normals.

Rendering Object Normals

I don't really know why at this very moment (it's 28th July 2014), why have I decided to implement rendering of normals into this tutorial, because it's completely unrelated with specular lighting . But what has been done is done now, so I will explain it here. Maybe it is, because the concept is really not that difficult that it would require separate dedicated tutorial. So listen now .

This is really easy thing to do. What we want to do is to render normals per vertex. So we create a shader program just for that! This shader program will need vertex, geometry and fragment shader. The key idea is to send vertex positions and normals, geometry shader will transform this to the line segment with starting point at vertex position and ending point at vertex position plus associated normal. Fragment shader just outputs white color. That's all .

I won't even show you vertex and fragment shader here, because vertex shader just sends data further to geometry shader, and fragment shader just outputs white color, nothing else. Only interesting thing at this is the geometry shader, which is shown below:

#version 330

layout(points) in;
layout(line_strip, max_vertices = 2) out;

in vec3 vNormalPass[];

uniform float fNormalLength;

uniform struct Matrices
{
	mat4 projMatrix;
	mat4 modelMatrix;
	mat4 viewMatrix;                                                                           
	mat4 normalMatrix;
} matrices;


void main()
{
  mat4 mMVP = matrices.projMatrix*matrices.viewMatrix*matrices.modelMatrix;
  
  vec3 vNormal = (matrices.normalMatrix*vec4(vNormalPass[0]*fNormalLength, 1.0)).xyz;
  //vNormal = normalize(vNormal);
  vec3 vPos = gl_in[0].gl_Position.xyz;
  gl_Position = mMVP*vec4(vPos, 1.0);
  EmitVertex();

  gl_Position = mMVP*vec4(vPos+vNormal, 1.0);
  EmitVertex();

  EndPrimitive();  
}

This whole shader program has only one additional uniform variable (besides matrices, they are ubiquitous ) - fNormalLength, i.e. length of rendered normal. It's just the line segment length, or how long should the rendered normal be. Before reverting any transformations on the normal with normal matrix, we multiply the incoming normal by this length. Rest of the shader should be really self-explanatory, nothing new / difficult there.

Result

This is how the result looks like:

I hope you guys enjoyed this tutorial again and that it helped you to expand your OpenGL horizons . Now you should be able to program pretty nice lighting effects throughout the scene, specular highlights will definitely add another bit of realism to this. If there is something unclear to you, don't hesitate to leave comments or e-mail me, as usual .

Funny thing is, that this article was written 5 months after the tutorial was uploaded. At the time of its creation I was extremely busy with university / work stuff, so I postponed to some later time. I didn't expect it to be this long though .

Download 7.18 MB (3888 downloads)