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
Pages: [1]
 ignore  |  Print
 LWJGL Tutorial Series - Lighting  (Read 2359 times) 0 Members and 1 Guest are viewing this topic.
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 Colorvarying vec4 vColor;// The Vertex Positionvarying vec3 vPosition;// The Vertex Normalvarying vec3 vNormal;// The view matrixuniform mat4 mView;// The projection matrixuniform mat4 mProjection;// The normal matrixuniform 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 Colorvarying vec4 vColor;// The Vertex Positionvarying vec3 vPosition;// The Vertex Normalvarying vec3 vNormal;// The Position of lightuniform vec3 lightPos;// The Color of lightconst vec3 lightColor = vec3(1.0, 1.0, 1.0);// Intensity of lightconst float lightIntensity = 2.0;// The ambientCoefficientconst float ambientCoefficient = 0.05;// Shinines of the modelconst 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
Camera.java

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

You need to renormalize interpolated normal at fragment shader.
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);`
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.

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?
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.

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.
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
 princec 36x Riven 29x Rayvolution 20x Drenius 14x saucymeatman 14x BurntPizza 13x Gibbo3771 13x kpars 12x ctomni231 11x Roquen 10x trollwarrior1 10x matheus23 9x jonjava 9x HeroesGraveDev 9x JFixby 8x SHC 8x
 List of Learning Resourcesby Longarmx2014-04-08 03:14:44Good Examples2014-04-05 13:51:37Good Examplesby Grunnt2014-04-03 15:48:46Good Examplesby Grunnt2014-04-03 15:48:37Good Examples2014-04-01 18:40:51Good Examples2014-04-01 18:40:34Anonymous/Local/Inner class gotchasby Roquen2014-03-11 15:22:30Anonymous/Local/Inner class gotchasby Roquen2014-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