Now that we have completed creating 3D geometry, we would like to enhance our scenes through the application of shadowing caused by lighting. To do this we will use a simple lighting model known as the Phong model. This model consists of defining four material properties, three corresponding light source properties, and some additional geometry parameters controlling the interaction of the two. Unfortunately because the pipeline contains no global information, i.e. once an object is passed through the pipeline any world information regarding it is lost, we are only able to apply lighting on a per object basis. While this provides for some lighting effects, the pipeline is not able to handle reflections amongst objects. Thus a shiny object will not (for now) appear to be reflecting any other objects in the scene.

The Phong model can be implemented either in the vertex shader or fragment shader. When it is implemented in the vertex shader, it is known as Gouraud shading, which has the advantage of efficiency at the expense of visual quality. When it is implemented in the fragment shader, it is known as Phong shading, which is less efficient but has better visual appearance. In this lab we will implement simple Gouraud shading for a basic light source and then next lab add additional light sources with Phong shading.

Getting Started

Download CS370_Lab09.zip, saving it into the CS370-Fall2022 directory.

Double-click on CS370_Lab09.zip and extract the contents of the archive into a subdirectory called CS370_Lab09

Open CLion, select CS370-Fall2022 from the main screen (you may need to close any open projects), and open the CMakeLists.txt file in this directory (not the one in the CS370_Lab09 subdirectory). Uncomment the line

	add_subdirectory("CS370_Lab09" "CS370_Lab09/bin")

Finally, select Reload changes which should build the project and add it to the dropdown menu at the top of the IDE window.

Solution

Download CS370_Lab09_Solution.zip, saving it into the CS370-Fall2022 directory.

Double-click on CS370_Lab09_Solution.zip and extract the contents of the archive into a subdirectory called CS370_Lab09_Solution

Open CLion, select CS370-Fall2022 from the main screen (you may need to close any open projects), and open the CMakeLists.txt file in this directory (not the one in the CS370_Lab09_Solution subdirectory). Uncomment the line

	add_subdirectory("CS370_Lab09_Solution" "CS370_Lab09_Solution/bin")

Finally, select Reload changes which should build the project and add it to the

Materials

The basic model we will be using for lighting is known as the Phong model. In this model, every surface has a particular material associated with it. This material is defined by four properties

Each of the first three properties is defined by a four element vector with one component of each array per color channel. These values indicate the percent of the incident light channel that is reflected, hence the higher the value the more the object will appear to have that color. The last property, shininess, is simply a floating point (GLfloat) value. We will define a MaterialProperties structure in the lighting.h header file to define a particular material as follows

struct MaterialProperties {
	vmath::vec4 ambient;
	vmath::vec4 diffuse;
	vmath::vec4 specular;
	GLfloat shininess;
	GLfloat pad[3];
};

(Note: The pad field is necessary for proper byte alignment in a uniform buffer that will be discussed later.)

We will then create MaterialProperties instances and specify the values for the various components. For example, to create a brass material

MaterialProperties brass = {
                            vec4(0.33f, 0.22f, 0.03f, 1.0f), //ambient
                            vec4(0.78f, 0.57f, 0.11f, 1.0f), //diffuse
                            vec4(0.99f, 0.91f, 0.81f, 1.0f), //specular
                            27.8f, //shininess
                            {0.0f, 0.0f, 0.0f}  //pad
};

Once we have created all our materials, similar to placing data in other buffers, we will create, bind, and load the data into a uniform buffer for our materials using the GL_UNIFORM_BUFFER type.

Tasks

Note: Notice how the data is then loaded into a GL_UNIFORM_BUFFER in a similar fashion to other buffers.

Light Sources

Similar to materials, each light source is defined by three properties - ambient (background), diffuse (scattered), and specular (focused). Again, each of these properties is specified using a (4 component) RGBA color array. The RGB channels describe the intensity for each color channel of the light source, e.g. (1,1,1) would produce white light. For now we will again simply set the alpha channel to 1. A LightProperties structure is defined in the lighting.h header file as follows

struct LightProperties {
	GLint type;
	GLfloat pad1[3];
	vmath::vec4 ambient;
	vmath::vec4 diffuse;
	vmath::vec4 specular;
	vmath::vec4 position;
	vmath::vec4 direction;
	GLfloat spotCutoff;
	GLfloat spotExponent;
	GLfloat pad2[2];
};

Light sources will also have other properties such as type, position, direction, etc. depending on the type of light we wish to create. We will discuss the different types of lights in the next lab.(Note: Again the pad field is necessary for proper byte alignment in a uniform buffer that will be discussed later.) For example, to create a white light

LightProperties whiteLight = {
                                DIRECTIONAL, //type
                            	{0.0f, 0.0f, 0.0f}, //pad
                                vec4(0.0f, 0.0f, 0.0f, 0.0f), //ambient
                                vec4(1.0f, 1.0f, 1.0f, 1.0f), //diffuse
                                vec4(1.0f, 1.0f, 1.0f, 1.0f), //specular
                                vec4(0.0f, 0.0f, 0.0f, 1.0f),  //position
                                vec4(-1.0f, -1.0f, -1.0f, 0.0f), //direction
                                0.0f,   //cutoff
                                0.0f,  //exponent
                                {0.0f, 0.0f}  //pad2
};

Tasks

Note: Notice how the data is then loaded into a GL_UNIFORM_BUFFER in a similar fashion to other buffers.

Phong Model and Normals

The Phong model computes the contribution of each lighting component (on a per channel basis) based on the relationship between four vectors as shown below

Phong Model Vectors

where the four vectors are as follows:

These vectors determine the final intensity of the diffusive and specular lighting components that are applied to the surface.

Normals

We will see that several of the components of the Phong model depend on the orientation of the surface with respect to the light source, thus to use lighting we must associate a normal with each vertex in our geometry. We will do this by creating an attribute buffer (similar to the color buffer from Lab 2), and loading the data for the normals either manually or from the model .obj file.

One issue we must address is that whenever we transform our objects, we must also perform a transformation of the normals. However, since normals are simply directions, they should not be affected by translations and we must be careful with non-uniform scalings which can change the direction of the normal vector. To correctly transform the normals, we will compute a normal matrix transformation based on the model transformation (the derivation is beyond the scope of this course) as the transpose of the inverse of the model matrix, i.e.

normal_matrix = model_matrix.inverse().transpose();

Phong Model

Ambient Reflection

The ambient reflection component is independent of the normals and can be thought of as an overall uniform illumination of the surface. Thus it is simply the product of the incident intensity with the material’s ambient array components

Ambient Term

where ka is the material ambient component (per color channel), La is the incident ambient light intensity, and Ia is the final ambient light intensity per color channel.

Diffuse Reflection

The diffuse reflection component is based on Lambert’s law which states that the more directly the light shines on the surface, the brighter it will appear. Mathematically this is computed using the dot product between n (the surface normal) and l (the light direction). When these two vectors are parallel (light shining directly onto surface), the dot product is one and hence there is maximal diffusive illumination. When the two vectors are perpendicular (light shining across surface), the dot product is zero and hence there is no diffusive illumination. Hence the formula based on the material’s diffusive array components is

Diffusive Term

where kd is the material diffusive component (per color channel), Ld is the incident diffusive light intensity, (nl) is the Lambert factor (clipped to a minimum value of 0), and Id is the final diffusive light intensity. This formula can also be extended to account for the attenuation due to distance the object is from the light source.

Specular Reflection

The specular reflection component is used to create highlights on an object (particular for shiny materials). These reflections will be greatest when the reflected light (which depends on the surface normal and the direction of the light source) is in the direction of the viewer. Mathematically this is computed using the dot product between r (the reflected light direction) and v (the viewer direction). When these two vectors are parallel (viewer looking directly at reflection), the dot product is one and hence there is maximal specular illumination. When the two vectors are perpendicular (viewer looking across reflection), the dot product is zero and hence there is no specular illumination. The shininess property determines how focused the highlight is, a high shininess coefficient creates a small bright spot whereas a low shininess coefficient creates a broader less bright spot. The formula based on the material’s specular array components is

Specular Term

where ks is the specular component (per color channel), Ls is the incident specular light intensity, (rv) is the specular factor (clipped to a minimum value of 0), α is the shininess exponent for the material, and Ls is the final specular light intensity. An attenuation factor can also be applied to this component to account for the distance between the object and the light source. However, this computation can be expensive since the reflection vector needs to be computed. Alternatively, an approximation to the specular term can be computed using the halfway vector (h = (l + v)/||l+v||) as nh

Phong model

The final intensity of each color channel is then simply the sum of the three reflection components as given by (not including attenuation)

Phong Formula

For Gouraud shading, we will implement this equation in the vertex shader and thus the lighting calculations, i.e. color, will be computed per vertex and then interpolated across the object. While this is more efficient for rendering, it may not produce as accurate of a visual effect.

Tasks

Note: In the fragment shader we will then clamp the color channel maximum values to 1.

Compiling and running the program

You should be able to build and run the program by selecting gouraudMesh from the dropdown menu and clicking the small green arrow towards the right of the top toolbar.

At this point you should see a spinning sphere and cube object with a gradient lighting effect. Spacebar should toggle the animation.

Gouraud Scene Window

To quit the program simply close the window.

Congratulations, you have now written an application with basic lighting.

Next we will investigate how to create different types of lights and move the Phong calculation into the fragment shader for better visual effect.