8
\$\begingroup\$

I've been trying to find a solution to this task and despite finding answers to similar issues, they were always somewhat overcomplicated (e.g. finding a position on a texture) or the problem was caused by something else and the solution didn't apply.

I'm working on a board game in 3D. As such, I need to frequently transform my viewport's mouse coordinates into world coordinates to move the pieces. Because the entire game happens on a plane, I'm not concerned with the Y axis - I just need to raycast from my camera, through the mouse cursor, onto the world plane and acquire the (x, 0, z) coordinates. I can't, however, rely on the board, as the game board itself is floating in a "void" and coordinates around the board should be detectable (and game-wise illegal).

I was unable to find a Node type to use as an infinite plane for ray collision.

The Camera's project_position ( Vector2 screen_point, float z_depth ) method doesn't seem to be a viable choice either, as my camera should be able to rotate around the board freely and not be locked in the top-down view (otherwise this method would be the perfect solution, to my understanding?).

I'm very new to game development and the techniques overall, any help and pointers how to best achieve the task are greatly appreciated!

\$\endgroup\$

1 Answer 1

8
\$\begingroup\$

I'm not sure I understand your limitations, so I will give you a few approaches, and hopefully this gets you to the solution along the way.

This is not an exhaustive list.


Pickable Object

If you have a CollisionObject (e.g. the board), you can set input_ray_pickable to true on it.

Then connect the input_event signal or override the _input_event method, where you will get - among other parameters - the position in 3D of the pointer when it is over the 3D object.


RayCast Node

If you can't - or rather not - depend on pickable objects, but still have something to collide with, you can use a RayCast node. To make it follow the pointer, you can do this:

var ray_length = 100 # some large number
var mouse_pos = get_viewport().get_mouse_position()
$RayCast.transform.origin = camera.project_ray_origin(mouse_pos)
$RayCast.cast_to = camera.project_ray_normal(mouse_pos) * ray_length
$RayCast.force_update_transform()
$RayCast.force_raycast_update()

Pay attention to project_ray_origin and project_ray_normal. Should give you the direction for the ray that goes toward the mouse_pos, given the camera projection and Viewport size. On the other hand, project_ray_origin should give you the origin for the ray, which is the position of the Camera when the camera projection is perspective, but the method also supports orthogonal projection.

Calling force_update_transform and force_raycast_update is only necessary if you are moving the RayCast multiple times in the same physics frame.

You would then check $RayCast.is_colliding() (which is a way to detect if the pointer is out of the board, for example), and read get_collision_point to get the position. You can also read get_collider which will give you the object the RayCast collided with (which you can check if it is the board).

Note: make sure the RayCast is enabled, it is disabled by default.


Ray-plane intersection

If you want to intersect with an axis aligned plane instead of some object, we can handle it with some vector algebra.

We need to define our ray, and our plane.

For the ray part, the code is basically what we did for the RayCast node above:

var mouse_pos = get_viewport().get_mouse_position()
var origin = camera.project_ray_origin(mouse_pos)
var direction = camera.project_ray_normal(mouse_pos)

Conceptually, we define a parametric form of the ray like this:

origin + direction * distance

Where distance is the parameter. We can assume that direction is a unit vector (a vector of length one) so that distance gives you the length from origin.

The plane will be defined by an coordinate set to a constant, in this case y = 0.

We want to find a point along the ray, that satisfy that (y = 0). We can have a parametric form of the ray like this:

(origin + direction * distance).y = 0

That is:

origin.y + direction.y * distance = 0

Solve for distance:

distance = -origin.y/direction.y

Note that this code would be a division by zero when direction.y = 0. That is, if the ray is parallel to the plane (in which case there is no intersection), so you want to check for that.

With that, we can write the rest of the code:

if direction.y == 0:
    return

var distance = -origin.y/direction.y
var position = origin + direction * distance

And that position should be intersection with the y = 0 plane.

Addendum: The intersection could be behind the camera (when the camera is looking away from the plane, for example the plane is below the camera and the camera is looking upwards), in which case the distance will be negative, you may want to check for that too.

\$\endgroup\$
2
  • \$\begingroup\$ This is exactly the answer I was hoping for - the last option was the one I had in mind but it seems I was just overcomplicating things because I wasn't aware there are simpler solutions! Thank you for the extensive answer! \$\endgroup\$ Commented Jul 11, 2021 at 21:09
  • \$\begingroup\$ Addendum for the last solution: if you have a generic plane (not necessarily axis-aligned, like y=0) you can use Plane(...).intersects_ray(from, dir) or Plane(...).intersects_segment(begin, end), I believe. \$\endgroup\$
    – geekley
    Commented Aug 8, 2023 at 23:51

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .