Skip to content

Commit a69b2f7

Browse files
authored
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.
1 parent 722bd11 commit a69b2f7

File tree

2 files changed

+67
-110
lines changed

2 files changed

+67
-110
lines changed

viewport/gui_in_3d/gui_3d.gd

Lines changed: 66 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
extends Node3D
22

3-
# The size of the quad mesh itself.
4-
var quad_mesh_size
5-
# Used for checking if the mouse is inside the Area3D
3+
# Used for checking if the mouse is inside the Area3D.
64
var is_mouse_inside = false
7-
# Used for checking if the mouse was pressed inside the Area3D
8-
var is_mouse_held = false
9-
# The last non-empty mouse position. Used when dragging outside of the box.
10-
var last_mouse_pos3D = null
115
# The last processed input touch/mouse event. To calculate relative movement.
12-
var last_mouse_pos2D = null
6+
var last_event_pos2D = null
7+
# The time of the last event in seconds since engine start.
8+
var last_event_time: float = -1.0
139

1410
@onready var node_viewport = $SubViewport
1511
@onready var node_quad = $Quad
1612
@onready var node_area = $Quad/Area3D
1713

1814
func _ready():
1915
node_area.mouse_entered.connect(self._mouse_entered_area)
16+
node_area.mouse_exited.connect(self._mouse_exited_area)
17+
node_area.input_event.connect(self._mouse_input_event)
2018

2119
# If the material is NOT set to use billboard settings, then avoid running billboard specific code
2220
if node_quad.get_surface_override_material(0).billboard_mode == BaseMaterial3D.BillboardMode.BILLBOARD_DISABLED:
@@ -32,134 +30,93 @@ func _mouse_entered_area():
3230
is_mouse_inside = true
3331

3432

33+
func _mouse_exited_area():
34+
is_mouse_inside = false
35+
36+
3537
func _unhandled_input(event):
3638
# Check if the event is a non-mouse/non-touch event
37-
var is_mouse_event = false
3839
for mouse_event in [InputEventMouseButton, InputEventMouseMotion, InputEventScreenDrag, InputEventScreenTouch]:
3940
if is_instance_of(event, mouse_event):
40-
is_mouse_event = true
41-
break
42-
43-
# If the event is a mouse/touch event and/or the mouse is either held or inside the area, then
44-
# we need to do some additional processing in the handle_mouse function before passing the event to the viewport.
45-
# If the event is not a mouse/touch event, then we can just pass the event directly to the viewport.
46-
if is_mouse_event and (is_mouse_inside or is_mouse_held):
47-
handle_mouse(event)
48-
elif not is_mouse_event:
49-
node_viewport.push_input(event)
41+
# If the event is a mouse/touch event, then we can ignore it here, because it will be
42+
# handled via Physics Picking.
43+
return
44+
node_viewport.push_input(event)
5045

5146

52-
# Handle mouse events inside Area3D. (Area3D.input_event had many issues with dragging)
53-
func handle_mouse(event):
47+
func _mouse_input_event(_camera: Camera3D, event: InputEvent, event_position: Vector3, _normal: Vector3, _shape_idx: int):
5448
# Get mesh size to detect edges and make conversions. This code only support PlaneMesh and QuadMesh.
55-
quad_mesh_size = node_quad.mesh.size
49+
var quad_mesh_size = node_quad.mesh.size
50+
51+
# Event position in Area3D in world coordinate space.
52+
var event_pos3D = event_position
5653

57-
# Detect mouse being held to mantain event while outside of bounds. Avoid orphan clicks
58-
if event is InputEventMouseButton or event is InputEventScreenTouch:
59-
is_mouse_held = event.pressed
54+
# Current time in seconds since engine start.
55+
var now: float = Time.get_ticks_msec() / 1000.0
6056

61-
# Find mouse position in Area3D
62-
var mouse_pos3D = find_mouse(event.global_position)
57+
# Convert position to a coordinate space relative to the Area3D node.
58+
# NOTE: affine_inverse accounts for the Area3D node's scale, rotation, and position in the scene!
59+
event_pos3D = node_quad.global_transform.affine_inverse() * event_pos3D
60+
61+
# TODO: Adapt to bilboard mode or avoid completely.
62+
63+
var event_pos2D: Vector2 = Vector2()
6364

64-
# Check if the mouse is outside of bounds, use last position to avoid errors
65-
# NOTE: mouse_exited signal was unrealiable in this situation
66-
is_mouse_inside = mouse_pos3D != null
6765
if is_mouse_inside:
68-
# Convert click_pos from world coordinate space to a coordinate space relative to the Area3D node.
69-
# NOTE: affine_inverse accounts for the Area3D node's scale, rotation, and position in the scene!
70-
mouse_pos3D = node_area.global_transform.affine_inverse() * mouse_pos3D
71-
last_mouse_pos3D = mouse_pos3D
72-
else:
73-
mouse_pos3D = last_mouse_pos3D
74-
if mouse_pos3D == null:
75-
mouse_pos3D = Vector3.ZERO
76-
77-
# TODO: adapt to bilboard mode or avoid completely
78-
79-
# convert the relative event position from 3D to 2D
80-
var mouse_pos2D = Vector2(mouse_pos3D.x, -mouse_pos3D.y)
81-
82-
# Right now the event position's range is the following: (-quad_size/2) -> (quad_size/2)
83-
# We need to convert it into the following range: 0 -> quad_size
84-
mouse_pos2D.x += quad_mesh_size.x / 2
85-
mouse_pos2D.y += quad_mesh_size.y / 2
86-
# Then we need to convert it into the following range: 0 -> 1
87-
mouse_pos2D.x = mouse_pos2D.x / quad_mesh_size.x
88-
mouse_pos2D.y = mouse_pos2D.y / quad_mesh_size.y
89-
90-
# Finally, we convert the position to the following range: 0 -> viewport.size
91-
mouse_pos2D.x = mouse_pos2D.x * node_viewport.size.x
92-
mouse_pos2D.y = mouse_pos2D.y * node_viewport.size.y
93-
# We need to do these conversions so the event's position is in the viewport's coordinate system.
66+
# Convert the relative event position from 3D to 2D.
67+
event_pos2D = Vector2(event_pos3D.x, -event_pos3D.y)
68+
69+
# Right now the event position's range is the following: (-quad_size/2) -> (quad_size/2)
70+
# We need to convert it into the following range: -0.5 -> 0.5
71+
event_pos2D.x = event_pos2D.x / quad_mesh_size.x
72+
event_pos2D.y = event_pos2D.y / quad_mesh_size.y
73+
# Then we need to convert it into the following range: 0 -> 1
74+
event_pos2D.x += 0.5
75+
event_pos2D.y += 0.5
76+
77+
# Finally, we convert the position to the following range: 0 -> viewport.size
78+
event_pos2D.x *= node_viewport.size.x
79+
event_pos2D.y *= node_viewport.size.y
80+
# We need to do these conversions so the event's position is in the viewport's coordinate system.
81+
82+
elif last_event_pos2D != null:
83+
# Fall back to the last known event position.
84+
event_pos2D = last_event_pos2D
9485

9586
# Set the event's position and global position.
96-
event.position = mouse_pos2D
97-
event.global_position = mouse_pos2D
87+
event.position = event_pos2D
88+
if event is InputEventMouse:
89+
event.global_position = event_pos2D
9890

99-
# If the event is a mouse motion event...
100-
if event is InputEventMouseMotion:
91+
# Calculate the relative event distance.
92+
if event is InputEventMouseMotion or event is InputEventScreenDrag:
10193
# If there is not a stored previous position, then we'll assume there is no relative motion.
102-
if last_mouse_pos2D == null:
94+
if last_event_pos2D == null:
10395
event.relative = Vector2(0, 0)
10496
# If there is a stored previous position, then we'll calculate the relative position by subtracting
105-
# the previous position from the new position. This will give us the distance the event traveled from prev_pos
97+
# the previous position from the new position. This will give us the distance the event traveled from prev_pos.
10698
else:
107-
event.relative = mouse_pos2D - last_mouse_pos2D
108-
# Update last_mouse_pos2D with the position we just calculated.
109-
last_mouse_pos2D = mouse_pos2D
110-
111-
# Finally, send the processed input event to the viewport.
112-
node_viewport.push_input(event)
113-
114-
115-
func find_mouse(global_position):
116-
var camera = get_viewport().get_camera_3d()
117-
var dist = find_further_distance_to(camera.transform.origin)
118-
119-
# From camera center to the mouse position in the Area3D.
120-
var parameters = PhysicsRayQueryParameters3D.new()
121-
parameters.from = camera.project_ray_origin(global_position)
122-
parameters.to = parameters.from + camera.project_ray_normal(global_position) * dist
123-
124-
# Manually raycasts the area to find the mouse position.
125-
parameters.collision_mask = node_area.collision_layer
126-
parameters.collide_with_bodies = false
127-
parameters.collide_with_areas = true
128-
var result = get_world_3d().direct_space_state.intersect_ray(parameters)
99+
event.relative = event_pos2D - last_event_pos2D
100+
event.velocity = event.relative / (now - last_event_time)
129101

130-
if result.size() > 0:
131-
return result.position
132-
else:
133-
return null
102+
# Update last_event_pos2D with the position we just calculated.
103+
last_event_pos2D = event_pos2D
134104

105+
# Update last_event_time to current time.
106+
last_event_time = now
135107

136-
func find_further_distance_to(origin):
137-
# Find edges of collision and change to global positions
138-
var edges = []
139-
edges.append(node_area.to_global(Vector3(quad_mesh_size.x / 2, quad_mesh_size.y / 2, 0)))
140-
edges.append(node_area.to_global(Vector3(quad_mesh_size.x / 2, -quad_mesh_size.y / 2, 0)))
141-
edges.append(node_area.to_global(Vector3(-quad_mesh_size.x / 2, quad_mesh_size.y / 2, 0)))
142-
edges.append(node_area.to_global(Vector3(-quad_mesh_size.x / 2, -quad_mesh_size.y / 2, 0)))
143-
144-
# Get the furthest distance between the camera and collision to avoid raycasting too far or too short
145-
var far_dist = 0
146-
var temp_dist
147-
for edge in edges:
148-
temp_dist = origin.distance_to(edge)
149-
if temp_dist > far_dist:
150-
far_dist = temp_dist
151-
152-
return far_dist
108+
# Finally, send the processed input event to the viewport.
109+
node_viewport.push_input(event)
153110

154111

155112
func rotate_area_to_billboard():
156113
var billboard_mode = node_quad.get_surface_override_material(0).params_billboard_mode
157114

158-
# Try to match the area with the material's billboard setting, if enabled
115+
# Try to match the area with the material's billboard setting, if enabled.
159116
if billboard_mode > 0:
160-
# Get the camera
117+
# Get the camera.
161118
var camera = get_viewport().get_camera_3d()
162-
# Look in the same direction as the camera
119+
# Look in the same direction as the camera.
163120
var look = camera.to_global(Vector3(0, 0, -100)) - camera.global_transform.origin
164121
look = node_area.position + look
165122

@@ -169,5 +126,5 @@ func rotate_area_to_billboard():
169126

170127
node_area.look_at(look, Vector3.UP)
171128

172-
# Rotate in the Z axis to compensate camera tilt
129+
# Rotate in the Z axis to compensate camera tilt.
173130
node_area.rotate_object_local(Vector3.BACK, camera.rotation.z)

viewport/gui_in_3d/gui_in_3d.tscn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ shadow_blur = 3.0
6363
omni_range = 10.0
6464

6565
[node name="Camera_Move" type="AnimationPlayer" parent="."]
66-
autoplay = "Move_camera"
6766
libraries = {
6867
"": SubResource("AnimationLibrary_uw4n0")
6968
}
69+
autoplay = "Move_camera"
7070

7171
[node name="Background" type="Node3D" parent="."]
7272

0 commit comments

Comments
 (0)