# Implementing spot lights in Vulkan and some pixels are black

TL;DR; I have come to the conclusion that the problem is linked to the spot light being too close to the camera. Which only made me more confused.

Long version; I have sat with this my whole day and now, my head just hurts and I need to ask for help. Basically, I have a spot light attached to my camera - pointing in the same direction as the camera, acting as a flashlight. I can toggle this on and off. When toggling this on and off, the whole scene gets covered in random black pixels and I have no idea why. For reference, here's my entire fragment shader;

/*
*   Calculates the distribution.
*/
float CalculateDistribution(vec3 halfwayDirection)
{
float roughnessSquared = roughness * roughness;
float roughnessSquaredSquared = roughnessSquared * roughnessSquared;
float normalHalfwayAngle = max(dot(normalDirection, halfwayDirection), 0.0f);
float normalHalfwayAngleSquared = normalHalfwayAngle * normalHalfwayAngle;

float denominator = normalHalfwayAngleSquared * (roughnessSquaredSquared - 1.0f) + 1.0f;

return roughnessSquaredSquared / (PI * denominator * denominator);
}

/*
*   Calculates the geometry.
*/
float CalculateGeometry(float lightAngle)
{
float geometryRoughness = roughness + 1.0f;
geometryRoughness = (geometryRoughness * geometryRoughness) / 8.0f;

float lightObstruction = lightAngle / (lightAngle * (1.0f - geometryRoughness) + geometryRoughness);
float viewObstruction = viewAngle / (viewAngle * (1.0f - geometryRoughness) + geometryRoughness);

return lightObstruction * viewObstruction;
}

/*
*   Calculates the fresnel, with roughness in mind.
*/
vec3 CalculateFresnelRoughness(float lightViewAngle)
{
return surfaceColor + (max(vec3(1.0f - roughness), surfaceColor) - surfaceColor) * pow(1.0f - lightViewAngle, 5.0f);
}

/*
*   Calculates the fresnel.
*/
vec3 CalculateFresnel(float lightViewAngle)
{
return surfaceColor + (1.0f - surfaceColor) * pow(1.0f - lightViewAngle, 5.0f);
}

/*
*   Calculates the ambient component of this fragment.
*/
vec3 CalculateAmbient()
{
return albedoColor * AmbientMultiplier;
}

/*
*   Calculates a light.
*/
vec3 CalculateLight(vec3 lightDirection, vec3 radiance, float intensity)
{
vec3 halfwayDirection = normalize(viewDirection + lightDirection);
float lightViewAngle = max(dot(halfwayDirection, viewDirection), 0.0f);
float lightAngle = max(dot(normalDirection, lightDirection), 0.0f);

float distribution = CalculateDistribution(halfwayDirection);
float geometry = CalculateGeometry(lightAngle);
vec3 fresnel = CalculateFresnel(lightViewAngle);

vec3 diffuseComponent = vec3(1.0f) - fresnel;
diffuseComponent *= 1.0f - metallic;

vec3 nominator = distribution * geometry * fresnel;
float denominator = 4 * viewAngle * lightAngle + 0.001f;
vec3 specularComponent = nominator / denominator;

//Return the combined components.
return (diffuseComponent * albedoColor / PI + specularComponent) * radiance * lightAngle * intensity;
}

/*
*   Calculates a single point light.
*/
vec3 CalculatePointLight(int index)
{
//Calculate the point light.
vec3 lightDirection = normalize(pointLightWorldPositions[index] - fragmentWorldPosition);

float distanceToLightSource = length(fragmentWorldPosition - pointLightWorldPositions[index]);
float attenuation = clamp(1.0f - distanceToLightSource / pointLightAttenuationDistances[index], 0.0f, 1.0f);
attenuation *= attenuation;

vec3 radiance = pointLightColors[index] * attenuation;

}

/*
*   Calculates a single spot light.
*/
vec3 CalculateSpotLight(int index)
{
//Calculate the spot light.
vec3 lightDirection = normalize(spotLightWorldPositions[index] - fragmentWorldPosition);
float angle = dot(lightDirection, -spotLightDirections[index]);

float distanceToLightSource = length(fragmentWorldPosition - spotLightWorldPositions[index]);
float attenuation = clamp(1.0f - distanceToLightSource / spotLightAttenuationDistances[index], 0.0f, 1.0f);
attenuation *= attenuation;

vec3 radiance = spotLightColors[index] * attenuation;

vec3 calculatedLight = CalculateLight(lightDirection, radiance, spotLightIntensities[index]);

//Calculate the inner/outer cone fade out.
float epsilon = spotLightInnerCutoffAngles[index] - spotLightOuterCutoffAngles[index];
float intensity = angle > spotLightInnerCutoffAngles[index] ? 1.0f : clamp((angle - spotLightOuterCutoffAngles[index]) / epsilon, 0.0f, 1.0f);

calculatedLight *= intensity;

return calculatedLight;
}

void main()
{
//Sample values from the textures.
albedoColor = pow(texture(albedoTexture, fragmentTextureCoordinate).rgb, vec3(2.2f));

//Calculate the normal direction.
normalDirection = texture(normalMapTexture, fragmentTextureCoordinate).xyz * 2.0f - 1.0f;
normalDirection = fragmentTangentSpaceMatrix * normalDirection;
normalDirection = normalize(normalDirection);

roughness = texture(roughnessTexture, fragmentTextureCoordinate).r;
metallic = texture(metallicTexture, fragmentTextureCoordinate).r;
ambientOcclusion = texture(ambientOcclusionTexture, fragmentTextureCoordinate).r;

//Calculate globals.
viewDirection = normalize(cameraWorldPosition - fragmentWorldPosition);
viewAngle = max(dot(normalDirection, viewDirection), 0.0f);
surfaceColor = mix(vec3(0.04f), albedoColor, metallic);

//Start off with just the ambient lighting.
vec3 finalFragment = CalculateAmbient();

//Calculate all point lights.
for (int i = 0; i < numberOfPointLights; ++i)
{
finalFragment += CalculatePointLight(i);
}

//Calculate all spot lights.
for (int i = 0; i < numberOfSpotLights; ++i)
{
finalFragment += CalculateSpotLight(i);
}

//Apply gamma correction.
finalFragment = pow(finalFragment, vec3(1.0f / 2.2f));

//Set the final fragment color.
fragmentColor = vec4(finalFragment, 1.0f);
}


PHEW. That's a lot of code. It's a PBR shader in the making, kind of. The function "CalculateSpotLight" is of the most interest. For some reason, changing out;

vec3 lightDirection = normalize(spotLightWorldPositions[index] - fragmentWorldPosition);


to;

vec3 lightDirection = vec3(0.0f, 1.0f, 0.0f);


or some other arbitrary value, removes the black pixels, which led me to believe that the normalize function receives a vec3(0.0f, 0.0f, 0.0f), but that's not the case, I've added numerous checks to detect division by zero, and checking afterward with isnan() and isinf(), and none of those tests trigger anything.

Weirdly enough, moving the spot light DOWN just a bit removes the black pixels. So basically, the problem is that the spot light is to close to the camera? However that would relate to anything. So I'm confused. Some posts suggested these kind of issues are actually just driver issues, and I've actually seen some users solve these kind of issues by just updating the drivers, so I tried updating my graphics driver aaand - Still there.

EDIT;

Okay, so now I've sat for around an hour testing everywhere for divisions by zero, and I've found where the fault... It was in the "CalculateFresnel()" function. The first argument to pow somehow was negative, so just adding;

return surfaceColor + (1.0f - surfaceColor) * pow(max(1.0f - lightViewAngle, 0.0f), 5.0f);


instead fixed the black pixels. Which perhaps kind of makes sense in the end? In the "CalculateLight()" function, I calculated the halfwayDirection by normalizing the viewDirection (as in, the direction from the camera to the fragment) added together with the lightDirection (as in, the direction from the spot light to the fragment) which basically did nothing because those vectors are the same in this specific, since the spot light position is at the camera. Then, I tried doing a dot product of the halfwayDirection and the viewDirection, which in this case are the same vectors. And the result calculating the dot product of two identical vectors result in the magnitude of either of those vectors squared. But then again, it doesn't make sense how that number could be negative? It's a unit vector, so the magnitude should be 1, and 1 multiplied by 1 is still one.. So this is where I can't wrap my head around what's going on again. If anyone has any input, feel free to share. (:

• When you solve your issue, it's best to post it as an Answer rather than edit it into the body of the question. As an Answer, it can be upvoted, and you can mark it Accepted (after a short delay) which signals to other users that this problem has been solved, and helps them jump right to the solution if they're having similar problems. – DMGregory Dec 17 '17 at 13:32

So, it appears that I have found the issue. Kind of. I know what I want to blame it on at least - floating point rounding errors. As I wrote in my edit, the problem seems to stem from lightViewAngle being larger than 1.0f, which will cause the fresnel calculation,

return surfaceColor + (1.0f - surfaceColor) * pow(1.0f - lightViewAngle, 5.0f);


to be faulty, because it passes in a negative value as the first argument to pow(), which is undefined behavior. In this case the behavior is defined in that my graphics card throws up it's hands and gives me a black pixel. Mathematically it's impossible to get a dot product over 1.0f, but after reading up on it a bit, apparently floating point rounding errors can cause it to be a bit larger. So instead of doing a max() I now do a clamp() between 0.0f and 1.0f, which fixed my problem.

Why this only happens on certain pixels, I don't know. As I wrote in my post, it stems from the fact that it tries to get a dot product from two (in theory) equal unit vectors, and this causes the dot product to be larger than 1.0f, for... Some reason. Might be a driver bug. At least I've found the issue now at least. Thank you all for the help. (:

Random black pixels usually means a division by zero somewhere in your shader producing a NaN.

Look at each one of your divisions and insure that none have a zero as the denominator, maybe using a y / max(x, 0.001) or something similar or rewriting your formula to avoid using divisions.

The angle between your spotlight and the camera most likely end up being exactly zero in those cases, moving the spotlight away changes the angle and avoids the divisions by zero.

• Hey Stephane, thanks a lot for your answer. I couldn't write my whole answer in one comment so I updated the original post with a kind of answer to your question, if you have any more input I would be extremely grateful! (: – Daybreak Dec 17 '17 at 11:19