Up until around 2011 Blinn-Phong was by far the most popular lighting technique in real-time applications. With an ambient, diffuse and specular contribution this per-fragment shader is both computationally cheap and of a high quality. The full source code can be found here.
The Pros
Essentially Gouraud shading done in the fragment shader, since neither fragment colours nor surface normals are interpolated across vertices, fixing the visual artifacts that Gouraud caused, offering much more accurate lighting and allowing for smaller specular dots whilst remaining relatively computationally cheap.
The Cons
Aside from being slightly more expensive than Gouraud there are few cons with the technique in itself. However compared to more modern alternatives that follow physically based calculations Blinn-Phong offers too much user control that, when set up incorrectly results in an unnatural and physically incorrect light, and also lacks certain properties of a light that will be outlined later on.
The Code
From here on out we won't explain the vertex shader as everything contained in there has been explained in the Gouraud Shading page already. There is a link to the source code for the vertex shader at the top of this page so that you know what you will need.
(Please note code comments are removed from these snippets)
#version 330
in vec3 position;
in vec3 uNormals;
in vec2 texCoord;
layout (location = 0) out vec4 FragColour;
uniform vec3 camPos;
uniform sampler2D textureMap;
#define NUM_LIGHTS 2
uniform struct Light
{
vec3 lightPos;
vec3 lightColour;
float shininess;
float glossiness;
float specularCoefficient;
} light[NUM_LIGHTS];
As with all shaders we start with the setup stuff. Firstly we tell the compiler to compile for GLSL version 330, or OpenGL 3.3 (The chosen version number for the demo project).
Then there are the uniforms. These are the values we have passed in from the CPU. Here we have the position of our camera, which can be hard coded however in this demo we are able to move around in the world space, and our texture map, set as type sampler2D, meaning 2D texture.
There is also a uniform for a structure, containing many values. This structure holds all the properties of a light and allows for multiple lights to be passed into the shader up to the defined amount.
And finally there is the output value. Note that this will usually be seen as "out vec4 FragColour" however the layout location refers to which buffer in the Frame Buffer Object (FBO) this will be saved to if multiple outputs are required. If a custom FBO isn't being used then location 0 will simply refer to the default output so it's good practice to just write it like that.
void main()
{
float ambientCoefficient = 0.01;
vec3 normals = normalize(uNormals);
vec3 camDir = normalize(camPos - position);
vec3 diffuse = vec3(0.0);
vec3 specular = vec3(0.0);
vec3 ambientColour = vec3(0.0);
vec4 texture = texture(textureMap, texCoord.xy);
So onto the main function. Since all the vertex stuff has already been explained and the lighting code we'll be using is extremely similar to that of the Gouraud shader we can go into more detail about what's actually going on here!
So Blinn-Phong follows the calculation: (Ka * Ia) + (Kd * (Il * (N.L))) + (Ks * (Il * ((N.H)^Ns))) which essentially translates to ambient + diffuse + specular. We'll be defining these specific values as they come. Here we are initialising values we'll be using later, setting up the cameras direction vector that will be used in all light calculations and reading the texture file. Again nothing we haven't seen before.
What is new is that we normalize the surface normals here in the fragment shader. In doing this, surface normals are interpolated linearly between fragments, removing the per-vertex artifacts of Gouraud and smoothly transitioning between each other to feign a rounded surface so flat lighting doesn't occur. This is what makes the low polygon sphere look perfectly round.
for (int i = 0; i < NUM_LIGHTS; i++)
{
vec3 lightDir = normalize(light[i].lightPos - position);
vec3 halfAngle = normalize(camDir + lightDir);
float NdotL = clamp(dot(normals, lightDir), 0.0, 1.0);
float NdotH = clamp(dot(normals, halfAngle), 0.0, 1.0);
vec3 D = light[i].glossiness * (light[i].lightColour * NdotL);
float specularHighlight = pow(NdotH, light[i].shininess);
vec3 S = light[i].specularCoefficient * (light[i].lightColour * specularHighlight);
diffuse += D;
specular += S;
ambientColour += light[i].lightColour;
}
Back to the loop. Just to note the lighting doesn't actually have to be in a loop, but if you want multiple lights in your scene, and I'm pretty sure you would, you need to add the sum of all the lights together. So one light per loop.
Similarly to the camera direction vector we calculate the current light direction vector and use both to find the half way angle between the two. This is where the perceived light is reflected and will be used for the specular contribution.
The half angle is where Phong and Blinn-Phong differs. Originally in the Phong calculation used the dot product between the lights reflected angle and the viewer (R.V) instead of the dot product between the half angle and the surface normal (N.H) which is a simpler and faster calculation, as the half angle only needs to be calculated once whereas the reflection angle would need to be recalculated.
Diffuse
The diffuse contribution is simple Lambertian. We use the dot product between the surface normal and the light direction to get a value between 1 and -1 (clamped to 1 and 0 as anything below 0 would be facing away from the light). This essentially acts as a multiplier and means a normal that faces directly towards the light will be most brightly lit. Lambertian is a very common method for calculating the diffuse component due to its simplicity.
We multiply this value by the lights colour so that the result gets its desired tint and then again by the diffuse coefficient, or glossiness. This all adds up to our diffuse contribution or (Kd * (Il * (N.L))) where Kd is the coefficient of the diffuse (glossiness), Il is the intensity of the light (lightColour) and N.L is the dot product between the normal and the light direction.
Specular
(Ks * (Il * ((N.H)^Ns))) is the specular contribution here and handles the bright dot that simulates the direct reflection of the light source. As previously mentioned we need the half angle between the light direction and the view direction for the specular dot in Blinn-Phong. We use the dot product again to obtain our intensity falloff and this time multiply it to the power of Ns. Ns is the shininess and the bigger it is, the smaller yet more intense the specular dot will be. This value will usually be in the hundreds. Similar to the diffuse we multiply it by the intensity of the light to give it the correct colour and then multiply that result by the coefficient of the specular.
Finally we send these two values up outside the loop, adding the result to the current values. This is so we have the sum of all the lights if two or more were to overlap. We also add the colours together for the colour of our ambient light and send that out of the loop.
vec3 ambience = ambientCoefficient * ambientColour;
vec3 BlinnPhong = ambience + diffuse + specular;
vec3 final = texture.rgb * BlinnPhong;
final = pow(final, vec3(1.0 / 2.2));
FragColour = vec4(final, 1.0);
Ambience
The ambient contribution is a very simple one at (Ka * Ia) yet serves a very important role. Ambient light is responsible for simulating global illumination and acts as all indirect light, i.e all light that bounces off every other surface to reach fragments that aren't directly affected by the light. Since doing this properly using raytracing would be extremely expensive, especially in outdoor environments, a solid colour contribution applied to every fragment is used instead. the intensity of the ambient light, or Ia, is the colour of the ambient light. We choose this colour to be a mixture of all light colours in the scene. This is then multiplied by the ambient coefficient, or Ka, to reduce the strength of this colour so that it doesn't affect the actual lights too much and so unlit fragments appear to have indirect lighting applied.
We then put the whole lighting calculation together and multiply it with the object colour. Just as before we put everything through gamma correction and set the output value to the result and all is good to go. Below is a visual representation of all the Blinn-Phong contributions to make up the light seen above.