3

I'm creating a 3D application using GLUT in C++.

Now, I want to implement a method similar to this:

Vector3* MyClass::get3DObjectfromMouse(int mouseX, int mouseY);

How can I implement this method?

glampert
  • 4,371
  • 2
  • 23
  • 49
user1956702
  • 85
  • 1
  • 8
  • 1
    How about casting a ray from screen coordinate and choose first object intersected by this ray? – vadimvolk May 14 '14 at 02:20
  • 2
    You cannot, you need a 3rd coordinate to do that. Usually, people use the near plane for point 1, the far plane for point 2 and then cast a ray that passes through both. That ray represents the infinite number of points that project to Window《x,y》. Alternatively, if you want the position of an object on screen at 《x,y》, you could read the depth buffer, which will give you Window《z》 necessary to get the unprojected position. – Andon M. Coleman May 14 '14 at 02:37

1 Answers1

15

As it was commented by Andon M. Coleman, one way you can achieve this is by doing a ray/object intersection test, with unprojected screen coordinates. This technique is commonly known as picking.

A pseudo-C++ code for picking:

Assume we have a 3D object type/class:

class Object3D { ... };

A 3D picking function would return a list of all objects that are intersected by a line going from the given 2D point in the near plane to the same point in the far plane.

struct LineSegment 
{
    Vector3 start;
    Vector3 end;
};

Object3D[] Pick(float x, float y)
{
    LineSegment lineSeg;
    Object3D[] intersectedObjs;

    // Do both un-projections for z-near (0) and z-far (1).
    // This produces a line segment going from z-near to far.
    UnProject(x, y, /* z = */ 0.0, modelViewMatrix, projectionMatrix, viewport, lineSeg.start);
    UnProject(x, y, /* z = */ 1.0, modelViewMatrix, projectionMatrix, viewport, lineSeg.end);

    // Iterate all object in the scene or in the current view:
    for (Object3D obj : scene)
    {
        if (TestLineIntersection(obj, lineSeg))
        {
            // This object is crossed by the picking line.
            intersectedObjs.Add(obj);
        }
    }

    // Optionally you might want sort them from distance 
    // to the camera/viewer before returning the intersections.
    return intersectedObjs;
}

And the UnProject() function would look like this:

bool UnProject(float winX, float winY, float winZ,
               const Matrix4 & modelView, const Matrix4 & projection,
               const ScreenRect viewport, Vector3 & worldCoordinates)
{
    // Compute (projection x modelView) ^ -1:
    const Matrix4 m = inverse(projection * modelView);

    // Need to invert Y since screen Y-origin point down,
    // while 3D Y-origin points up (this is an OpenGL only requirement):
    winY = viewport.Height() - winY;

    // Transformation of normalized coordinates between -1 and 1:
    Vector4 in;
    in[0] = (winX - viewport.X()) / viewport.Width()  * 2.0 - 1.0;
    in[1] = (winY - viewport.Y()) / viewport.Height() * 2.0 - 1.0;
    in[2] = 2.0 * winZ - 1.0;
    in[3] = 1.0;

    // To world coordinates:
    Vector4 out(m * in);
    if (out[3] == 0.0) // Avoid a division by zero
    {
        worldCoordinates = Vector3Zero;
        return false;
    }

    out[3] = 1.0 / out[3];
    worldCoordinates[0] = out[0] * out[3];
    worldCoordinates[1] = out[1] * out[3];
    worldCoordinates[2] = out[2] * out[3];
    return true;
}

To clarify, TestLineIntersection() does a line vs AABB intersection test. The bounding box should be transformed to world-space, since it is usually expressed as a set of points in local model-space.

bool TestLineIntersection(const Object3D & obj, const LineSegment & lineSeg)
{
    AABB aabb = obj.GetAABB();
    aabb.TransformBy(obj.modelMatrix);
    return aabb.LineIntersection(lineSeg.start, lineSeg.end);
}

// AABB.cpp:
bool AABB::LineIntersection(const Vector3 & start, const Vector3 & end) const
{
    const Vector3 center     = (mins + maxs) * 0.5;
    const Vector3 extents    = maxs - center;
    const Vector3 lineDir    = 0.5 * (end - start);
    const Vector3 lineCenter = start + lineDir;
    const Vector3 dir        = lineCenter - center;

    const float ld0 = Mathf::Abs(lineDir[0]);
    if (Mathf::Abs(dir[0]) > (extents[0] + ld0))
    {
        return false;
    }

    const float ld1 = Mathf::Abs(lineDir[1]);
    if (Mathf::Abs(dir[1]) > (extents[1] + ld1))
    {
        return false;
    }

    const float ld2 = Mathf::Abs(lineDir[2]);
    if (Mathf::Abs(dir[2]) > (extents[2] + ld2))
    {
        return false;
    }

    const Vector3 vCross = cross(lineDir, dir);
    if (Mathf::Abs(vCross[0]) > (extents[1] * ld2 + extents[2] * ld1))
    {
        return false;
    }
    if (Mathf::Abs(vCross[1]) > (extents[0] * ld2 + extents[2] * ld0))
    {
        return false;
    }
    if (Mathf::Abs(vCross[2]) > (extents[0] * ld1 + extents[1] * ld0))
    {
        return false;
    }

    return true;
}
Community
  • 1
  • 1
glampert
  • 4,371
  • 2
  • 23
  • 49
  • -1 This question doesn't deserve such a nice answer. ;) – Qix - MONICA WAS MISTREATED May 14 '14 at 03:23
  • 2
    Incidentally, I've just implemented this a few days back, so it was still fresh in my head :) – glampert May 14 '14 at 03:32
  • @glampert. Can you please share the snippet for TestLineIntersection? Thank you! – AlvinfromDiaspar Jun 11 '14 at 03:01
  • No probs @AlvinfromDiaspar, added! Also, I'd recommend you take a look at the mathematical definitions of line/box intersection. It will help you get a good grasp of what is going on in the code. – glampert Jun 11 '14 at 05:42
  • Sorry, but what about TransformBy? Also, can the near and far viewport values be (0.1, and 100)? – AlvinfromDiaspar Jun 12 '14 at 19:17
  • Btw, i just want clarification on what the Unproject method does. My understanding is that it returns the vertex in world coordinates. If this is true, then how does the camera's world coordinates affect this? I ask this because my start and end vectors appear to be with respect to the world origin (and not my camera). – AlvinfromDiaspar Jun 12 '14 at 20:53
  • The `TransformBy` function just performs a standard point transformation by the modelMatrix for the min and max points of the AABB. As for the Unproject, the result is in world coords. I recommend you take a look at the following link, it explains the old `gluUnProject()` function, which does the same thing: http://myweb.lmu.edu/dondi/share/cg/unproject-explained.pdf – glampert Jun 12 '14 at 22:28
  • Im just a bit confused right now if i have to incorporate my camera's position when calculating the screen-to-world conversion. Reference: http://gamedev.stackexchange.com/questions/6940/3d-ray-casting-picking?lq=1 – AlvinfromDiaspar Jun 12 '14 at 23:04
  • Just to add to me previous comment/question, im getting a near and far vectors where the x,y coordinates are > -1 and < 1. Should this be multiplied by the camera's x,y coordinates? – AlvinfromDiaspar Jun 12 '14 at 23:56
  • You should pass your camera's view and projection matrixes to the Unproject() function. This will already account for the camera positioning. The returned points for both the 0 and 1 Z are two world space points. If you try to draw a line with them, which I recommend you do, you'll se that the line goes from the point where the camera is at to the farthest Z of your scene. – glampert Jun 13 '14 at 03:18
  • But my x,y coordinates from my unproject calls are between -1 and 1. So maybe my unproject is incorrect? I know this must be wrong because im clicking on objects that i know have large x coordinate values. – AlvinfromDiaspar Jun 13 '14 at 23:06
  • Hard to tell. I think it is best that you ask a question here or on the game-dev section and provide a bit more context. – glampert Jun 14 '14 at 02:12
  • Here is my posted question: http://stackoverflow.com/questions/24221764/unproject-results-for-object-picking – AlvinfromDiaspar Jun 16 '14 at 15:57
  • Quick question regarding the viewport.X() and viewport.Y(). Wouldn't these always be 0? If not, when can there be X and Y values > 0? – AlvinfromDiaspar Jun 16 '14 at 16:46
  • If you are rendering from the start of the screen, then yes. You could in theory render to a portion of the screen, for whatever reasons. In such case, then x,y would be > 0. – glampert Jun 16 '14 at 18:21