gui_3d.gd 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. extends Node3D
  2. # The size of the quad mesh itself.
  3. var quad_mesh_size
  4. # Used for checking if the mouse is inside the Area3D
  5. var is_mouse_inside = false
  6. # Used for checking if the mouse was pressed inside the Area3D
  7. var is_mouse_held = false
  8. # The last non-empty mouse position. Used when dragging outside of the box.
  9. var last_mouse_pos3D = null
  10. # The last processed input touch/mouse event. To calculate relative movement.
  11. var last_mouse_pos2D = null
  12. @onready var node_viewport = $SubViewport
  13. @onready var node_quad = $Quad
  14. @onready var node_area = $Quad/Area3D
  15. func _ready():
  16. node_area.mouse_entered.connect(self._mouse_entered_area)
  17. # If the material is NOT set to use billboard settings, then avoid running billboard specific code
  18. if node_quad.get_surface_override_material(0).billboard_mode == BaseMaterial3D.BillboardMode.BILLBOARD_DISABLED:
  19. set_process(false)
  20. func _process(_delta):
  21. # NOTE: Remove this function if you don't plan on using billboard settings.
  22. rotate_area_to_billboard()
  23. func _mouse_entered_area():
  24. is_mouse_inside = true
  25. func _unhandled_input(event):
  26. # Check if the event is a non-mouse/non-touch event
  27. var is_mouse_event = false
  28. for mouse_event in [InputEventMouseButton, InputEventMouseMotion, InputEventScreenDrag, InputEventScreenTouch]:
  29. if is_instance_of(event, mouse_event):
  30. is_mouse_event = true
  31. break
  32. # If the event is a mouse/touch event and/or the mouse is either held or inside the area, then
  33. # we need to do some additional processing in the handle_mouse function before passing the event to the viewport.
  34. # If the event is not a mouse/touch event, then we can just pass the event directly to the viewport.
  35. if is_mouse_event and (is_mouse_inside or is_mouse_held):
  36. handle_mouse(event)
  37. elif not is_mouse_event:
  38. node_viewport.push_input(event)
  39. # Handle mouse events inside Area3D. (Area3D.input_event had many issues with dragging)
  40. func handle_mouse(event):
  41. # Get mesh size to detect edges and make conversions. This code only support PlaneMesh and QuadMesh.
  42. quad_mesh_size = node_quad.mesh.size
  43. # Detect mouse being held to mantain event while outside of bounds. Avoid orphan clicks
  44. if event is InputEventMouseButton or event is InputEventScreenTouch:
  45. is_mouse_held = event.pressed
  46. # Find mouse position in Area3D
  47. var mouse_pos3D = find_mouse(event.global_position)
  48. # Check if the mouse is outside of bounds, use last position to avoid errors
  49. # NOTE: mouse_exited signal was unrealiable in this situation
  50. is_mouse_inside = mouse_pos3D != null
  51. if is_mouse_inside:
  52. # Convert click_pos from world coordinate space to a coordinate space relative to the Area3D node.
  53. # NOTE: affine_inverse accounts for the Area3D node's scale, rotation, and position in the scene!
  54. mouse_pos3D = node_area.global_transform.affine_inverse() * mouse_pos3D
  55. last_mouse_pos3D = mouse_pos3D
  56. else:
  57. mouse_pos3D = last_mouse_pos3D
  58. if mouse_pos3D == null:
  59. mouse_pos3D = Vector3.ZERO
  60. # TODO: adapt to bilboard mode or avoid completely
  61. # convert the relative event position from 3D to 2D
  62. var mouse_pos2D = Vector2(mouse_pos3D.x, -mouse_pos3D.y)
  63. # Right now the event position's range is the following: (-quad_size/2) -> (quad_size/2)
  64. # We need to convert it into the following range: 0 -> quad_size
  65. mouse_pos2D.x += quad_mesh_size.x / 2
  66. mouse_pos2D.y += quad_mesh_size.y / 2
  67. # Then we need to convert it into the following range: 0 -> 1
  68. mouse_pos2D.x = mouse_pos2D.x / quad_mesh_size.x
  69. mouse_pos2D.y = mouse_pos2D.y / quad_mesh_size.y
  70. # Finally, we convert the position to the following range: 0 -> viewport.size
  71. mouse_pos2D.x = mouse_pos2D.x * node_viewport.size.x
  72. mouse_pos2D.y = mouse_pos2D.y * node_viewport.size.y
  73. # We need to do these conversions so the event's position is in the viewport's coordinate system.
  74. # Set the event's position and global position.
  75. event.position = mouse_pos2D
  76. event.global_position = mouse_pos2D
  77. # If the event is a mouse motion event...
  78. if event is InputEventMouseMotion:
  79. # If there is not a stored previous position, then we'll assume there is no relative motion.
  80. if last_mouse_pos2D == null:
  81. event.relative = Vector2(0, 0)
  82. # If there is a stored previous position, then we'll calculate the relative position by subtracting
  83. # the previous position from the new position. This will give us the distance the event traveled from prev_pos
  84. else:
  85. event.relative = mouse_pos2D - last_mouse_pos2D
  86. # Update last_mouse_pos2D with the position we just calculated.
  87. last_mouse_pos2D = mouse_pos2D
  88. # Finally, send the processed input event to the viewport.
  89. node_viewport.push_input(event)
  90. func find_mouse(global_position):
  91. var camera = get_viewport().get_camera_3d()
  92. var dist = find_further_distance_to(camera.transform.origin)
  93. # From camera center to the mouse position in the Area3D.
  94. var parameters = PhysicsRayQueryParameters3D.new()
  95. parameters.from = camera.project_ray_origin(global_position)
  96. parameters.to = parameters.from + camera.project_ray_normal(global_position) * dist
  97. # Manually raycasts the area to find the mouse position.
  98. parameters.collision_mask = node_area.collision_layer
  99. parameters.collide_with_bodies = false
  100. parameters.collide_with_areas = true
  101. var result = get_world_3d().direct_space_state.intersect_ray(parameters)
  102. if result.size() > 0:
  103. return result.position
  104. else:
  105. return null
  106. func find_further_distance_to(origin):
  107. # Find edges of collision and change to global positions
  108. var edges = []
  109. edges.append(node_area.to_global(Vector3(quad_mesh_size.x / 2, quad_mesh_size.y / 2, 0)))
  110. edges.append(node_area.to_global(Vector3(quad_mesh_size.x / 2, -quad_mesh_size.y / 2, 0)))
  111. edges.append(node_area.to_global(Vector3(-quad_mesh_size.x / 2, quad_mesh_size.y / 2, 0)))
  112. edges.append(node_area.to_global(Vector3(-quad_mesh_size.x / 2, -quad_mesh_size.y / 2, 0)))
  113. # Get the furthest distance between the camera and collision to avoid raycasting too far or too short
  114. var far_dist = 0
  115. var temp_dist
  116. for edge in edges:
  117. temp_dist = origin.distance_to(edge)
  118. if temp_dist > far_dist:
  119. far_dist = temp_dist
  120. return far_dist
  121. func rotate_area_to_billboard():
  122. var billboard_mode = node_quad.get_surface_override_material(0).params_billboard_mode
  123. # Try to match the area with the material's billboard setting, if enabled
  124. if billboard_mode > 0:
  125. # Get the camera
  126. var camera = get_viewport().get_camera_3d()
  127. # Look in the same direction as the camera
  128. var look = camera.to_global(Vector3(0, 0, -100)) - camera.global_transform.origin
  129. look = node_area.position + look
  130. # Y-Billboard: Lock Y rotation, but gives bad results if the camera is tilted.
  131. if billboard_mode == 2:
  132. look = Vector3(look.x, 0, look.z)
  133. node_area.look_at(look, Vector3.UP)
  134. # Rotate in the Z axis to compensate camera tilt
  135. node_area.rotate_object_local(Vector3.BACK, camera.rotation.z)