From 553b1cb864a0aa865d24712c4999df68c4365624 Mon Sep 17 00:00:00 2001 From: Markus Sauermann <6299227+Sauermann@users.noreply.github.com> Date: Thu, 10 Aug 2023 09:54:46 +0200 Subject: [PATCH] Rewrite Gui in 3D to use Physics Picking for mouse events Rework Gui in 3D Demo to handle mouse events via Physics Picking instead of in _unhandled_input. This brings several benefits: - Correctly handle cases, where the 3D-GUI is located behind other collision objects. - Proper passive hovering support This allows also to make simplifications in the code, because 3D-mouse position no longer needs to be calculated manually. --- viewport/gui_in_3d/gui_3d.gd | 157 +++++++++++-------------------- viewport/gui_in_3d/project.godot | 2 +- 2 files changed, 58 insertions(+), 101 deletions(-) diff --git a/viewport/gui_in_3d/gui_3d.gd b/viewport/gui_in_3d/gui_3d.gd index fda8ab64ea2..84439e3b23d 100644 --- a/viewport/gui_in_3d/gui_3d.gd +++ b/viewport/gui_in_3d/gui_3d.gd @@ -1,15 +1,11 @@ extends Node3D -# The size of the quad mesh itself. -var quad_mesh_size # Used for checking if the mouse is inside the Area3D var is_mouse_inside = false -# Used for checking if the mouse was pressed inside the Area3D -var is_mouse_held = false -# The last non-empty mouse position. Used when dragging outside of the box. -var last_mouse_pos3D = null # The last processed input touch/mouse event. To calculate relative movement. -var last_mouse_pos2D = null +var last_event_pos2D = null +# The time of the last event in seconds since engine start. +var last_event_time: float = -1.0 @onready var node_viewport = $SubViewport @onready var node_quad = $Quad @@ -17,6 +13,8 @@ var last_mouse_pos2D = null func _ready(): node_area.mouse_entered.connect(self._mouse_entered_area) + node_area.mouse_exited.connect(self._mouse_exited_area) + node_area.input_event.connect(self._mouse_input_event) # If the material is NOT set to use billboard settings, then avoid running billboard specific code if node_quad.get_surface_override_material(0).billboard_mode == BaseMaterial3D.BillboardMode.BILLBOARD_DISABLED: @@ -32,124 +30,83 @@ func _mouse_entered_area(): is_mouse_inside = true +func _mouse_exited_area(): + is_mouse_inside = false + + func _unhandled_input(event): # Check if the event is a non-mouse/non-touch event - var is_mouse_event = false for mouse_event in [InputEventMouseButton, InputEventMouseMotion, InputEventScreenDrag, InputEventScreenTouch]: if is_instance_of(event, mouse_event): - is_mouse_event = true - break - - # If the event is a mouse/touch event and/or the mouse is either held or inside the area, then - # we need to do some additional processing in the handle_mouse function before passing the event to the viewport. - # If the event is not a mouse/touch event, then we can just pass the event directly to the viewport. - if is_mouse_event and (is_mouse_inside or is_mouse_held): - handle_mouse(event) - elif not is_mouse_event: - node_viewport.push_input(event) + # If the event is a mouse/touch event, then we can ignore it here, because it will be + # handled via Physics Picking + return + node_viewport.push_input(event) -# Handle mouse events inside Area3D. (Area3D.input_event had many issues with dragging) -func handle_mouse(event): +func _mouse_input_event(_camera: Camera3D, event: InputEvent, event_position: Vector3, _normal: Vector3, _shape_idx: int): # Get mesh size to detect edges and make conversions. This code only support PlaneMesh and QuadMesh. - quad_mesh_size = node_quad.mesh.size + var quad_mesh_size = node_quad.mesh.size - # Detect mouse being held to mantain event while outside of bounds. Avoid orphan clicks - if event is InputEventMouseButton or event is InputEventScreenTouch: - is_mouse_held = event.pressed + # Event position in Area3D in world coordinate space + var event_pos3D = event_position - # Find mouse position in Area3D - var mouse_pos3D = find_mouse(event.global_position) + # Current time in seconds since engine start. + var now: float = Time.get_ticks_msec() / 1000.0 - # Check if the mouse is outside of bounds, use last position to avoid errors - # NOTE: mouse_exited signal was unrealiable in this situation - is_mouse_inside = mouse_pos3D != null - if is_mouse_inside: - # Convert click_pos from world coordinate space to a coordinate space relative to the Area3D node. - # NOTE: affine_inverse accounts for the Area3D node's scale, rotation, and position in the scene! - mouse_pos3D = node_area.global_transform.affine_inverse() * mouse_pos3D - last_mouse_pos3D = mouse_pos3D - else: - mouse_pos3D = last_mouse_pos3D - if mouse_pos3D == null: - mouse_pos3D = Vector3.ZERO + # Convert position to a coordinate space relative to the Area3D node. + # NOTE: affine_inverse accounts for the Area3D node's scale, rotation, and position in the scene! + event_pos3D = node_quad.global_transform.affine_inverse() * event_pos3D # TODO: adapt to bilboard mode or avoid completely - # convert the relative event position from 3D to 2D - var mouse_pos2D = Vector2(mouse_pos3D.x, -mouse_pos3D.y) - - # Right now the event position's range is the following: (-quad_size/2) -> (quad_size/2) - # We need to convert it into the following range: 0 -> quad_size - mouse_pos2D.x += quad_mesh_size.x / 2 - mouse_pos2D.y += quad_mesh_size.y / 2 - # Then we need to convert it into the following range: 0 -> 1 - mouse_pos2D.x = mouse_pos2D.x / quad_mesh_size.x - mouse_pos2D.y = mouse_pos2D.y / quad_mesh_size.y + var event_pos2D: Vector2 = Vector2() - # Finally, we convert the position to the following range: 0 -> viewport.size - mouse_pos2D.x = mouse_pos2D.x * node_viewport.size.x - mouse_pos2D.y = mouse_pos2D.y * node_viewport.size.y - # We need to do these conversions so the event's position is in the viewport's coordinate system. + if is_mouse_inside: + # convert the relative event position from 3D to 2D + event_pos2D = Vector2(event_pos3D.x, -event_pos3D.y) + + # Right now the event position's range is the following: (-quad_size/2) -> (quad_size/2) + # We need to convert it into the following range: -0.5 -> 0.5 + event_pos2D.x = event_pos2D.x / quad_mesh_size.x + event_pos2D.y = event_pos2D.y / quad_mesh_size.y + # Then we need to convert it into the following range: 0 -> 1 + event_pos2D.x += 0.5 + event_pos2D.y += 0.5 + + # Finally, we convert the position to the following range: 0 -> viewport.size + event_pos2D.x *= node_viewport.size.x + event_pos2D.y *= node_viewport.size.y + # We need to do these conversions so the event's position is in the viewport's coordinate system. + + elif last_event_pos2D != null: + # Fall back to the last known event position + event_pos2D = last_event_pos2D # Set the event's position and global position. - event.position = mouse_pos2D - event.global_position = mouse_pos2D + event.position = event_pos2D + if event is InputEventMouse: + event.global_position = event_pos2D - # If the event is a mouse motion event... - if event is InputEventMouseMotion: + # Calculate the relative event distance + if event is InputEventMouseMotion or event is InputEventScreenDrag: # If there is not a stored previous position, then we'll assume there is no relative motion. - if last_mouse_pos2D == null: + if last_event_pos2D == null: event.relative = Vector2(0, 0) # If there is a stored previous position, then we'll calculate the relative position by subtracting # the previous position from the new position. This will give us the distance the event traveled from prev_pos else: - event.relative = mouse_pos2D - last_mouse_pos2D - # Update last_mouse_pos2D with the position we just calculated. - last_mouse_pos2D = mouse_pos2D + event.relative = event_pos2D - last_event_pos2D + event.velocity = event.relative / (now - last_event_time) - # Finally, send the processed input event to the viewport. - node_viewport.push_input(event) + # Update last_event_pos2D with the position we just calculated. + last_event_pos2D = event_pos2D + # Update last_event_time to current time. + last_event_time = now -func find_mouse(global_position): - var camera = get_viewport().get_camera_3d() - var dist = find_further_distance_to(camera.transform.origin) - - # From camera center to the mouse position in the Area3D. - var parameters = PhysicsRayQueryParameters3D.new() - parameters.from = camera.project_ray_origin(global_position) - parameters.to = parameters.from + camera.project_ray_normal(global_position) * dist - - # Manually raycasts the area to find the mouse position. - parameters.collision_mask = node_area.collision_layer - parameters.collide_with_bodies = false - parameters.collide_with_areas = true - var result = get_world_3d().direct_space_state.intersect_ray(parameters) - - if result.size() > 0: - return result.position - else: - return null - - -func find_further_distance_to(origin): - # Find edges of collision and change to global positions - var edges = [] - edges.append(node_area.to_global(Vector3(quad_mesh_size.x / 2, quad_mesh_size.y / 2, 0))) - edges.append(node_area.to_global(Vector3(quad_mesh_size.x / 2, -quad_mesh_size.y / 2, 0))) - edges.append(node_area.to_global(Vector3(-quad_mesh_size.x / 2, quad_mesh_size.y / 2, 0))) - edges.append(node_area.to_global(Vector3(-quad_mesh_size.x / 2, -quad_mesh_size.y / 2, 0))) - - # Get the furthest distance between the camera and collision to avoid raycasting too far or too short - var far_dist = 0 - var temp_dist - for edge in edges: - temp_dist = origin.distance_to(edge) - if temp_dist > far_dist: - far_dist = temp_dist - - return far_dist + # Finally, send the processed input event to the viewport. + node_viewport.push_input(event) func rotate_area_to_billboard(): diff --git a/viewport/gui_in_3d/project.godot b/viewport/gui_in_3d/project.godot index 27fbe07b798..cae1291f10a 100644 --- a/viewport/gui_in_3d/project.godot +++ b/viewport/gui_in_3d/project.godot @@ -14,7 +14,7 @@ config/name="GUI in 3D" config/description="A demo showing a GUI instanced within a 3D scene using viewports, as well as forwarding mouse and keyboard input to the GUI." run/main_scene="res://gui_in_3d.tscn" -config/features=PackedStringArray("4.0") +config/features=PackedStringArray("4.2") config/icon="res://icon.webp" config/tags=PackedStringArray("3d", "demo", "gui", "official")