I have a number of spheres in 3d space which the user should be able to select with a mouse click. Now I've seen some examples around using gluUnProject so I gave it a shot. So I have (please correct me every step of the way if I'm wrong because I'm not 100% sure of any part of it):
def compute_pos(x, y, z):
'''
Compute the 3d opengl coordinates for 3 coordinates.
@param x,y: coordinates from canvas taken with mouse position
@param z: coordinate for z-axis
@return; (gl_x, gl_y, gl_z) tuple corresponding to coordinates in OpenGL context
'''
modelview = numpy.matrix(glGetDoublev(GL_MODELVIEW_MATRIX))
projection = numpy.matrix(glGetDoublev(GL_PROJECTION_MATRIX))
viewport = glGetIntegerv(GL_VIEWPORT)
winX = float(x)
winY = float(viewport[3] - float(y))
winZ = z
return gluUnProject(winX, winY, winZ, modelview, projection, viewport)
Then, having the x and y of a mouse click and the position of the center of the sphere:
def is_picking(x, y, point):
ray_start = compute_pos(x, y, -1)
ray_end = compute_pos(x, y, 1)
d = _compute_2d_distance( (ray_start[0], ray_start[1]),
(ray_end[0], ray_end[1]),
(point[0], point[1]))
if d > CUBE_SIZE:
return False
d = _compute_2d_distance( (ray_start[0], ray_start[2]),
(ray_end[0], ray_end[2]),
(point[0], point[2]))
if d > CUBE_SIZE:
return False
d = _compute_2d_distance( (ray_start[1], ray_start[2]),
(ray_end[1], ray_end[2]),
(point[1], point[2]))
if d > CUBE_SIZE:
return False
return True
So because my 3d geometry is not good at all, I compute two points as a ray start and end point, the go into 2d 3 times eliminating one dimension at a time and compute the distance there between my line and the center of the sphere. If any of those distances are bigger than my sphere ray the it's not clicked. I think the formula for the distance is correct but just in case:
def _compute_2d_distance(p1, p2, target):
'''
Compute the distance between the line defined by two points and a target point.
@param p1: first point that defines the line
@param p2: second point that defines the line
@param target: the point to which distance needs to be computed
@return: distance from point to line
'''
if p2[0] != p1[0]:
if p2[1] == p1[1]:
return abs(p2[0] - p1[0])
a = (p2[1] - p1[1])/(p2[0] - p1[0])
b = -1
c = p1[1] + p1[0] * (p2[1] - p1[1]) / (p2[0] - p1[0])
d = abs(a * target[0] + b * target[1] + c) / math.sqrt(a * a + b * b)
return d
if p2[0] == p1[0]:
d = abs(p2[1] - p1[1])
return d
return None
Now the code seems to work fine in the start position. But after you use to mouse and rotate the screen even for a little bit, nothing works as expected anymore.
Hi there are a lot of solutions for this kind of problem.
Ray casting is one of the best but it involves a lot of geometry knowledge and it is not easy at all.
Moreover the gluUnProject is not available in other OpenGL implementations such as ES for mobile devices (though you can write it in your matrices manipulation functions).
I personally prefer the color picking solution which is quite flexible and very fast computing wise.
The idea is to render the select-able (only the select-able for performance boost) with a given unique color on an offscreen buffer.
Then you take the color of the pixel at the coordinates clicked by the user and you select the corresponding 3D object.
Cheers Maurizio Benedetti