5
\$\begingroup\$

I've recently been following a tutorial for writing a game engine in Java using LWJGL 2. I followed the tutorial for writing a fragment shader that utilizes spotlights in GLSL.

The example game currently running in the engine is a spotlight that follows the camera and a plane mesh with a tile texture. When I run the game, when viewing the mesh at certain distances and angles, namely close distances and sharp angles, I run into this interesting bug when certain pixels just go black. This has only occurred when I modified the fragment shader to include spotlights. This can only mean that there is a bug in the fragment shader itself.

The fragment shader code is below. What I believe to be the problematic code is marked with a comment below in the function calcSpotLight.

Update: I have found that the problem was indeed in my graphics card's driver. I just updated the driver and reran the program and indeed received no black pixels.

Edit: It appears that the problem is indeed in the calcSpotLight function, but within the calcSpotLight function, the problem is the calcPointLight function. I still don't know what the exact problem is, but that should narrow down the possible errors.

#version 330

const int MAX_POINT_LIGHTS = 4;
const int MAX_SPOT_LIGHTS = 4;

in vec2 texCoord0;
in vec3 normal0;
in vec3 worldPos0;

out vec4 fragColor;

struct BaseLight
{
    vec3 color;
    float intensity;
};

struct DirectionalLight
{
    BaseLight base;
    vec3 direction;
};

struct Attenuation
{
    float constant;
    float linear;
    float exponent;
};

struct PointLight
{
    BaseLight base;
    Attenuation atten;
    vec3 position;
    float range;
};

struct SpotLight
{
    PointLight pointLight;
    vec3 direction;
    float cutoff;
};

uniform vec3 baseColor;
uniform vec3 eyePos;
uniform vec3 ambientLight;
uniform sampler2D sampler;

uniform float specularIntensity;
uniform float specularPower;

uniform DirectionalLight directionalLight;
uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform SpotLight spotLights[MAX_SPOT_LIGHTS];

vec4 calcLight(BaseLight base, vec3 direction, vec3 normal)
{
    float diffuseFactor = dot(normal, -direction);

    vec4 diffuseColor = vec4(0,0,0,0);
    vec4 specularColor = vec4(0,0,0,0);

    if(diffuseFactor > 0)
    {
        diffuseColor = vec4(base.color, 1.0) * base.intensity * diffuseFactor;

        vec3 directionToEye = normalize(eyePos - worldPos0);
        vec3 reflectDirection = normalize(reflect(direction, normal));

        float specularFactor = dot(directionToEye, reflectDirection);
        specularFactor = pow(specularFactor, specularPower);

        if(specularFactor > 0)
        {
            specularColor = vec4(base.color, 1.0) * specularIntensity * specularFactor;
        }
    }

    return diffuseColor + specularColor;
}

vec4 calcDirectionalLight(DirectionalLight directionalLight, vec3 normal)
{
    return calcLight(directionalLight.base, -directionalLight.direction, normal);
}

vec4 calcPointLight(PointLight pointLight, vec3 normal)
{
    vec3 lightDirection = worldPos0 - pointLight.position;
    float distanceToPoint = length(lightDirection);

    if(distanceToPoint > pointLight.range)
        return vec4(0,0,0,0);

    lightDirection = normalize(lightDirection);

    vec4 color = calcLight(pointLight.base, lightDirection, normal);

    float attenuation = pointLight.atten.constant +
                        pointLight.atten.linear * distanceToPoint +
                        pointLight.atten.exponent * distanceToPoint * distanceToPoint +
                        0.0001;

    return color / attenuation;
}

vec4 calcSpotLight(SpotLight spotLight, vec3 normal)
{
    vec3 lightDirection = normalize(worldPos0 - spotLight.pointLight.position);
    float spotFactor = dot(lightDirection, spotLight.direction);

    vec4 color = vec4(0,0,0,0);

    //This is likely the problematic code, but I'm not 100 percent sure

    if(spotFactor > spotLight.cutoff)
    {
        color = calcPointLight(spotLight.pointLight, normal) *
                (1.0 - (1.0 - spotFactor)/(1.0-spotLight.cutoff));
    }

    return color;
}
void main()
{
    vec4 totalLight = vec4(ambientLight,1);
    vec4 color = vec4(baseColor, 1);
    vec4 textureColor = texture(sampler, texCoord0.xy);

    if(textureColor != vec4(0,0,0,0))
        color *= textureColor;

    vec3 normal = normalize(normal0);

    totalLight += calcDirectionalLight(directionalLight, normal);

    for(int i = 0; i < MAX_POINT_LIGHTS; i++)
    {
        if(pointLights[i].base.intensity > 0)
            totalLight += calcPointLight(pointLights[i],normal);
    }

    for(int i = 0; i < MAX_SPOT_LIGHTS; i++)
    {
        if(spotLights[i].pointLight.base.intensity > 0)
            totalLight += calcSpotLight(spotLights[i],normal);
    }

    fragColor = color * totalLight;
}

I checked to see if there was maybe some division by zero problem or something of the like, but I can't for the life of me figure out what the problem actually is. And since debugging in GLSL is nearly impossible, I've decided that my last possible option is to post my question here.

Here are some example images of the bug in game:

enter image description here Here, the black pixels can be seen at the bottom of the spotlight

enter image description here Here, the black pixels are mostly at the edge of the spotlight

Any help would be appreciated.

\$\endgroup\$
  • \$\begingroup\$ Have you tried isolating the problem by replacing the sampler with a single diffuse color or removing lights one at a time to see where the problem could lie? \$\endgroup\$ – user5665 Jul 15 '16 at 21:47
  • \$\begingroup\$ I tried a single diffuse color instead of the equation. That rendered no black pixels. I then tried to remove different parts of the equation and it appears that the problem is coming from 'calcPointLight(spotLight.pointLight, normal)'. Meaning that the 'calcPointLight' function is actually probably at fault here. \$\endgroup\$ – Orren Ravid Jul 15 '16 at 21:59
  • \$\begingroup\$ When you used a single diffuse color, did you try something with non-zero red, green, and blue values while not being a gradation of white? If so, it sounds like you could have had an issue with your texture or texture coordinates as well. \$\endgroup\$ – user5665 Jul 15 '16 at 22:01
  • \$\begingroup\$ Yes I used a solid color red, and the code worked fine, which is what leads me to believe it's not a texture coordinate problem. \$\endgroup\$ – Orren Ravid Jul 15 '16 at 22:03
  • \$\begingroup\$ Just for sake of experimental reproduction, try vec4 textureColor = vec4(0.964, 0.325, 0.078, 1.0); and see if your issue shows up. If your black pixels disappear when you replace your texture sampler with a solid color, that's a good indication that everything else performs within expected parameters but your sampler or coordinates are out of expected parameters; on the other hand, your lighting code may perform fine with some expected inputs and have edge cases where it fails, but that looks slightly less likely with your screenshot. \$\endgroup\$ – user5665 Jul 15 '16 at 22:10
7
\$\begingroup\$

Unexpectedly black pixels sometimes indicate that you've got an infinity or NaN in the shader somewhere. For example normalize(vec3(0,0,0)) will generate a NaN. To me, the most obvious candidate in the above shader is the reflectDirection variable, but I could be wrong. GLSL has isinf() and isnan() functions that you can use to detect those cases.

If that doesn't help, I'd suggest using a graphics debugging tool like the one at http://renderdoc.org/ to debug the problem.

\$\endgroup\$
  • \$\begingroup\$ Since this is a java application, what exe do I specify for the renderdoc to capture? Since java uses JIT compilation, I can't just give it an exe. \$\endgroup\$ – Orren Ravid Jul 16 '16 at 2:43
  • \$\begingroup\$ You probably want to specify java.exe with an appropriate command line to launch your code. docs.oracle.com/javase/7/docs/technotes/tools/windows/java.html \$\endgroup\$ – Adam Jul 16 '16 at 10:33
  • \$\begingroup\$ In my experience, also denormalized numbers might generate unexpected black spots. \$\endgroup\$ – Gabriele Giuseppini Nov 18 '18 at 18:57
2
\$\begingroup\$

There is at least one serious problem in the code. In calcLight(), the test below is invalid. Calling pow with a negative first argument is undefined behaviour, so you need to test specularFactor sooner.

    float specularFactor = dot(directionToEye, reflectDirection);
    specularFactor = pow(specularFactor, specularPower);

    if(specularFactor > 0)

Just change it to:

    float specularFactor = dot(directionToEye, reflectDirection);
    if(specularFactor > 0)
    {
        specularFactor = pow(specularFactor, specularPower);

Some random remarks:

  • you have an exponent uniform that is actually for quadratic attenuation; you should name it quadratic for readability.
  • adding 0.0001 to attenuation probably avoids division by zero but if that case ever happens your light will be 10000 times too powerful; I suggest removing it and doing return color / max(attenuation, 1.0) instead).
\$\endgroup\$
  • 1
    \$\begingroup\$ Both of those fixes make sense, however, they did not solve the problem. I still get the same black pixels. \$\endgroup\$ – Orren Ravid Jul 16 '16 at 19:11
2
\$\begingroup\$

A guess (too long to put in a comment):

If spotFactor becomes 0.6 at a specific pixel, from this

float spotFactor = dot(lightDirection, spotLight.direction);

then color goes negative here (as you said spotLight.cutoff is 0.71):

color = calcPointLight(spotLight.pointLight, normal) *
            (1.0 - (1.0 - spotFactor)/(1.0-spotLight.cutoff));   

will be:

1 - (1 - 0.6) / (1 - 0.71) -> -0.379310345

and the breakpoint is ofc where the dot product and spotLight.cutoff are equal or close to equal.

After this i cannot guess what happens. Maybe you simply produce black where the dot product is 0.71 - may happen at only a few pixels, where the spotlight values are much on negative. Or maybe the GPU driver goes nuts there :-).

Other notes:

  • You calculate lightDirection twice for the spot light, once in calcSpotlight and once in calcPointlight. I may rather write the different parts separately...

  • The 0.0001 is highly dubious. Potentially you add a big value with a small value in float32 space - this may be impossible, as you only have 7 significants to use. Cannot add 1000 + 0.0001 and expect to get 1000.0001, since 32-bit space isn't sufficient for that precision (or is at the limit). I didn't try to follow all of the code, but high exponents and division by almost-zero are potential sources for weirdness. As is [huge specular value] / 0.0001, if that occurs.

\$\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.