4
\$\begingroup\$

I am using a fragment shader to create circular light sources in my 2D game. Full code at the bottom.

Essentially, an array of light source objects (called Lights in the code) are passed into the shader in groups of 40. The shader runs through a for loop for each light source in the batch (up to four) and attenuates the brightness of that pixel based on how far away from the light source it is. If there are more than forty light sources, additional passes of this shader are done until all the light sources on the screen are processed. Limiting the lights per shader pass is done to limit how much data is pushed to the GPU at once (this is what the book I am reading is telling me to do) to improve performance.

It works fine, but I notice pretty dramatic frame rate loss when the number of light sources on the screen exceeds 30 or so. By the time I'm at 80 or 90 light sources, the frame rate is down to about 10 fps. Not cool.

Some questions:

  1. What sticks out in the loop as the most expensive operations, and how can they be done better? I have been commenting out some of these I try to see what effect each line has. Is there a better way to do this? How do I profile a shader program? I am new to this. Any help would be appreciated.
  2. As you can see, I am using an older version of GLSL because I am using macOS X (version 1.20). Would this shader work more efficiently on a different platform with a new version of OpenGL?
  3. I'm considering the possibility that 30+ light sources is simply too much I need to seek a way to optimize the shading process outside of this shader loop. I am using a deferred rendering process, but I have read about how some games render lighting using a partition light sources into different regions. I.e., a light source only affects pixels within a region around it (that it could reasonably affect), so that you don't have to run a calculation for every single pixel on the screen for each light source. Kind of like doing a broad-phase pass for collision detection. How do 2D games typically handle many light sources?
#version 120
uniform sampler2D LastPass;
uniform sampler2D DiffuseMap;
uniform vec3 AmbientLight;
uniform int LightCount;
uniform int PassNumber;

struct LightInfo {
    vec3 position;
    vec3 color;
    float radius;
    float falloff;
};

const int MaxLights = 40;
uniform LightInfo Lights[MaxLights];


void main()
{
    vec4 pixel = texture2D(LastPass, gl_TexCoord[0].xy);
    vec4 diffusepixel = texture2D(DiffuseMap, gl_TexCoord[0].xy);

    vec4 finalPixel = gl_Color * pixel;

    for(int i = 0; i < LightCount; ++i) {

        LightInfo light = Lights[i];
        vec3 L = light.position - gl_FragCoord.xyz;
        float distance = length(L);
        float d = max(distance - light.radius, 0);
        L /= distance;
        // calculate basic light attenuation
        float attenuation = 1 / pow(d/light.radius + 1, 2);
        // scale and bias attenuation such that:
        //   attenuation == 0 at extent of max influence
        //   attenuation == 1 when d == 0
        attenuation = (attenuation - light.falloff) / (1 - light.falloff);
        attenuation = max(attenuation, 0);

        //This line runs VERY slow when there are many light sources.
        finalPixel += (diffusepixel * ((vec4(light.color, 0.4) * attenuation)));

    }
    gl_FragColor = finalPixel;

}

Thanks for any help.

\$\endgroup\$
2
\$\begingroup\$

The method used by Doom 3 - which is, admittedly, fairly old, but that probably makes it more relevant to the downlevel GLSL version you're using - goes like this:

  • Clear to black (you could make this your ambient light colour if you wish).
  • Do an initial depth-only pre-pass of the entire scene.
  • Enable additive blending (via glBlendFunc (GL_ONE, GL_ONE)).
  • For each light:
    • Draw all surfaces hit by that light.

This obviously requires multiple passes over your scene rather than doing everything in a single pass, but - as I indicated - the existence proof that it works is Doom 3.

The key difference is that your shader now only handles one light, but you get the ability to have any arbitrary number of total lights in your scene; it also simplifies your shader and removes any ambiguity about whether that's a bottleneck for you.

Of course Doom 3 is not a 2D game, but there's no reason why this method couldn't be used in a 2D game as much as in a 3D game.

| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

Switching to deferred shading would be the best solution with this many lights. ( https://en.wikipedia.org/wiki/Deferred_shading )

I am using a deferred rendering process

vec4 pixel = texture2D(LastPass, gl_TexCoord[0].xy);

That is not proper deferred rendering.

You're supposed to accumulate all the light values by drawing additively into a lighting buffer (letting the GPU's memory/cache subsystem do the accumulation) and when this is completed combine both the diffuse and lighting buffers in one final pass.

And draw each light as a quad/triangle over the lighting buffer covering only their visible radius.

But without re-engineering the whole pipeline the first thing would be to remove some of the divisions and use pre-calculated reciprocals.

Transform:

attenuation = (attenuation - light.falloff) / (1 - light.falloff);

Into:

attenuation = (attenuation - light.falloff) * light.one_minus_falloff_inverse;

By pre-calculating one_minus_falloff_inverse as 1.0f/(1 - light.falloff) on the CPU.

Turn d/light.radius into d*light.radius_inverse...

And this is a red-herring:

    //This line runs VERY slow when there are many light sources.
    finalPixel += (diffusepixel * ((vec4(light.color, 0.4) * attenuation)));

Commenting out this line makes everything faster because the shader compiler eliminates the entire loop and removes one texture lookup because nothing in the entire calculation is useful when that line of code is gone.

See "Dead code elimination" ( https://en.wikipedia.org/wiki/Dead_code ) done by compilers.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.