Bladeren bron

Rewrite GUI in 3D demo to use Physics Picking for mouse events (#925)

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.
Markus Sauermann 9 maanden geleden
bovenliggende
commit
a69b2f7e21
2 gewijzigde bestanden met toevoegingen van 67 en 110 verwijderingen
  1. 66 109
      viewport/gui_in_3d/gui_3d.gd
  2. 1 1
      viewport/gui_in_3d/gui_in_3d.tscn

+ 66 - 109
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
+# 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,134 +30,93 @@ 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
+
+	# Event position in Area3D in world coordinate space.
+	var event_pos3D = event_position
 
-	# 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
+	# Current time in seconds since engine start.
+	var now: float = Time.get_ticks_msec() / 1000.0
 
-	# Find mouse position in Area3D
-	var mouse_pos3D = find_mouse(event.global_position)
+	# 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.
+
+	var event_pos2D: Vector2 = Vector2()
 
-	# 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
-
-	# 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
-
-	# 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.
+		# 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
+		# 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
-
-	# Finally, send the processed input event to the viewport.
-	node_viewport.push_input(event)
-
-
-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)
+			event.relative = event_pos2D - last_event_pos2D
+			event.velocity = event.relative / (now - last_event_time)
 
-	if result.size() > 0:
-		return result.position
-	else:
-		return null
+	# 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_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():
 	var billboard_mode = node_quad.get_surface_override_material(0).params_billboard_mode
 
-	# Try to match the area with the material's billboard setting, if enabled
+	# Try to match the area with the material's billboard setting, if enabled.
 	if billboard_mode > 0:
-		# Get the camera
+		# Get the camera.
 		var camera = get_viewport().get_camera_3d()
-		# Look in the same direction as the camera
+		# Look in the same direction as the camera.
 		var look = camera.to_global(Vector3(0, 0, -100)) - camera.global_transform.origin
 		look = node_area.position + look
 
@@ -169,5 +126,5 @@ func rotate_area_to_billboard():
 
 		node_area.look_at(look, Vector3.UP)
 
-		# Rotate in the Z axis to compensate camera tilt
+		# Rotate in the Z axis to compensate camera tilt.
 		node_area.rotate_object_local(Vector3.BACK, camera.rotation.z)

+ 1 - 1
viewport/gui_in_3d/gui_in_3d.tscn

@@ -63,10 +63,10 @@ shadow_blur = 3.0
 omni_range = 10.0
 
 [node name="Camera_Move" type="AnimationPlayer" parent="."]
-autoplay = "Move_camera"
 libraries = {
 "": SubResource("AnimationLibrary_uw4n0")
 }
+autoplay = "Move_camera"
 
 [node name="Background" type="Node3D" parent="."]