Search code examples
c#openglglslopentkgeometry-shader

OpenGL - Geometry shader shadow mapping pass performing terribly


I'm calculating shadows for a number of point lights using Variance Shadow Mapping. All 6 faces of the cubemap are rendered in a single pass with a geometry shader, this repeats for each light source, and the whole lot is stored in a cubemap array. This all runs fine, 16 lights at 60fps no problem.

Chasing further optimisation, I tried to move the entire process to a single geometry shader pass, only to hit the only 113 vertex output limit of my hardware. Out of curiosity I decided to render 4 lights only (72 emitted vertices) and to my surprise it dropped to 24fps.

So why is it that 16 lights with 16 render passes perform significantly better than 4 lights in a single pass?

The code is essentially identical.

#version 400 core

layout(triangles) in;
layout (triangle_strip, max_vertices=18) out;

uniform int lightID;
out vec4 frag_position;

uniform mat4 projectionMatrix;
uniform mat4 shadowTransforms[6];

void main()
{   
    for(int face = 0; face < 6; face++)
    {
        gl_Layer = face + (lightID * 6);

        for(int i=0; i<3; i++)
        {
            frag_position = shadowTransforms[face] * gl_in[i].gl_Position;
            gl_Position = projectionMatrix * shadowTransforms[face] * gl_in[i].gl_Position;

            EmitVertex();
        }
        EndPrimitive();
    }
}

versus

#version 400 core

layout(triangles) in;
layout (triangle_strip, max_vertices=72) out;

out vec4 frag_position;

uniform mat4 projectionMatrix;
uniform mat4 shadowTransforms[24];

void main()
{   
    for (int lightSource = 0; lightSource < 4; lightSource++)
    {
        for(int face = 0; face < 6; face++)
        {
            gl_Layer = face + (lightSource * 6);

            for(int i=0; i<3; i++)
            {
                frag_position = shadowTransforms[gl_Layer] * gl_in[i].gl_Position;
                gl_Position = projectionMatrix * shadowTransforms[gl_Layer] * gl_in[i].gl_Position;
                EmitVertex();
            }
            EndPrimitive();
        }
    }
}

And

public void ShadowMapsPass(Shader shader)
{
    // Setup
    GL.UseProgram(shader.ID);
    GL.Viewport(0, 0, CubeMapArray.size, CubeMapArray.size);

    // Clear the cubemarray array data from the previous frame
    GL.BindFramebuffer(FramebufferTarget.Framebuffer, shadowMapArray.FBO_handle);
    GL.ClearColor(Color.White);
    GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

    for (int j = 0; j < lights.Count; j++)
    {
        // Create the light's view matrices
        List<Matrix4> shadowTransforms = new List<Matrix4>();
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(1, 0, 0), new Vector3(0, -1, 0)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(-1, 0, 0), new Vector3(0, -1, 0)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(0, 1, 0), new Vector3(0, 0, 1)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(0, -1, 0), new Vector3(0, 0, -1)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(0, 0, 1), new Vector3(0, -1, 0)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(0, 0, -1), new Vector3(0, -1, 0)));

        // Send uniforms to the shader
        for (int i = 0; i < 6; i++)
        {
            Matrix4 shadowTransform = shadowTransforms[i];
            GL.UniformMatrix4(shader.getUniformID("shadowTransforms[" + i + "]"), false, ref shadowTransform);
        }
        GL.Uniform1(shader.getUniformID("lightID"), j);
        DrawScene(shader, false);
    }
}

versus

public void ShadowMapsPass(Shader shader)
{
    // Setup
    GL.UseProgram(shader.ID);
    GL.Viewport(0, 0, CubeMapArray.size, CubeMapArray.size);

    // Clear the cubemarray array data from the previous frame
    GL.BindFramebuffer(FramebufferTarget.Framebuffer, shadowMapArray.FBO_handle);
    GL.ClearColor(Color.White);
    GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

    // Create the light's view matrices
    List<Matrix4> shadowTransforms = new List<Matrix4>();
    for (int j = 0; j < lights.Count; j++)
    {
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(1, 0, 0), new Vector3(0, -1, 0)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(-1, 0, 0), new Vector3(0, -1, 0)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(0, 1, 0), new Vector3(0, 0, 1)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(0, -1, 0), new Vector3(0, 0, -1)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(0, 0, 1), new Vector3(0, -1, 0)));
        shadowTransforms.Add(Matrix4.LookAt(lights[j].position, lights[j].position + new Vector3(0, 0, -1), new Vector3(0, -1, 0)));
    }

    // Send uniforms to the shader
    for (int i = 0; i < shadowTransforms.Count; i++)
    {
        Matrix4 shadowTransform = shadowTransforms[i];
        GL.UniformMatrix4(shader.getUniformID("shadowTransforms[" + i + "]"), false, ref shadowTransform);
    }      
    DrawScene(shader, false);
}

Solution

  • I'd guess fewer opportunities for parallel code execution in the second form. The first version of the geometry shader generates 18 vertices and must be executed 4 times, but those 4 executions can run in parallel. The second version generates 72 vertices one after the other.