Nvidia GPU Gems 3
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.
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.
The general lighting equation is:
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.
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.
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:
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 uses the concept of Halfway vector(H) between Light source(L) and View direction (V) instead of Reflection(R).
Lighting in vertex shader, Gouraud reflection model:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
//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);
}
//pass light to fragment shader
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);
}
|
1
2
3
4
5
6
7
8
9
10
|
//Fragment Shader
#version 330
layout(location = 0) out vec4 out_color;</p>
//from vertex shader
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//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;
//send them to fragment shader
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);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
//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:
- 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
//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.
Gamma correction is very easy to implement in our fragment shader:
1
2
3
4
5
6
7
|
//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
https://developer.valvesoftware.com/wiki/Constant-Linear-Quadratic_Falloff
http://www.farbrausch.de/~fg/stuff/phong.pdf
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.
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.
The general lighting equation is:
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.
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.
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:
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 uses the concept of Halfway vector(H) between Light source(L) and View direction (V) instead of Reflection(R).
Lighting in vertex shader, Gouraud reflection model:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
//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);
}
//pass light to fragment shader
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);
}
|
1
2
3
4
5
6
7
8
9
10
|
//Fragment Shader
#version 330
layout(location = 0) out vec4 out_color;</p>
//from vertex shader
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//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;
//send them to fragment shader
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);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
//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:
- 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
//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.
Gamma correction is very easy to implement in our fragment shader:
1
2
3
4
5
6
7
|
//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
https://developer.valvesoftware.com/wiki/Constant-Linear-Quadratic_Falloff