This commit is contained in:
sharpoff
2026-01-14 13:01:37 +09:00
commit 49d7e22132
660 changed files with 41243 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
class_name Grab
extends Grabber
## Grab Class
##
## This class encodes information about an active grab. Additionally it applies
## hand poses and collision exceptions as appropriate.
## Priority for grip poses
const GRIP_POSE_PRIORITY := 100
## Priority for grip targeting
const GRIP_TARGET_PRIORITY := 100
## Grab target
var what : XRToolsPickable
## Grab point information
var point : XRToolsGrabPoint
## Hand grab point information
var hand_point : XRToolsGrabPointHand
## Grab transform
var transform : Transform3D
## Position drive strength
var drive_position : float = 1.0
## Angle drive strength
var drive_angle : float = 1.0
## Aim drive strength
var drive_aim : float = 0.0
## Has target arrived at grab point
var _arrived : bool = false
## Collision exceptions we manage
var _collision_exceptions : Array[RID]
## Initialize the grab
func _init(
p_grabber : Grabber,
p_what : XRToolsPickable,
p_point : XRToolsGrabPoint,
p_precise : bool) -> void:
# Copy the grabber information
by = p_grabber.by
pickup = p_grabber.pickup
controller = p_grabber.controller
hand = p_grabber.hand
collision_hand = p_grabber.collision_hand
# Set the point
what = p_what
point = p_point
hand_point = p_point as XRToolsGrabPointHand
# Calculate the grab transform
if hand_point:
# Get our adjusted grab point (palm position)
transform = hand_point.get_palm_transform()
elif point:
transform = point.transform
elif p_precise:
transform = p_what.global_transform.affine_inverse() * by.global_transform
else:
transform = Transform3D.IDENTITY
# Set the drive parameters
if hand_point:
drive_position = hand_point.drive_position
drive_angle = hand_point.drive_angle
drive_aim = hand_point.drive_aim
# Apply collision exceptions
if collision_hand:
collision_hand.max_distance_reached.connect(_on_max_distance_reached)
_add_collision_exceptions(what)
## Set the target as arrived at the grab-point
func set_arrived() -> void:
# Ignore if already arrived
if _arrived:
return
# Set arrived and apply any hand pose
print_verbose("%s> arrived at %s" % [what.name, point])
_arrived = true
_set_hand_pose()
# Report the grab
print_verbose("%s> grabbed by %s", [what.name, by.name])
what.grabbed.emit(what, by)
## Set the grab point
func set_grab_point(p_point : XRToolsGrabPoint) -> void:
# Skip if no change
if p_point == point:
return
# Remove any current pose override
_clear_hand_pose()
# Update the grab point
point = p_point
hand_point = point as XRToolsGrabPointHand
# Update the transform
if point:
# Get our adjusted grab point (palm position)
transform = p_point.get_palm_transform()
# Apply the new hand grab-point settings
if hand_point:
drive_position = hand_point.drive_position
drive_angle = hand_point.drive_angle
drive_aim = hand_point.drive_aim
# Apply any pose overrides
if _arrived:
_set_hand_pose()
# Report switch
print_verbose("%s> switched grab point to %s", [what.name, point.name])
what.released.emit(what, by)
what.grabbed.emit(what, by)
## Release the grip
func release() -> void:
# Clear any hand pose
_clear_hand_pose()
# Remove collision exceptions with a small delay
if is_instance_valid(collision_hand) and not _collision_exceptions.is_empty():
# Use RIDs instead of the objects directly in case they get freed while
# we are waiting for the object to fall away
var copy : Array[RID] = _collision_exceptions.duplicate()
_collision_exceptions.clear()
# Delay removing our exceptions to give the object time to fall away
collision_hand.get_tree().create_timer(0.5).timeout \
.connect(_remove_collision_exceptions \
.bind(copy) \
.bind(collision_hand.get_rid()))
# Report the release
print_verbose("%s> released by %s", [what.name, by.name])
what.released.emit(what, by)
# Hand has moved too far away from object, can no longer hold on to it.
func _on_max_distance_reached() -> void:
pickup.drop_object()
# Set hand-pose overrides
func _set_hand_pose() -> void:
# Skip if not hand
if not is_instance_valid(hand) or not is_instance_valid(hand_point):
return
# Apply the hand-pose
if hand_point.hand_pose:
hand.add_pose_override(hand_point, GRIP_POSE_PRIORITY, hand_point.hand_pose)
# Apply hand snapping
if hand_point.snap_hand:
hand.add_target_override(hand_point, GRIP_TARGET_PRIORITY)
# Clear any hand-pose overrides
func _clear_hand_pose() -> void:
# Skip if not hand
if not is_instance_valid(hand) or not is_instance_valid(hand_point):
return
# Remove hand-pose
hand.remove_pose_override(hand_point)
# Remove hand snapping
hand.remove_target_override(hand_point)
# Add collision exceptions for the grabbed object and any of its children
func _add_collision_exceptions(from : Node):
if not is_instance_valid(collision_hand):
return
if not is_instance_valid(from):
return
# If this is a physics body, add an exception
if from is PhysicsBody3D:
# Make sure we don't collide with what we're holding
_collision_exceptions.push_back(from.get_rid())
PhysicsServer3D.body_add_collision_exception(collision_hand.get_rid(), from.get_rid())
PhysicsServer3D.body_add_collision_exception(from.get_rid(), collision_hand.get_rid())
# Check all children
for child in from.get_children():
_add_collision_exceptions(child)
# Remove the exceptions in our passed array. We call this with a small delay
# to give an object a chance to drop away from the hand before it starts
# colliding.
# It is possible that another object is picked up in the meanwhile
# and we thus fill _collision_exceptions with new content.
# Hence using a copy of this list at the time of dropping the object.
#
# Note, this is static because our grab object gets destroyed before this code gets run.
static func _remove_collision_exceptions( \
on_collision_hand : RID, \
exceptions : Array[RID]):
# This can be improved by checking if we're still colliding and only
# removing those objects from our exception list that are not.
# If any are left, we can restart a new timer.
# This will also allow us to use a much smaller timer interval
# For now we'll remove all.
for body : RID in exceptions:
PhysicsServer3D.body_remove_collision_exception(on_collision_hand, body)
PhysicsServer3D.body_remove_collision_exception(body, on_collision_hand)

View File

@@ -0,0 +1 @@
uid://c6pkfifsm2fy1

View File

@@ -0,0 +1,239 @@
class_name XRToolsGrabDriver
extends RemoteTransform3D
## Grab state
enum GrabState {
LERP,
SNAP,
}
## Drive state
var state : GrabState = GrabState.SNAP
## Target pickable
var target : XRToolsPickable
## Primary grab information
var primary : Grab = null
## Secondary grab information
var secondary : Grab = null
## Lerp start position
var lerp_start : Transform3D
## Lerp total duration
var lerp_duration : float = 1.0
## Lerp time
var lerp_time : float = 0.0
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _physics_process(delta : float) -> void:
# Skip if no primary node
if not is_instance_valid(primary):
return
# Set destination from primary grab
var destination := primary.by.global_transform * primary.transform.affine_inverse()
# If present, apply secondary-node contributions
if is_instance_valid(secondary):
# Calculate lerp coefficients based on drive strengths
var position_lerp := _vote(primary.drive_position, secondary.drive_position)
var angle_lerp := _vote(primary.drive_angle, secondary.drive_angle)
# Calculate the transform from secondary grab
var x1 := destination
var x2 := secondary.by.global_transform * secondary.transform.affine_inverse()
# Independently lerp the angle and position
destination = Transform3D(
x1.basis.slerp(x2.basis, angle_lerp),
x1.origin.lerp(x2.origin, position_lerp))
# Test if we need to apply aiming
if secondary.drive_aim > 0.0:
# Convert destination from global to primary-local
destination = primary.by.global_transform.affine_inverse() * destination
# Calculate the from and to vectors in primary-local space
var secondary_from := destination * secondary.transform.origin
var secondary_to := primary.by.to_local(secondary.by.global_position)
# Build shortest arc
secondary_from = secondary_from.normalized()
secondary_to = secondary_to.normalized()
var spherical := Quaternion(secondary_from, secondary_to)
# Build aim-rotation
var rotate := Basis.IDENTITY.slerp(Basis(spherical), secondary.drive_aim)
destination = Transform3D(rotate, Vector3.ZERO) * destination
# Convert destination from primary-local to global
destination = primary.by.global_transform * destination
# Handle update
match state:
GrabState.LERP:
# Progress the lerp
lerp_time += delta
if lerp_time < lerp_duration:
# Interpolate from lerp_start to destination
destination = lerp_start.interpolate_with(
destination,
lerp_time / lerp_duration)
else:
# Lerp completed
state = GrabState.SNAP
_update_weight()
if primary: primary.set_arrived()
if secondary: secondary.set_arrived()
if global_transform.is_equal_approx(destination):
return
# Apply the destination transform
global_transform = destination
force_update_transform()
if is_instance_valid(target):
target.force_update_transform()
## Set the secondary grab point
func add_grab(p_grab : Grab) -> void:
# Set the secondary grab
if p_grab.hand_point and p_grab.hand_point.mode == XRToolsGrabPointHand.Mode.PRIMARY:
print_verbose("%s> new primary grab %s" % [target.name, p_grab.by.name])
secondary = primary
primary = p_grab
else:
print_verbose("%s> new secondary grab %s" % [target.name, p_grab.by.name])
secondary = p_grab
# If snapped then report arrived at the new grab
if state == GrabState.SNAP:
_update_weight()
p_grab.set_arrived()
## Get the grab information for the grab node
func get_grab(by : Node3D) -> Grab:
if primary and primary.by == by:
return primary
if secondary and secondary.by == by:
return secondary
return null
func remove_grab(p_grab : Grab) -> void:
# Remove the appropriate grab
if p_grab == primary:
# Remove primary (secondary promoted)
print_verbose("%s> %s (primary) released" % [target.name, p_grab.by.name])
primary = secondary
secondary = null
elif p_grab == secondary:
# Remove secondary
print_verbose("%s> %s (secondary) released" % [target.name, p_grab.by.name])
secondary = null
if state == GrabState.SNAP:
_update_weight()
# Discard the driver
func discard():
remote_path = NodePath()
queue_free()
# Create the driver to lerp the target from its current location to the
# primary grab-point.
static func create_lerp(
p_target : Node3D,
p_grab : Grab,
p_lerp_speed : float) -> XRToolsGrabDriver:
print_verbose("%s> lerping %s" % [p_target.name, p_grab.by.name])
# Construct the driver lerping from the current position
var driver := XRToolsGrabDriver.new()
driver.name = p_target.name + "_driver"
driver.top_level = true
driver.process_physics_priority = -80
driver.state = GrabState.LERP
driver.target = p_target
driver.primary = p_grab
driver.global_transform = p_target.global_transform
# Calculate the start and duration
var end := p_grab.by.global_transform * p_grab.transform
var delta := end.origin - p_target.global_position
driver.lerp_start = p_target.global_transform
driver.lerp_duration = delta.length() / p_lerp_speed
# Add the driver as a neighbor of the target as RemoteTransform3D nodes
# cannot be descendands of the targets they drive.
p_target.get_parent().add_child(driver)
driver.remote_path = driver.get_path_to(p_target)
# Return the driver
return driver
# Create the driver to instantly snap to the primary grab-point.
static func create_snap(
p_target : Node3D,
p_grab : Grab) -> XRToolsGrabDriver:
print_verbose("%s> snapping to %s" % [p_target.name, p_grab.by.name])
# Construct the driver snapped to the held position
var driver := XRToolsGrabDriver.new()
driver.name = p_target.name + "_driver"
driver.top_level = true
driver.process_physics_priority = -80
driver.state = GrabState.SNAP
driver.target = p_target
driver.primary = p_grab
driver.global_transform = p_grab.by.global_transform * p_grab.transform.affine_inverse()
# Snapped to grab-point so report arrived
p_grab.set_arrived()
# Add the driver as a neighbor of the target as RemoteTransform3D nodes
# cannot be descendands of the targets they drive.
p_target.get_parent().add_child(driver)
driver.remote_path = driver.get_path_to(p_target)
driver._update_weight()
# Return the driver
return driver
# Calculate the lerp voting from a to b
static func _vote(a : float, b : float) -> float:
if a == 0.0 and b == 0.0:
return 0.0
return b / (a + b)
# Update the weight on collision hands
func _update_weight():
if primary:
var weight : float = target.mass
if secondary:
# Each hand carries half the weight
weight = weight / 2.0
if secondary.collision_hand:
secondary.collision_hand.set_held_weight(weight)
if primary.collision_hand:
primary.collision_hand.set_held_weight(weight)

View File

@@ -0,0 +1 @@
uid://blacqnxi11y3x

View File

@@ -0,0 +1,47 @@
class_name XRToolsGrabPoint
extends Marker3D
## XR Tools Grab Point Base Script
##
## This script is the base for all grab points. Pickable object extending from
## [XRToolsPickable] can have numerous grab points to control where the object
## is grabbed from.
# Signal emitted when the user presses the action button while holding this grab point
signal action_pressed(pickable, grab_point)
# Signal emitted when the user releases the action button while holding this grab point
signal action_released(pickable, grab_point)
## If true, the grab point is enabled for grabbing
@export var enabled : bool = true
## Evaluate fitness of the proposed grab, with 0.0 for not allowed.
func can_grab(grabber : Node3D, _current : XRToolsGrabPoint) -> float:
if not enabled:
return 0.0
# Return the distance-weighted fitness
return _weight(grabber)
# Return a distance-weighted fitness weight in the range (0.0 - max]
func _weight(grabber : Node3D, max : float = 1.0) -> float:
var distance := global_position.distance_to(grabber.global_position)
return max / (1.0 + distance)
# action is called when user presses the action button while holding this grab point
func action(pickable : XRToolsPickable):
# let interested parties know
action_pressed.emit(pickable, self)
# action_release is called when user releases the action button while holding this grab point
func action_release(pickable : XRToolsPickable):
# let interested parties know
action_released.emit(pickable, self)

View File

@@ -0,0 +1 @@
uid://b8uwrf51jd5xm

View File

@@ -0,0 +1,236 @@
@tool
class_name XRToolsGrabPointHand
extends XRToolsGrabPoint
## XR Tools Grab Point Hand Script
##
## This script allows specifying a grab point for a specific hand. Additionally
## the grab point can be used to control the pose of the hand, and to allow the
## grab point position to be fine-tuned in the editor.
## Hand for this grab point
enum Hand {
LEFT, ## Left hand
RIGHT, ## Right hand
}
## Grab mode for this grab point
enum Mode {
GENERAL, ## General grab point
PRIMARY, ## Primary-hand grab point
SECONDARY ## Secondary-hand grab point
}
## Hand preview option
enum PreviewMode {
CLOSED, ## Preview hand closed
OPEN, ## Preview hand open
}
## Left hand scene path (for editor preview)
const LEFT_HAND_PATH := "res://addons/godot-xr-tools/hands/scenes/lowpoly/left_hand_low.tscn"
## Right hand scene path (for editor preview)
const RIGHT_HAND_PATH := "res://addons/godot-xr-tools/hands/scenes/lowpoly/right_hand_low.tscn"
## Grab-point handle
@export var handle : String
## Which hand this grab point is for
@export var hand : Hand: set = _set_hand
## Hand grab mode
@export var mode : Mode = Mode.GENERAL
## Snap the hand mesh to the grab-point
@export var snap_hand : bool = true
## Hand pose
@export var hand_pose : XRToolsHandPoseSettings: set = _set_hand_pose
## If true, the hand is shown in the editor
@export var editor_preview_mode : PreviewMode = PreviewMode.CLOSED: set = _set_editor_preview_mode
## How much this grab-point drives the position
@export var drive_position : float = 1.0
## How much this grab-point drives the angle
@export var drive_angle : float = 1.0
## How much this grab-point drives the aim
@export var drive_aim : float = 0.0
## Hand to use for editor preview
var _editor_preview_hand : XRToolsHand
# Adjust the grab point from our old aim positioning, to our palm positioning.
func get_palm_transform(global : bool = false) -> Transform3D:
var adj_transform : Transform3D = global_transform if global else transform
# Historically our hands have always been positioned based on our aim,
# So we apply our old hardcoded offset, but adjusted for our palm center.
var aim_offset := Transform3D()
aim_offset.origin = Vector3(-0.02 if hand == Hand.LEFT else 0.02, -0.05, 0.10)
adj_transform = adj_transform * aim_offset
return adj_transform
## Called when the node enters the scene tree for the first time.
func _ready():
# If in the editor then update the preview
if Engine.is_editor_hint():
_update_editor_preview()
## Test if a grabber can grab by this grab-point
func can_grab(grabber : Node3D, current : XRToolsGrabPoint) -> float:
# Skip if not enabled
if not enabled:
return 0.0
# Verify the hand matches
if not _is_correct_hand(grabber):
return 0.0
# Fail if the hand grab is not permitted
if not _is_valid_hand_grab(current):
return 0.0
# Get the distance-weighted fitness in the range (0.0 - 0.5], but boost
# to [0.5 - 1.0] for valid "specific" grabs.
var fitness := _weight(grabber, 0.5)
if mode != Mode.GENERAL:
fitness += 0.5
# Return the grab fitness
return fitness
func _set_hand(new_value : Hand) -> void:
hand = new_value
if Engine.is_editor_hint():
_update_editor_preview()
func _set_hand_pose(new_value : XRToolsHandPoseSettings) -> void:
# Unsubscribe from the old hand-pose changed signal
if Engine.is_editor_hint() and hand_pose:
hand_pose.changed.disconnect(_update_editor_preview)
# Save the hand pose
hand_pose = new_value
# Update the editor preview
if Engine.is_editor_hint() and hand_pose:
hand_pose.changed.connect(_update_editor_preview)
_update_editor_preview()
func _set_editor_preview_mode(new_value : PreviewMode) -> void:
editor_preview_mode = new_value
if Engine.is_editor_hint():
_update_editor_preview()
func _update_editor_preview() -> void:
# Discard any existing hand model
if _editor_preview_hand:
remove_child(_editor_preview_hand)
_editor_preview_hand.queue_free()
_editor_preview_hand = null
# Pick the hand scene
var hand_path := LEFT_HAND_PATH if hand == Hand.LEFT else RIGHT_HAND_PATH
var hand_scene : PackedScene = load(hand_path)
if !hand_scene:
return
# Construct the model
_editor_preview_hand = hand_scene.instantiate()
_editor_preview_hand.hand_offset_mode = 4 # Disabled
# Set the pose
if hand_pose:
_editor_preview_hand.add_pose_override(self, 0.0, hand_pose)
# Set the grip override
if editor_preview_mode == PreviewMode.CLOSED:
_editor_preview_hand.force_grip_trigger(1.0, 1.0)
else:
_editor_preview_hand.force_grip_trigger(0.0, 0.0)
# Add the editor-preview hand as a child
add_child(_editor_preview_hand, false, Node.INTERNAL_MODE_BACK)
# Keep this backwards compatible,
# position the hand according to the original aim logic
var hand_node : Node3D = _editor_preview_hand.get_child(0)
if hand_node:
var custom_offset := Transform3D()
custom_offset.origin = Vector3(-0.03 if hand == Hand.LEFT else 0.03, -0.05, 0.15)
hand_node.transform = custom_offset
# Is the grabber for the correct hand
func _is_correct_hand(grabber : Node3D) -> bool:
# Find the controller
var controller := _get_grabber_controller(grabber)
if not controller:
return false
# Get the positional tracker
var tracker := XRServer.get_tracker(controller.tracker) as XRPositionalTracker
# If left hand then verify left controller
if hand == Hand.LEFT and tracker.hand != XRPositionalTracker.TRACKER_HAND_LEFT:
return false
# If right hand then verify right controller
if hand == Hand.RIGHT and tracker.hand != XRPositionalTracker.TRACKER_HAND_RIGHT:
return false
# Controller matches hand
return true
# Test if hand grab is permitted
func _is_valid_hand_grab(current : XRToolsGrabPoint) -> bool:
# Not a valid hand grab if currently held by something other than a hand
var current_hand := current as XRToolsGrabPointHand
if current and not current_hand:
return false
# Not valid if grabbing the same named handle
if handle and current_hand and handle == current_hand.handle:
return false
# Not valid if attempting PRIMARY grab while current is PRIMARY
if mode == Mode.PRIMARY and current_hand and current_hand.mode == Mode.PRIMARY:
return false
# Not valid if attempting SECONDARY grab while no current
if mode == Mode.SECONDARY and not current_hand:
return false
# Hand is allowed to grab
return true
# Get the controller associated with a grabber
static func _get_grabber_controller(grabber : Node3D) -> XRController3D:
# Ensure the grabber is valid
if not is_instance_valid(grabber):
return null
# Ensure the pickup is a function pickup for a controller
var pickup := grabber as XRToolsFunctionPickup
if not pickup:
return null
# Get the controller associated with the pickup
return pickup.get_controller()

View File

@@ -0,0 +1 @@
uid://dx3swqg2owe4p

View File

@@ -0,0 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://c25yxb0vt53vc"]
[ext_resource type="Script" uid="uid://dx3swqg2owe4p" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand.gd" id="1"]
[node name="GrabPointHandLeft" type="Marker3D"]
visible = false
script = ExtResource("1")
hand_offset_mode = null

View File

@@ -0,0 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://ctw7nbntd5pcj"]
[ext_resource type="Script" uid="uid://dx3swqg2owe4p" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand.gd" id="1"]
[node name="GrabPointHandRight" type="Marker3D"]
visible = false
script = ExtResource("1")
hand = 1

View File

@@ -0,0 +1,17 @@
@tool
class_name XRToolsGrabPointRedirect
extends XRToolsGrabPoint
## Grab point to redirect grabbing to
@export var target : XRToolsGrabPoint
## Evaluate fitness of the proposed grab, with 0.0 for not allowed.
func can_grab(grabber : Node3D, current : XRToolsGrabPoint) -> float:
# Fail if no target
if not is_instance_valid(target):
return 0.0
# Consult the target
return target.can_grab(grabber, current)

View File

@@ -0,0 +1 @@
uid://cdrjykm7xjl0n

View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://ca3daqmpo0tua"]
[ext_resource type="Script" uid="uid://cdrjykm7xjl0n" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_redirect.gd" id="1_vyuch"]
[node name="GrabPointRedirect" type="Marker3D"]
script = ExtResource("1_vyuch")

View File

@@ -0,0 +1,49 @@
@tool
class_name XRToolsGrabPointSnap
extends XRToolsGrabPoint
## XR Tools Grab Point Snap Script
##
## This script allows specifying a grab point for snap zones. It supports
## group-filters if different points are required for different snap zones.
## Require grab-by to be in the specified group
@export var require_group : String = ""
## Deny grab-by if in the specified group
@export var exclude_group : String = ""
# Called when the node enters the scene tree for the first time.
func _ready():
# Add a Position3D child to help editor visibility
if Engine.is_editor_hint():
add_child(Marker3D.new())
## Evaluate fitness of the proposed grab, with 0.0 for not allowed.
func can_grab(grabber : Node3D, current : XRToolsGrabPoint) -> float:
# Skip if not enabled or current grab
if not enabled or current:
return 0.0
# Ensure the pickup is valid
if not is_instance_valid(grabber):
return 0.0
# Ensure the grabber is a snap-zone
if not grabber is XRToolsSnapZone:
return 0.0
# Refuse if the grabber is not in the required group
if not require_group.is_empty() and not grabber.is_in_group(require_group):
return 0.0
# Refuse if the grabber is in the excluded group
if not exclude_group.is_empty() and grabber.is_in_group(exclude_group):
return 0.0
# Return the distance-weighted fitness
return _weight(grabber)

View File

@@ -0,0 +1 @@
uid://dn7grmvmgjiqv

View File

@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://dh8grd7s3n8kg"]
[ext_resource type="Script" uid="uid://dn7grmvmgjiqv" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_snap.gd" id="1"]
[node name="GrabPointSnap" type="Marker3D"]
visible = false
script = ExtResource("1")

View File

@@ -0,0 +1,32 @@
class_name Grabber
## Grabber Class
##
## This class contains relevant information for a grabber including any
## assocated pickup, controller, and hand nodes.
## Grabber node
var by : Node3D
## Pickup associated with the grabber
var pickup : XRToolsFunctionPickup
## Controller associated with the grabber
var controller : XRController3D
## Hand associated with the grabber
var hand : XRToolsHand
## Collision hand associated with the grabber
var collision_hand : XRToolsCollisionHand
## Initialize the grabber
func _init(p_by : Node3D) -> void:
by = p_by
pickup = p_by as XRToolsFunctionPickup
controller = pickup.get_controller() if pickup else null
if controller:
hand = XRToolsHand.find_instance(controller)
collision_hand = XRToolsCollisionHand.find_instance(controller)

View File

@@ -0,0 +1 @@
uid://bu86qn5rnylas