Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / OpenGL

Light Source Features and Related Material Properties in OpenGL

5.00/5 (3 votes)
16 Apr 2021CPOL4 min read 3.6K  
Basic light source approaches and related material properties handling for OpenGL
In this article, you will learn about the overall principles and ready-to-use code snippets to deal with light source features and related material properties for OpenGL applications.

Introduction

I want to write a simple X3DOM viewer, based on OpenGL, and searched the web for information on how to apply light sources and material properties. What I found were a lot of good forum posts, but very few "overall principle" descriptions - one of the better ones is the OpenGL Wiki article, How lighting works. In this tip, I would like to share my findings with all those, who are also looking for an introduction to the lighting and material topic. So please don't expect "rocket science" in the following, but just "overall principle" descriptions and some "ready-to-use" code snippets, that take into account the relationship between light sources and material properties.

There is another tip about "Geometric Primitives for OpenGL" that I also wrote in connection with the simple X3DOM viewer. This tip here uses the algorithms presented there to create geometric primitives.

Background

The X3DOM specification supports the Material object, so I need to set up lighting in a way, that the majority of the Material properties can be applied to objects successfully.

I am not yet very confident with OpenGL, so it is important to me that the "overall principle" and "ready-to-use" code snippets are easy to understand and to follow. (One of my sources was the OpenGL Wiki article, How lighting works, the second was the OpenGL Programming Guide.)

One of my most important requirements for the subject of lighting features and material properties was the unconditional traceability of the results. So I decided

  1. to include a spot, representing the light source in the scene (I use a directional light source, which emulates perfectly parallel light beams),
  2. to circulate the light source non-axis-parallel around the scene (which illuminates all objects once brightly with reflections and once darkly),
  3. to make the spatial axes visible (which greatly facilitates the correction of errors) and
  4. to take a non-axis-parallel viewing position (which enhances the spatial impression of the scene).

The following pictures show the non-axis-parallel circulating light source including the associated variation in the illumination of the objects. If you compare the first image with the second, you can clearly see the reflections introduced by the spatial light in the second image - especially at the cone. If you compare the first image with the third, you can clearly see the difference between an illuminated surface and a surface in the shadow.

Images 1, 2 and 3

For the non-axis-parallel viewing position, the GL_PROJECTION matrix was tilted 15 degrees about each of the X and Y axes, causing the spatial axes in X (red) and Z (blue) to be non-parallel to the screen coordinates.

Image 4

Using the Code

World Preparation

The circulating light source, realized by fLightSourceRotationAngle, requires some sort of scene animation, which I implemented as an on-idle process, WindowProcOnIdle():

C++
void X3DomLightMainFrame::WindowProcOnIdle(const HWND hWnd)
{
    static float    fLightSourceRotationAngle = 0.0F;
    static Vector3f oSceneRotationVector = { 1.0F, 1.0F, 0.0F };
 
    RECT rc = _pweakMainContent->GetClientRect();
    LONG width  = rc.right - rc.left;
    LONG height = rc.bottom - rc.top;
 
    if (_viewportSize.cx != width ||
        _viewportSize.cy != height ||
        _oWorld.GetProjectionDirty() == true)
    {
        OpenGL::ResetViewport((GLsizei)width, (GLsizei)height,
                              _oWorld.GetOrthographicProjection(), 10.0F / _oWorld.GetZoom(),
                              10.0F / _oWorld.GetZoom(), -100.0F, 100.0F);
        _viewportSize.cx = width;
        _viewportSize.cy = height;
        _oWorld.SetProjectionDirty(false);
 
        OpenGL::ShadeModel(GL_SMOOTH); // alternative is GL_FLAT
        OpenGL::Enable(GL_CULL_FACE);  // enable default hidden face culling: GL_BACK
        OpenGL::Enable(GL_DEPTH_TEST); // perform depth comparisons and update the depth buffer
        OpenGL::DepthMask(GL_TRUE);    // ensure depth buffer can be written to
    }
 
    OpenGL::ClearColor(0.0F, 0.0F, 0.0F, 0.0F);
    OpenGL::Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // --- BEGIN: Provide unchanged start conditions from frame to frame.
    OpenGL::PushMatrix();
    // --- END: Provide unchanged start conditions from frame to frame.

    ...

A little bit of magic is hidden in ResetViewport() - for details, see the last code snippet in this tip. This method provides a unified call to create an orthographic projection and a perspective projection (determined by _oWorld.GetOrthographicProjection()), as well as the ability to zoom (determined by _oWorld.GetZoom()).

Lighting

My simple X3DOM viewer shall be able to switch between individual lighting (application of ligth sources), as it can be seen in the four pictures above, and basic/default lighting, as shown in the following picture:

Image 5

The determination of the lighting model is realized by _oWorld.GetUseIndividualLighting(). Now we can introduce the light sources, stored in _oWorld.CountLightSources(). OpenGL supports eight light sources by default:

C++
 ---

 if (_oWorld.GetUseIndividualLighting() == true)
 {
     OpenGL::Enable(GL_LIGHTING);

     for (int iLigthSourceCnt = 0;
          iLigthSourceCnt < _oWorld.CountLightSources() && iLigthSourceCnt < 8;
          iLigthSourceCnt++)
     {
         switch (iLigthSourceCnt)
         {
         case 0:
             OpenGL::Enable(GL_LIGHT0);
             break;
         case 1:
             OpenGL::Enable(GL_LIGHT1);
             break;
         case 2:
             OpenGL::Enable(GL_LIGHT2);
             break;
         case 3:
             OpenGL::Enable(GL_LIGHT3);
             break;
         case 4:
             OpenGL::Enable(GL_LIGHT4);
             break;
         case 5:
             OpenGL::Enable(GL_LIGHT5);
             break;
         case 6:
             OpenGL::Enable(GL_LIGHT6);
             break;
         case 7:
             OpenGL::Enable(GL_LIGHT7);
             break;
         default:
             break;
         }

         LightSource* pLightSource = _oWorld.GetLightSource(iLigthSourceCnt);
         OpenGL::PushMatrix();
         OpenGL::Rotatef(+fLightSourceRotationAngle, oSceneRotationVector.x,
                         oSceneRotationVector.y, oSceneRotationVector.z);
         Vector4f oLightVector = Vector4f{ pLightSource->GetLightSourcePosition(), 0.001F };
         OpenGL::Lightfv(GL_LIGHT0, GL_POSITION, oLightVector.Get());
         OpenGL::PopMatrix();

         // Light ambient reflection.
         SFColor oAmbientLightColor = pLightSource->GetAmbientLightColor();
         GLfloat pfAmbientLightColor[] = { oAmbientLightColor.r, oAmbientLightColor.g,
                                           oAmbientLightColor.b, 1.0F };
         OpenGL::Lightfv(GL_LIGHT0, GL_AMBIENT, pfAmbientLightColor);

         // Light diffuse reflection.
         SFColor oDiffuseLightColor = pLightSource->GetDiffuseLightColor();
         GLfloat pfDiffuseLightColor[] = { oDiffuseLightColor.r, oDiffuseLightColor.g,
                                           oDiffuseLightColor.b, 1.0F };
         OpenGL::Lightfv(GL_LIGHT0, GL_DIFFUSE, pfDiffuseLightColor);

         // Light mirror reflection.
         SFColor oSpecularLightColor = pLightSource->GetSpecularLightColor();
         GLfloat pfSpecularLightColor[] = { oSpecularLightColor.r, oSpecularLightColor.g,
                                            oSpecularLightColor.b, 1.0F };
         OpenGL::Lightfv(GL_LIGHT0, GL_SPECULAR, pfSpecularLightColor);
     }

     // Set affected face (front, back) and the relevant subset (ambient, diffuse,
     // emission, specular) of lights to enable a material to track the current color.
     OpenGL::ColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);

     // Switch on the capability of a material to track the current color.
     OpenGL::Enable(GL_COLOR_MATERIAL);
 }
 else
 {
     OpenGL::Disable(GL_LIGHT0);
     OpenGL::Disable(GL_LIGHT1);
     OpenGL::Disable(GL_LIGHT2);
     OpenGL::Disable(GL_LIGHT3);
     OpenGL::Disable(GL_LIGHT4);
     OpenGL::Disable(GL_LIGHT5);
     OpenGL::Disable(GL_LIGHT6);
     OpenGL::Disable(GL_LIGHT7);
     OpenGL::Disable(GL_LIGHTING);
 }

...

Visualize the Coordinate System

Next is the visualization of the coordinate system. For easy differentiation, the color assignment of the coordinates (x = red, y = green, z = blue) is done according to the sequences of color vector and coordinate vector. I already introduced the OpenGL::DrawCylinder() and OpenGL::DrawCone() methods in my tip about "Geometric Primitives for OpenGL" - the rest is OpenGL standard:

MC++
...

// --- Mark the coordinate system.
OpenGL::PushMatrix();
    OpenGL::Color3f(1.0F, 0.5F, 0.5F);
    OpenGL::Rotatef(-90, 0.0F, 0.0F, 1.0F);
    OpenGL::DrawCylinder(0.1F, 6.0F, 0.1F, M_PI / 90.0);
    OpenGL::Translatef(0.0F, 6.0F, 0.0F);
    OpenGL::DrawCone(0.25F, 0.4F, 0.25F, M_PI / 90);
OpenGL::PopMatrix();
OpenGL::PushMatrix();
    OpenGL::Color3f(0.5F, 1.0F, 0.5F);
    OpenGL::DrawCylinder(0.1F, 6.0F, 0.1F, M_PI / 90.0);
    OpenGL::Translatef(0.0F, 6.0F, 0.0F);
    OpenGL::DrawCone(0.25F, 0.4F, 0.25F, M_PI / 90);
OpenGL::PopMatrix();
OpenGL::PushMatrix();
    OpenGL::Color3f(0.5F, 0.5F, 1.0F);
    OpenGL::Rotatef(90, 1.0F, 0.0F, 0.0F);
    OpenGL::DrawCylinder(0.1F, 6.0F, 0.1F, M_PI / 90.0);
    OpenGL::Translatef(0.0F, 6.0F, 0.0F);
    OpenGL::DrawCone(0.25F, 0.4F, 0.25F, M_PI / 90);
OpenGL::PopMatrix();

...

Visualize the Circulating Light Source

The light source is represented by an abstracted spotlight, composed of a cylinder and a cone. The cylinder uses different colors for top, cover and bottom COLORREF oCylinderColors[] = { 0xFF444444, 0xFFAAAAAA, 0xFF444444 };. To make the cone appear like a cone of light, it must emit light itself. To do this, the material property of the cone is set to self-luminous GLfloat oEmissionColors1[] = { 0.8F, 0.8F, 0.6F, 1.0F }; and then reset afterwards GLfloat oEmissionColors2[] = { 0.0F, 0.0F, 0.0F, 1.0F };. For a realistic light cone effect, different colors are used for cover and bottom COLORREF oConeColors[] = { 0xFF668888, 0xFFEEFFFF };.

C++
...

// --- Mark the light source.
if (_oWorld.GetUseIndividualLighting() == true)
{
    for (int iLigthSourceCnt = 0;
         iLigthSourceCnt < _oWorld.CountLightSources() && iLigthSourceCnt < 8;
         iLigthSourceCnt++)
    {
        LightSource* pLightSource = _oWorld.GetLightSource(iLigthSourceCnt);

        OpenGL::PushMatrix();

        OpenGL::Rotatef(+fLightSourceRotationAngle, oSceneRotationVector.x,
                        oSceneRotationVector.y, oSceneRotationVector.z);
        OpenGL::Translatef(pLightSource->GetLightSourcePosition());

        // Direct to coordinate center.
        OpenGL::Rotatef(+45.0F, 0.0F, 0.0F, 1.0F);
        OpenGL::Rotatef(-45.0F, 1.0F, 0.0F, 0.0F);

        COLORREF oCylinderColors[] = { 0xFF444444, 0xFFAAAAAA, 0xFF444444 };
        OpenGL::DrawCylinder(0.2F, 0.3F, 0.2F, M_2PI / 20.0, oCylinderColors, false);
        OpenGL::Translatef(0.0F, -0.1F, 0.0F);
        OpenGL::Color3f(0.9F, 0.9F, 0.8F);

        GLfloat oEmissionColors1[] = { 0.8F, 0.8F, 0.6F, 1.0F };
        OpenGL::Materialfv(GL_FRONT_AND_BACK, GL_EMISSION, oEmissionColors1);

        COLORREF oConeColors[] = { 0xFF668888, 0xFFEEFFFF };
        OpenGL::DrawCone(0.4F, 0.4F, 0.4F, M_PI / 20.0, oConeColors);

        GLfloat oEmissionColors2[] = { 0.0F, 0.0F, 0.0F, 1.0F };
        OpenGL::Materialfv(GL_FRONT_AND_BACK, GL_EMISSION, oEmissionColors2);

        OpenGL::PopMatrix();
    }
}

Render the Scene

The displayed objects, cone, box and sphere, are in the scene and are rendered by a call to _pScene->RenderToOpenGL(). There is no magic behind this - any scene can be rendered at this point. The animation is completed by incrementing fLightSourceRotationAngle and a small pause MainFrame::Sleep(30), which is chosen a bit shorter for ReactOS.

C++
    ...

    // --- Scene rendering code goes here.
    _pScene->RenderToOpenGL(NULL, _pweakMainContent->GetOpenGlHDC());
 
    // --- Begin: Restore unchanged start conditions from frame to frame.
    OpenGL::PopMatrix();
    // --- END: Restore unchanged start conditions from frame to frame.
 
    ::SwapBuffers(_pweakMainContent->GetOpenGlHDC());

    fLightSourceRotationAngle += 1.0F;

#if defined(__GNUC__) || defined(__MINGW32__)
    MainFrame::Sleep(30);
#else
    MainFrame::Sleep(15);
#endif
 
    // Keep the idle part of the message loop running.
    ::PostMessage(GetHWnd(), WM_NULL, (WPARAM)0, (LPARAM)0);
}

Preparation of the Viewport

From the first code snippet, I still owe the source code of the OpenGL::ResetViewport() method - here it is:

C++
/// <summary>
/// Prepares an orthogonal projection.
/// </summary>
/// <param name="nWidth">The view-port width in pixels.</param>
/// <param name="nHeight">The view-port height in pixels.</param>
/// <param name="bInitOrthoProjection">Determine whether to fall back to
/// perspective projection or to initialize orthographic projection.</param>
/// <param name="dHorzClip">The left/right (we are symmetric) distance of
/// the clipping plane from the camera vector.</param>
/// <param name="dVertClip">The top/bottom (we are symmetric) distance of
/// the clipping plane from the camera vector.</param>
/// <param name="dNearZClip">The orthogonal view near clip plane. Can be negative.
/// Typically too big, if nothing is visible. Can realize a view 'into' the objects.</param>
/// <param name="dFarZClip">The orthogonal view far clip plane.
/// Can cut off the far background (for speed or focus on important things).</param>
/// <remarks>Recommended reading: https://sjbaker.org/steve/omniv/projection_abuse.html
static inline void ResetViewport(GLsizei nWidth, GLsizei nHeight, bool bInitOrthoProjection,
                                 GLdouble dHorzClip, GLdouble dVertClip, GLdouble dNearZClip,
                                 GLdouble dFarZClip)
{
    GLdouble fAspect = ((GLdouble)nWidth) / (nHeight != 0 ? nHeight : 1);
 
    ::glViewport(0, 0, nWidth, nHeight);
    if (bInitOrthoProjection)
    {
        ::glMatrixMode(GL_PROJECTION);
        ::glLoadIdentity();
 
        // Set up an orthographic projection.
        ::glOrtho(-dHorzClip, dHorzClip, -dVertClip, dVertClip, dNearZClip, dFarZClip);
 
        // Tilt scene for 3D/volume effect.
        ::glRotatef(+15.0F, 1.0F, 0.0F, 0.0F);
        ::glRotatef(-15.0F, 0.0F, 1.0F, 0.0F);
 
        ::glMatrixMode(GL_MODELVIEW);
    }
    else
    {
        ::glMatrixMode(GL_PROJECTION);
        ::glLoadIdentity();
 
        // EITHER THIS WAY: Set up an perspective projection with the appropriate aspect ratio.
        //::gluPerspective(45.0, fAspect, 0.1, 100.0);
 
        // OR THIS WAY: Set up an perspective projection with clip planes.
        ::glFrustum(-dHorzClip * 0.01, dHorzClip * 0.01, -dVertClip * 0.01, dVertClip * 0.01,
                    1.0F, 1.0 + dFarZClip);
        ::glTranslatef(0.0F, 0.0F, (float)dNearZClip);
 
        // Tilt scene for 3D/volume effect.
        ::glRotatef(+15.0F, 1.0F, 0.0F, 0.0F);
        ::glRotatef(-15.0F, 0.0F, 1.0F, 0.0F);
 
        ::glMatrixMode(GL_MODELVIEW);
    }
}

That's it. Have fun with OpenGL!

Points of Interest

Even though the individual OpenGL functions for lighting and material are well described - organizing their correct interaction took some trial and error.

History

  • 16th April, 2021: Initial tip

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)