ofCamera - Mouse picking - how does it work?

I have a code which draws a multiple 2d rectangle inside cam.begin() and cam.end(). (I assume this adds the z axis, which actually translates the dimension to 3d.)

(Question 1)
Now, I want to check whether a mouse cursor is currently hovering a specific rectangle. I assume cam.screenToWorld(mousePos) does the trick but I can’t figure out the actual code.

(Question 2)
I also don’t know how to get the mousePos.z value of the mouse cursor (because mouse coordinates are 2d.)

(Question 3)
Same applies to the rectangle. How to get the normalized z value of an actually 2d-drawn rectangle?

Could someone help me on this? Thanks.

2 Likes

Do you actually use the 3rd dimension at all in your project? If everything is 2d (orthographic projection) you should be fine with using screenToWorld and worldToScreen i guess.
This addon gives a nice pattern for interactive objects:

If you are using 3d have a look at this addon:

If you have any trouble post some code :slight_smile:

@Jordi No, I currently have no 3d objects in my project.

Here’s my code for now:

glm::vec3 mouse;
mouse.x = ofGetMouseX();
mouse.y = ofGetMouseY();
mouse.z = 1.f; // Magical Z value here

// Convert the mouse coords into world coords
mouse = swap_y(cam.screenToWorld(mouse));

// A vector with four corners of the target rectangle
std::vector<glm::vec3> rect = get_target_rect();

// Convert the screen rectangle coords to world coords
rect[0] = swap_y(cam.screenToWorld(rect[0])); // Left top
rect[1] = swap_y(cam.screenToWorld(rect[1])); // Left bottom
rect[2] = swap_y(cam.screenToWorld(rect[2])); // Right bottom
rect[3] = swap_y(cam.screenToWorld(rect[3])); // Right top

// Simple calculations to check whether the mouse coords are within the target rect
if (within(rect, mouse)) {
    std::cout << "Hit!" << std::endl;
}

function within:

bool within(std::vector<glm::vec3> const& rect, glm::vec3 const& mouse)
{
    return (rect[0].y <= mouse.y && mouse.y <= rect[1].y) &&
           (rect[1].x <= mouse.x && mouse.x <= rect[2].x) &&
           (rect[2].y >= mouse.y && mouse.y >= rect[3].y) &&
           (rect[3].x >= mouse.x && mouse.x >= rect[0].x);
}

It looked fine at first, but it doesn’t handle the coords properly when the camera is zoomed.
It calculates the world coords based on the default zoom level, i.e. I get the “Hit!” displayed on a larger radius when the camera distance is larger.

So I’m guessing that I somehow need to pass the camera distance for the z value of the rectangle:

rect[0].z = cam.getPosition().z;
rect[1].z = cam.getPosition().z;
rect[2].z = cam.getPosition().z;
rect[3].z = cam.getPosition().z;

But unfortunately this does not work.

Any suggestions?

Update: So since then I’ve realized that screenToWorld() actually takes z value from 0 to 1 (am I correct?), I tried the following code:

static float const exponent_base = 0.025f;
float const rect_z = (1.0f - exponent_base) +
    (exponent_base * (cam.getPosition().z / MAX_CAMERA_DISTANCE));

This looks much better, but still has a problem; it looks fine when the camera is zoomed further, but when it comes to nearer distance, it seems the target rectangle (in world coords) is smaller compared to the actually drawn rectangle. (This issue has been resolved by subsequent post)

So my guess is that I need more mathematical solution to calculate the z value for the rectangle. Am I correct?

Any suggestions are welcome!

Update: Now I have managed to get the intersection a bit closer to the ideal.

I’ve removed the swap_y() function (which was totally a mess) and fixed the within() function to get the proper results on the world coordinate system.

Now I get the rendered result shown below:

The coordinates for the mouse cursor is not calculated properly. I think the z value (not again…) has something to do with it, but I have no idea how to set the correct value. Current values are as follows:

glm::vec3 mouse;
mouse.x = ofGetMouseX();
mouse.y = ofGetMouseY();
mouse.z = rect_z; // The same as rectangle corners' `z` value. Seems not correct though.
mouse = cam.screenToWorld(mouse);

EDIT: I’m starting to think that this might be related to either FOV or aspect ratio… When the cursor goes left, the world coords stays at right and when the cursor goes right, the world coords stays at left.

If you guys have any idea about this issue please tell me :slight_smile:

So I’ve FINALLY came up with the following solution:

// Convert the coordinate system from/to [center <=> left top] of the screen
auto const to_centered = [] (glm::vec3 const& from) {
    return glm::vec3(
        from.x - float(ofGetWidth()) / 2.0f,
        from.y - float(ofGetHeight()) / 2.0f,
        from.z
    );
};
auto const to_origin = [] (glm::vec3 const& from) {
    return glm::vec3(
        from.x + float(ofGetWidth()) / 2.0f,
        from.y + float(ofGetHeight()) / 2.0f,
        from.z
    );
};

// You should already know the default zoom distance
float const zoom_factor_from_default = cam.getPosition().z / DEFAULT_ZOOM_DISTANCE;
glm::vec3 mouse(x, y, rect_z);

mouse = to_centered(cam.screenToWorld(mouse));
mouse.x *= zoom_factor_from_default;
mouse.y *= zoom_factor_from_default;
mouse = to_origin(mouse);

…and this works like a charm. Problem solved. :slight_smile:

Sorry for my delay. I’m glad you found a solution yourself.

@Jordi np - I’ve got much further learning on camera stuff.

Reopening this issue - My code has a problem when the camera has been dollied (i.e. strafed, moved left-right-top-bottom).

It does not add the camera position offset into the mouse’s world coordinates.

Any idea about this?

are you still using orthogonal projection?
Otherwise I suggest using:
https://github.com/elliotwoods/ofxRay1

Sorry i don’t have much time to prepare code… but if you have a simple example we can look at it.

Yeah, I’ve also checked the ofxRay and I tried the following code:

glm::vec3 const mouse_sample = cam_.screenToWorld({x, y, 0.0f});
ofxRay::Ray const ray(cam.getPosition(), mouse_sample - cam.getPosition());
ofxRay::Plane const plane({0, 0, 0}, {0, 0, 1});

glm::vec3 mouse;
plane.intersect(ray, mouse);

But has same problem; mouse position is wrong when camera is strafed. When the camera is dragged to the left, mouse pos goes too much left than expected, when the camera is dragged to the right, mouse pos goes too much right than expected.

I’m totally confused now…

Update: I also tried

glm::vec3 point_on_plane;
plane.intersect(ray, point_on_plane);

auto const dir = glm::normalize((point_on_plane - cam.getPosition()));
auto const distance = -cam.getPosition().z / dir.z;
auto mouse = cam.getPosition() + dir * distance;

Which was taken from Three.js code: http://stackoverflow.com/questions/13055214/mouse-canvas-x-y-to-three-js-world-x-y-z

But this seems not working, too. (Same behavior as I explained before)

Here’s what I get for now, when dragging the camera to the left:

So FINALLY I’ve reached a solution: Simply add the camera offset into the mouse coordinates. No need to use ofxRay at all.

Here’s my completely working code:

// Convert the coordinate system from/to [center <=> left top] of the screen
auto const to_centered = [] (glm::vec3 const& from) {
    return glm::vec3(
        from.x - float(ofGetWidth()) / 2.0f,
        from.y - float(ofGetHeight()) / 2.0f,
        from.z
    );
};
auto const to_origin = [] (glm::vec3 const& from) {
    return glm::vec3(
        from.x + float(ofGetWidth()) / 2.0f,
        from.y + float(ofGetHeight()) / 2.0f,
        from.z
    );
};

// The mouse position, on the screen coords
glm::vec3 mouse(x, y, 0.0f);
glm::vec3 const camera_origin(float(ofGetWidth()) / 2.0f, float(ofGetHeight()) / 2.0f, 0);
glm::vec3 const delta_camera(cam.getPosition() - camera_origin);
glm::vec3 const camera_offset(delta_camera.x, delta_camera.y, 0.0f);

// Tweaks to zoomed coords
// You should already know the default zoom distance
float const zoom_factor_from_default = cam.getPosition().z / DEFAULT_ZOOM_DISTANCE;

mouse = to_centered(mouse);
mouse.x *= zoom_factor_from_default;
mouse.y *= zoom_factor_from_default;
mouse = to_origin(mouse);

// Add the camera offset
mouse += camera_offset;

// Finalize
mouse = cam.screenToWorld(mouse);

Hope this helps!

2 Likes

For anyone wondering how to get the screen coords without the camera scope (i.e. cam.begin() ~ cam.end()), here’s the code:

auto p = arbitrary_point_on_the_screen();
p -= camera_offset / zoom_factor_from_default;

looks interesting.

how do you get the default zoom from the ofCamera object?

and is it ok to replace the glm::vec3 with ofVec3f?
thx

Do you have a full example of mouse picking simple 2d rectangles in 3d space (with ofEasyCam)?