Search code examples
c++openglgraphicsgeometryhittest

Why is mapping window coordinates to a sphere difficult when the OpenGL camera is at a low altitude (low Z-coordinate)?


I have a sphere (representing Earth) and I want the user to be able to move the mouse while I track the point underneath that mouse location on the sphere.

Everything works fine as long as the camera is at a reasonable altitude from the surface of the sphere (say, the equivalent of at least a few hundred meters in real life).

But if I zoom in too closely to the surface of the sphere (say, 30 meters above the surface), then I start observing a bizarre behavior: all the points I draw now seem to start "snapping" to some predefined lattice in space, and if I try to draw a few lines that intersect at the point on the surface directly beneath the mouse, they instead "snap" to some nearby point, nowhere underneath the cursor.

Specifically, I'm using the following code to map the point from 3D to 2D and back:

double line_sphere_intersect(double const (&o)[3], double const (&d)[3], double const r)
{
    double const
        dd = d[0] * d[0] + d[1] * d[1] + d[2] * d[2],
        od = o[0] * d[0] + o[1] * d[1] + o[2] * d[2],
        oo = o[0] * o[0] + o[1] * o[1] + o[2] * o[2],
        left = -od,
        right = sqrt(od * od - dd * (oo - r * r)),
        r1 = (left + right) / dd,
        r2 = (left - right) / dd;
    return ((r1 < 0) ^ (r1 < r2)) ? r1 : r2;
}

Point3D mouse_pos_to_coord(int x, int y)
{
    GLdouble model[16];  glGetDoublev(GL_MODELVIEW_MATRIX, model);
    GLdouble  proj[16];  glGetDoublev(GL_proj_MATRIX, proj);
    GLint view[4];       glGetIntegerv(GL_view, view);  
    y = view[3] - y;  // invert y axis

    GLdouble a[3]; if (!gluUnProject(x, y, 0       , model, proj, view, &a[0], &a[1], &a[2])) { throw "singular"; }
    GLdouble b[3]; if (!gluUnProject(x, y, 1 - 1E-4, model, proj, view, &b[0], &b[1], &b[2])) { throw "singular"; }
    for (size_t i = 0; i < sizeof(b) / sizeof(*b); ++i) { b[i] -= a[i]; }
    double const t = line_sphere_intersect(a, b, earth_radius / 1000);
    Point3D result = Point3D(t * b[0] + a[0], t * b[1] + a[1], t * b[2] + a[2]);
    Point3D temp;
    if (false /* changing this to 'true' changes things, see question */)
    {
        gluProject(result.X, result.Y, result.Z, model, proj, view, &temp.X, &temp.Y, &temp.Z);
        gluUnProject(temp.X, temp.Y, 1 - 1E-4, model, proj, view, &result.X, &result.Y, &result.Z);
        gluProject(result.X, result.Y, result.Z, model, proj, view, &temp.X, &temp.Y, &temp.Z);
    }
    return result;
}

with the following matrices:

glMatrixMode(GL_PROJECTION);
gluPerspective(
    60, (double)viewport[2] / (double)viewport[3], pow(FLT_EPSILON, 0.9),
    earth_radius_in_1000km * (0.5 + diag_dist / tune_factor / zoom_factor));
glMatrixMode(GL_MODELVIEW);
gluLookAt(eye.X, eye.Y, eye.Z, 0, 0, 0, 0, 1, 0);

where eye is the current camera location above Earth.

(And yes, I'm using double everywhere, so it shouldn't be a precision issue with float.)

Furthermore, I've observed that if I change the if (false) to if (true) in my code, then the red lines now seem to intersect directly underneath the cursor, which I find baffling. (Edit: I'm not sure if the mapped point is still correct, though... it's hard for me to tell.)

This implies that the red lines intersect correctly when the corresponding "Z" coordinate of the 2D cursor position (i.e. the relative to the window) is nearly 1... but when it degenerates to approximately 0.9 or lower, then I start seeing the "snapping" issue.

I don't understand how or why this affects anything, though. Why does the Z coordinate affect things like this? Is this normal? How do I fix this issue?


Solution

  • It definitely seems like the graphics card is internally truncating to 32-bit floats for rendering (but possibly using 64-bit floats for some other calculations, I'm not sure).

    This seems to be true both of my Intel card and my NVIDIA card.

    Re-centering the coordinate system around the center of the map seems to fix the issue.