gluProject

Ok, so I’m trying to implement a basic depth bounds test to offload my pixel shaders a bit, however I’m stuck when trying to compute the needed znear and zfar arguments.

As far as I know, the default perspective projection matrix takes the z coordinate in eye space and transforms it into clipspace, according to the specified znear and zfar clip planes. After the perspective divide (w), we end up with a device space z coordinate in the interval [-1; 1], which is then scaled and biased to fit in the [0; 1] interval (the actual z-buffer value is then obtained by multipliyng by 2^precision_bits - 1).

That’s what gluProject should do - I checked the Mesa implementation; so, if I haven’t totally misunderstood the above, if a point in eye space is between the znear and zfar planes, the obtained device z coordinate should be in the specified interval [0; 1]; anything in front of znear should yield z < 0, and obviously, anything behind zfar > 1.

What amazes me, however, is that obviously that’s not true… I have tested it a dozen times; so what I’m doing is, I calculate two bounding points of a given light (a “near” and a “far” one) by subtracting/adding a scaled “forward” vector from/to the light position. The forward vector is obtained either from the modelview matrix (elements 2, 6 and 10) or by my own routines, with the same results. Furthermore, it’s scaled by the light radius.

So far so good, but I get totally strange results. The device space z coordinate of a point in front of the znear plane is greater than 1, while points between znear/zfar are little below 1. I am sure there isn’t a bug in my code (i.e. the world space “near” and “far” points are correct).

Any help is appreciated, thanks!

Come on guys, where are the OGL gurus?

Hard to tell without seeing the code.
You got the projection and value range expected by the viewport scaling right in theory.
You’re sure your direction vector is in the correct handedness?
Model and eye-space are right handed. Projection flips it to left handed.

Originally posted by Relic:
You’re sure your direction vector is in the correct handedness?
Model and eye-space are right handed. Projection flips it to left handed.

Actually it’s not a direction vector, it’s just a point in world space - it is in the same coordinate system as the rest of the wolrd, so I assume the handedness is right.
I’m aware of the right-to-left hand transformation, however, how does this affect the ndc z coordinate?

Anywyays, I’m at work right now and I don’t have my real code at hand, but the pseudecode below should resemble it closely enough:

//lightPos - position of light
//lightRadius - radius
//modelview - modelview matrix
//projection - projection matrix

vec3f znear, zfar, zbounds, forward;
float near, far;

//one way to get the forward vector; using my own math routines yields the same results
forward[0] = modelview[2];
forward[1] = modelview[6];
forward[2] = modelview[10];

forward *= lightRadius;

//forward is inverted, so we add it to the light position to find the "near" z bounds point
znear = lightPos + forward;
zfar = lightPos - forward;

gluProject(znear[0], znear[1], znear[2], modelview, ..., &near);
gluProject(zfar[0], zfar[1], zfar[2], modelview, ..., &far);

//now, near and far should contain the ndc z coordinates

glDepthBoundsEXT(near, far);

>>I’m aware of the right-to-left hand transformation, however, how does this affect the ndc z coordinate?<<

I was just shooting in the dark.

Put in the code you’re actually running. Above mixes float and double requirements and won’t work.

Originally posted by Relic:
Put in the code you’re actually running. Above mixes float and double requirements and won’t work.
Sorry, as I said it was just pseudocode, the actual implementation uses doubles for near and far, as well as for the modelview and projection matrices.

Yes, I read that, that’s why I was asking for the real code which compiles. :wink:

What do you use as local forward vector? The object’s z-axis (0, 0, 1)?
Matrices are left-multiplied in OpenGL which makes vectors columns, and matrices are filled column-major.
That would make matrix elements (8, 9, 10) the forward vector.

Originally posted by Relic:
Yes, I read that, that’s why I was asking for the real code which compiles. :wink:
I’ll upload it today, but it’s actually almost the same thing.

Originally posted by Relic:
What do you use as local forward vector? The object’s z-axis (0, 0, 1)?
Matrices are left-multiplied in OpenGL which makes vectors columns, and matrices are filled column-major.
That would make matrix elements (8, 9, 10) the forward vector.

Hehe, I was shocked for a moment :slight_smile: But usually there’s a difference between the memory representation and matrices “on paper”. I was curious myself, so here’s a part of the actual Mesa implementation of gluLookAt:

    forward[0] = centerx - eyex;
    forward[1] = centery - eyey;
    forward[2] = centerz - eyez;

    //...

    m[0][2] = -forward[0];
    m[1][2] = -forward[1];
    m[2][2] = -forward[2];

As you can clearly see, the inverted forward vector is stored in the elements 2, 6 and 10.

I interpreted your modelview[i] coming from the usual GLdouble[16] you use in glGetDoublev for matrices. Then my indexing would be the right one. Another reason to post real code. :wink:

I looked in to the gluLookAt implementation and what side, up and forward do, is to create a basis transformation which is post-multiplied to the current matrix.

Mesa is throwing in the float[4][4] matrix via glMultMatrixf(&m[0][0]) which effectively transposes the matrix because OpenGL interprets that as float[16] and loads column-major.

A misunderstanding on my side. I was thinking about model space forward vector z-axis transformed into eye-space.
You want the eye-space z-direction in modelspace which is the inverse transformations, which with no scaling is the transpose. There you go, both right. :wink:

Yeah, I’m a little confused now :slight_smile:

All OGL matrices are column-major (vectors are columns), which means that the translation occupies the 12th, 13th and 14th element (counting from 0), which is what glGetDoublev returns.

gluLookAt computes a column-major matrix as well and, as you said, post-multiplies it with the current one - I don’t see a reason why it should be effectively transposed (it is and is interpreted as column-major), right?

Anyways, the forward vector is right, so the question remains why gluProject doesn’t return the expected ndc z values…

Yeah, I can be like that at times.

The transpose operation happens when interpreting float[4][4] as float[16] and using glLoadMatrix or glMultMatrix with the pointer to the first element.
Assuming 2D float[4][4] C layout “the matrix on paper”, memory locations are actually a0 to a15 in that order:

M_usr[0][0] = a0
M_usr[0][1] = a1
M_usr[0][2] = a2
M_usr[0][3] = a3

M_usr[1][0] = a4
M_usr[1][1] = a5
M_usr[1][2] = a6
M_usr[1][3] = a7

M_usr[2][0] = a8
M_usr[2][1] = a9
M_usr[2][2] = a10
M_usr[2][3] = a11

M_usr[3][0] = a12
M_usr[3][1] = a13
M_usr[3][2] = a14
M_usr[3][3] = a15

when you now throw that at OpenGL via glLoadMatrix(&m[0][0]) or glMultMatrix(&m[0][0]) and it will read the 16 floats and put them into it’s internal matrix.
Assuming the implementation also uses float[4][4] matrices the assignment would be

M_ogl[0][0] = a0
M_ogl[0][1] = a4
M_ogl[0][2] = a8
M_ogl[0][3] = a12

M_ogl[1][0] = a1
M_ogl[1][1] = a5
M_ogl[1][2] = a9
M_ogl[1][3] = a13

M_ogl[2][0] = a2
M_ogl[2][1] = a6
M_ogl[2][2] = a10
M_ogl[2][3] = a14

M_ogl[3][0] = a3
M_ogl[3][1] = a7
M_ogl[3][2] = a11
M_ogl[3][3] = a15

See? Transposed.

Inside Mesa the internal gluLookAt matrix has been transposed by glMultMatrixf(&m[0][0]) this way. If it’s post-multiplied to an identity nothing changes in the matrix, it’s still this transposed thing compared to the “on paper” layout.
If you now throw in a modelspace vector of (0,0,1) you get returned which indices of the float[16] array? Mine.

glDepthBounds needs values in the [0,1] range. so you need the z-values of the min and max environment around your pointlight in modelspace transformed to window space.
To calculate that, you need the forward vector in eye-space transformed back to modelspace. Inverse == transpose. => Your indices. (That’s where I wasn’t on track before.)

Normalized and scaled, added to the modelspace light position and transformed by the modelview projection matrix => gluProject.
Ok. Looks about right.
You don’t normalize the direction in modelspace. Is it a unit vector?

Thinking about that you can directly calculate the two z-values if you just reduce all this matrix stuff to the z-componenent and do the modelview projection viewport scaling thing yourself. Then you can also just add the light range to the eye-space coordinates (You don’t have scaling in your matrices right?) and do the few remaining transformations on that.

You have a reproducer project to download somewhere?

Oh well, my post was quite longer before my Opera client just crashed… Anyways, many thanks for the answers, you’re the only one so far :slight_smile:

I’m still not getting it why any column-major in-memory matrix loaded with glMultMatrix is transposed, but I guess that’s another thread :slight_smile:

Yes, my modelview matrix isn’t scaled.

I’ve already implemented the single z coordinate transformation, getting the same results “unfortunately”.

My project is quite large (a binary wouldn’t help much), so I’ll just post the link to the relevant function here:

http://www.rafb.net/paste/results/iH8qdA71.html

The relevant part is inside the #if 1 block, the #else block implementes the “z only” transformation. GL_Project is just a wrapper for gluProject…

Cool, I have fun digging that. :wink:

My point is that float[4][4] as used in maths and C notation (row-major) and normally left multiplied, if thrown at OpenGL matrix entry functions as block of linear memory is not the same (interpreted as column-major by OpenGL).

I’ll might just sit down in a quiet hour tomorrow and write my formulas down the way I think it should working.

Here you go. Worked on the first try.
The code below puts a light in the center of the frustum and sets a radius of the half frustum size to make the expected depth bounds 0.0 and 1.0 and that just works.
I added some rotations then and except for floating point rounding errors that gets the same result.
Hmm, I didn’t test rotations which turn more than 90 degrees, but I’m confident.
I guess the “divide by w” step is the key to your inverted results. Watch your matrices. :wink:

Have fun.

 
#define EPSILON 0.00001
  
    GLdouble matProjection[16];
    GLdouble matModelview[16];
    GLint    viewport[4];
    GLdouble dir[3]; // modelspace direction
    GLdouble norm;
    GLdouble posNear[3]; 
    GLdouble posFar[3]; 
    GLdouble xWin, yWin;
    GLdouble zLight, zNear, zFar;
    int i;

    GLdouble lightPos[3] = {0.0, 0.0, 0.0};
    GLdouble lightRadius = 4.0; // Means light borders are exactly on the zNear and zFar frustum planes.
    // Expected result is zNear == 0.0 and zFar == 1.0
    
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(10.0, 1.0, 1.0, 9.0);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    // Center the modelspace origin in viewing frustum.
    glTranslatef(0.0f, 0.0f, -5.0f);
    // Rotate a little to make it more "real world-ish".
    glRotatef(30.0f, 1.0f, 0.0f, 0.0f); 
    glRotatef(45.0f, 0.0f, 1.0f, 0.0f); 


    glGetDoublev(GL_PROJECTION_MATRIX, matProjection);
    glGetDoublev(GL_MODELVIEW_MATRIX, matModelview);
    glGetIntegerv(GL_VIEWPORT, viewport);

    // Eye-space is right-handed, viewing direction is (0, 0, -,1, 0)
    // Transforming this direction back into modelspace requires the inverse modelview matrix.
    // For vectors, translation is irrelevant, and if there is no scaling, 
    // the inverse is the transpose (means only rotation affects the vector)
    
    // Means, if the modelview matrix in OpenGL float[16] column major is depicted as 
    // a_0, a_4, a_8, a12 
    // a_1, a_5, a_9, a13 
    // a_2, a_6, a10, a14 
    // a_3, a_7, a11, a15 

    // The inverse is 
    // a_0, a_1, a_2, a_3 
    // a_4, a_5, a_6, a_7 
    // a_8, a_9, a10, a11 
    // a12, a13, a14, a15 

    // Multiplied by the eye-space viewing direction
    // a_0, a_1, a_2, a_3     0     -a_2
    // a_4, a_5, a_6, a_7  *  0  =  -a_6
    // a_8, a_9, a10, a11    -1     -a10
    // a12, a13, a14, a15     0     -a14
    
    // we only need the direction, so if w == -a14 == 0.0 we're set.
    // Otherwise divide by w (this can flip the direction if w is negative, and it is!)

    dir[0] = -matModelview[ 2];
    dir[1] = -matModelview[ 6];
    dir[2] = -matModelview[10];

    if (fabs(matModelview[14]) > EPSILON)
    {
        dir[0] /= -matModelview[14];
        dir[1] /= -matModelview[14];
        dir[2] /= -matModelview[14];
    }

    // Normalize the modelspace direction. This shouldn't be zero. 
    norm = sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);

    // If the light in modelspace is at lightPos
    // the worldspace projection of posNear and posFar are the nearest and farthest points
    // We need the projected z-values in the end.
    for (i = 0; i < 3; i++)
    {
      dir[i] /= norm; // Normalize
      dir[i] *= lightRadius; // Scale with light radius in modelspace.
      posNear[i] = lightPos[i] - dir[i];
      posFar[i]  = lightPos[i] + dir[i];
    }

    gluProject(lightPos[0], lightPos[1], lightPos[2],
               matModelview, matProjection, viewport,
               &xWin, &yWin, &zLight); // For verification.
    // Interestingly a point in the center of the eye-space z-range gets mapped to 0.9 with the above setup. 
    // That shows how different the depth value distribution is between near and far values.

    gluProject(posNear[0], posNear[1], posNear[2],
               matModelview, matProjection, viewport,
               &xWin, &yWin, &zNear);

    gluProject(posFar[0], posFar[1], posFar[2],
               matModelview, matProjection, viewport,
               &xWin, &yWin, &zFar);


 

Indeed :slight_smile:

However, now try setting the translation to 0, 0, 5 and see what happens… zNear becomes +2.25 and zFar is little above +1.25 (which is, naturally, wrong, let alone that zNear > zFar; this would throw a GL_INVALID_OPERATION if passed to the real depth bounds extension).

I guess that’s what really irritated me, it seems my code actually works when the light bounds fall inside the near and far clip planes (the direction vector is correct)…

On second thought, the reason seems obvious: if a point in front of znear is projected onto the viewplane, w becomes negative as well, and after the division we get a positive number again.

I guess the easiest way to catch that situation is to check w right after the multiplication with the projection matrix (which, in your code, is hidden in gluProject).

Thanks a lot :slight_smile:

EDIT: Works like a charm :slight_smile:

Cool, a boring Saturday nicely spent, eh?
Show some pics when you can.

Originally posted by Relic:
Cool, a boring Saturday nicely spent, eh?

Yeah hehe, now it’s time to celebrate :smiley:

Originally posted by Relic:
Show some pics when you can.
Click. Still missing a lot of things, but I’m slowly getting there :slight_smile: Now I can render ~15-20 non-shadow casting lights with a really unoptimized pixel shader, still getting ~50fps on a GF6600GT (with a lot of glFinish’s to measure the different stages).

You’re right, I take the “divide by w back”. Only the sign is interesting. The vector is normalized anyway. Only had one cup of coffee today. :wink:

I see, a level editor. I play Nexuiz for fun regularly.