535 lines
20 KiB
GDScript
535 lines
20 KiB
GDScript
@tool
|
|
extends RefCounted
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Handles keeping track of brush strokes, brush position and some of the brush settings
|
|
# Also notifies others of painting lifecycle updates
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
const FunLib = preload("../utility/fun_lib.gd")
|
|
const DponDebugDraw = preload("../utility/debug_draw.gd")
|
|
const Toolshed_Brush = preload("../toolshed/toolshed_brush.gd")
|
|
const Globals = preload("../utility/globals.gd")
|
|
|
|
|
|
enum ModifierKeyboardKey {KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_TAB}
|
|
enum BrushPrimaryKeyboardKey {MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_XBUTTON1, MOUSE_BUTTON_XBUTTON2}
|
|
enum BrushPropEditFlag {MODIFIER, NONE, SIZE, STRENGTH}
|
|
|
|
|
|
var owned_spatial:Node3D = null
|
|
# Used for immediate updates when changes happen to the brush
|
|
# This should NOT be used in update() or each frame in general
|
|
var _cached_camera: Camera3D = null
|
|
|
|
const sphere_brush_material = preload("../shaders/shm_sphere_brush.tres")
|
|
const circle_brush_material = preload("../shaders/shm_circle_brush.tres")
|
|
var paint_brush_node:MeshInstance3D = null
|
|
var detached_paint_brush_container:Node = null
|
|
|
|
# Temporary variables to store current quick prop edit state
|
|
var brush_prop_edit_flag = BrushPropEditFlag.NONE
|
|
const brush_prop_edit_max_dist:float = 500.0
|
|
var brush_prop_edit_max_val:float = 0.0
|
|
var brush_prop_edit_cur_val:float = 0.0
|
|
var brush_prop_edit_start_pos:Vector2 = Vector2.ZERO
|
|
var brush_prop_edit_offset:float = 0.0
|
|
|
|
var can_draw:bool = false
|
|
var is_drawing:bool = false
|
|
var pending_movement_update:bool = false
|
|
var brush_collision_mask:int : set = set_brush_collision_mask
|
|
|
|
# Used to pass during stroke-state signals sent to Gardener/Arborist
|
|
# Meant to avoid retrieving transform from an actual 3D node
|
|
# And more importantly to cache a raycast normal at every given point in time
|
|
var active_brush_data:Dictionary = {'brush_pos': Vector3.ZERO, 'brush_normal': Vector3.UP, 'brush_basis': Basis()}
|
|
|
|
# Variables to sync quick brush property edit with UI and vice-versa
|
|
# And also for keeping brush state up-to-date without needing a reference to actual active brush
|
|
var active_brush_overlap_mode: int = Toolshed_Brush.OverlapMode.VOLUME
|
|
var active_brush_size:float : set = set_active_brush_size
|
|
var active_brush_strength:float : set = set_active_brush_strength
|
|
var active_brush_max_size:float : set = set_active_brush_max_size
|
|
var active_brush_max_strength:float : set = set_active_brush_max_strength
|
|
|
|
# A queue of methods to be called once _cached_camera becomes available
|
|
var when_camera_queue: Array = []
|
|
|
|
# Ooooh boy
|
|
# Go to finish_brush_prop_edit() for explanation
|
|
var mouse_move_call_delay: int = 0
|
|
|
|
|
|
signal changed_active_brush_prop(prop, val, final)
|
|
|
|
signal stroke_started
|
|
signal stroke_finished
|
|
signal stroke_updated
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Lifecycle
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
# When working with this object, we assume it does not exist outside the editor
|
|
func _init(_owned_spatial):
|
|
set_meta("class", "Painter")
|
|
|
|
owned_spatial = _owned_spatial
|
|
#
|
|
paint_brush_node = MeshInstance3D.new()
|
|
paint_brush_node.name = "active_brush"
|
|
set_brush_mesh()
|
|
|
|
#owned_spatial.add_child(paint_brush_node)
|
|
detached_paint_brush_container = Node.new()
|
|
owned_spatial.add_child(detached_paint_brush_container)
|
|
detached_paint_brush_container.add_child(paint_brush_node)
|
|
set_can_draw(false)
|
|
|
|
|
|
func update(delta):
|
|
if _cached_camera:
|
|
# Handle queue of methods that need a _cached_camera
|
|
for queue_item in when_camera_queue.duplicate():
|
|
callv(queue_item.method_name, queue_item.args)
|
|
when_camera_queue.erase(queue_item)
|
|
consume_brush_drawing_update(delta)
|
|
|
|
|
|
func set_brush_mesh(is_sphere: bool = false):
|
|
if is_sphere:
|
|
paint_brush_node.mesh = SphereMesh.new()
|
|
paint_brush_node.mesh.radial_segments = 32
|
|
paint_brush_node.mesh.rings = 16
|
|
paint_brush_node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
paint_brush_node.material_override = sphere_brush_material.duplicate()
|
|
else:
|
|
paint_brush_node.mesh = QuadMesh.new()
|
|
paint_brush_node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
paint_brush_node.material_override = circle_brush_material.duplicate()
|
|
|
|
|
|
# Queue a call to method that needs a _cached_camera to be set
|
|
func queue_call_when_camera(method_name: String, args: Array = []):
|
|
when_camera_queue.append({'method_name': method_name, 'args': args})
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Editing lifecycle
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
func start_editing():
|
|
set_can_draw(true)
|
|
|
|
|
|
func stop_editing():
|
|
stop_brush_stroke()
|
|
set_can_draw(false)
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Input
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
func forwarded_input(camera:Camera3D, event):
|
|
if !can_draw: return
|
|
|
|
_cached_camera = camera
|
|
var handled = false
|
|
|
|
# If inactive property edit
|
|
# And event == mouseMotion
|
|
# -> move the brush
|
|
if brush_prop_edit_flag <= BrushPropEditFlag.NONE:
|
|
if (is_instance_of(event, InputEventMouseMotion)
|
|
|| (is_instance_of(event, InputEventMouseButton) && event.button_index == MOUSE_BUTTON_WHEEL_UP)
|
|
|| (is_instance_of(event, InputEventMouseButton) && event.button_index == MOUSE_BUTTON_WHEEL_DOWN)):
|
|
|
|
if mouse_move_call_delay > 0:
|
|
mouse_move_call_delay -= 1
|
|
else:
|
|
move_brush()
|
|
pending_movement_update = true
|
|
# Don't handle input - moving a brush is not destructive
|
|
|
|
# If inactive property edit
|
|
# And event == overlap mode key
|
|
# -> cycle overlap modes
|
|
if brush_prop_edit_flag <= BrushPropEditFlag.NONE && is_instance_of(event, InputEventKey) && event.keycode == get_overlap_mode_key():
|
|
if event.pressed && !event.is_echo():
|
|
cycle_overlap_modes()
|
|
handled = true
|
|
|
|
# If inactive property edit/modifier key pressed
|
|
# And event == modifier key pressed
|
|
# -> remember/forget the modifier
|
|
if brush_prop_edit_flag <= BrushPropEditFlag.NONE && is_instance_of(event, InputEventKey) && event.keycode == get_property_edit_modifier():
|
|
if event.pressed:
|
|
brush_prop_edit_flag = BrushPropEditFlag.MODIFIER
|
|
if !event.pressed:
|
|
brush_prop_edit_flag = BrushPropEditFlag.NONE
|
|
handled = true
|
|
|
|
# If inactive property edit or modifier key pressed
|
|
# And event == property edit trigger pressed
|
|
# -> start property edit
|
|
if brush_prop_edit_flag <= BrushPropEditFlag.NONE && is_instance_of(event, InputEventMouseButton) && event.button_index == get_property_edit_button():
|
|
if event.pressed:
|
|
brush_prop_edit_flag = BrushPropEditFlag.SIZE if brush_prop_edit_flag != BrushPropEditFlag.MODIFIER else BrushPropEditFlag.STRENGTH
|
|
start_brush_prop_edit(event.global_position)
|
|
handled = true
|
|
|
|
# If editing property
|
|
# And event == property edit trigger released
|
|
# -> stop property edit
|
|
if brush_prop_edit_flag > BrushPropEditFlag.NONE && is_instance_of(event, InputEventMouseButton) && event.button_index == get_property_edit_button():
|
|
if !event.pressed:
|
|
finish_brush_prop_edit(camera)
|
|
brush_prop_edit_flag = BrushPropEditFlag.NONE
|
|
handled = true
|
|
|
|
# If editing property
|
|
# And event == mouseMotion
|
|
# -> update property value
|
|
if brush_prop_edit_flag > BrushPropEditFlag.NONE && is_instance_of(event, InputEventMouseMotion):
|
|
brush_prop_edit_calc_val(event.global_position)
|
|
handled = true
|
|
|
|
# If editing property
|
|
# And event == paint trigger pressed/releasedq
|
|
# -> start/stop the brush stroke
|
|
if brush_prop_edit_flag == BrushPropEditFlag.NONE && is_instance_of(event, InputEventMouseButton) && event.button_index == MOUSE_BUTTON_LEFT:
|
|
if event.pressed:
|
|
move_brush()
|
|
start_brush_stroke()
|
|
else:
|
|
stop_brush_stroke()
|
|
handled = true
|
|
|
|
return handled
|
|
|
|
|
|
func get_property_edit_modifier():
|
|
# This convolution exists because a project setting with default value is not saved for some reason and load as "null"
|
|
# See https://github.com/godotengine/godot/issues/56598
|
|
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_prop_edit_modifier", Globals.KeyboardKey.KEY_SHIFT)
|
|
return Globals.index_to_enum(key, Globals.KeyboardKey)
|
|
|
|
|
|
func get_property_edit_button():
|
|
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_prop_edit_button", Globals.MouseButton.MOUSE_BUTTON_XBUTTON1)
|
|
return Globals.index_to_enum(key, Globals.MouseButton)
|
|
|
|
|
|
func get_overlap_mode_key():
|
|
var key = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_overlap_mode_button", Globals.KeyboardKey.KEY_QUOTELEFT)
|
|
return Globals.index_to_enum(key, Globals.KeyboardKey)
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Painting lifecycle
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
func set_can_draw(state):
|
|
can_draw = state
|
|
if state:
|
|
paint_brush_node.visible = true
|
|
else:
|
|
paint_brush_node.visible = false
|
|
|
|
|
|
func start_brush_stroke():
|
|
if is_drawing: return
|
|
is_drawing = true
|
|
stroke_started.emit(active_brush_data)
|
|
|
|
|
|
func stop_brush_stroke():
|
|
if !is_drawing: return
|
|
is_drawing = false
|
|
active_brush_data = {'brush_pos': Vector3.ZERO, 'brush_normal': Vector3.UP, 'brush_basis': Basis()}
|
|
stroke_finished.emit(active_brush_data)
|
|
|
|
|
|
# Actually update the stroke only if it was preceeded by the input event
|
|
func consume_brush_drawing_update(delta):
|
|
if !can_draw: return
|
|
if !is_drawing: return
|
|
if !pending_movement_update: return
|
|
|
|
pending_movement_update = false
|
|
stroke_updated.emit(active_brush_data)
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Brush movement
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
func move_brush():
|
|
if !_cached_camera: return
|
|
update_active_brush_data()
|
|
refresh_brush_transform()
|
|
|
|
|
|
# Update brush data that is passed through signals to Gardener/Arborist
|
|
# Raycast overrides exist for compatability with gardener tests
|
|
func update_active_brush_data(raycast_overrides: Dictionary = {}):
|
|
var space_state = paint_brush_node.get_world_3d().direct_space_state
|
|
var start = project_mouse_near() if !raycast_overrides.has('start') else raycast_overrides.start
|
|
var end = project_mouse_far() if !raycast_overrides.has('end') else raycast_overrides.end
|
|
|
|
var params = PhysicsRayQueryParameters3D.create(start, end, brush_collision_mask, [])
|
|
var ray_result:Dictionary = space_state.intersect_ray(params)
|
|
|
|
if !ray_result.is_empty():
|
|
active_brush_data.brush_pos = ray_result.position
|
|
active_brush_data.brush_normal = ray_result.normal
|
|
else:
|
|
# If raycast failed - align to camera plane, retaining current distance to camera
|
|
var camera_normal = -_cached_camera.global_transform.basis.z
|
|
var planar_dist_to_camera = (active_brush_data.brush_pos - _cached_camera.global_transform.origin).dot(camera_normal)
|
|
var brush_pos:Vector3 = project_mouse(planar_dist_to_camera)
|
|
active_brush_data.brush_pos = brush_pos
|
|
|
|
# It's possible we don't have _cached_camera defined here since
|
|
# Gardener tests might call update_active_brush_data() without setting it
|
|
if _cached_camera:
|
|
# Cache to use with Projection brush
|
|
active_brush_data.brush_basis = _cached_camera.global_transform.basis
|
|
|
|
|
|
# Update transform of a paint brush 3D node
|
|
func refresh_brush_transform():
|
|
if active_brush_data.is_empty(): return
|
|
|
|
match active_brush_overlap_mode:
|
|
Toolshed_Brush.OverlapMode.VOLUME:
|
|
paint_brush_node.global_transform.origin = active_brush_data.brush_pos
|
|
paint_brush_node.global_transform.basis = Basis()
|
|
Toolshed_Brush.OverlapMode.PROJECTION:
|
|
paint_brush_node.global_transform.origin = active_brush_data.brush_pos
|
|
paint_brush_node.global_transform.basis = active_brush_data.brush_basis
|
|
# Projection brush size is in viewport-space, but it will move forward and backward
|
|
# Thus appearing smaller or bigger
|
|
# So we need to update it's size to keep it consistent
|
|
set_brush_diameter(active_brush_size)
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Brush quick property edit lifecycle
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
# Quickly edit a brush property without using the UI (aka like in Blender)
|
|
# The flow here is as follows:
|
|
# 1. Respond to mouse events, calculate property value, emit a signal
|
|
# 2. Signal is received in the Gardener, passed to an active Toolshed_Brush
|
|
# 3. Active brush updates it's values
|
|
# 4. Toolshed notifies Painter of a value change
|
|
# 5. Painter updates it's helper variables and visual representation
|
|
|
|
# Switching between Volume/Projection brush is here too, but it's not connected to the whole Blender-like process
|
|
# It's just a hotkey handling
|
|
|
|
# Set the initial value of edited property and mouse offset
|
|
func start_brush_prop_edit(mouse_pos):
|
|
match brush_prop_edit_flag:
|
|
BrushPropEditFlag.SIZE:
|
|
brush_prop_edit_cur_val = active_brush_size
|
|
brush_prop_edit_max_val = active_brush_max_size
|
|
BrushPropEditFlag.STRENGTH:
|
|
brush_prop_edit_cur_val = active_brush_strength
|
|
brush_prop_edit_max_val = active_brush_max_strength
|
|
|
|
brush_prop_edit_start_pos = mouse_pos
|
|
brush_prop_edit_offset = brush_prop_edit_cur_val / brush_prop_edit_max_val * brush_prop_edit_max_dist
|
|
|
|
|
|
# Calculate edited property value based on mouse offset
|
|
func brush_prop_edit_calc_val(mouse_pos):
|
|
brush_prop_edit_cur_val = clamp((mouse_pos.x - brush_prop_edit_start_pos.x + brush_prop_edit_offset) / brush_prop_edit_max_dist, 0.0, 1.0) * brush_prop_edit_max_val
|
|
|
|
match active_brush_overlap_mode:
|
|
Toolshed_Brush.OverlapMode.VOLUME:
|
|
match brush_prop_edit_flag:
|
|
BrushPropEditFlag.SIZE:
|
|
changed_active_brush_prop.emit("shape/shape_volume_size", brush_prop_edit_cur_val, false)
|
|
BrushPropEditFlag.STRENGTH:
|
|
changed_active_brush_prop.emit("behavior/behavior_strength", brush_prop_edit_cur_val, false)
|
|
Toolshed_Brush.OverlapMode.PROJECTION:
|
|
match brush_prop_edit_flag:
|
|
BrushPropEditFlag.SIZE:
|
|
changed_active_brush_prop.emit("shape/shape_projection_size", brush_prop_edit_cur_val, false)
|
|
|
|
|
|
# Stop editing brush property and reset helper variables and mouse position
|
|
func finish_brush_prop_edit(camera:Camera3D):
|
|
match active_brush_overlap_mode:
|
|
Toolshed_Brush.OverlapMode.VOLUME:
|
|
match brush_prop_edit_flag:
|
|
BrushPropEditFlag.SIZE:
|
|
changed_active_brush_prop.emit("shape/shape_volume_size", brush_prop_edit_cur_val, true)
|
|
BrushPropEditFlag.STRENGTH:
|
|
changed_active_brush_prop.emit("behavior/behavior_strength", brush_prop_edit_cur_val, true)
|
|
Toolshed_Brush.OverlapMode.PROJECTION:
|
|
match brush_prop_edit_flag:
|
|
BrushPropEditFlag.SIZE:
|
|
changed_active_brush_prop.emit("shape/shape_projection_size", brush_prop_edit_cur_val, true)
|
|
|
|
Input.warp_mouse(brush_prop_edit_start_pos)
|
|
|
|
brush_prop_edit_flag = BrushPropEditFlag.NONE
|
|
brush_prop_edit_start_pos = Vector2.ZERO
|
|
brush_prop_edit_max_val = 0.0
|
|
brush_prop_edit_cur_val = 0.0
|
|
|
|
# Apparently warp_mouse() sometimes takes a few mouse motion events to actually take place
|
|
# Sometimes it's instant, sometimes it takes 1, and sometimes 2 events (at least on my machine)
|
|
# This leads to brush jumping to position used in prop edit and then back. Like it's on a string
|
|
# As an workaround, we delay processing motion input for 2 events (which should be enough for 99% of cases?)
|
|
mouse_move_call_delay = 2
|
|
|
|
|
|
# Cycle between brush overlap modes on a button press
|
|
func cycle_overlap_modes():
|
|
active_brush_overlap_mode += 1
|
|
if active_brush_overlap_mode > Toolshed_Brush.OverlapMode.PROJECTION:
|
|
active_brush_overlap_mode = Toolshed_Brush.OverlapMode.VOLUME
|
|
changed_active_brush_prop.emit("behavior/behavior_overlap_mode", active_brush_overlap_mode, true)
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Setters for brush parameters meant to be accessed from outside
|
|
# In response to UI inputs
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
func update_all_props_to_active_brush(brush: Toolshed_Brush):
|
|
var max_size = 1.0
|
|
var max_strength = 1.0
|
|
var curr_size = 1.0
|
|
var curr_strength = brush.behavior_strength
|
|
|
|
match brush.behavior_overlap_mode:
|
|
Toolshed_Brush.OverlapMode.VOLUME:
|
|
max_size = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_volume_size_slider_max_value", 100.0)
|
|
curr_size = brush.shape_volume_size
|
|
Toolshed_Brush.OverlapMode.PROJECTION:
|
|
max_size = FunLib.get_setting_safe("dreadpons_spatial_gardener/input_and_ui/brush_projection_size_slider_max_value", 1000.0)
|
|
curr_size = brush.shape_projection_size
|
|
|
|
set_active_brush_overlap_mode(brush.behavior_overlap_mode)
|
|
set_active_brush_max_size(max_size)
|
|
set_active_brush_max_strength(max_strength)
|
|
set_active_brush_size(curr_size)
|
|
set_active_brush_strength(curr_strength)
|
|
|
|
|
|
# Update helper variables and visuals
|
|
func set_active_brush_size(val):
|
|
active_brush_size = val
|
|
paint_brush_node.material_override.set_shader_parameter("proximity_multiplier", active_brush_size * 0.5)
|
|
queue_call_when_camera('set_brush_diameter', [active_brush_size])
|
|
|
|
|
|
# Update helper variables and visuals
|
|
func set_active_brush_max_size(val):
|
|
active_brush_max_size = val
|
|
queue_call_when_camera('set_brush_diameter', [active_brush_size])
|
|
|
|
|
|
# Update helper variables
|
|
func set_active_brush_strength(val):
|
|
active_brush_strength = val
|
|
|
|
|
|
# Update helper variables
|
|
func set_active_brush_max_strength(val):
|
|
active_brush_max_strength = val
|
|
|
|
|
|
# Update visuals
|
|
func set_brush_diameter(diameter: float):
|
|
match active_brush_overlap_mode:
|
|
|
|
Toolshed_Brush.OverlapMode.VOLUME:
|
|
paint_brush_node.mesh.radius = diameter * 0.5
|
|
paint_brush_node.mesh.height = diameter
|
|
|
|
Toolshed_Brush.OverlapMode.PROJECTION:
|
|
var camera_normal = -_cached_camera.global_transform.basis.z
|
|
var planar_dist_to_camera = (active_brush_data.brush_pos - _cached_camera.global_transform.origin).dot(camera_normal)
|
|
var circle_center:Vector3 = active_brush_data.brush_pos
|
|
var circle_edge:Vector3
|
|
# If we're editing props (or just finished it as indicated by 'mouse_move_call_delay')
|
|
# Then to prevent size doubling/overflow use out brush position as mouse position
|
|
# (Since out mouse WILL be offset due to us dragging it to the side)
|
|
if brush_prop_edit_flag > BrushPropEditFlag.NONE || mouse_move_call_delay > 0:
|
|
var screen_space_brush_pos = _cached_camera.unproject_position(active_brush_data.brush_pos)
|
|
circle_edge = _cached_camera.project_position(screen_space_brush_pos + Vector2(diameter * 0.5, 0), planar_dist_to_camera)
|
|
else:
|
|
circle_edge = project_mouse(planar_dist_to_camera, Vector2(diameter * 0.5, 0))
|
|
var size = (circle_edge - circle_center).length()
|
|
paint_brush_node.mesh.size = Vector2(size, size) * 2.0
|
|
|
|
|
|
func set_brush_collision_mask(val):
|
|
brush_collision_mask = val
|
|
|
|
|
|
# Update helper variables and visuals
|
|
func set_active_brush_overlap_mode(val):
|
|
active_brush_overlap_mode = val
|
|
|
|
match active_brush_overlap_mode:
|
|
Toolshed_Brush.OverlapMode.VOLUME:
|
|
set_brush_mesh(true)
|
|
Toolshed_Brush.OverlapMode.PROJECTION:
|
|
set_brush_mesh(false)
|
|
|
|
# Since we are rebuilding the mesh here
|
|
# It means that we need to move it in a proper position as well
|
|
move_brush()
|
|
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
# Camera3D/raycasting methods
|
|
#-------------------------------------------------------------------------------
|
|
|
|
|
|
func project_mouse_near() -> Vector3:
|
|
return project_mouse(_cached_camera.near)
|
|
|
|
|
|
func project_mouse_far() -> Vector3:
|
|
return project_mouse(_cached_camera.far - 0.1)
|
|
|
|
|
|
func project_mouse(distance: float, offset: Vector2 = Vector2.ZERO) -> Vector3:
|
|
return _cached_camera.project_position(_cached_camera.get_viewport().get_mouse_position() + offset, distance)
|