Renderer

Lights On

Lights change the looks of scenes dramatically. Lights affect the colors that we see as observers and are very useful to give ambiance to a scene.

Here we explore different types of lights and add them to the scene.

Lights are mainly a shader topic. This means that they are calculated in the fragment shader and then used to modify the color of pixels.

Phong Lighting Model

For this renderer we will be using a famous lighting model called the Phong Lighting. The main idea of this model is that light is divided in 3 different parts, the ambient light, the diffuse light, and the specular reflections (highlights).

An example of the phong shading model

The next code snippet is an example of the lighting model applied on the fragment shader.

#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 FragPos;

uniform vec3 u_object_color;
uniform vec3 u_light_color;
uniform vec3 u_light_pos;
uniform vec3 u_view_pos;

void main()
{
    // ambient
    float ambient_strength = 0.1;
    vec3 ambient = ambient_strength * u_light_color;

    // diffuse
    vec3 norm = normalize(Normal);
    vec3 light_dir = normalize(u_light_pos - FragPos);
    float diff = max(dot(norm, light_dir), 0.0);
    vec3 diffuse = diff * u_light_color;

    // specular
    float specular_strength = 0.5;
    vec3 view_dir = normalize(u_view_pos - FragPos);
    vec3 reflect_dir = reflect(-light_dir, norm);
    float spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32);
    vec3 specular = specular_strength * spec * u_light_color;

    vec3 result = (ambient + diffuse + specular) * u_object_color;
    FragColor = vec4(result, 1.0);
}

Ambient light is a light that affects every object in the same proportion in every direction.

Diffuse light is applied to a surface of the object with a direction. This means that the light hits the object with some angle. When the light hits the surface of the object directly (parallel to the normal vector of the surface), the intensity of the light is higher. When the light direction is perpendicular to the surface (or behind) the diffuse light is at it’s minimum.

Specular light is the light that gets reflected to the observer (view). This light usually depends on the objects material properties. More reflective materials will reflect more light.

Next we want to explore 2 different types of light that we will use in the renderer. All of them use the same lighting model but affect the scene in different ways.

Directional Light

The first light that we want to introduce to the game is the Directional Light. This could be thought as a very distant light source (like the sun) that casts light with some direction. Because the light is so far away it hits the surfaces with the same intensity.

struct DirLight {
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

uniform DirLight u_dir_light;

...

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 view_dir) {
    vec3 light_dir = normalize(-light.direction);

    // diffuse shading
    float diff = max(dot(normal, light_dir), 0.0);

    // specular shading
    vec3 reflect_dir = reflect(-light_dir, normal);
    float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess);

    // combine results
    vec3 ambient  = light.ambient  * vec3(texture(u_material.diffuse, TexCoords));
    vec3 diffuse  = light.diffuse  * diff * vec3(texture(u_material.diffuse, TexCoords));
    vec3 specular = vec3(0.0);
    if (u_material.has_specular) {
        specular = light.specular * spec * vec3(texture(u_material.specular, TexCoords));
    }

    return (ambient + diffuse + specular);
}

void main() {
    // properties
    vec3 norm = normalize(Normal);
    vec3 view_dir = normalize(u_view_pos - FragPos);

    // phase 1: Directional lighting
    vec3 result = CalcDirLight(u_dir_light, norm, view_dir);

    FragColor = vec4(result, 1.0);
}

In the previous snippet we have created a new struct in the shader that describes a DirectionalLight. This light has the 3 components of the Phong Lighting model. We have also added a new function that is used to calculate the light on the fragment.

Point Light

This next type of light is called the Point Light. This light source is positioned somewhere in the scene and will affect objects in a radius. We like to think of it as torches or street lights. A scene can have many of them.

struct PointLight {
    vec3 position;

    float constant;
    float linear;
    float quadratic;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
#define NR_POINT_LIGHTS 4

uniform PointLight u_point_lights[NR_POINT_LIGHTS];

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 frag_pos, vec3 view_dir) {
    vec3 light_dir = normalize(light.position - frag_pos);

    // diffuse shading
    float diff = max(dot(normal, light_dir), 0.0);

    // specular shading
    vec3 reflect_dir = reflect(-light_dir, normal);
    float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess);

    // attenuation
    float distance = length(light.position - frag_pos);
    float attenuation = 1.0 / (light.constant + light.linear * distance +
                         light.quadratic * (distance * distance));

    // combine results
    vec3 ambient  = light.ambient  * vec3(texture(u_material.diffuse, TexCoords));
    vec3 diffuse  = light.diffuse  * diff * vec3(texture(u_material.diffuse, TexCoords));
    vec3 specular = vec3(0.0);
    if (u_material.has_specular) {
        specular = light.specular * spec * vec3(texture(u_material.specular, TexCoords));
    }

    ambient  *= attenuation;
    diffuse  *= attenuation;
    specular *= attenuation;

    return (ambient + diffuse + specular);
}

void main() {
    // properties
    vec3 norm = normalize(Normal);
    vec3 view_dir = normalize(u_view_pos - FragPos);

    // phase 1: Directional lighting
    vec3 result = CalcDirLight(u_dir_light, norm, view_dir);

    // phase 2: Point lights
    for(int i = 0; i < NR_POINT_LIGHTS; i++) {
        result += CalcPointLight(u_point_lights[i], norm, FragPos, view_dir);
    }

    FragColor = vec4(result, 1.0);
}

Similar to the DirectionalLight, we have created a struct for the PointLight and a function to calculate and add the contribution of all the point lights in the scene on the fragment.

This type of light requires a bit more setup as we need to pass an array of lights that needs to be configured upfront in the CPU code.

Spot Light

Finally, the last type of light that we will add is the SpotLight. This type of light can be thought as a lantern or the lights on a stage that point the main point of attraction. This light usually creates a cone of light that only adds light to objects that are inside that cone of light.

struct SpotLight {
    vec3 position;
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;
    float linear;
    float quadratic;

    float cut_off;
    float outer_cut_off;
};

uniform SpotLight u_spot_light;

vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 frag_pos, vec3 view_dir) {
    vec3 light_dir = normalize(light.position - frag_pos);

    // diffuse shading
    float diff = max(dot(normal, light_dir), 0.0);

    // specular shading
    vec3 reflect_dir = reflect(-light_dir, normal);
    float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess);

    // attenuation
    float distance = length(light.position - frag_pos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));

    // spot light (soft-edges)
    float theta = dot(light_dir, normalize(-light.direction));
    float epsilon = light.cut_off - light.outer_cut_off;
    float intensity = clamp((theta - light.outer_cut_off) / epsilon, 0.0, 1.0);

    // combine results
    vec3 ambient = light.ambient * vec3(texture(u_material.diffuse, TexCoords));
    vec3 diffuse = light.diffuse * diff * vec3(texture(u_material.diffuse, TexCoords));

    vec3 specular = vec3(0.0);
    if (u_material.has_specular) {
        specular = light.specular * spec * vec3(texture(u_material.specular, TexCoords));
    }

    ambient *= attenuation * intensity;
    diffuse *= attenuation * intensity;
    specular *= attenuation * intensity;

    return (ambient + diffuse + specular);
}

void main() {
    // properties
    vec3 norm = normalize(Normal);

    if (u_material.has_normal) {
        norm = texture(u_material.normal, TexCoords).rgb;
        // remap from [0,1] to [-1,1]
        norm = normalize(norm * 2.0 - 1.0);
    }

    vec3 view_dir = normalize(u_view_pos - FragPos);

    // phase 1: Directional lighting
    vec3 result = CalcDirLight(u_dir_light, norm, view_dir);

    // phase 2: Point lights
    for(int i = 0; i < NR_POINT_LIGHTS; i++) {
        result += CalcPointLight(u_point_lights[i], norm, FragPos, view_dir);
    }

    // phase 3: Spot light
    result += CalcSpotLight(u_spot_light, norm, FragPos, view_dir);

    FragColor = vec4(result, 1.0);
}

The previous snippet follows the same pattern of the other types of lights. It adds a new struct for the SpotLight and a function that calculates the contribution of the light.

Lights On Scene

The shader is ready to render some lights, now we just need to add some lights to the scene and setup some of the uniforms before we can see those lights taking effect on the objects.

We first need some type of new entity that we can add to the scene. This one is a bit special because it has some properties that we will need for the lights.

enum class light_type {
    Directional,
    Point,
    Spot,
};

struct light {
    entity Entity;
    light_type LightType;
    float AmbientStrength;
    float SpecularStrength;
};

...

// In scene.h

struct scene {
    std::vector<entity> Entities;
    std::vector<light> Lights;
};

void Scene_AddLight(scene &Scene, light &Light);

We have also created an array of Lights on the scene to keep lights separate from other objects and We have added a new function to add a light to the scene.

We will now add some lights to the scene.

// Lights
glm::vec3 PointLightPositions[] = {
    glm::vec3(0.7f, 0.2f, 2.0f),
    glm::vec3(2.3f, -3.3f, -4.0f),
    glm::vec3(-4.0f, 2.0f, -12.0f),
    glm::vec3(0.0f, 0.0f, -3.0f),
};
size_t PointLightPositionsLength =
    sizeof(PointLightPositions) / sizeof(PointLightPositions[0]);

for (size_t i = 0; i < PointLightPositionsLength; i++) {
    light PointLight = {
        .Entity =
            {
                .Position = PointLightPositions[i],
                .Scale = glm::vec3(1.0f),
                .Rotation = glm::vec4(0.0f),
                .Color = glm::vec4(0.05f, 0.05f, 0.05f, 1.0f),
            },
        .LightType = light_type::Point,
        .AmbientStrength = 0.1f,
        .SpecularStrength = 0.5,
    };
    Scene_AddLight(Scene, PointLight);
}

light SpotLight = {
    .Entity =
        {
            .Position = Camera.Position,
            .Scale = glm::vec3(1.0f),
            .Rotation = glm::vec4(0.0f),
            .Color = glm::vec4(0.05f, 0.05f, 0.05f, 1.0f),
        },
    .LightType = light_type::Spot,
    .AmbientStrength = 0.1f,
    .SpecularStrength = 0.5,
};
Scene_AddLight(Scene, SpotLight);

light DirectionalLight = {
    .Entity =
        {
            .Position = glm::vec3(0.0f),
            .Scale = glm::vec3(1.0f),
            .Rotation = glm::vec4(0.0f),
            .Color = glm::vec4(0.05f, 0.05f, 0.05f, 1.0f),
        },
    .LightType = light_type::Directional,
    .AmbientStrength = 0.1f,
    .SpecularStrength = 0.5,
};
Scene_AddLight(Scene, DirectionalLight);

The last thing to do is to setup all the OpenGL state that turns those lights on. We have created a function to setup all the lights that are in the scene in one place.

void Renderer_DrawSceneLights(const renderer &Renderer,
                              shader_program ShaderProgram, const scene &Scene,
                              const camera &Camera) {
    glUseProgram(ShaderProgram.ID);

    // Lights
    for (size_t i = 0; i < Scene.Lights.size(); i++) {
        switch (Scene.Lights[i].LightType) {
        case light_type::Directional: {
            // Directional light
            float DirectionalLight[4] = {1.0f, 1.0f, 1.0f, 1.0f};
            glm::vec3 DirectionalLightDir = glm::vec3(
                DirectionalLight[0], DirectionalLight[1], DirectionalLight[2]);
            glm::vec3 DirectionalLightAmbient = glm::vec3(0.05f, 0.05f, 0.05f);
            glm::vec3 DirectionalLightDiffuse = glm::vec3(0.4f, 0.4f, 0.4f);
            glm::vec3 DirectionalLightSpecular = glm::vec3(0.5f, 0.5f, 0.5f);

            glUniform3fv(ShaderProgram.Uniforms.DirectionalLight.DirUniformLoc,
                         1, glm::value_ptr(DirectionalLightDir));
            glUniform3fv(
                ShaderProgram.Uniforms.DirectionalLight.AmbientUniformLoc, 1,
                glm::value_ptr(DirectionalLightAmbient));
            glUniform3fv(
                ShaderProgram.Uniforms.DirectionalLight.DiffuseUniformLoc, 1,
                glm::value_ptr(DirectionalLightDiffuse));
            glUniform3fv(
                ShaderProgram.Uniforms.DirectionalLight.SpecularUniformLoc, 1,
                glm::value_ptr(DirectionalLightSpecular));
            break;
        }
        case light_type::Point: {
            glm::vec3 PointLightAmbient = glm::vec3(
                Scene.Lights[i].Entity.Color.r, Scene.Lights[i].Entity.Color.g,
                Scene.Lights[i].Entity.Color.b);
            glm::vec3 PointLightDiffuse = glm::vec3(0.8f, 0.8f, 0.8f);
            glm::vec3 PointLightSpecular = glm::vec3(1.0f, 1.0f, 1.0f);
            glUniform3fv(
                ShaderProgram.Uniforms.PointLights[i].PositionUniformLoc, 1,
                glm::value_ptr(Scene.Lights[0].Entity.Position));
            glUniform3fv(
                ShaderProgram.Uniforms.PointLights[i].AmbientUniformLoc, 1,
                glm::value_ptr(PointLightAmbient));
            glUniform3fv(
                ShaderProgram.Uniforms.PointLights[i].DiffuseUniformLoc, 1,
                glm::value_ptr(PointLightDiffuse));
            glUniform3fv(
                ShaderProgram.Uniforms.PointLights[i].SpecularUniformLoc, 1,
                glm::value_ptr(PointLightSpecular));

            float Constant = 1.0f;
            float Linear = 0.09f;
            float Quadratic = 0.032f;
            glUniform1f(
                ShaderProgram.Uniforms.PointLights[i].ConstantUniformLoc,
                Constant);
            glUniform1f(ShaderProgram.Uniforms.PointLights[i].LinearUniformLoc,
                        Linear);
            glUniform1f(
                ShaderProgram.Uniforms.PointLights[i].QuadraticUniformLoc,
                Quadratic);
            break;
        }
        case light_type::Spot: {
            glm::vec3 SpotLightAmbient = glm::vec3(0.0f, 0.0f, 0.0f);
            glm::vec3 SpotLightDiffuse = glm::vec3(1.0f, 1.0f, 1.0f);
            glm::vec3 SpotLightSpecular = glm::vec3(1.0f, 1.0f, 1.0f);
            glUniform3fv(ShaderProgram.Uniforms.SpotLight.PositionUniformLoc, 1,
                         glm::value_ptr(Camera.Position));
            glUniform3fv(ShaderProgram.Uniforms.SpotLight.DirectionUniformLoc,
                         1, glm::value_ptr(Camera.Front));
            glUniform3fv(ShaderProgram.Uniforms.SpotLight.AmbientUniformLoc, 1,
                         glm::value_ptr(SpotLightAmbient));
            glUniform3fv(ShaderProgram.Uniforms.SpotLight.DiffuseUniformLoc, 1,
                         glm::value_ptr(SpotLightDiffuse));
            glUniform3fv(ShaderProgram.Uniforms.SpotLight.SpecularUniformLoc, 1,
                         glm::value_ptr(SpotLightSpecular));

            float Constant = 1.0f;
            float Linear = 0.09f;
            float Quadratic = 0.032f;
            float CutOff = glm::cos(glm::radians(12.5f));
            float OuterCutOff = glm::cos(glm::radians(15.0f));
            glUniform1f(ShaderProgram.Uniforms.SpotLight.ConstantUniformLoc,
                        Constant);
            glUniform1f(ShaderProgram.Uniforms.SpotLight.LinearUniformLoc,
                        Linear);
            glUniform1f(ShaderProgram.Uniforms.SpotLight.QuadraticUniformLoc,
                        Quadratic);
            glUniform1f(ShaderProgram.Uniforms.SpotLight.CutOffUniformLoc,
                        CutOff);
            glUniform1f(ShaderProgram.Uniforms.SpotLight.OuterCutOffUniformLoc,
                        OuterCutOff);
            break;
        }
        }
    }
}

Finally, we can render those lights when rendering all the objects:

void Renderer_DrawScene(const renderer &Renderer, const scene &Scene,
                        const context &Context) {

    // Sets the view & projection uniforms for all the programs
    Renderer_SetCameraUniforms(Renderer, Context.Camera, Context.ScreenWidth,
                               Context.ScreenHeight);

    // Entities
    for (entity Entity : Scene.Entities) {
        // Draw entities
        ...
    }

    // Draw Lights
    Renderer_DrawSceneLights(Renderer, Renderer.ShaderProgram, Scene,
                             Context.Camera);

    ...
}

Now that the scene has lights we want to give more realistic views to the objects by drawing textures on top. In the next section we add textures to the objects and introduce the concept of Materials.

Squared Wave SVG