Help with mouse click position to OGL coords using gluUnproject

hyn
Unregistered

Post: #1
Hi,

I am trying to convert the coordinates of a mouse click (x, y) to OpenGL object-space coordinates (x, y, z).

Both the OpenGL FAQ and NeHe at gamedev.net suggests that I use glReadPixels() to get the depth value of the mouse click (which is kind of vague and confusing). Then pass that depth value, along with x, y mouse coordinates to gluUnproject() to get the x, y, z OpenGL coordinates.

I've done this and two odd things happen:

1) The z (depth) value is initially 3.663975 wherever I click on the screen. I read somewhere that this z value should be between 0 and 1. When I translate into or out of the screen (along z axis), the z value changes but still the value is same at any place on the screen.

2) Say I click point A. The OpenGL coordinates (x, y, z respectively) are -0.109827, 0.001119, 3.603975. Then I translate along the z axis, and click point A. The coordinates are exactly the same, except the z value. I think this means that my transformations aren't taken into consideration. But I did pass in my modelview and projection matrices into gluProject(), so why is it not working?

The x and y of the resulting OpenGL coordinates have the correct system with (0, 0) at the middle of the screen. (pos, pos) is at the top-right space, and (neg, neg) at the lower-left. But the values are not correct (much smaller).

Here is my mouseDown code (irrelevant parts ommited, forgive my lack of proper spacing and tabbing, I can't copy paste the code due to computer issues):

Code:
```- (void)mouseDown: (NSEvent *)aEvent; { GLdouble pixelDepth; GLdouble modelMatrix[16]; GLdouble projectMatrix[16]; GLint viewport[4]; GLdouble objSpaceCoords[3]; NSPoint mouseLocation = [[self window] convertScreenToBase: [NSEvent mouseLocation]]; glGetDoublev(GL_MODELVIEW_MATRIX, modelMatrix); glGetDoublev(GL_PROJECTION_MATRIX, projectMatrix); glGetIntegerv(GL_VIEWPORT, viewport); glReadPixels((GLint)mouseLocation.x, (GLint)mouseLocation.y, 1, 1, GL_DEPTH_COMPONENT, GL_DOUBLE, &pixelDepth); gluUnproject(mouseLocation.x, mouseLocation.y, pixelDepth, modelMatrix, projectMatrix, viewport, &objSpaceCoords[0], &objSpaceCoords[1], &objSpaceCoords[2]); //  .... (ommitted) }```

Member
Posts: 72
Joined: 2006.10
Post: #2
The first question that springs to mind here is: What are you drawing?

Actually, what have you drawn in your OpenGL context before the mouseDown: method is called? Since this is what determines the content of the depth buffer, it's an important piece of the puzzle

Since the depth buffer value is constant accros the board, then I would assume that either:

A. you are using an orthogonal projection matrix and draw everything at the same depth
B. you draw a rather precise elipsoid
C. you haven't drawn anything yet
D. writting to the depth buffer is disabled

Maybe there's something I'm not seeing though

- Sohta
hyn
Unregistered

Post: #3

The scene is already drawn when the mouse is clicked.
It consists only of a textured sphere (drawn using gluSphere()) and the rest is just empty.

My timer calls the drawRect method at around 60 FPS, and when the mouse is clicked, there is always something on the screen.

My init part might be of importance, so here it is:

Code:
```- (void)initGL {     if(kVerbose) NSLog(@"preparing OpenGL");          //  prepare OpenGL     glClearColor(0.0, 0.0, 0.0, 0.0);     glEnable(GL_DEPTH_TEST);     glEnable(GL_NORMALIZE);     glEnable(GL_LIGHTING);     glEnable(GL_TEXTURE_2D);     glPixelStorei(GL_UNPACK_CLIENT_STORAGE_APPLE, 1);     glEnable(GL_CULL_FACE);     glEnable(GL_BLEND);     glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA);     glShadeModel(GL_SMOOTH);     glClearDepth(1.0);     glDepthFunc(GL_LEQUAL);     //  create a pointer to the quadric object     quadric = gluNewQuadric();        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);          glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);          //  set viewport     glViewport(0, 0, (int)NSRectFromString([[NSUserDefaults standardUserDefaults] objectForKey: @"resolution"]).size.width,         (int)NSRectFromString([[NSUserDefaults standardUserDefaults] objectForKey: @"resolution"]).size.height);          //  change to projection mode and reset the projection matrix     glMatrixMode(GL_PROJECTION);     glLoadIdentity();          //  calculate aspect ratio of view     gluPerspective(45.0, NSRectFromString([[NSUserDefaults standardUserDefaults] objectForKey: @"resolution"]).size.width         / NSRectFromString([[NSUserDefaults standardUserDefaults] objectForKey: @"resolution"]).size.height, 1.0, 100.0);        //  reset the modelview matrix     glMatrixMode(GL_MODELVIEW);     glLoadIdentity(); }```
Member
Posts: 184
Joined: 2004.07
Post: #4
If the depth value you read is not between 0.0 and 1.0, gluUnproject will not work correctly. A lot of OpenGL drivers will give an incorrect value- I recently experienced such a problem with an ATI card. One fix that works most of the time is for you to readPixels with a cleared depth value to give you what 1.0 is mapped to, and then divide whatever value you read by that 'zero depth value' in order to get a value from 0.0 to 1.0. This is what I use in my code:

Code:
```glClear(color bit & depth bit) GLfloat zerodepth, newdepth; glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &zerodepth); ... draw ... glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &newdepth); unproject(x,y, newdepth/zerodepth, ...);```

-p.
hyn
Unregistered

Post: #5
Phydeux, I tried your method, and I get a 'nan' or infinity for the pixelDepth / zeroDepth operation. My zeroDepth is 0.000000.
Member
Posts: 184
Joined: 2004.07
Post: #6
Hm, if you read a 0.0 in the depth buffer after you have cleared it, perhaps your depth values are inverted. I'm not sure if it will work, since the driver might mess up all depth values, but you might want to try playing around with glDepthRange- you can try glDepthRange(1.0, 0.0) to referse the range, and that might work. Also on some cards, the depth value is mapped in an odder way to the correct mapped (1.0 to 0.0) depth, though I don't know the exact details on this.

You may want to try using GL_FLOAT instead of GL_DOUBLE (with a GLfloat variable) for reading the pixels instead. Sometimes drivers also screw this up. I'm not even sure if the OpenGL standard allows GL_DOUBLE to be passed to glReadPixels, so that could be your problem too.

p.
hyn
Unregistered

Post: #7
You're right, glReadPixels() doesn't allow GL_DOUBLE. I tried GL_FLOAT and my Z values are very natural! Thanks for pointing that out.

However there seems to be another problem,
My OpenGL coordinates at the point of mouse click still has some discrepancies.
I am reading the "Red Book", the official OpenGL guide and on page 131, where it explains glDepthRange(), I spotted this:

"In perspective projection, the transformed depth coordinate (like the x- and y-coordinates) is subject to perspective division by the w-coordinate. As the transformed depth coordinate moves farther away from the near clipping plane, its location becomes increasingly less precise. [...] Therefore, perspective division affects the accuracy of operations that rely on the transformed depth coordinate, especially depth buffering, which is used for hidden-surface removal."

And I am using a perspecting projection. Could this be the reason for the discrepancies? If I click the middle of the screen, where the z value is 1.0, there are no discrepancies between the point of mouseclick and the actual geography.
But where z value is far form 1.0, the point of click never matches the point of geography.

How can I make this precise for z values far from 1.0?
Member
Posts: 184
Joined: 2004.07
Post: #8
That's correct- perspective trasformation can mean that your depth buffer won't be all that precise in certain areas. You can try to alleviate some of the problem by making your depth range a lot smaller- right now it looks like it's 1 to 100, though if you know your objects are much smaller you could make it a smaller range.

If that's still not acceptable you can use the actual geometry and do ray intersection- there are a lot of collision detection libraries. This is actually a good idea for another reason- when you do a glReadPixels you can screw up the pipeline on the 3D card, making your program run a lot slower. This may not be much of a concern for you- it depends what exactly your program is doing.

Another alternative that also uses readPixels is to use a second pass. Since you know your depth pixel is in a certain depth range based on your first readPixel, you can resample it- you could set your viewport to just the one pixel you want, and then set your depth range to something close to it. Using this method you can then get much more precise (though it still may not be 100% precise... again for that you really should use the ray intersection with your geometry.)

p.
hyn
Unregistered

Post: #9
I will try this ray intersection approach, though it seems like there is a lot to learn for me. i.e. collision detection

I'll report back if I run into any problems.

Thanks,

-hyn
GioFX
Unregistered

Post: #10
hyn,
my code is exactly as your and works fine, really strange ... are you sure to do the "mousedetect" after the swapbuffer? There are some front/transparent object (just for fade fx...)? Ok ok, stupid question...
I'm in GL_MODELVIEW and my range vary from 1 to 8000 (sometimes to 32000...).
I dont use depthrange.
I've tryied the intersection test but it is slowly than glReadPixels ... at least for ten or more calls per frame (I tryied it for occlusion flares)

GiofX

my amazing code:

//viewport
glMatrixMode(GL_PROJECTION);
gluPerspective(
(GLdouble)fovAngle,
(GLdouble)((float)resolution.x/(float)resolution.y),
(GLdouble)1,
(GLdouble)8000);
glMatrixMode(GL_MODELVIEW);

//detect
double sx,sy,sz;
float depth;
double pMat[16];
double mMat[16];
Point mouseCoord;

glGetDoublev(GL_MODELVIEW_MATRIX, mMat);
glGetDoublev(GL_PROJECTION_MATRIX,pMat);

mouseCoord.h,
mouseCoord.v,
1,1,GL_DEPTH_COMPONENT,
GL_FLOAT,
&depth);

gluUnProject(
(double)mouseCoord.h,(double)mouseCoord.v,
depth,
mMat,pMat,viewport,
&sx,&sy,&sz);

//sx,sy and sz are now in world coordinates
//I check if they are inside the object bounding boxes
Member
Posts: 184
Joined: 2004.07
Post: #11
How fast your intersection test runs is directly related to what collision detection routines you are using. If you, for example, have several objects, you can quickly rule out an object if the ray does not intersect its bounding box. Then within each object, you could do the same thing with an octree or BSP tree or what have you. With these things in place it's not likely you'll probably do logarithmic in the number of triangles, which should be significantly less calculation than your game logic.
hyn
Unregistered

Post: #12
GioFX,
I don't do anything fancy after initializing my OpenGL view. You use 8000 for your zFar value and yet your point of mouse click is accurate?

Perhaps it's my pixel attribute that is different from yours? Although it may have nothing to do with it.

Code:
```GLuint attributes[] = { NSOpenGLPFANoRecovery, NSOpenGLPFAWindow, NSOpenGLPFAAccelerated, NSOpenGLPFADoubleBuffer, NSOpenGLPFAColorSize, 24, NSOpenGLPFAAlphaSize, 8, NSOpenGLPFADepthSize, 24, 0 }```

I use GL_UNPACK_CLIENT_STORAGE_APPLE, maybe this is it?
I checked right now and it doesn't seem to affect it.

P.S. I haven't tried the ray intersection approach yet.
GioFX
Unregistered

Post: #13
hyn,
yes it's accurate but I dont use (and dont know...) "GL_UNPACK_CLIENT_STORAGE_APPLE"
my Z-buffer is set to 16 (faster and less accurate than 32 I suppose)

Gio

PS: also glFeedbackBuffer can be useful to test object (tricky...)
hyn
Unregistered

Post: #14
I tried other values for glPixelStore() and didn't make a difference.
It's still inaccurate.

The APPLE constant, I read somewhere, is Apple's optimized method, suposedly faster.
GioFX
Unregistered

Post: #15
I avoid apple-constant 'cause I port my code from win32 ... but why you are using a 24 bit z-buffer?

(vacation finally !!!! bye at all)

Gio