PDA

View Full Version : FAQ - Fixed function lighting position problems



Zenja
12-29-2010, 03:01 AM
PLEASE NOTE: This post contains incorrect information. Read the entire thread before continuing. ( Authors errata (http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=288667#Post2886 67) )

Why is my fixed function OpenGL lighting wrong? It's suprising how often this question comes up on these message boards. When hundreds of students ask the same question again and again, the problem may no longer be with the students, but with the text books and the tutorials. Hopefully this post will explain to the students what's happening behind the scenes, and how they can fix it. Let's banish this question from the message boards once and for all.

POSITIONAL LIGHT

According to the OpenGL RedBook, the "location is transformed by the modelview matrix and stored in eye coordinates". What it doesn't say is after every transform (eg. translation, rotation), the light position is updated again, which is not what people expect. In complex scenes, people expect the light position to be set once in world coordinates (eg. a street light), but are then suprised when they draw another object (eg. a car), the lighting is incorrect. This is because OpenGL will reposition the light by the car modelview matrix (and this repositioning happens behind your back).

C = camera transform
S = object spatial
MV = C*S (modelview matrix)
EL = C*L (eye coordinates of light, what we want)
ELgl = MV * L (what OpenGL fixed function does)
Lf = MV(-1) * C * L (this is how we counteract the transform)
Verification: ELgl = MV*Lf = MV*MV(-1)*C*L = C*L

In code, you'd do something like this EVERY TIME after modifying the modelview matrix:



Matrix4 cam = camera modelview matrix
Matrix4 mv = current modelview matrix
Vector3 light_world = light world position
Vector4 light_fixed = mv.GetInverse() * cam * light_world;
glLightfv(light_index, GL_POSITION, light_fixed);

So, if you have a list of scenes to render, you'd do something like this:



for (i=0; i < number_scenes; i++)
{
scene[i].UpdateSpatial();
for (int j = 0; j < number_lights; j++)
lights[j].Update();
scene[i].Render();
}


DIRECTIONAL_LIGHTS
According to the OpenGL RedBook, the "direction is transformed by the modelview matrix". This only happens once, when setting the light direction, so you only have to do this once AFTER setting the camera matrix. Eg.



Matrix4 cam = camera transform
Vector4 dir = light direction, make sure 4th component is zero

LoadMatrix(cam);
glLightfv(light_index, GL_POSITION, dir);
for (i=0; i < number_scenes; i++)
{
scene[i].UpdateSpatial();
scene[i].Render();
}


Be aware that the direction vector points TOWARDS the light.

SPOT LIGHTS
The spotlight position adheres to the same rules as Positional lights. You will have to update the spot light position AFTER every change to the current modelview matrix.

According to the RedBook, "the spotlight's direction is transformed by the modelview matrix just as though it were a normal vector, and the result is stored in eye coordinates". What it doesn't mention (but the BlueBook does), is that it only uses the upper leftmost 3x3 portion of the modelview matrix, since we're only concerned with rotations at this point. Just like with the lights position, we need to reverse the transform which will be performed by OpenGL.

C = camera transform
MV3 = 3x3 upper leftmost of modelview matrix
D = spotlight direction (world)
ED = C*D (direction in eye coordinates, what we want)
EDgl = MV3*D (what OpenGL fixed function does)
Df = MV3(-1) * C * D (this is how we counteract the transform)
Verification: EDgl = MV3*Df = MV3*MV3(-1)*C*D = C*D

In code:


Matrix4 cam = camera transform matrix
Matrix3 mv = upper left 3x3 of modelview matrix (caution - modelview is 4x4)
Vector3 dir_world = desired spotlight direction
Vector3 dir_fixed = mv.GetInverse() * cam * dir_world;
dir_fixed.Normalise();
glLightfv(light_index, GL_SPOT_DIRECTION, dir_fixed);


Be aware that the spotlight direction is FROM the light source.

Just like with Positional lights, you will have to update the light direction after every modelview matrix modification.



for (i=0; i < number_scenes; i++)
{
scene[i].UpdateSpatial();
for (int j = 0; j < number_lights; j++)
lights[j].Update(); // update position and spotlight direction
scene[i].Render();
}


Summary
The above FAQ should help you resolve your light position / direction issues, and your scenes should finally be lit correctly. Also worth mentioning is that with programmable shaders, you generally don't have to do this, since you control when the light positions and direction are converted to eye coordinates.

kaerimasu
12-29-2010, 08:00 AM
Erm... I've never had this issue of positional and spotlights moving on me.

- Chris

Dark Photon
12-29-2010, 10:36 AM
According to the OpenGL RedBook, the "location [of a light source] is transformed by the modelview matrix and stored in eye coordinates".
True.


What it doesn't say is after every transform (eg. translation, rotation), the light position is updated again...
That's because this doesn't happen (unless there's a driver bug). The MODELVIEW active when you register the light source position and direction is the one used to transform the light into EYE-SPACE. No other.

What made you think that the light sources get "munged" everytime the MODELVIEW is modified?


In complex scenes, people expect the light position to be set once in world coordinates (eg. a street light)...
Applications frequently provide that interface to their users, yes. But there are very good reasons why we (applications) don't register positions of things, including light sources, with OpenGL in WORLD-SPACE coordinates. Not the least of which is, when combined with 32-bit float precision, it constrains the application to only dealing with "toy-sized" worlds.

OpenGL as delivered does not know about WORLD-SPACE, and that is intentional. Now if you write your own shaders, you can make GPU operate in WORLD-SPACE, but then you add limitations on your application that otherwise don't exist.


In code, you'd do something like this EVERY TIME after modifying the modelview matrix:
Again, no you don't. You only need to re-register the light's position/direction when the EYE-SPACE position/direction of the light source changes.

* OBJ SPACE --(MODELING XFORM)--> WORLD SPACE --(VIEWING XFORM)--> EYE SPACE

In practice, you'd typically update your EYE-SPACE light positions/directions once per view rendered. The most common cause for needing to is that you have a moving eyepoint (VIEWING XFORM changes). But if your light source position and/or direction in WORLD-SPACE changes (MODELING XFORM changes and/or OBJ SPACE position/direction changes), then that's another cause for updating the light's EYE-SPACE positions/directions with OpenGL.


DIRECTIONAL_LIGHTS

...Be aware that the direction vector points TOWARDS the light.
This is somewhat misleading. In truth, the "position" vector points toward the light (GL_POSITION), with w = 0 signifying a directional light source.

Light source direction (GL_SPOT_DIRECTION) is only used for point light sources with a light cone enabled.


SPOT LIGHTS
The spotlight position adheres to the same rules as Positional lights.
That's because spot lights "are" positional lights, but just with a light cone added.


According to the RedBook, "the spotlight's direction is transformed by the modelview matrix just as though it were a normal vector, and the result is stored in eye coordinates". What it doesn't mention (but the BlueBook does), is that it only uses the upper leftmost 3x3 portion of the modelview matrix...
That's because this is apparent from vector math. 3D vectors in a homogenous coordinate system are represented as (x, y, z, 0). So do your matrix multiplication and you find that that 0 kills the 4th column (i.e. the translation). This makes intuitive sense, as direction vectors don't care about location, just orientation.

Zenja
12-29-2010, 01:48 PM
What it doesn't say is after every transform (eg. translation, rotation), the light position is updated again...
That's because this doesn't happen (unless there's a driver bug). The MODELVIEW active when you register the light source position and direction is the one used to transform the light into EYE-SPACE. No other.

Dark Photon, conceptually I agree with you, and this is the behaviour I originally expected to see, but that is NOT the behaviour observed. I can not get OpenGL fixed function lights to work correctly if I change the modelview matrix, and in my engine, each spatial scene has its own transform, hence every scene modifies the modelview matrix before issuing rendering instructions. To get lights to work, I must reset the light position after every modelview update. I've looked at the sources to MESA, and even they recalculate the light position after the modelview matrix stack is dirty.

I honestly would not have gone out of my way to bother with calculating inverses if the position didn't change. The fact is that it does. It's not a driver issue, since I can replicate this with 4 different boxes / operating systems (Linux Radeon 4650, MacOSX Radeon X1650, Windows XP GeForce 8800GT and GT250, and even Haiku with Mesa 7.4 driver). Mesa source code (file light.c) validates when the light position is updated when the modelview matrix is dirty. Every single transform will "dirty" the modelview matrix. Hence the light position will be altered.

Hundreds of people have complained about OpenGL lights going haywire after simple transforms. They have stumbled onto the issue discussed above. My game engine has 4 code paths (core, compatibility, ES2.0 and ES1.1) and supports both fixed function and shader based lights. My shader code path (where I control when light position gets updated) doesn't need the inverse calculation outlined above. However, the fixed function code paths do. They use the same matrix stack back end and light parameters.

I would love to skip these calculations, but without them, lighting in the fixed function code path simply doesn't work for me. I've struggled for ages until I figured out why.

kaerimasu
12-29-2010, 02:35 PM
Does glGetLight report different GL_POSITIONs under these different modelview matrices?

- Chris

Alfonse Reinheart
12-29-2010, 05:41 PM
Hundreds of people have complained about OpenGL lights going haywire after simple transforms.

And I'm sure 10x that many have not.

I could understand if you're talking about a driver bug. But you're not. You're saying that no OpenGL implementations implement fixed-function lighting according to the spec.

This is not true. At all.

If lighting is "going haywire" after changing the modelview matrix, then there is a very, very good chance that it is your code, and not OpenGL, that is doing something wrong. Indeed, I would need to see some code that's verified broken on multiple platforms before I'd buy it.

glLight() will multiply the light position with the modelview matrix that is current at the time of the lighting call. If you are seeing unexpected lighting behavior, the first place I would look would be whether the current modelview matrix is in fact what you think it is. glIntercept or gDEBugger would be of help in this process. Odds are, you're calling glLight() in the wrong place or the matrix stack is not being properly maintained.

Zenja
12-30-2010, 03:41 AM
Hundreds of people have complained about OpenGL lights going haywire after simple transforms.
And I'm sure 10x that many have not.

I was puzzled as well, but never got good results unless I corrected the light position. I do my own matrix stack management, so before rendering each spatial scene, I need to upload the current stack using glLoadMatrixf(). Originally I would only set the light position once after setting the camera transform, but lights would be weird after each transform. Looking at the source of Mesa, I can see that each call to glLoadMatrix() toggles a DirtyFlag, so this may be causing the problem I'm experiencing. I would gladly avoid this work if I didn't need it, but there is something in my code path which is triggering this behaviour, and I expect it may very well be calling to glLoadMatrixf. I only see this behaviour with the fixed function pipeline, my shader based pipeline doesn't do this.

Anyhow, thanks everyone for their input. I'm glad to know that others do not run into this problem. I have, and have found a way to work around it for my legacy code path. I just thought I'd share my observations so that if others run into the problem, they know one method of working around the problem.

kaerimasu
12-30-2010, 09:36 AM
Your confidence in your code concerns me. Please don't sell me your software.

Can you recreate the problem with the following code? I see the same values for eye space light positions.



#include <stdio.h>
#include <GL/glut.h>

void checklight(const char *when) {
GLfloat light_eye_position[4];
glGetLightfv(GL_LIGHT0, GL_POSITION, light_eye_position);
printf("%s: %f %f %f %f\n", when,
light_eye_position[0],
light_eye_position[1],
light_eye_position[2],
light_eye_position[3]);
}

void display(void) {
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(1.0f, 1.0f, 1.0f);
GLfloat light_object_position[] = {10.0f, 10.0f, 10.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, light_object_position);

checklight("right after initialization");

glPushMatrix();
glTranslatef(-4.0f, -3.0f, -1.0f);
glRotatef(37.0f, 1.0f, 0.0f, 0.0f);
checklight("after rotating about x-axis");
glPopMatrix();

float m[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
glLoadMatrixf(m);
checklight("after loading garbage with glLoadMatrix");

glutSwapBuffers();
}

int main(int argc, char ** argv) {
glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);
glutInitWindowSize(512, 512);
glutInit(&amp;argc, argv);

glutCreateWindow("3-D Application");
glutDisplayFunc(display);
glutMainLoop();

return 0;
}


- Chris

Alfonse Reinheart
12-30-2010, 10:36 AM
I do my own matrix stack management, so before rendering each spatial scene, I need to upload the current stack using glLoadMatrixf().

And therein lies the fundamental source of your problem. What matrix do you load before you call glLight? I don't know what it is, but I'd bet it's not the world-to-camera matrix that it should be.

V-man
12-30-2010, 06:05 PM
I have to agree with the others. Once you call glLight with GL_POSITION, the modelview is used to transform it.
The POSITION stored by GL no longer changes. From what I remember, this is one reason why the light "follows you" when you walk around.
The solution is to call glLight again.

It always seemed ridiculous to me that you could use the modelview matrix to transform your lights.
In Direct3D, it doesn't.

In order to match Direct3D's behavior, I would do a glLoadIdentity before glLight.

Alfonse Reinheart
12-30-2010, 06:47 PM
It always seemed ridiculous to me that you could use the modelview matrix to transform your lights.

The reason for that is because lighting is done in camera/eye/view space. If it weren't transformed by the modelview matrix, then you would have to manually recompute the light position every frame, assuming the camera moves. OpenGL doesn't have an explicit world-to-camera matrix, so it has to rely on the user providing said matrix as the model-to-view transform.

It's a convenience, so long as you're not setting up groups of lights specifically for each object. Then it's kind of a pain.


In order to match Direct3D's behavior, I would do a glLoadIdentity before glLight.

Don't forget the push/pop around that, so it doesn't screw up the current modelview matrix.

Zenja
01-03-2011, 12:44 AM
Public apology to everyone who accessed this post. I have determined that all of the above is not necessary. Even though I had unit tests for my custom matrix stacks which proved all the math operations were equivalent, I found a bug in my code base which made the wrong stack active at render time, which was causing lights to go haywire. Essentially, the above code worked around my bug by recalculating the light position using the correct matrix stack, which of course isn't necessary if the correct stack is selected in the first place.

Beers on me, and I will now go back to my corner and ponder about life and the mistakes I've made ... :) Thanks everyone for convincing me that I had to recheck my code - it was reassuring to know that others did not run into the same problem.

Dark Photon
01-03-2011, 08:16 AM
No worries. It happens to all of us sometimes. ;)