Java-Gaming.org    
Featured games (91)
games approved by the League of Dukes
Games in Showcase (576)
games submitted by our members
Games in WIP (497)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: [1]
  ignore  |  Print  
  LWJGL Tutorial Series - Lighting  (Read 2359 times)
0 Members and 1 Guest are viewing this topic.
Offline SHC
« Posted 2013-10-13 15:43:50 »

LWJGL Tutorial Series - Lighting


Welcome to the fifteenth part of the LWJGL Tutorial Series. In this tutorial, I'm going to show you how to implement lighting with GLSL. We are going to implement a point light, a light which emits lighting in all the directions. I'm going to explain only the important concepts since this topic (in physics) comes in high school. If you've found any mistakes, please notify them through comments.

Vertex Normals


Though we've been using normals since the past few tutorials, I've not explained them in detail. Now I'm explaining them since they are the the key to do lighting. Before getting into lighting, let's first learn what normals actually are and how they relate to lighting. Actually the normals exist for faces, i.e., each face has a normal which is a vector perpendicular to the face like in this diagram.


You should note that a normal is a directional vector only. The above image shows a normal N which is perpendicular to the triangle formed by the vertices V1, V2 and V3. So it is also said that the normal of V1 with respect to the face is N. It is also possible that each vertex can have many normals depending on the current face. Don't be worried since we are using Blender to generate the normals for us and we are simply using them with our model loader. All we need now is how to do lighting. So we are using the Phong Reflection Model to do lighting for us.

Phong Reflection Model


The Phong Reflection Model is an algorithm that we can use to do Per Fragment Lighting which means we do most of the lighting code in the fragment shader. Phong shading improves upon Gouraud shading and provides a better approximation of the shading of a smooth surface. Here's an image on the wikipedia showing the difference between flat shading and phong shading.


Now let's get into the different components of the Phong Shading Model. This model has three components, namely Ambient, Diffuse and Specular components. There's another image on the wikipedia which explains what they actually are.


Here, ambient refers to the color when there is no light on that part. To be more precise, it's the light that is present all over the scene whose direction is impossible to determine. Diffuse light is a light that comes from a specific direction (from position of light towards the fragment). And finally, specular light comes from a specific direction and bounces off the surface of the face. Combining all these will cause the final phong reflection to be seen. Now let's learn how each light works by implementing them.

Diffuse Component


To understand how it actually works, examine the following image. It helps us understand how it actually works.


It shows how a light ray reflects when incident on the surface. The incident ray collides with the surface and bounces as the reflected ray. The normal bisects both the incident ray and the reflected ray. Now assume that the incident ray started from the light and fell on the vertex. If
lightPos
is the position of the light,
vNormal
be the vertex normal and
vPosition
be the vertex position, we can find the direction of light, which is called as
surfaceToLight
by using this piece of code.

1  
vec3 surfaceToLight = normalize(lightPos - vPosition);

The expression
lightPos - vPosition
gives a vector that is along the incident ray but in opposite direction. This is because the color of the object is determined by the color of light it emits. We then normalize that vector to get the direction of the light, we don't need the position of the light ray. Next, we need to calculate the diffuse coefficient. If you don't know what it is, here's a simple experiment. Position a card or a book vertically and point a torch to it. If the torch is perpendicular to it, then more light is reflected back to your eye making the book appear brighter. Now, try the same by rotating the book and you will see some decrease in brightness. This is because of the ambient coefficient. I don't go into much detail and I'm giving out some formulas.


Since both the vectors are normalized vectors, there is no need to divide the dot product of those vectors with the product of their magnitudes. So we can remove them from the above equation. This creates the following equation.


So the diffuse coefficient is the cos(θ) in the above equation. The default range of the cosine function is [-1, 1] but negative diffuse coefficient makes the dark areas even darker which we do not want. So we use the
max
function to get only the positive values. This can be achieved in GLSL with the following.

1  
float diffuseCoefficient = max(0.0, dot(vNormal, surfaceToLight));

And finally, we calculate the diffuse color by multiplying the vertex color with the diffuse coefficient and the intensity of light.

1  
vec3 diffuse = diffuseCoefficient * vColor.rgb * lightIntensity;

This is all about the diffuse light. We still had to see about ambient and specular lights. Don't get asleep yet, we have more to learn.

Ambient Component


Now, we are going to see the second component of the Phong Shading Model called the ambient component. What this component says is basically the color of the face when there is no light. This is used since we don't see completely dark models due to having no light in the area. We calculate the ambient color by using a percentage of the intensity of light. We keep this percentage in a variable called as
ambientComponent
and calculate the ambient color by using a similar formula which is used to calculate the defuse color. It is achieved with this code.

1  
vec3 ambient = ambientCoefficient * vColor.rgb * lightIntensity;

The field
ambientCoefficient
is stored as a constant and we define it to be 0.05 meaning that we have 5 percent of light at places where there is no light. That's all with ambient component. The only component left by us is the specular component which we are going to learn now.

Specular Component


Now comes the specular component. This is the component that makes the surfaces look shiny. Before going through that, let's see the image I've shown earlier.


There are two things I didn't explain before when I showed the image. They are the angles.


For a flat surface, the angle of incidence is equal to the angle of the reflectance. Now consider that the surface is irregular, the light can reflect in any direction depending on the surface. This is a key difference between specular and diffuse components, the diffuse component models the irregular surface and the specular component is used to model regular surfaces. Now that we are ready to calculate the specular component, let's start that by some equations.


These are fairly simple. The reflection vector is calculated by using the
reflect
function of GLSL. Here cos(θ) is used in the calculation of the specular coefficient by raising this cos angle to the power of the shininess. Like this.

1  
2  
3  
4  
5  
6  
float specularCoefficient = 0.0;
if(diffuseCoefficient > 0.0)
{
    specularCoefficient = pow(max(0.0, dot(surfaceToLight, reflect(-surfaceToLight, vNormal))), shininess);
}
vec3 specular = specularCoefficient * vec3(1.0, 1.0, 1.0) * lightIntensity;

We used the if clause to only calculate the specular coefficient for the faces that are visible since if the diffuse coefficient is equal to zero, then the face is actually behind another face and is not visible. This explains the specular component and the next part is putting them together. But before we do that, we need to learn about normal matrices.

Normal Matrices


Normals are usually provided in the model space, relative to the vertices for which we are using the view matrix. Though normals are in the model space as well, since they are unit vectors, we need a separate matrix called as the normal matrix. The problem with using the same view matrix doesn't keep the normals as unit vectors since that matrix is affected with scaling and translation. So we need to remove scaling and translations from the matrix. So we make a new method in the
Camera
class called as
getNormalMatrix()
which looks like this.

1  
2  
3  
4  
5  
6  
7  
8  
public Matrix4f getNormalMatrix()
{
    Matrix4f mat = getViewMatrix();
    mat.m30 = 0;    mat.m31 = 0;    mat.m32 = 0;    mat.m33 = 1;
    Matrix4f.invert(mat, mat);
    Matrix4f.transpose(mat, mat);
    return mat;
}

What it does is very simple. It first changes the last row of the matrix with the last row of an identity matrix so that rotations are removed from the matrix. Now, scaling is removed by inverting the matrix and transposing it. This removes the scaling from the matrix and the matrix can now be used for transformation of the normals. All you had to do is to update the correct uniforms which I've explained in the previous tutorial and I'm not going through that code in this tutorial. We are now ready for the final part, putting all together.

Putting it all together


Now it's time to write the actual shaders that does these actual calculations. Since I've explained all of the shader code earlier, I'm now just listing the code. First let's see the vertex shader.

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  
// The Vertex Color
varying vec4 vColor;
// The Vertex Position
varying vec3 vPosition;
// The Vertex Normal
varying vec3 vNormal;

// The view matrix
uniform mat4 mView;
// The projection matrix
uniform mat4 mProjection;
// The normal matrix
uniform mat4 mNormal;

void main()
{
    // Pass the color to the fragment shader
   vColor = gl_Color;
   
    // Pass the vertex to the fragment shader
   vPosition = (mView * gl_Vertex).xyz;
   
    // Pass the normal to the fragment shader
   vNormal = normalize(mNormal * vec4(gl_Normal, 1.0)).xyz;
   
    // Calculate the transformed vertex
   gl_Position = mProjection * mView * gl_Vertex;
}

This is the vertex shader. What it does is it sends some data to the fragment shader and transforms the vertex. Now let's see the source code of the fragment shader. The source is easier to understand since I've commented each line.

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  
// The Vertex Color
varying vec4 vColor;
// The Vertex Position
varying vec3 vPosition;
// The Vertex Normal
varying vec3 vNormal;

// The Position of light
uniform vec3 lightPos;

// The Color of light
const vec3 lightColor = vec3(1.0, 1.0, 1.0);
// Intensity of light
const float lightIntensity = 2.0;
// The ambientCoefficient
const float ambientCoefficient = 0.05;
// Shinines of the model
const float shininess = 128.0;

void main()
{
    // Vector from surface to light
   vec3 surfaceToLight = normalize(lightPos - vPosition);
   
    // ambient light
   vec3 ambient = ambientCoefficient * vColor.rgb * lightIntensity;

    // diffuse light
   float diffuseCoefficient = max(0.0, dot(vNormal, surfaceToLight));
    vec3 diffuse = diffuseCoefficient * vColor.rgb * lightIntensity;
   
    // specular light
   float specularCoefficient = 0.0;
    if(diffuseCoefficient > 0.0)
    {
        specularCoefficient = pow(max(0.0, dot(surfaceToLight, reflect(-surfaceToLight, vNormal))), shininess);
    }
    vec3 specular = specularCoefficient * vec3(1.0, 1.0, 1.0) * lightIntensity;
   
    // Send the color to opengl
   gl_FragColor = vColor + vec4(ambient, 1.0) + vec4(diffuse, 1.0) * vec4(lightColor, 1.0) + vec4(specular, 1.0);
}

This makes the fragment shader which does all of the hard work. You can see that I've implemented the values as constants but you can use uniforms too. And now when we run it,


That's the end of this tutorial and in the next tutorial, let's see how to load textured models. If you've found any mistakes, please notify them to me with comments.

Source Code


Tutorial15.java
shader.vert
shader.frag
Camera.java

Offline pitbuller
« Reply #1 - Posted 2013-10-13 16:44:05 »

You need to renormalize interpolated normal at fragment shader.
Offline pitbuller
« Reply #2 - Posted 2013-10-13 16:52:04 »

1  
gl_FragColor = vColor + vec4(ambient, 1.0) + vec4(diffuse, 1.0) * vec4(lightColor, 1.0) + vec4(specular, 1.0);


Alpha value is 3.0 after three sums. Light does not have alpha component so don't use it.
Specular shoud also use light color. Light color and intesity can be combined cpu so no need for different uniforms and waste full per pixel math.


Better code would be.
1  
gl_FragColor = vColor + vec4(ambient + lightColor * (diffuse  + specular), 0.0);
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline SHC
« Reply #3 - Posted 2013-10-14 03:08:18 »

@pitbuller
You are right. I'm going to give updates in the next tutorial. Also could you explain what is the error caused when alpha is 3? I think OpenGL automatically clamps that value to 1.0.

Offline Nouht

Senior Newbie





« Reply #4 - Posted 2014-04-05 13:47:55 »

Hey SHC, great tutorial. I implemented this into my own engine and I was having some problems. There is a circular ball of  light on the vertices but it changes size and position whenever I move the camera. Do you have a solution to this?
Offline SHC
« Reply #5 - Posted 2014-04-05 13:58:15 »

@Nouht

I think that is due to your model. That happens if the model has big flat faces.

Offline Nouht

Senior Newbie





« Reply #6 - Posted 2014-04-05 15:06:00 »

@SHC So are you saying that this type of light won't work in first person shooters? Aren't you also neglecting the W component of the entire calculation? Apparently they're important for this type of thing. Yes my model is a large cube.
Offline SHC
« Reply #7 - Posted 2014-04-06 05:08:38 »

@Nouht

I didn't mean this doesn't work for FPS games. It brightens the face which is perpendicular to the light more than the faces which are at an angle. Since you are having a large cube, with flat faces, it always happen.

Pages: [1]
  ignore  |  Print  
 
 

 

Add your game by posting it in the WIP section,
or publish it in Showcase.

The first screenshot will be displayed as a thumbnail.

xsi3rr4x (12 views)
2014-04-15 18:08:23

BurntPizza (10 views)
2014-04-15 03:46:01

UprightPath (23 views)
2014-04-14 17:39:50

UprightPath (10 views)
2014-04-14 17:35:47

Porlus (27 views)
2014-04-14 15:48:38

tom_mai78101 (49 views)
2014-04-10 04:04:31

BurntPizza (107 views)
2014-04-08 23:06:04

tom_mai78101 (207 views)
2014-04-05 13:34:39

trollwarrior1 (176 views)
2014-04-04 12:06:45

CJLetsGame (182 views)
2014-04-01 02:16:10
List of Learning Resources
by Longarmx
2014-04-08 03:14:44

Good Examples
by matheus23
2014-04-05 13:51:37

Good Examples
by Grunnt
2014-04-03 15:48:46

Good Examples
by Grunnt
2014-04-03 15:48:37

Good Examples
by matheus23
2014-04-01 18:40:51

Good Examples
by matheus23
2014-04-01 18:40:34

Anonymous/Local/Inner class gotchas
by Roquen
2014-03-11 15:22:30

Anonymous/Local/Inner class gotchas
by Roquen
2014-03-11 15:05:20
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!