11extends 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.
64var 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
1814func _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+
3537func _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
155112func 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 )
0 commit comments