mirror of
https://github.com/godotengine/godot-demo-projects.git
synced 2026-01-06 16:00:08 +01:00
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.
131 lines
4.8 KiB
GDScript
131 lines
4.8 KiB
GDScript
extends Node3D
|
|
|
|
# Used for checking if the mouse is inside the Area3D.
|
|
var is_mouse_inside = false
|
|
# The last processed input touch/mouse event. To calculate relative movement.
|
|
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
|
|
@onready var node_area = $Quad/Area3D
|
|
|
|
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:
|
|
set_process(false)
|
|
|
|
|
|
func _process(_delta):
|
|
# NOTE: Remove this function if you don't plan on using billboard settings.
|
|
rotate_area_to_billboard()
|
|
|
|
|
|
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
|
|
for mouse_event in [InputEventMouseButton, InputEventMouseMotion, InputEventScreenDrag, InputEventScreenTouch]:
|
|
if is_instance_of(event, mouse_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)
|
|
|
|
|
|
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.
|
|
var quad_mesh_size = node_quad.mesh.size
|
|
|
|
# Event position in Area3D in world coordinate space.
|
|
var event_pos3D = event_position
|
|
|
|
# Current time in seconds since engine start.
|
|
var now: float = Time.get_ticks_msec() / 1000.0
|
|
|
|
# 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()
|
|
|
|
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 = event_pos2D
|
|
if event is InputEventMouse:
|
|
event.global_position = event_pos2D
|
|
|
|
# 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_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 = event_pos2D - last_event_pos2D
|
|
event.velocity = event.relative / (now - last_event_time)
|
|
|
|
# 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
|
|
|
|
# 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.
|
|
if billboard_mode > 0:
|
|
# Get the camera.
|
|
var camera = get_viewport().get_camera_3d()
|
|
# 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
|
|
|
|
# Y-Billboard: Lock Y rotation, but gives bad results if the camera is tilted.
|
|
if billboard_mode == 2:
|
|
look = Vector3(look.x, 0, look.z)
|
|
|
|
node_area.look_at(look, Vector3.UP)
|
|
|
|
# Rotate in the Z axis to compensate camera tilt.
|
|
node_area.rotate_object_local(Vector3.BACK, camera.rotation.z)
|