gui_3d.gd 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. extends Node3D
  2. # Used for checking if the mouse is inside the Area3D.
  3. var is_mouse_inside = false
  4. # The last processed input touch/mouse event. To calculate relative movement.
  5. var last_event_pos2D = null
  6. # The time of the last event in seconds since engine start.
  7. var last_event_time: float = -1.0
  8. @onready var node_viewport = $SubViewport
  9. @onready var node_quad = $Quad
  10. @onready var node_area = $Quad/Area3D
  11. func _ready():
  12. node_area.mouse_entered.connect(self._mouse_entered_area)
  13. node_area.mouse_exited.connect(self._mouse_exited_area)
  14. node_area.input_event.connect(self._mouse_input_event)
  15. # If the material is NOT set to use billboard settings, then avoid running billboard specific code
  16. if node_quad.get_surface_override_material(0).billboard_mode == BaseMaterial3D.BillboardMode.BILLBOARD_DISABLED:
  17. set_process(false)
  18. func _process(_delta):
  19. # NOTE: Remove this function if you don't plan on using billboard settings.
  20. rotate_area_to_billboard()
  21. func _mouse_entered_area():
  22. is_mouse_inside = true
  23. func _mouse_exited_area():
  24. is_mouse_inside = false
  25. func _unhandled_input(event):
  26. # Check if the event is a non-mouse/non-touch event
  27. for mouse_event in [InputEventMouseButton, InputEventMouseMotion, InputEventScreenDrag, InputEventScreenTouch]:
  28. if is_instance_of(event, mouse_event):
  29. # If the event is a mouse/touch event, then we can ignore it here, because it will be
  30. # handled via Physics Picking.
  31. return
  32. node_viewport.push_input(event)
  33. func _mouse_input_event(_camera: Camera3D, event: InputEvent, event_position: Vector3, _normal: Vector3, _shape_idx: int):
  34. # Get mesh size to detect edges and make conversions. This code only support PlaneMesh and QuadMesh.
  35. var quad_mesh_size = node_quad.mesh.size
  36. # Event position in Area3D in world coordinate space.
  37. var event_pos3D = event_position
  38. # Current time in seconds since engine start.
  39. var now: float = Time.get_ticks_msec() / 1000.0
  40. # Convert position to a coordinate space relative to the Area3D node.
  41. # NOTE: affine_inverse accounts for the Area3D node's scale, rotation, and position in the scene!
  42. event_pos3D = node_quad.global_transform.affine_inverse() * event_pos3D
  43. # TODO: Adapt to bilboard mode or avoid completely.
  44. var event_pos2D: Vector2 = Vector2()
  45. if is_mouse_inside:
  46. # Convert the relative event position from 3D to 2D.
  47. event_pos2D = Vector2(event_pos3D.x, -event_pos3D.y)
  48. # Right now the event position's range is the following: (-quad_size/2) -> (quad_size/2)
  49. # We need to convert it into the following range: -0.5 -> 0.5
  50. event_pos2D.x = event_pos2D.x / quad_mesh_size.x
  51. event_pos2D.y = event_pos2D.y / quad_mesh_size.y
  52. # Then we need to convert it into the following range: 0 -> 1
  53. event_pos2D.x += 0.5
  54. event_pos2D.y += 0.5
  55. # Finally, we convert the position to the following range: 0 -> viewport.size
  56. event_pos2D.x *= node_viewport.size.x
  57. event_pos2D.y *= node_viewport.size.y
  58. # We need to do these conversions so the event's position is in the viewport's coordinate system.
  59. elif last_event_pos2D != null:
  60. # Fall back to the last known event position.
  61. event_pos2D = last_event_pos2D
  62. # Set the event's position and global position.
  63. event.position = event_pos2D
  64. if event is InputEventMouse:
  65. event.global_position = event_pos2D
  66. # Calculate the relative event distance.
  67. if event is InputEventMouseMotion or event is InputEventScreenDrag:
  68. # If there is not a stored previous position, then we'll assume there is no relative motion.
  69. if last_event_pos2D == null:
  70. event.relative = Vector2(0, 0)
  71. # If there is a stored previous position, then we'll calculate the relative position by subtracting
  72. # the previous position from the new position. This will give us the distance the event traveled from prev_pos.
  73. else:
  74. event.relative = event_pos2D - last_event_pos2D
  75. event.velocity = event.relative / (now - last_event_time)
  76. # Update last_event_pos2D with the position we just calculated.
  77. last_event_pos2D = event_pos2D
  78. # Update last_event_time to current time.
  79. last_event_time = now
  80. # Finally, send the processed input event to the viewport.
  81. node_viewport.push_input(event)
  82. func rotate_area_to_billboard():
  83. var billboard_mode = node_quad.get_surface_override_material(0).params_billboard_mode
  84. # Try to match the area with the material's billboard setting, if enabled.
  85. if billboard_mode > 0:
  86. # Get the camera.
  87. var camera = get_viewport().get_camera_3d()
  88. # Look in the same direction as the camera.
  89. var look = camera.to_global(Vector3(0, 0, -100)) - camera.global_transform.origin
  90. look = node_area.position + look
  91. # Y-Billboard: Lock Y rotation, but gives bad results if the camera is tilted.
  92. if billboard_mode == 2:
  93. look = Vector3(look.x, 0, look.z)
  94. node_area.look_at(look, Vector3.UP)
  95. # Rotate in the Z axis to compensate camera tilt.
  96. node_area.rotate_object_local(Vector3.BACK, camera.rotation.z)