Shadow map with multiple lights

Hello,

I am currently drawing objects with shadow map / single point light and would like to have more lights displayed on views, it works perfectly fine with single light as below, however it doesn’t seemed to work at all when I add more lights. :frowning:

Following are my codes, can you please point me out what is wrong with?

    .
    .
    .
    // Create and bind depth FBO/cube map
    for (int i = 0; i < lights.size; i++) {
        if (lights.data[i].depthMapFBO == 0)
            glGenFramebuffers(1, &lights.data[i].depthMapFBO);

        // Create depth cubemap texture
        if (lights.data[i].depthCubemap == 0)
            glGenTextures(1, &lights.data[i].depthCubemap);

        glBindTexture(GL_TEXTURE_CUBE_MAP, lights.data[i].depthCubemap);

        for (unsigned int j = 0; j < 6; ++j)
            glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + j + (i * lights.size), 0, GL_DEPTH_COMPONENT, lights.data[i].ShadowWidth, lights.data[i].ShadowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);

        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

        // Attach depth texture as FBO's depth buffer
        glBindFramebuffer(GL_FRAMEBUFFER, lights.data[i].depthMapFBO);
        glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, lights.data[i].depthCubemap, 0);
        glDrawBuffer(GL_NONE);
        glReadBuffer(GL_NONE);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }

    // Clear background
    glClearColor(window.bgColor.r, window.bgColor.g, window.bgColor.b, window.bgColor.a);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);




    // Render objects to depth cubemap
    for (int i = 0; i < lights.size; i++) {
        // Render scene to depth cubemap
        glViewport(0, 0, lights.data[i].ShadowWidth, lights.data[i].ShadowHeight);

        // Projection
        lights.data[i].projection = glm::perspective(glm::radians(90.0f), (float)lights.data[i].ShadowWidth / (float)lights.data[i].ShadowHeight, lights.data[i].near_plane, lights.data[i].far_plane);

        std::vector<glm::mat4> shadowMatrices;

        // Create depth cubemap
        shadowMatrices.push_back(lights.data[i].projection * glm::lookAt(lights.data[i].Position, lights.data[i].Position + glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)));
        shadowMatrices.push_back(lights.data[i].projection * glm::lookAt(lights.data[i].Position, lights.data[i].Position + glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)));
        shadowMatrices.push_back(lights.data[i].projection * glm::lookAt(lights.data[i].Position, lights.data[i].Position + glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)));
        shadowMatrices.push_back(lights.data[i].projection * glm::lookAt(lights.data[i].Position, lights.data[i].Position + glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)));
        shadowMatrices.push_back(lights.data[i].projection * glm::lookAt(lights.data[i].Position, lights.data[i].Position + glm::vec3(0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)));
        shadowMatrices.push_back(lights.data[i].projection * glm::lookAt(lights.data[i].Position, lights.data[i].Position + glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)));

        // Bind depthMap FBO and clear screen
        glBindFramebuffer(GL_FRAMEBUFFER, lights.data[i].depthMapFBO);
        glClear(GL_DEPTH_BUFFER_BIT);
        sShadowDepth.use();

        // Transformation matrices
        for (unsigned int x = 0; x < 6; ++x)
            sShadowDepth.setMat4("shadowMatrices[" + std::to_string(x) + "]", shadowMatrices[x]);

        sShadowDepth.setFloat("far_plane", lights.data[i].far_plane);
        sShadowDepth.setVec3("lightPosition", lights.data[i].Position);

        // Render objects
        RenderObjects(window, objects, sShadowDepth, lights, i);

        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }




    // Render objects as normal
    glViewport(0, 0, window.width, window.height);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    if (window.projection != PROJECTION_PERSPECTIVE) {
        window.prjMatrix = glm::ortho(-(float)window.width * window.scale, (float)window.width * window.scale,
            -(float)window.height * window.scale, (float)window.height * window.scale, 0.1f, 100.0f);

        window.view = glm::lookAt(glm::vec3(window.eye_x, window.eye_y, window.eye_z),
            glm::vec3(window.center_x, window.center_y, window.center_z),
            glm::vec3(window.up_x, window.up_y, window.up_z));
    }

    else {
        window.prjMatrix = glm::perspective(glm::radians(window.camera.Zoom), (float)window.width / (float)window.height, 0.1f, 100.0f);
        window.view = window.camera.GetViewMatrix();
    }

    shader.use();
    shader.setMat4("projection", window.prjMatrix);
    shader.setMat4("view", window.view);
    
    // Camera
    shader.setVec3("viewPos", window.camera.Position);

    for (int i = 0; i < lights.size; i++) {
        // Set lighting uniforms
        shader.setVec3("lightPosition", lights.data[i].Position);
        shader.setVec3("lightColor", lights.data[i].Color);

        // Set shadow brightness
        shader.setFloat("shadowBrightness", lights.data[i].shadowBrightness);

        // Set far plane
        shader.setFloat("far_plane", lights.data[i].far_plane);
    }

    // Enable or disable shadow
    shader.setBool("withShadow", window.withShadow);
    

    // Render objects
    RenderObjects(window, objects, sShadowDepth, lights, -1);
    .
    .
    .

[Base Vertex Shader]

#version 450 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;

out vec2 TexCoords;

out VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

uniform bool reverse_normals;

void main() {
    vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
    
    // A slight hack to make sure the outer large cube displays lighting from the 'inside' instead of the default 'outside'
    if (reverse_normals)
        vs_out.Normal = transpose(inverse(mat3(model))) * (-1.0 * aNormal);
    
    else
        vs_out.Normal = transpose(inverse(mat3(model))) * aNormal;
    
    vs_out.TexCoords = aTexCoords;
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

[Base Fragment Shader]

#version 450 core
out vec4 FragColor;

in VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} fs_in;

uniform sampler2D diffuseTexture;
uniform samplerCube depthMap;

uniform vec3 lightPosition;
uniform vec3 lightColor;
uniform vec3 viewPos;

uniform float far_plane;
uniform bool withShadow;

uniform vec3 customAmbient;
uniform bool withLight;
uniform float shadowBrightness;

// Array of offset direction for sampling
vec3 gridSamplingDisk[20] = vec3[] (
   vec3(1, 1,  1), vec3( 1, -1,  1), vec3(-1, -1,  1), vec3(-1, 1,  1), 
   vec3(1, 1, -1), vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1, 1, -1),
   vec3(1, 1,  0), vec3( 1, -1,  0), vec3(-1, -1,  0), vec3(-1, 1,  0),
   vec3(1, 0,  1), vec3(-1,  0,  1), vec3( 1,  0, -1), vec3(-1, 0, -1),
   vec3(0, 1,  1), vec3( 0, -1,  1), vec3( 0, -1, -1), vec3( 0, 1, -1)
);

float ShadowCalculation(vec3 fragPos) {
    // Get vector between fragment position and light position
    vec3 fragToLight = fragPos - lightPosition;
    
    // Get current linear depth as the length between the fragment and light position
    float currentDepth = length(fragToLight);

    float shadow = 0.0;
    float bias = 0.15;
    int samples = 20;
    float viewDistance = length(viewPos - fragPos);
    float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;
   
   for (int i = 0; i < samples; ++i) {
        float closestDepth = texture(depthMap, fragToLight + gridSamplingDisk[i] * diskRadius).r;
        closestDepth *= far_plane;   // Undo mapping [0;1]
        
        if (currentDepth - bias > closestDepth)
            shadow += 1.0;
    }

    shadow /= float(samples);

    return shadow;
}

void main() {
    vec3 ambient;
    vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
    vec3 normal = normalize(fs_in.Normal);

    // Ambient
    if (!withLight)
        ambient = customAmbient;

    else
      ambient = 0.0 * color;

    // diffuse
    vec3 lightDir = normalize(lightPosition - fs_in.FragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // specular
    vec3 viewDir = normalize(viewPos - fs_in.FragPos);
    vec3 reflectDir = reflect(-lightDir, normal);
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    float spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
    vec3 specular = spec * lightColor;    
    
    // calculate shadow
    float shadow = withShadow ? ShadowCalculation(fs_in.FragPos) : 0.0;                      
    vec3 lighting = (ambient + (shadowBrightness - shadow) * (diffuse + specular)) * color;    
    
    if (withLight)
        FragColor = vec4(lighting, 1.0);

    else
        FragColor = vec4(vec3(0.015, 0.015, 0.015), 1.0);
}

[Shadow Depth Vertex Shader]

#version 450 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;

void main() {
    gl_Position = model * vec4(aPos, 1.0);
}

[Shadow Depth Geometry Shader]

#version 450 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 18) out;

uniform mat4 shadowMatrices[6];

// FragPos from GS (output per emitvertex)
out vec4 FragPos;

void main() {
    for (int face = 0; face < 6; ++face) {
        // Built-in variable that specifies to which face we render
        gl_Layer = face;
        
        // For each triangle's vertices
        for (int i = 0; i < 3; ++i) {
            FragPos = gl_in[i].gl_Position;
            gl_Position = shadowMatrices[face] * FragPos;
            EmitVertex();
        }
        
        EndPrimitive();
    }
} 

[Shadow Depth Fragment Shader]

#version 450 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;

void main() {
    gl_Position = model * vec4(aPos, 1.0);
}

First of all, you have 2 needs here:

firstly, multiple lighting calculation, with a forward shading method (as opposed to a Deffered Shading)
secondly, multiple shadow mappping, which is independent from lighting calculations.

for the first issue, you are only perfomring one calculation for a single light, as you have in your [Base Fragment Shader].

So in order to acomplish more lighting, you’ll need to pass an array of lights to the shader, and perform the needed light calculations per each light.

example:


const int LIGHT_COUNT = 8; // Added
uniform vec3 lightPosition[LIGHT_COUNT]; // Added
uniform vec3 lightColor[LIGHT_COUNT];  //Added

void main() {
    vec3 ambient;
    vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
    vec3 normal = normalize(fs_in.Normal);

    // Ambient
    if (!withLight)
        ambient = customAmbient;

    else
      ambient = 0.0 * color;

    vec3 diffuse = vec3(0);
    vec3 specular = vec3(0);
    
    for(int l; l < LIGHT_COUNT; l++){ // Added
       // diffuse
       vec3 lightDir = normalize(lightPosition[l] - fs_in.FragPos);
       float diff = max(dot(lightDir[l], normal), 0.0);
       diffuse = diffuse + (diff * lightColor[l]);
    
       // specular
       vec3 viewDir = normalize(viewPos - fs_in.FragPos);
       vec3 reflectDir = reflect(-lightDir, normal);
       vec3 halfwayDir = normalize(lightDir + viewDir);  
       float spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
       specular = specular + (spec * lightColor);              
    }

    // calculate shadow
    float shadow = withShadow ? ShadowCalculation(fs_in.FragPos) : 0.0; 
    vec3 lighting = (ambient + (shadowBrightness - shadow) * (diffuse + specular)) * color;   
    
    if (withLight)
        FragColor = vec4(lighting, 1.0);

    else
        FragColor = vec4(vec3(0.015, 0.015, 0.015), 1.0);
}

and on the application end, you need to change you lighting data transfer:

    for (int i = 0; i < lights.size; i++) { //lights.size sould be <= 8
        // Set lighting uniforms
        shader.setVec3("lightPosition[" + i + "]", lights.data[i].Position);
        shader.setVec3("lightColor[" + i + "]", lights.data[i].Color);

//These 2 lines below are just being resend without any effect, only the last one will be used
        // Set shadow brightness
        shader.setFloat("shadowBrightness", lights.data[i].shadowBrightness);

        // Set far plane
        shader.setFloat("far_plane", lights.data[i].far_plane);
    }

As for the shadow mapping, you will need to have separate samplers, one fo each map. But having a lot can be expensive, even only with 2 or 3 objects in the scene the ufferes will consume the same (even worsse, when using cube maps), memory-wise will have a huge impact. So I would recomend maybe 2 at the most, and the rest of the lighting without any shadows if possible, otherwise, try the multiple pass approach to apply the shadows, but this method will lead you to Deffered Lighting (as mentioned above) and that can drastically change your code

Hello @Manel_Goucha
That’s something I’ve tried but what I get is no shadow drawn on the screen.
So that I’ve posted question here as I couldn’t make it work.

[1 light]

[2 lights]

I’ve tried separating samples, but no luck, there is no shadow when I have multiple lights(>= 2)

And correction should be like this, right?

#version 450 core
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;  
//out vec4 FragColor;

in VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} fs_in;

struct Light {
    vec3 Position;
    vec3 Color;
};

uniform int lightSize;
uniform Light lights[10];
uniform float far_plane[10];
uniform float shadowBrightness[10];

uniform sampler2D diffuseTexture;
uniform samplerCube depthMap;

uniform vec3 viewPos;

uniform bool withLight;
uniform bool withShadow;

uniform vec3 customAmbient;

// Array of offset direction for sampling
vec3 gridSamplingDisk[] = {
   vec3(1, 1,  1), vec3( 1, -1,  1), vec3(-1, -1,  1), vec3(-1, 1,  1), 
   vec3(1, 1, -1), vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1, 1, -1),
   vec3(1, 1,  0), vec3( 1, -1,  0), vec3(-1, -1,  0), vec3(-1, 1,  0),
   vec3(1, 0,  1), vec3(-1,  0,  1), vec3( 1,  0, -1), vec3(-1, 0, -1),
   vec3(0, 1,  1), vec3( 0, -1,  1), vec3( 0, -1, -1), vec3( 0, 1, -1)
};

float ShadowCalculation(vec3 fragToLight, float viewDistance, float farPlane) {
    float closestDepth = 0.0;

    // Get current linear depth as the length between the fragment and light position
    float currentDepth = length(fragToLight);

    float shadow = 0.0;
    float bias = 0.15;
    int samples = 20;
    float diskRadius = (1.0 + (viewDistance / farPlane)) / 25.0;
   
   for (int i = 0; i < samples; ++i) {
        closestDepth = texture(depthMap, fragToLight + gridSamplingDisk[i] * diskRadius).r;
        closestDepth *= farPlane;   // Undo mapping [0;1]
        
        if (currentDepth - bias > closestDepth)
            shadow += 1.0;
    }

    shadow /= float(samples);

    return shadow;
}

void main() {
    vec3 ambient;
    float shadow = 0.0;

    vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
    vec3 normal = normalize(fs_in.Normal);

    // Ambient
    if (!withLight)
        ambient = customAmbient;

    else
      ambient = 0.0 * color;
    
    vec3 lighting = vec3(0.0);
    vec3 viewDir = normalize(viewPos - fs_in.FragPos);
    for (int i = 0; i < lightSize; i++) {
        // Diffuse
        vec3 lightDir = normalize(lights[i].Position - fs_in.FragPos);
        float diff = max(dot(lightDir, normal), 0.0);
        vec3 diffuse = diff * lights[i].Color;
    
        // Specular
        vec3 reflectDir = reflect(-lightDir, normal);
        vec3 halfwayDir = normalize(lightDir + viewDir);  

        float spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
        vec3 specular = spec * lights[i].Color;    
    
        // Calculate shadow
        shadow = withShadow ? ShadowCalculation(fs_in.FragPos - lights[i].Position, length(viewPos - fs_in.FragPos), far_plane[i]) : 0.0;
        lighting += ((ambient + (shadowBrightness[i] - shadow) * (diffuse + specular)) * color);
    }

    if (withLight)
        FragColor = vec4(lighting, 1.0);

    else
        FragColor = vec4(vec3(0.015, 0.015, 0.015), 1.0);

    float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
    
    if (brightness > 1.0)
        BrightColor = vec4(FragColor.rgb, 1.0);

    else
        BrightColor = vec4(0.0, 0.0, 0.0, 1.0);
}

Like I said before, you have multiple needs, and one solution is not right to solve this.

you will need more than multiple samples to do a shadow calculation, you also need to create more sample textures on your application, and perform more render passes, 1 for each shadow.

first, I would try to see if your multiple lights are working without shadow.
after that, I would go applying one shadow map for each light.

tanking such big steps would only lead to confusion.

for example, you are sending data to the shader on a for-loop, and sending different data to the same uniform location, this is useless:

Setting the same uniform different values, will make the shader only use the last one you set.

 shader.setFloat("far_plane", lights.data[i].far_plane);

you put the shadow calculation inside your loop, which is performing different calculations with the same texturemap but with different light positions, this will lead to bad results.

if you are calculating only diffuse and specular, then why perform the ambient and shadow calculations multiple times:

lighting = ((ambient + (shadowBrightness[i] - shadow) * (diffuse + specular)) * color);

Let me give you a macro idea of what the sctructure should be:

Edit: I meant to say “number of lights: 3” (not 2)

1 Like

Thanks,

I’m not really good at shaders, can understand some but some not :sob:
Not really sure I understand and be able to implement its changes…

But thanks anyway!

Sometimes when trying to understand what we think its basic, in reality its more than that, and that is the main reason I said to operate on an even smaller scale for each step in order to understand everything we are working on, otherwise the monster will grow to a point on which we don’t even know where to start.

so, I would try to light up the scene, without any shadow effect, only to understand the basic lighting mechanics, and then progress to use multiple lights (also without shadowmapping).

Only then, when my lights are all setup nice and the way you like and want, you start to adapt to use shadowmapping for 1 light only. And then move on to 2 lights, and then to all lights. (keeping in mind the performance drop, obviously because its not optimized).

Each individual tecnhique can be a whole new world per se with its own intrinsic quirks that can de extendend wide enought to make it a whole area of study on its own.

This topic was automatically closed 183 days after the last reply. New replies are no longer allowed.