This page describes how use the EduPhong C++ software to describe material properties and light properties and render a scene with Phong lighting. It is a full-featured implementation that includes:

The documentation below uses code fragments from SimpleLightModern.cpp and from EduPhong.cpp and EduPhong.h. It may be helpful to consult those as you read the documentation. The sample codes below are at times slightly edited from the original.

The page describes only the C++ commands for using the EduPhong code. For a description of how the GLSL shader code works, see the planned second edition of the book 3D Computer Graphics, A Mathematical Introduction with OpenGL by the author.

We assume basic familiarity with the Phong lighting model, and its use of emissive, ambient, diffuse and specular light.

I. Overview of EduPhong

The main portion of the EduPhong program is to calculate Phong lighting, and render triangles with either Phong shading or Gouraud shading. For this it uses the following C++ classes:

The EduPhong code also provides two shader programs, one which performs Phong lighting with Phong shading, and a second one which performs Phong lighting with Gouraud shading.

EduPhong also provides a rudimentary ability to use textures; the texture is multiplicatively combined with the non-specular lighting (namely, with the emissive, ambient and diffuse colors).

II. Setting Material Properties

Material properties give the color/reflective of a surface; namely, they describe how the surface reflects light. The C++ class phMaterial contains these properties. The relevant part of the C++ class description is:

// ********
// phMaterial - 
//   Material properies describe the color/reflectively of the surface.
//   Material properies are vertex attributes; however, they are
//    generally the same across a single object and do not vary per vertex,
//    so they are therefore implemented as generic vertex attributes.
// ********
class phMaterial {
public:
    VectorR3 EmissiveColor;
    VectorR3 AmbientColor;
    VectorR3 DiffuseColor;
    VectorR3 SpecularColor;
    float SpecularExponent;
    bool UseFresnel;
    
    […]  Constructor omitted here

    // Load uniform values in the two shader programs.
    void LoadIntoShaders();
};

The four vectors EmissiveColor, AmbientColor, DiffuseColor and AmbientColor and the scalar SpecularExponent hold the Phong material properties for the surface. Each vector consists of red, green and blue components. These are assigned by the usual commands for VectorR3's. For instance, the emissive and specular components of the materials for the six spheres in SimpleLightModern.cpp are set up by the following commands:

    // Set the unchanging parts of myMaterial1 (for the large spheres)
    phMaterial myMaterial1;
    myMaterial1.EmissiveColor.SetZero();
    myMaterial1.SpecularColor.Set(1.0, 1.0, 1.0);
    myMaterial1.SpecularExponent = 50.0;

The Set is a convenience function: for example, the specular color could have equivalently been set by using the following command. (See the GlLinearMath page for more information on working with VectorR3 and LinearMapR4 objects.)

   myMaterial1.SpecularColor = VectorR3(1.0, 1.0, 1.0);

Each sphere gets its own ambient and diffuse colors: these are set a few lines later by the commands:

    myMaterial1.AmbientColor = AmbDiffColors[i];        // Ambient color for the i-th sphere
    myMaterial1.DiffuseColor = AmbDiffColors[i];        // Diffuse color for the i-th sphere

Here AmbDiffColors is an array of VectorR3's. By the way, it is usual to set the ambient and diffuse colors to be the same value, or at least scalar multiples of each other. It also is usual to set the specular material color to be a white or gray color so that the specular highlights will have take on the specular color of the light.

UseFresnel can be set to either true or false to enable or disable the Fresnel term for specular lights. It defaults to false, without the Fresnel term.

Once the entries of the phMaterial are set, they need to be loaded into OpenGL's memory as generic vertex attributes. This is done by the following command:

    myMaterial1.LoadIntoShaders();                       // Load the phMaterial information into the shaders

LoadIntoShaders() is is typically called right before the call to a glDrawArrays... or glDrawElements... function which renders the object. In SimpleLightModern.cpp, this command is given before the call to mySphere.Render().

III. Setting Global Light Properties

The global light properties give the Phong lighting values with are independent of particular light sources. Traditionally, this includes just the global ambient color (GlobalAmbientColor). The C++ class phGlobal also contains the number of lights and contains booleans indicating whether the view is local, and whether the emissive, ambient, diffuse and specular components of Phong lighting should be rendered. phGlobal is defined by:

// ********
// phGlobal - Global illumination properties.
//   These are uniform values.
//   The viewer is presumed to be at the origin, looking down the negative z-axis
// ********
class phGlobal {
public:
    VectorR3 GlobalAmbientColor;    // Global ambient light color
    unsigned int NumLights;         // Number of lights.
    bool LocalViewer;               // true for local viewer ; false for directional viewer (default is false)
    bool EnableEmissive;            // Control whether emissive colors are rendered (default is true)
    bool EnableDiffuse;             // Control whether diffuse colors are rendered (default is true)
    bool EnableAmbient;             // Control whether ambient colors are rendered (default is true)
    bool EnableSpecular;            // Control whether specular colors are rendered (default is true)
    bool UseHalfwayVector;          // Control whether to use the halfway vector (default is false)
    
    […] 

    void LoadIntoShaders();         // Load the global lighting data into the shaders
};

SimpleLightModern declares a phGlobal as a global variable:

phGlobal globalPhongData;

The global lighting information is initialized by the commands

    globalPhongData.LocalViewer = true;
    globalPhongData.EnableEmissive = true;      // The four "Enable" values default to true (enabled)
    globalPhongData.EnableDiffuse = true;       //      and thus they do not really need to be specified
    globalPhongData.EnableAmbient = true;
    globalPhongData.EnableSpecular = true;
    globalPhongData.NumLights = 1;
    globalPhongData.GlobalAmbientColor.Set(0.2, 0.2, 0.2);

Then, after the shaders have been setup by a calling setup_phong_shaders(), the global lighting data is loaded into OpenGL's buffers for use by the shaders by calling

    globalPhongData.LoadIntoShaders();   // Must do this after the shaders are set up.

This does not need to be called every render cycle. It is only necessary to call globalPhongData.LoadIntoShaders() when there is change to the global lighting parameters. (For this, see the key_callback routine in SimpleLightModern.)

IV. Setting a Light's Properties.

In Phong lighting, each individual light has its own, rather long, list of light parameters. In EduPhong, these are stored in an object belonging to the C++ class phLight defined by:

// phLight - Light properties describe the color/brightness of a light.
//   Light properties may need to be accessed by the fragment shader
//      as well as the vertex shader (at least for Phong shading),
//      so they are uniform values, not vertex attributes.
class phLight {
public:
    bool IsEnabled;             // True if light is turned on (default is false) 
    bool IsAttenuated;          // True if attenuation is active (default is false)
    bool IsSpotLight;           // True if spotlight (default is false)
    bool IsDirectional;         // True if directional (default is false) 
private:
    VectorR3 PosOrDir;          // Position or direction, already transformed by the model view matrix
public:
    VectorR3 AmbientColor;
    VectorR3 DiffuseColor;
    VectorR3 SpecularColor;
private:
    VectorR3 SpotDirection;     // Spot light center direction, already transformed by the model view matrix
public:
    float SpotCosCutoff;        // Cosine of cutoff angle
    float SpotExponent;
    float ConstantAttenuation;
    float LinearAttenuation;
    float QuadraticAttenuation;

    […]

    void SetPosition(const LinearMapR4& modelviewMatrix, const VectorR3& position);
    void SetDirection(const LinearMapR4& modelviewMatrix, const VectorR3& direction);
    void SetSpotlightDirection(const LinearMapR4& modelviewMatrix, const VectorR3& direction);
    void LoadIntoShaders(int lightNumber);
};

Most of these values can be set directly. For example, SimpleLightModern declares movingLight as a phLight object and sets its initial parameters by calling:

    movingLight.IsEnabled = true;                       // Must set this, default value is false!
    movingLight.IsAttenuated = false;                   // Optional to set this, default is false.
    movingLight.IsSpotLight = false;                    // Optional to set this, default is false.
    movingLight.AmbientColor.Set(0.3, 0.3, 0.3);        // Contributes fairly low level of ambient light
    movingLight.DiffuseColor.Set(1.0, 1.0, 1.0);        // Bright white for diffuse
    movingLight.SpecularColor.Set(1.0, 1.0, 1.0);       // Bright white for specular

However, the PosOrDir and SpotDirection cannot be set so simply. PosOrDir will contain either the (x,y,z) position of the light (if the light is positional) or the (x,y,z) direction of the light (if the light is directional). This vector value of PosOrDir must be the value expressed in the "world coordinates". The "world coordinate" system is the target coordinate system of the ModelView matrix; it is also the coordinate system where the viewer either is placed at the origin (if positional) or is looking in the negative z direction (if directional). The vertex shader does all Phong lighting calculations using the "world coordinates" system: the vertex shader is given a vertex's position and normal in local coordinates; the shader transforms the vertex's position and normal with the model view matrix which is in effect when the vertex is rendered. Then, the vertex shader needs the light's position or direction to be in the same coordinate system for the Phong lighting calculation.

The routines SetPosition and SetDirection thus let you set the position or direction in this way. They take a 4x4 matrix, modelviewMatrix, as input and use it to transform the position or direction. At the same time, they set the boolean variable IsDirectional to the corresponding appropriate value. SimpleLightModern does this with:

    if (movingLight.IsDirectional) {
        movingLight.SetDirection(theViewMatrix, -lightPosition);  // Direction
    }
    else {
        movingLight.SetPosition(theViewMatrix, lightPosition);  // Position
    }

The above code uses -lightPosition, with a minus sign, since the direction is taken to be the direction from the light's position towards the origin.

Finally, the phLight parameters need to be loaded into an OpenGL buffer for use by the shaders. This done with the routine phLight::LoadIntoShaders. In SimpleLightModern, it is

    movingLight.LoadIntoShaders(0);                      // Upload as light #0.

This call to movingLight.LoadIntoShaders(0) loads all of the lighting parameters from movingLight into the OpenGL buffers for the uniform light values. The parameter 0 specifies the light number. Valid light numbers are 0 through 7 (controlled by the variable phMaxNumLights in EduPhong.h and in the shader programs). The NumLights of the phGlobal object must set high enough; for example, if you are using lights 0, 1, 2, and 3, then NumLights must be at least 4. In addition, do not forget to set the IsEnabled flag for each phLight object.

A similar routine, SetSpotlightDirection, can be used to set the direction of a spotlight. It is necessary to also set IsSpotLight true to activate the light as a spotlight. SpotCosCutoff and SpotExponent both default to the value 0. In almost all applications, SpotCosCutoff should be set to a larger value (closer to 1) to make a spotlight with a tighter beam. Optionally, SpotExponent can be set larger to form a spotlight that attenuates away from the center of the spotlight cone.

V. Overview/Summary of using EduPhong

The following steps were needed to render with Phong lighting in the program SimpleLightModern.

  1. Call setup_phong_shaders. This compiles and links the shaders, and must be done before any LoadIntoShaders routine is called.
  2. Allocate a projection matrix and modelview matrix. Initialize them as appropriate. Load the projection matrix into OpenGL as a uniform matrix whenever the window size changes or the perspective changes.
  3. Allocate one or more phLight objects describing the properties of the lights. Initialize them, update them as needed. Use the appropriate modelview matrix when setting a light's position or direction, or spotlight direction. Load them into OpenGL's buffers by calling LoadIntoShaders for each phLight object (with distinct light numbers).
  4. Allocate a phGlobal object describing the global lighting properties. Initialize it, update it as needed. Load it into OpenGL's buffers by calling LoadIntoShaders before rendering.
  5. Loop, rendering different objects:
    • Load a phMaterial with the material properies for the next object. (If different from the previous item rendered.) Use LoadIntoShaders to upload it to the shader program.
    • Render the object (generally as triangles). Typically, each geometric object has its own modelview matrix and this matrix needs to uploaded as a uniform variable before the object is rendered.

VI. Phong shading versus Gouraud shading

EduPhong provides a routine setup_phong_shaders() for setting two (2) shader programs. The first one, called phShaderPhongGouraud does Gouraud shading. The second one, called phShaderPhongPhong does Phong shading. Gouraud shading is faster, since it does the Phong lighting calculation only the vertices, whereas the Phong shading does the Phong lighting calculation at every vertex. Phong shading can give better visual effects, however, especially when there is animation.

The two shader programs are compiled and linked by calling

    setup_phong_shaders();

SimpleLightModern selects which shader program to use with the following commands. The first three lines choose the active shader program, and save the vertex shader's location for the projection matrices and model view matrices. The final glUseProgram tells OpenGL which shader program to use. (It is not actually necessary to call glUseProgram every render cycle if the choice of shader program is not changing.)

    currentShaderProgram = UsePhongGouraud ? phShaderPhongGouraud : phShaderPhongPhong;
    projMatLocation = glGetUniformLocation(currentShaderProgram, phProjMatName);
    modelviewMatLocation = glGetUniformLocation(currentShaderProgram, phModelviewMatName);
     
    […]
    
    glUseProgram(currentShaderProgram);

VII. Using texture maps with EduPhong.

EduPhong includes a rudimentary ability to add texture maps to surfaces. This is an extraneous feature, but is included for convenience mainly because it is always useful in programming assignments for Math 155A, but also because it illustrates how to apply a texture map that affects only the non-specular component of lighting.

To use EduPhong's texture mapping, you must enable the texture mapping in the shader. This can done by the following two lines:

    unsigned int applyTextureLocation = phGetApplyTextureLoc(currentShaderProgram);
    glUniform1i(applyTextureLocation, true);     // Enable applying the texture!

where currentShaderProgram is, as above, the shader program to be used for rendering with a texture. (The call to phGetApplyTextureLoc returns the results of glGetUniformLocation(programID, "applyTexture").) The call to glUniform1i sets the uniform variable true to enable texture mapping. Setting it false disables texture mapping.

To be sure the shader program is using the default texture map, you can use the command

    glUniform1i(glGetUniformLocation(shaderProgramBitmap, "theTextureMap"), 0);

You also must select the texture to be used by shader program: this is called "binding" the texture, and is done with commands such as

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, myTextureName);

The texture specified with glActiveTexture should correspond with the value set earlier to theTextureMap. The variable myTextureName is an unsigned int which "names" a texture. It must be the name of an OpenGL two-dimensional texture (a GL_TEXTURE_2D) that was already created by the OpenGL program, say by calls to glTexImage2D() and glGenerateMipmap(). For examples of how to read texture data from a file to form a 2D texture in OpenGL, see the programs TextureBmpModern or FourTexturesModern.

This collection of C++ programs does not currently have any examples of using textures with EduPhong, but you can refer to the 2019 Math 155A course's Project 6 assignment for a code example.

VIII. Compiling custom versions of the Phong lighting shaders.

If you want to customise the Phong lighting shaders for applications, you will need to rewrite them. A common example would be it do more sophisticated uses of texture maps in conjunction with Phong lighting. Fortunately, the code is structured to facilitate writing your own versions. As an example of how Phong shaders are compiled, here is the code from the routine setup_phong_shaders().

    GlShaderMgr::LoadShaderSource("EduPhong.glsl");

    unsigned int shader_VPG = GlShaderMgr::CompileShader("vertexShader_PhongGouraud", "calcPhongLighting");
    unsigned int shader_FPG = GlShaderMgr::CompileShader("fragmentShader_PhongGouraud", "applyTextureMap");
    unsigned int shaders_PG[2] = { shader_VPG, shader_FPG };
    phShaderPhongGouraud = GlShaderMgr::LinkShaderProgram(2, shaders_PG);
    phRegisterShaderProgram(phShaderPhongGouraud);

    unsigned int shader_VPP = GlShaderMgr::CompileShader("vertexShader_PhongPhong");
    unsigned int shader_FPP = GlShaderMgr::CompileShader("fragmentShader_PhongPhong", "calcPhongLighting", "applyTextureMap");
    unsigned int shaders_PP[2] = { shader_VPP, shader_FPP };
    phShaderPhongPhong = GlShaderMgr::LinkShaderProgram(2, shaders_PP);
    phRegisterShaderProgram(phShaderPhongPhong);

This code uses the standard GlShaderMgr routines for compilation and linkage. It also needs to call phRegisterShaderProgram for each shader. phRegisterShaderProgram links the uniform structures in the EduPhong shaders to a common Uniform Buffer Object. This allows multiple versions of the Phong shader to use the data from the same Uniform Buffer Object for the global lighting properties and the lights' properties.

Explaining how to link uniform blocks to Uniform Buffer Objects is beyond the scope of this web page. But, the code for phRegisterShaderProgram shows an example of how it is done. The LoadIntoShaders routines show how to update the data in the Uniform Buffer Objects.

For an example of using custom changes to the EduPhong routines, please see again the 2019 Math 155A course's Project 6 assignment.