Sunday, March 01, 2015

Lighting in OpenGL


In last post, I successfully loaded multiple mesh formats using Assimp. I decided to stick to FBX mesh format itself as that being quite famous format these days with Gaming. Also, latest version of Assimp supported fbx importing too, so there was no doubt about the same.

Next obvious choice was to incorporate Lighting solution within the framework. As we all know that there are major three types of lights used in realtime rendering, Point, Directional & Spot ( latest addition to these are Area lights & Skydome lights which will be tackled later! ). With lighting in place, it was also equally important to have decent material shaders for all the objects. I played around with Different specular models & lambertian diffuse model for the shader, but then Cook Torrance looked like a viable choice. With this, I decided to write a single Uber Shader using glsl for all the objects. Obviously, this shader will evolve over the period of time & lots of things will be added to it, but somewhere down the line I had to start doing it. Other interesting additions to this shader will be physically plausible workflow with linear lighting, but that is for some other time.



Another challenge while implementing lighting within the framework was presence of multiple light objects. Although, I implemented this earlier in DirectX, I had fair bit of idea for the same, but still it had to be done.

Vertex shader is pretty straightforward for this. However, real crux lies in pixel shader.

?
01
02
03
04
05
06
07
08
09
10
11
uniform int            numPointLights;       
struct PointLight
{
    float radius;
    float intensity;
    vec3 position;
    vec4 color;
};
#define MAX_POINT_LIGHTS 8
uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform int            numPointLights;        
struct PointLight
{
    float radius;
    float intensity;
    vec3 position;
    vec4 color;
};

#define MAX_POINT_LIGHTS 8
uniform PointLight pointLights[MAX_POINT_LIGHTS];

I created a struct for holding individual light properties, then create instance out of it to represent multiple points lights in the scene. Right now, I am supporting maximum of upto eight point lights in the scene. Later on, I iterated over each point light & added its contribution to final DiffuseDirect & SpecularDirect component.
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//--- Point Light contribution
    for(int i = 0 ; i < numPointLights ; ++i)
    {
        LightDir = normalize(vs_outPosition - pointLights[i].position);
        float dist = length(LightDir);
        float r = pointLights[i].radius;
        atten = 1 / (1 + ((2/r)*dist) + ((1/r*r)*(dist*dist)));
        // diffuse
        NdotL = max(dot(vs_outNormal, -LightDir), 0);
        // specular
        half = normalize(-LightDir + view);
        if(NdotL > 0)
            Spec = BlinnBRDF(vs_outNormal, view, half);
        // accumulate...
        DiffusePoint += pointLights[i].color * atten * NdotL * pointLights[i].intensity;
        SpecularPoint += pointLights[i].color * atten * Spec * pointLights[i].intensity;
    }
//--- Point Light contribution

    for(int i = 0 ; i < numPointLights ; ++i)
    {
        LightDir = normalize(vs_outPosition - pointLights[i].position);
        float dist = length(LightDir);
        float r = pointLights[i].radius;

        // ref : https://imdoingitwrong.wordpress.com/2011/01/31/lightattenuation/
        atten = 1 / (1 + ((2/r)*dist) + ((1/r*r)*(dist*dist)));

        // diffuse
        NdotL = max(dot(vs_outNormal, -LightDir), 0);

        // specular
        half = normalize(-LightDir + view);

        if(NdotL > 0)
            Spec = BlinnBRDF(vs_outNormal, view, half);

        // accumulate...
        DiffusePoint += pointLights[i].color * atten * NdotL * pointLights[i].intensity;
        SpecularPoint += pointLights[i].color * atten * Spec * pointLights[i].intensity;
    }



Final color was evaluated using

?
1
2
3
4
5
6
// Final Color components...
vec4 Emissive            = baseColor;
vec4 Ambient            = vec4(0.4, 0.4, 0.4, 1);
vec4 DiffuseDirect        = DiffusePoint;
vec4 SpecularDirect        = SpecularPoint;
outColor = Emissive * (Ambient + DiffuseDirect) + specColor * SpecularDirect;
// Final Color components...
vec4 Emissive            = baseColor;
vec4 Ambient            = vec4(0.4, 0.4, 0.4, 1);
vec4 DiffuseDirect        = DiffusePoint;
vec4 SpecularDirect        = SpecularPoint;
outColor = Emissive * (Ambient + DiffuseDirect) + specColor * SpecularDirect;

where DiffuseDirect & SpecularDirect considers ontribution only due to point lights in the scene.  Coming back to the OpenGL part of the code, I had to create a hierarchy so that in future it becomes very easy to dynamically add light sources in the scene & every object gets lit automatically, be it just point lights or directional lights.

Proven approach was to have common class called GameObject which represent a generic entity in the framework. This GameObject holds information about transformations of the object in the scene.  I created / inherited PointLightObject out of GameObject class with additional properties such as Visual representation mesh for the light source called LightMesh & four properties same as in the Uber shader known as LightPosition, radius, intensity & color.

Creation of LightObject in the scene was similar to StaticObject, you create an instance of PointLightObject with its color as an argument. I wanted to have a common class managing or book keeping all the lights in the scene for easy management. Thus was born class LightsManager.  LightManager's sole task is to add all the scene lights into a list which can be later used for any purpose. We use std::vector for this.Gather functions & Getter functions can be used to do all the book keeping tasks.

?
01
02
03
04
05
06
07
08
09
10
void LightsManager::GatherPointLights(GameObject* obj)
{
    m_vecPointLights.push_back(static_cast<PointLightObject*> (obj));
    m_iNumPointLights = m_vecPointLights.size();
}
PointLightObject* LightsManager::GetPointLight(int id)
{
    return m_vecPointLights.at(id);
}
void LightsManager::GatherPointLights(GameObject* obj)
{
    m_vecPointLights.push_back(static_cast<PointLightObject*> (obj));
    m_iNumPointLights = m_vecPointLights.size();
}

PointLightObject* LightsManager::GetPointLight(int id)
{
    return m_vecPointLights.at(id);
}



Finally, we make sure to use all this information while drawing each mesh so that it reflects all the lighting. Here is how I did it :

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int numPointLights = LightsManager::getInstance()->GetPointlightsCount();
glUniform1i(glGetUniformLocation(shaderID, "numPointLights"), numPointLights);
for (GLuint i = 0 ; i<numPointLights ; ++i)
{
   PointLightObject* light = LightsManager::getInstance()->GetPointLight(i);
        
   glm::vec3 position = light->GetLightPosition();
   glm::vec4 color    = light->GetLightColor();
   float intensity    = light->GetLightIntensity();
   float radius       = light->GetLightRadius();
   // form a string out of point light Ids
   std::string pointLightPosStr = "pointLights["+ std::to_string(i) + "].position";
   std::string pointLightColStr = "pointLights["+ std::to_string(i) + "].color";
   std::string pointLightIntStr = "pointLights["+ std::to_string(i) + "].intensity";
   std::string pointLightRadStr = "pointLights["+ std::to_string(i) + "].radius";
   glUniform3fv(glGetUniformLocation(shaderID, pointLightPosStr.c_str()), 1, glm::value_ptr(position));
   glUniform4fv(glGetUniformLocation(shaderID,  pointLightColStr.c_str()), 1, glm::value_ptr(color));
   glUniform1f(glGetUniformLocation(shaderID, pointLightIntStr.c_str()), intensity);
   glUniform1f(glGetUniformLocation(shaderID, pointLightRadStr.c_str()), radius);
}
int numPointLights = LightsManager::getInstance()->GetPointlightsCount();
glUniform1i(glGetUniformLocation(shaderID, "numPointLights"), numPointLights);

for (GLuint i = 0 ; i<numPointLights ; ++i)
{
   PointLightObject* light = LightsManager::getInstance()->GetPointLight(i);
       
   glm::vec3 position = light->GetLightPosition();
   glm::vec4 color    = light->GetLightColor();
   float intensity    = light->GetLightIntensity();
   float radius       = light->GetLightRadius();

   // form a string out of point light Ids
   std::string pointLightPosStr = "pointLights["+ std::to_string(i) + "].position";
   std::string pointLightColStr = "pointLights["+ std::to_string(i) + "].color";
   std::string pointLightIntStr = "pointLights["+ std::to_string(i) + "].intensity";
   std::string pointLightRadStr = "pointLights["+ std::to_string(i) + "].radius";

   glUniform3fv(glGetUniformLocation(shaderID, pointLightPosStr.c_str()), 1, glm::value_ptr(position));
   glUniform4fv(glGetUniformLocation(shaderID,  pointLightColStr.c_str()), 1, glm::value_ptr(color));
   glUniform1f(glGetUniformLocation(shaderID, pointLightIntStr.c_str()), intensity);
   glUniform1f(glGetUniformLocation(shaderID, pointLightRadStr.c_str()), radius);
}



Here are some of the screenshots with single and multiple point lights in the scene.

Single Point light source

Two point light sources

Three Point light sources

Three Point light with more intensity for Green light

Lighting with better model FatDude

Red light with more intensity & changed position

Once Multiple points lights were taken care off, directional light was piece of cake. Just like point lights, we replicate the mechanism for gathering illumination for all the directional lights in the scene too. We add struct for Directional light too, however, this time, directional light contains only direction vector, color information & intensity parameter. Just like point lights, we have a for loop for directional lights as well. Final color however is now addition of both Point light contribution & Directional light contribution.
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
for(int i = 0 ; i < numDirLights ; ++i)
{
    // diffuse
    NdotLDir = max(dot(vs_outNormal, -dirLights[i].direction), 0);
    // specular
    halfDir = normalize(-dirLights[i].direction + view);
    if(NdotLDir > 0)
        SpecDir = BlinnBRDF(vs_outNormal, view, halfDir);
    // accumulate...
    DiffuseDir += dirLights[i].color * NdotLDir * dirLights[i].intensity;
    SpecularDir += dirLights[i].color * SpecDir * dirLights[i].intensity;
}
for(int i = 0 ; i < numDirLights ; ++i)
{
    // diffuse
    NdotLDir = max(dot(vs_outNormal, -dirLights[i].direction), 0);

    // specular
    halfDir = normalize(-dirLights[i].direction + view);

    if(NdotLDir > 0)
        SpecDir = BlinnBRDF(vs_outNormal, view, halfDir);

    // accumulate...
    DiffuseDir += dirLights[i].color * NdotLDir * dirLights[i].intensity;
    SpecularDir += dirLights[i].color * SpecDir * dirLights[i].intensity;
}

We also create new class for DirectionalLights inheriting from GameObject class & also update our LightManager class to do book keeping for both Point lights & Directional lights.

Usually, I prefer setting single directional light in the scene depicting Sun light or one global light. Here is a screenshot with just a single directional light in the scene.

Single Directional Light
 And here is the output with everything combined ! We have three Point lights with one Directional light in the scene. Remember we can play with intensities if things appear burned out!! 

 
Single Directional light with Three Point lights




Spot Lights are something that I will be doing after some time. But yes, now the framework has support for dynamic addition of lights in the scene ( Upto 8 Point & 8 Directional  ). 
Cheers!




No comments: