Light following me around the room. Something is wrong with my shader!

I'm trying to do a spot (Blinn) light, with falloff and attenuation. It seems to be working OK except I have a bit of a space problem. That is, whenever I move the camera the light moves to maintain the same relative position, rather than changing with the camera. This results in the light moving around, i.e. not always falling on the same surfaces. It's as if there's a flashlight attached to the camera.

I'm transforming the lights beforehand into view space, so Light_Position and Light_Direction are already in eye space (I hope!). I made a little movie of what it looks like here:

The light is fixed in the centre up and its "look at" point in a fixed position in front of it. As you can see, as the camera rotates around the origin (always looking at the centre), so don't think the box is rotating (!). The lighting follows it around. To start, some code. This is how I'm transforming the light into view space (it gets passed into the shader already in view space):

``````// Compute eye-space light position.

Math::Vector3d eyeSpacePosition = MyCamera->ViewMatrix() * MyLightPosition;

// Compute eye-space light direction vector.

Math::Vector3d eyeSpaceDirection = Math::Unit(MyLightLookAt - MyLightPosition);

MyCamera->ViewMatrixInverseTranspose().TransformNormal(eyeSpaceDirection);

``````

Can anyone give me a clue as to what I'm doing wrong here? I think the light should remain looking at a fixed point on the box, regardless of the camera orientation.

Here are the vertex and pixel shaders:

``````///////////////////////////////////////////////////
///////////////////////////////////////////////////

#version 420

///////////////////////////////////////////////////
// Uniform Buffer Structures
///////////////////////////////////////////////////

// Camera.

layout (std140) uniform Camera
{
mat4 Camera_View;
mat4 Camera_ViewInverseTranspose;
mat4 Camera_Projection;
};

// Matrices per model.

layout (std140) uniform Model
{
mat4 Model_World;
mat4 Model_WorldView;
mat4 Model_WorldViewInverseTranspose;
mat4 Model_WorldViewProjection;
};

// Spotlight.

layout (std140) uniform OmniLight
{
float Light_Intensity;
vec3 Light_Position;
vec3 Light_Direction;
vec4 Light_Ambient_Colour;
vec4 Light_Diffuse_Colour;
vec4 Light_Specular_Colour;
float Light_Attenuation_Min;
float Light_Attenuation_Max;
float Light_Cone_Min;
float Light_Cone_Max;
};

///////////////////////////////////////////////////
// Streams (per vertex)
///////////////////////////////////////////////////

layout(location = 0) in vec3 attrib_Position;
layout(location = 1) in vec3 attrib_Normal;
layout(location = 2) in vec3 attrib_Tangent;
layout(location = 3) in vec3 attrib_BiNormal;
layout(location = 4) in vec2 attrib_Texture;

///////////////////////////////////////////////////
// Output streams (per vertex)
///////////////////////////////////////////////////

out vec3 attrib_Fragment_Normal;
out vec4 attrib_Fragment_Position;
out vec2 attrib_Fragment_Texture;
out vec3 attrib_Fragment_Light;
out vec3 attrib_Fragment_Eye;

///////////////////////////////////////////////////
// Main
///////////////////////////////////////////////////

void main()
{
// Transform normal into eye space
attrib_Fragment_Normal = (Model_WorldViewInverseTranspose * vec4(attrib_Normal, 0.0)).xyz;
// Transform vertex into eye space (world * view * vertex = eye)
vec4 position = Model_WorldView * vec4(attrib_Position, 1.0);
// Compute vector from eye space vertex to light (light is in eye space already)
attrib_Fragment_Light = Light_Position - position.xyz;
// Compute vector from the vertex to the eye (which is now at the origin).
attrib_Fragment_Eye = -position.xyz;
// Output texture coord.
attrib_Fragment_Texture = attrib_Texture;
// Compute vertex position by applying camera projection.
gl_Position = Camera_Projection * position;
}
``````

and the pixel shader:

``````///////////////////////////////////////////////////
///////////////////////////////////////////////////

#version 420

///////////////////////////////////////////////////
// Samplers
///////////////////////////////////////////////////

uniform sampler2D Map_Diffuse;

///////////////////////////////////////////////////
// Global Uniforms
///////////////////////////////////////////////////

// Material.

layout (std140) uniform Material
{
vec4  Material_Ambient_Colour;
vec4  Material_Diffuse_Colour;
vec4  Material_Specular_Colour;
vec4  Material_Emissive_Colour;
float Material_Shininess;
float Material_Strength;
};

// Spotlight.

layout (std140) uniform OmniLight
{
float Light_Intensity;
vec3 Light_Position;
vec3 Light_Direction;
vec4 Light_Ambient_Colour;
vec4 Light_Diffuse_Colour;
vec4 Light_Specular_Colour;
float Light_Attenuation_Min;
float Light_Attenuation_Max;
float Light_Cone_Min;
float Light_Cone_Max;
};

///////////////////////////////////////////////////
// Input streams (per vertex)
///////////////////////////////////////////////////

in vec3 attrib_Fragment_Normal;
in vec3 attrib_Fragment_Position;
in vec2 attrib_Fragment_Texture;
in vec3 attrib_Fragment_Light;
in vec3 attrib_Fragment_Eye;

///////////////////////////////////////////////////
// Result
///////////////////////////////////////////////////

out vec4 Out_Colour;

///////////////////////////////////////////////////
// Main
///////////////////////////////////////////////////

void main(void)
{
// Compute N dot L.

vec3 N = normalize(attrib_Fragment_Normal);
vec3 L = normalize(attrib_Fragment_Light);
vec3 E = normalize(attrib_Fragment_Eye);
vec3 H = normalize(L + E);

float NdotL = clamp(dot(L,N), 0.0, 1.0);
float NdotH = clamp(dot(N,H), 0.0, 1.0);

// Compute ambient term.

vec4 ambient = Material_Ambient_Colour * Light_Ambient_Colour;

// Diffuse.

vec4 diffuse = texture2D(Map_Diffuse, attrib_Fragment_Texture) * Light_Diffuse_Colour * Material_Diffuse_Colour * NdotL;

// Specular.

float specularIntensity = pow(NdotH, Material_Shininess) * Material_Strength;

vec4 specular = Light_Specular_Colour * Material_Specular_Colour * specularIntensity;

// Light attenuation (so we don't have to use 1 - x, we step between Max and Min).

float d = length(-attrib_Fragment_Light);
float attenuation = smoothstep(Light_Attenuation_Max, Light_Attenuation_Min, d);

// Adjust attenuation based on light cone.

float LdotS = dot(-L, Light_Direction), CosI = Light_Cone_Min - Light_Cone_Max;

attenuation *= clamp((LdotS - Light_Cone_Max) / CosI, 0.0, 1.0);

// Final colour.

Out_Colour = (ambient + diffuse + specular) * Light_Intensity * attenuation;
}
``````

UPDATE:

I know this was a long time ago, but I thought I'd revisit it, because I've finally found the reason for the behaviour I was seeing. It was a fundamental misunderstanding and misuse of uniform buffers. I wasn't binding them to shader programs correctly. By chance it kind-of worked.

Yes, it took me a while and yes, I did give up on GLSL for a while because of it. Now I've nailed it, I'm back in business and OpenGL is back on the menu :).

-

• You do not use Light_Position, nor do you fill attrib_Fragment_Light in the vertex shader.

Could it be that there is a line break missing?

``````// Compute vector from eye space vertex to light (light is in eye space already) attrib_Fragment_Light = Light_Position - position.xyz;
``````

should be

``````// Compute vector from eye space vertex to light (light is in eye space already)
attrib_Fragment_Light = Light_Position - position.xyz;
``````
• I don't know which framework you use. Are you sure

Math::Vector3d eyeSpacePosition = MyCamera->ViewMatrix() * MyLightPosition;

does apply translation, as Input and output are Vector3?

• Does TransformNormal do anything in addition to not applying translation/projection on the passed vector? After InverseTranspose a normal multiplication with w set to 0.0 should be sufficient.

MyCamera->ViewMatrixInverseTranspose().TransformNormal(eyeSpaceDirection);

EDIT

Does TransformNormal really transform the normal passed by reference or should it rather be:

``````eyeSpaceDirection = MyCamera->ViewMatrixInverseTranspose().TransformNormal(eyeSpaceDirection);
``````

I did not think it through if any of these could lead to your problem, but some oddities:

length(-attrib_Fragment_Light) -> the minus sign has no effect

Im not sure how Light_Cone_Min and Light_Cone_Max are filled, is Min bigger than Max, otherwise CosI will get negative which will destroy the angle attenuation

EDIT2

You said Light_Attenuation_Min < Light_Attenuation_Max and Light_Cone_Min < Light_Cone_Max:

• Smoothstep performs smooth Hermite interpolation between 0 and 1 when edge0 < x < edge1. Results are undefined if edge0 ≥ edge1.But you call it

smoothstep(Light_Attenuation_Max, Light_Attenuation_Min, d);

• CosI is always negative which sounds kind of odd as LdotS is bigger the nearer the fragment is to the center of the cone
-
The Fragment_Light thing was an error in my editing. Transform normal simply does a matrix multiply, providing a 0.0 for the w component in the supplied vector, rather than a 1.0. All my other matrix/vector stuff is OK I think - I have unit tests to test it against the glm:: library and outputs are the same (I wrote my own math library just... because!). – Robinson Dec 11 '12 at 18:06
I guess I need to add a framework for visualising normals, light cones and so on, otherwise this is going to be hell to fix! – Robinson Dec 11 '12 at 18:23
Yes, the TransformNormal function takes a non-const reference and modifies the reference. It also returns the reference for ease of use. The minus sign is superfluous indeed. Not sure how that happened. – Robinson Dec 11 '12 at 18:29
I tried it a different way, transforming the light position and the light "look at" point into view space before then making the vector. Same result. Light follows the camera around the room :(. – Robinson Dec 11 '12 at 18:32
I would start by limiting the fragment shader to diffuse lighting only with a binary distance attenuation active, no spot. Reduce the light distance and but it near a wall. – Archy Dec 11 '12 at 18:32