Lighting in vertex and fragment shader

In this post, my friend Adrian Visan explained the importance of lighting in a 3D environment and the importance of all lighting model coefficients required to compute the actual light. Here I will talk about Gouraud , Phong and Blinn-Phong lighting models and a brief about multiple light sources, attenuation and gamma correction which are used to implement light in modern games.

Gouraud reflection model can be calculated in vertex shader. This model computes a normal vector and an intensity for each vertex. To display the entire surface, points intensity on the lines are computed using linear interpolation between vertex intensities; Points intensity between the lines are computed using linear interpolation between the lines intensities.

Gouraud model. Only diffuse and specular coefficients

Phong shading model(reflection model) can be calculated in pixel shader. This model computes a normal vector for each vertex. After that computes a normal for each point on the surface using linear interpolation. Normal points on the lines are computing with linear interpolation between vertex normals. Normal points between the lines are computed using linear interpolation between the normal lines. Now we compute an intensity for each point.

Phong model. Only diffuse and specular coefficients

The general lighting equation is:

Lighting equation

Now let’s see how the reflected direction (R) is calculated: First, we simplify the image above keeping only incident ray (L or I), Normal Vector(N) and of course R.

Denote L = I . It is the same thing(light ray/ reverse incident ray)

Now we translate both u vectors and add them. And we can see that R is the result from vector addition between vector I and vector 2u. Keep in mind that here we reversed I, the final formula changes the signs if I is not reversed.

Reflected Ray

In GLSL and HLSL, there is a function called reflect(L,N) which computes the reflected direction for us. But be careful, because incident ray(I) is not reversed, so in order to have the same result as in the above images, you must make it negative  -reflect(L,N).

Before jumping into writing code we should take a look at some aspects of diffuse lighting, material coefficients, shininess and Blinn-Phong model. Lambert’s cosine law says that the amount of reflected light is proportional with the cosine(dot product) of the angle between the normal and incident vector (angle of incidence). To get light on our object, the angle of incidence should vary between 0 and 90 degrees:

Lambert’s law.How angle affects the diffuse lighting

We also need this angle to check for specular lighting otherwise it computes specular lighting for the back of our object and we don’t need this effect.

Speaking of specular we should talk about shininess coefficient(n). If  n is larger you will get narrower curves. Usually for metallic surface n is between 100 and 300, for plastic materials should be under 10. This diagram shows how cos function behaves for different n values.

Below is a list with some material coefficients. Of course you can experiment with other values if you want. However, these values ​​are found experimentally in research laboratories.

Blinn – Phong reflection model or modified Phong model was the default shading model in fixed-function pipeline for OpenGL and DirectX. It produces more accurate results than Phong reflection for many types of surfaces and it’s cheaper to compute for a single light source. For multiple lighting sources it’s more expensive to compute than Phong model.

Blinn-Phong model

Blinn – Phong model uses the concept of Halfway vector(H) between Light source(L) and View direction (V) instead of Reflection(R).

Blinn-Phong halfway vector

Lighting in vertex shader, Gouraud reflection model:

```//Gouraud Vertex Shader
//To keep it simple didn't add ambient and emissive lights;
//only diffuse and specular with white intensity
#version 330

layout(location = 0) in vec3 in_position;
layout(location = 1) in vec3 in_normal;

uniform mat4 model_matrix, view_matrix, projection_matrix;

uniform vec3 light_position;
uniform vec3 eye_position;

uniform int material_shininess;
uniform float material_kd;
uniform float material_ks;

out float light;

void main(){

vec3 world_position = mat3(model_matrix) * in_position;//careful here

vec3 world_normal = normalize(mat3(model_matrix) * in_normal);

//don't forget to normalize
vec3 L = normalize(light_position - world_position);//light direction
vec3 V = normalize(eye_position - world_position); //view direction

//Lambert term
float LdotN = max(0, dot(L,world_normal));

//consider diffuse light color white(1,1,1)
//all color channels have the same float value
float diffuse = material_kd * LdotN;

float specular = 0;

if(LdotN > 0.0)
{
//can use built-in max or saturate function
vec3 R = -normalize(reflect(L,world_normal);//reflection
specular = material_ks * pow( max(0, dot( R, V)), material_shininess);
}

light = diffuse + specular;

//How about with ambinetal and emissive?
//Final light(with white(1,1,1)) would be:
//light = ke + material_ka + diffuse + specular;

//final vertex position
gl_Position = projection_matrix*view_matrix*model_matrix*vec4(in_position,1);
}
```
```//Fragment Shader
#version 330
layout(location = 0) out vec4 out_color;</p>
in float light;

void main()
{
out_color = vec4(light,light, light,1);
}
```

Of course, Gouraud model is cheaper to compute than Phong model but inferior in quality especially for specular light. Although if the LOD(level of detail) is high, Gouraud model can give good results and it’s ok to use. Also you can use Gouraud model if you develop mobile games where you don’t have the hardware resources(…for the moment). Gouraud is way better than Flat Shading .

Lighting in pixel shader, Phong reflection model:

```//Phong Reflection Model Vertex Shader
#version 330

layout(location = 0) in vec3 in_position;
layout(location = 1) in vec3 in_normal;

uniform mat4 model_matrix, view_matrix, projection_matrix;

out vec3 world_pos;
out vec3 world_normal;

void main()
{
//convert in world coords
world_pos = mat3(model_matrix) * in_position;//careful here
world_normal = normalize(mat3(model_matrix) * in_normal);
gl_Position = projection_matrix*view_matrix*model_matrix*vec4(in_position,1);
}
```
```//Phong reflection model; Fragment Shader
//To keep it simple didn't add ambient and emissive lights;
//only diffuse and specular with white intensity
#version 330
layout(location = 0) out vec4 out_color;

uniform vec3 light_position;
uniform vec3 eye_position;

uniform int material_shininess;

uniform float material_kd;
uniform float material_ks;

in vec3 world_pos;
in vec3 world_normal;

void main(){

vec3 L = normalize( light_position - world_pos);//light direction
vec3 V = normalize( eye_position - world_pos);//view direction

float LdotN = max(0, dot(L,world_normal));

float diffuse = material_kd * LdotN;

float specular = 0;

if(LdotN > 0.0)
{

//choose H or R to see the difference
vec3 R = -normalize(reflect(L, world_normal));//Reflection
specular = material_ks * pow(max(0, dot(R, V)), material_shininess);

//Blinn-Phong
// vec3 H = normalize(L + V );//Halfway
//specular = material_ks * pow(max(0, dot(H, world_normal)), material_shininess);

}

float light = diffuse + specular;

out_color = vec4(light,light, light,1);
}

```

Other things you should  consider when you implement lighting is multiple light sources, attenuation and gamma correction.

• If you have more than one light source the equation for reflection model will be:

Lighting equation with multiple light sources

• Light attenuation. You probably want to add this to your lighting model because it simulates the fading of light with the distance. In the real world the light intensity is proportional with the inverse of the distance squared but we can’t use it just like this because this it is not “natural” in a virtual environment due to fast chaning rate. There are several ways to compute attenuation but the most  commonly used one is the constant, linear and quadratic method:

Attenuation Equation

```//Blinn-Phong reflection model wiath attenuation; Fragment Shader
//To keep it simple didn't add ambient and emissive lights;
//only diffuse and specular with white intensity
#version 330
layout(location = 0) out vec4 out_color;

uniform vec3 light_position;
uniform vec3 eye_position;

uniform int material_shininess;

uniform float material_kd;
uniform float material_ks;

//attenuation coefficients
uniform float att_kC;
uniform float att_kL;
uniform float att_kQ;

in vec3 world_pos;
in vec3 world_normal;

void main(){

vec3 L = normalize( light_position - world_pos);//light direction
vec3 V = normalize( eye_position - world_pos);//view direction

float LdotN = max(0, dot(L,world_normal));

float diffuse = material_kd * LdotN;

//attenuation
float d = distance(light_position, world_pos);
float att = 1.0 / (att_kC + d * att_kL + d*d*att_kQ);

float specular = 0;

if(LdotN > 0.0)
{
vec3 H = normalize(L + V );//Halfway(Blinn-Phong)
specular = material_kd * pow(max(0, dot(H, world_normal)), material_shininess);
}

float light = att * diffuse + att * specular;

out_color = vec4(light,light, light,1);
}
```
• Last thing to discuss is gamma correction. Human eye doesn’t perceive lights  in a linear way. Old CRT monitors and also  new ones(LCD) are engineerd to have non linear response to pixel intensity.  For example if a pixel has 25% brightness, at 50% it should be twice as bright, but is about 4.6 times brighter. Final color of the monitor can pe approximated by the  following function:

The standard gamma for monitors is 2.2 but may vary depending on the manufacturer. To undestand how gamma correction works here is a well-known diagram that shows how it’s done:

In this diagram, the bottom curve is the monitor response, the above curve is the gamma correction and the dotted middle line is the linear color space. Let’s say that you calculated a RGB value of 0.5  but because of monitor gamma, the final color will be 0.218. In order to see the correct result you have to apply gamma correction  which will add brightness to final color.

Nvidia GPU Gems 3: a) With gamma correction b) Without gamma correction

Gamma correction is very easy to implement in our fragment shader:

```//fragment shader
//remeber that we have the same value on all RGB channels
//...
float gamma = 1/2.2;
float final_light = pow(light,gamma);</p>
out_color = vec4(final_light,final_light, final_light,1);
}

```

You have to be careful not to apply gamma correction twice. For example, textures can already have gamma correction and if you make another gamma correction the restult would be brighter. Here we didn’t applied any textures on the sphere but if you have textures and you want to keep gamma correction in shader you can load the image changing the internalFormat of the texture when you load it. Just change GL_RGB to GL_SRGB argument in glTexImage2D function.

Source Code (VS 2013 OpengGL with C++ framework is  based on one of my school projects)

Other resources:

http://people.csail.mit.edu/wojciech/BRDFValidation/ExperimentalValidation-talk.pdf

http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html