Easiest way of detecting a click on a 2D object in 3D Space

Godot Version



Hello everybody,

I have a grid map (think hex-map, but slightly different) that is 2D functionally but lives in 3D space. When I click a mouse button I want to know where on that 2D grid I clicked.

So far I have done this manually using a Plane() .

	private Vector3? ScreenToGridPlane()
		Vector2 mousePos = GetViewport().GetMousePosition();
		Camera3D camera = GetTree().Root.GetCamera3D();
		Vector3 rayOrigin = camera.ProjectRayOrigin(mousePos);
		Vector3 rayDirection = camera.ProjectRayNormal(mousePos);

		Plane plane = new (_gridPlaneNormal, _centerPos);

		return plane.IntersectsRay(rayOrigin, rayDirection);

This however fails if the Grid Transform is changed in any way (other then rotating, since I manuallyadjusted _gridPlaneNormal). My method returns the worldPosition and not localPosition.

Thus I thought it might be a lot easier to just an Area3D with a CollisionShape3D.

Sadly there is not really a Plane Shape. Only thing I can think of is using a very flat Box Shape.

Am I missing any other method if I don’t want to manually calculate the local Position from what Plane.IntersectsRay() returns.

Thanks already!

Assuming you’re running this in _physics_process() you could use get_collision_point() to get the global coordinates point where the ray hits.
Then you do some trigonometry.

extends Node3D

func _physics_process(delta):
	var ray: RayCast3D = $RayCast3D
	var c: PhysicsBody3D =  ray.get_collider()
	if c:
		var p = ray.get_collision_point() # World space point
		var lp = p * c.transform # Local space point
		var sp = lp / c.scale # Scaled back
		var uv = Vector2(sp.x, sp.y) # Discard depth
		var uvc = Vector2.ONE - uv * 0.5 # Make top left (0, 0) and bottom right (1, 1)
		# the rest of your local calculations...
		var result
		return result

This is an example of a Node3D with a RayCast3D child getting a point in UV-space on a cube, but it could be any mesh. You local calculations would be converting to map coords from the UV [0-1] and then to tile and whatever else you need in that.
The important part is the face is aligned to the z-axis, so we discard the z coord. For non-aligned faces it is more complicated.

1 Like

Thanks for your answer.

I just realised that I am somewhat of a dunce:

  1. An Area3D with a flat CollisionShape3D also only returns the global Position (when using the InputEvent Signal
  2. There two methods called ToLocal and ToGlobal, which should solve my problem when I use it with Area3D.

Sorry one more question.

I just used a CollisionObject3D with a BoxShape object, where I’ve set Y = 0. The InputEvent signal from the Area3D seems to work just fine.

I was kind of thinking setting Y = 0 might cause a problem, but I guess it might only be problematic for collisions betweens two colliders as its paper-thin and would often be ignored.

The only time a 0-thin obstacle is a problem is if an object (physicsbody) is moving faster than it is long along its movement. Then it can skip through.