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,109 @@
@tool
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
class_name XRToolsClimbable
extends Node3D
## XR Tools Climbable Object
##
## This script adds climbing support to any [StaticBody3D].
##
## For climbing to work, the player must have an [XRToolsMovementClimb] node
## configured appropriately.
## If true, the grip control must be held to keep holding the climbable
var press_to_hold : bool = true
## Array of permanent grab points.
var _grab_points : Array[XRToolsGrabPoint] = []
## Dictionary of temporary grabs keyed by the pickup node
var _grab_temps : Dictionary = {}
## Dictionary of active grabs keyed by the pickup node
var _grabs : Dictionary = {}
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsClimbable"
# Called when the node becomes "ready"
func _ready() -> void:
# Get all grab points
for child in get_children():
var grab_point := child as XRToolsGrabPoint
if grab_point:
_grab_points.push_back(grab_point)
# Called by XRToolsFunctionPickup
func is_picked_up() -> bool:
return false
func can_pick_up(_by: Node3D) -> bool:
return true
# Called by XRToolsFunctionPickup when user presses the action button while holding this object
func action():
pass
# Ignore highlighting requests from XRToolsFunctionPickup
func request_highlight(_from, _on) -> void:
pass
# Called by XRToolsFunctionPickup when this is picked up by a controller
func pick_up(by: Node3D) -> void:
# Get the best permanent grab-point
var point := _get_grab_point(by)
if not point:
# Get a temporary grab-point for the pickup
point = _grab_temps.get(by)
if not point:
# Create a new temporary grab-point childed to the climbable
point = Node3D.new()
add_child(point)
_grab_temps[by] = point
# Set the temporary to the current positon
point.global_transform = by.global_transform
# Save the grab
_grabs[by] = point
# Called by XRToolsFunctionPickup when this is let go by a controller
func let_go(by: Node3D, _p_linear_velocity: Vector3, _p_angular_velocity: Vector3) -> void:
_grabs.erase(by)
# Get the grab handle
func get_grab_handle(by: Node3D) -> Node3D:
return _grabs.get(by)
## Find the most suitable grab-point for the grabber
func _get_grab_point(by : Node3D) -> Node3D:
# Find the best grab-point
var fitness := 0.0
var point : XRToolsGrabPoint = null
for p in _grab_points:
var f := p.can_grab(by, null)
if f > fitness:
fitness = f
point = p
# Resolve redirection
while point is XRToolsGrabPointRedirect:
point = point.target
# Return the best grab point
print_verbose("%s> picked grab-point %s" % [name, point])
return point

View File

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

View File

@@ -0,0 +1,10 @@
[gd_scene load_steps=2 format=3 uid="uid://cjyilbm4ucc7s"]
[ext_resource type="Script" uid="uid://bqb3cy0ekk685" path="res://addons/godot-xr-tools/objects/climbable.gd" id="1"]
[node name="Climbable" type="StaticBody3D"]
collision_layer = 262145
collision_mask = 0
script = ExtResource("1")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]

View File

@@ -0,0 +1,187 @@
@tool
class_name XRToolsForceBody
extends AnimatableBody3D
## XRTools Force Body script
##
## This script enhances AnimatableBody3D with move_and_slide and the ability
## to push bodies by emparting forces on them.
## Force Body Collision
class ForceBodyCollision:
## Collider object
var collider : Node3D
## Collision point
var position : Vector3
## Collision normal
var normal : Vector3
## Enables or disables pushing bodies
@export var push_bodies : bool = true
## Control the stiffness of the body
@export var stiffness : float = 10.0
## Control the maximum push force
@export var maximum_force : float = 1.0
## Maximum slides
@export var max_slides : int = 4
## Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsForceBody"
## This function moves and slides along the [param move] vector. It returns
## information about the last collision, or null if no collision
func move_and_slide(move : Vector3) -> ForceBodyCollision:
# Make sure this is off or weird shit happens...
sync_to_physics = false
# Loop performing the movement steps
var step_move := move
var ret : ForceBodyCollision = null
for step in max_slides:
# Take the next step
var collision := move_and_collide(step_move)
# If we didn't collide with anything then we have finished the entire
# move_and_slide operation
if not collision:
break
# Save relevant collision information
var collider := collision.get_collider()
var postion := collision.get_position()
var normal := collision.get_normal()
# Save the collision information
if not ret:
ret = ForceBodyCollision.new()
ret.collider = collider
ret.position = postion
ret.normal = normal
# Calculate the next move
var next_move := collision.get_remainder().slide(normal)
# Handle pushing bodies
if push_bodies:
var body := collider as RigidBody3D
if body:
# Calculate the momentum lost by the collision
var lost_momentum := step_move - next_move
# TODO: We should consider the velocity of the body such that
# we never push it away faster than our own velocity.
# Apply the lost momentum as an impulse to the body we hit
body.apply_impulse(
(lost_momentum * stiffness).limit_length(maximum_force),
position - body.global_position)
# Update the remaining movement
step_move = next_move
# Prevent bouncing back along movement path
if next_move.dot(move) <= 0:
break
# Return the last collision data
return ret
## Attempts to rotate our object until it collides
func rotate_and_collide( \
target_global_basis : Basis, \
step_angle : float = deg_to_rad(5.0) \
) -> ForceBodyCollision:
# Make sure this is off or weird shit happens...
sync_to_physics = false
var ret : ForceBodyCollision = null
var space = PhysicsServer3D.body_get_space(get_rid())
var direct_state = PhysicsServer3D.space_get_direct_state(space)
# We don't seem to have a rotational movement query for collisions,
# so best we can do is to rotate in steps and test
var from_quat : Quaternion = Quaternion(global_basis)
var to_quat : Quaternion = Quaternion(target_global_basis)
var angle : float = from_quat.angle_to(to_quat)
var steps : float = ceil(angle / step_angle)
# Convert collision exceptions to a RID array
var exception_rids : Array[RID]
for collision_exception in get_collision_exceptions():
# It is our responsibility to remove exceptions before freeing the object, but sometimes
# that is hard.
if is_instance_valid(collision_exception):
exception_rids.push_back(collision_exception.get_rid())
else:
push_warning("freed object still exists in a collision exception")
# Prevent collisions with ourselves
exception_rids.push_back(get_rid())
# Find our shape ids
var shape_rids : Array[RID]
for node in get_children(true):
if node is CollisionShape3D:
var col_shape : CollisionShape3D = node
if not col_shape.disabled:
shape_rids.push_back(col_shape.shape.get_rid())
# Our physics query
var query : PhysicsShapeQueryParameters3D = PhysicsShapeQueryParameters3D.new()
query.collide_with_areas = false
query.collide_with_bodies = true
query.collision_mask = collision_mask
query.exclude = exception_rids
# Check our collisions
var step : float = 0.0
var new_quat : Quaternion = from_quat
var t = global_transform
while step < steps and not ret:
step += 1.0
var test_quat : Quaternion = from_quat.slerp(to_quat, step / steps)
t.basis = Basis(test_quat)
query.transform = t
for rid in shape_rids:
query.shape_rid = rid
var collision = direct_state.get_rest_info(query)
if not collision.is_empty():
ret = ForceBodyCollision.new()
ret.collider = instance_from_id(collision["collider_id"])
ret.position = collision["point"]
ret.normal = collision["normal"]
# TODO May need to see about applying a rotational force
# if pushbodies is true
break
if not ret:
# No collision, we can rotate this far!
new_quat = test_quat
# Update our rotation to our last successful rotation
global_basis = Basis(new_quat)
# Return the last collision data
return ret
func _ready():
process_physics_priority = -90

View File

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

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

View File

@@ -0,0 +1,37 @@
@tool
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
class_name XRToolsHandPoseArea
extends Area3D
## XR Tools Hand Pose Area
##
## This area works with the XRToolsFunctionPoseArea to control the pose
## of the VR hands.
## Priority level for this hand pose area
@export var pose_priority : int
## Left hand pose settings (XRToolsHandPoseSettings)
@export var left_pose : XRToolsHandPoseSettings
## Right hand pose settings (XRToolsHandPoseSettings)
@export var right_pose : XRToolsHandPoseSettings
## Array of grabpoints this hand pose area disables when active
@export var grabpoints : Array[XRToolsGrabPointHand]
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsHandPoseArea"
# Disables grabpoints
func disable_grab_points():
for grabpoint in grabpoints:
grabpoint.enabled = false
# Enables grabpoints
func enable_grab_points():
for grabpoint in grabpoints:
grabpoint.enabled = true

View File

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

View File

@@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://dc5t2qgmhb2nf"]
[ext_resource type="Script" uid="uid://colsb5moktl1e" path="res://addons/godot-xr-tools/objects/hand_pose_area.gd" id="1"]
[node name="HandPoseArea" type="Area3D"]
collision_layer = 2097152
collision_mask = 0
monitoring = false
script = ExtResource("1")

View File

@@ -0,0 +1,57 @@
@tool
class_name XRToolsHighlightMaterial
extends Node
## Mesh to highlight
@export var highlight_mesh_instance : NodePath
## Material to set
@export var highlight_material : Resource
var _original_materials = Array()
var _highlight_mesh_instance: MeshInstance3D
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsHighlightMaterial"
# Called when the node enters the scene tree for the first time.
func _ready():
# Get the mesh to highlight
_highlight_mesh_instance = get_node(highlight_mesh_instance)
# Save the materials
if _highlight_mesh_instance:
# if we can find a node remember which materials are currently set on each surface
for i in range(0, _highlight_mesh_instance.get_surface_override_material_count()):
_original_materials.push_back(_highlight_mesh_instance.get_surface_override_material(i))
# Hook the highlight update
get_parent().connect("highlight_updated", _on_highlight_updated)
# Called when the pickable highlight changes
func _on_highlight_updated(_pickable, enable: bool) -> void:
# Set the materials
if _highlight_mesh_instance:
for i in range(0, _highlight_mesh_instance.get_surface_override_material_count()):
if enable:
_highlight_mesh_instance.set_surface_override_material(i, highlight_material)
else:
_highlight_mesh_instance.set_surface_override_material(i, _original_materials[i])
# This method verifies the node
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
# Verify parent supports highlighting
var parent := get_parent()
if not parent or not parent.has_signal("highlight_updated"):
warnings.append("Parent does not support highlighting")
return warnings

View File

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

View File

@@ -0,0 +1,35 @@
@tool
class_name XRToolsHighlightRing
extends MeshInstance3D
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsHighlightRing"
# Called when the node enters the scene tree for the first time.
func _ready():
# Turn off until requested
if not Engine.is_editor_hint():
visible = false
# Hook the highlight update
get_parent().connect("highlight_updated", _on_highlight_updated)
# Called when the pickable highlight changes
func _on_highlight_updated(_pickable, enable: bool) -> void:
visible = enable
# This method verifies the node
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
# Verify parent supports highlighting
var parent := get_parent()
if not parent or not parent.has_signal("highlight_updated"):
warnings.append("Parent does not support highlighting")
return warnings

View File

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

View File

@@ -0,0 +1,10 @@
[gd_resource type="StandardMaterial3D" load_steps=2 format=2]
[ext_resource path="res://addons/godot-xr-tools/images/ring.png" type="Texture2D" id=1]
[resource]
flags_transparent = true
flags_unshaded = true
flags_no_depth_test = true
params_billboard_mode = 1
albedo_texture = ExtResource( 1 )

View File

@@ -0,0 +1,12 @@
[gd_scene load_steps=4 format=3 uid="uid://da2qgxxwwitl6"]
[ext_resource type="Script" uid="uid://b8waaso7b6bsr" path="res://addons/godot-xr-tools/objects/highlight/highlight_ring.gd" id="1"]
[ext_resource type="Material" path="res://addons/godot-xr-tools/objects/highlight/highlight_ring.tres" id="2"]
[sub_resource type="QuadMesh" id="1"]
size = Vector2(0.05, 0.05)
[node name="HighlightRing" type="MeshInstance3D"]
mesh = SubResource("1")
surface_material_override/0 = ExtResource("2")
script = ExtResource("1")

View File

@@ -0,0 +1,35 @@
@tool
class_name XRToolsHighlightVisible
extends Node3D
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsHighlightVisible"
# Called when the node enters the scene tree for the first time.
func _ready():
# Turn off until requested
if not Engine.is_editor_hint():
visible = false
# Hook the highlight update
get_parent().connect("highlight_updated", _on_highlight_updated)
# Called when the pickable highlight changes
func _on_highlight_updated(_pickable, enable: bool) -> void:
visible = enable
# This method verifies the node
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
# Verify parent supports highlighting
var parent := get_parent()
if not parent or not parent.has_signal("highlight_updated"):
warnings.append("Parent does not support highlighting")
return warnings

View File

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

View File

@@ -0,0 +1,12 @@
@tool
class_name XRToolsInteractableArea
extends Area3D
## Signal when pointer event occurs on area
signal pointer_event(event)
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsInteractableArea"

View File

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

View File

@@ -0,0 +1,7 @@
class_name XRToolsInteractableBody
extends Node3D
# This should extend from PhysicsBody3D but https://github.com/godotengine/godot/issues/46073
## Signal when pointer event occurs on body
signal pointer_event(event)

View File

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

View File

@@ -0,0 +1,150 @@
@tool
class_name XRToolsVirtualKey
extends Node2D
## Key pressed event
signal pressed
## Key released event
signal released
## Key location
@export var key_size := Vector2(32, 32) : set = _set_key_size
## Key text
@export var key_text := "" : set = _set_key_text
## Key normal color
@export var key_normal := Color(0.1, 0.1, 0.1) : set = _set_key_normal
## Key highlight color
@export var key_highlight := Color(0.2, 0.2, 0.2) : set = _set_key_highlight
## Text normal color
@export var text_normal := Color(1.0, 1.0, 1.0) : set = _set_text_normal
## Text highlight color
@export var text_highlight := Color(0.0, 0.0, 0.0) : set = _set_text_highlight
## Key highlighted
@export var highlighted := false : set = _set_highlighted
# TouchScreenButton node
var _button : TouchScreenButton
# ColorRect node
var _color : ColorRect
# Label node
var _label : Label
# Called when the node enters the scene tree for the first time.
func _ready():
# Construct the ColorRect node
_color = ColorRect.new()
# Construct the Label node
_label = Label.new()
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
# Construct the TouchScreenButton node
_button = TouchScreenButton.new()
_button.shape = RectangleShape2D.new()
# Attach the nodes
_color.add_child(_label)
_button.add_child(_color)
add_child(_button)
# Handle button presses
_button.pressed.connect(_on_button_pressed)
_button.released.connect(_on_button_released)
# Apply initial updates
_update_key_size()
_update_key_text()
_update_highlighted()
func _on_button_pressed() -> void:
pressed.emit()
func _on_button_released() -> void:
released.emit()
func _set_key_size(p_key_size : Vector2) -> void:
key_size = p_key_size
if is_inside_tree():
_update_key_size()
func _set_key_text(p_key_text : String) -> void:
key_text = p_key_text
if is_inside_tree():
_update_key_text()
func _set_key_normal(p_key_normal : Color) -> void:
key_normal = p_key_normal
if is_inside_tree():
_update_highlighted()
func _set_key_highlight(p_key_highlight : Color) -> void:
key_highlight = p_key_highlight
if is_inside_tree():
_update_highlighted()
func _set_text_normal(p_text_normal : Color) -> void:
text_normal = p_text_normal
if is_inside_tree():
_update_highlighted()
func _set_text_highlight(p_text_highlight : Color) -> void:
text_highlight = p_text_highlight
if is_inside_tree():
_update_highlighted()
func _set_highlighted(p_highlighted : bool) -> void:
highlighted = p_highlighted
if is_inside_tree():
_update_highlighted()
func _update_key_size() -> void:
var half_size := key_size / 2
# Set the TouchScreenButton position and shape size
_button.position = half_size
_button.shape.size = key_size
# Size and position the ColorRect
_color.size = key_size
_color.position = -half_size
# Size the label
_label.size = key_size
func _update_key_text() -> void:
_label.text = key_text
func _update_highlighted() -> void:
# Pick colors
var key := key_highlight if highlighted else key_normal
var text := text_highlight if highlighted else text_normal
# Set colors
_color.color = key
_label.add_theme_color_override("font_color", text)

View File

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

View File

@@ -0,0 +1,45 @@
@tool
class_name XRToolsVirtualKeyChar
extends XRToolsVirtualKey
## Godot scan-code text
@export var scan_code_text := ""
## Unicode character
@export var unicode := 0
## Shift modifier
@export var shift_modifier := false
# Keyboard associated with this button
var _keyboard : XRToolsVirtualKeyboard2D
# Called when the node enters the scene tree for the first time.
func _ready():
# Call the base
super()
# Find the keyboard
_keyboard = XRTools.find_xr_ancestor(
self,
"*",
"XRToolsVirtualKeyboard2D")
# Handle button presses
pressed.connect(_on_pressed)
released.connect(_on_released)
# Handler for button pressed
func _on_pressed() -> void:
highlighted = true
if _keyboard:
_keyboard.on_key_pressed(scan_code_text, unicode, shift_modifier)
# Handler for button released
func _on_released() -> void:
highlighted = false

View File

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

View File

@@ -0,0 +1,102 @@
@tool
class_name XRToolsVirtualKeyboard2D
extends CanvasLayer
## Enumeration of keyboard view modes
enum KeyboardMode {
LOWER_CASE, ## Lower-case keys mode
UPPER_CASE, ## Upper-case keys mode
ALTERNATE ## Alternate keys mode
}
# Shift button down
var _shift_down := false
# Caps button down
var _caps_down := false
# Alt button down
var _alt_down := false
# Current keyboard mode
var _mode: int = KeyboardMode.LOWER_CASE
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsVirtualKeyboard2D"
# Handle key pressed from VirtualKey
func on_key_pressed(scan_code_text: String, unicode: int, shift: bool):
# Find the scan code
var scan_code := OS.find_keycode_from_string(scan_code_text)
# Create the InputEventKey
var input := InputEventKey.new()
input.physical_keycode = scan_code
input.unicode = unicode if unicode else scan_code
input.pressed = true
input.keycode = scan_code
input.shift_pressed = shift
# Dispatch the input event
Input.parse_input_event(input)
# Pop any temporary shift key
if _shift_down:
_shift_down = false
_update_visible()
func _on_toggle_shift_pressed() -> void:
# Update toggle keys
_shift_down = not _shift_down
_caps_down = false
_alt_down = false
_update_visible()
func _on_toggle_caps_pressed() -> void:
# Update toggle keys
_caps_down = not _caps_down
_shift_down = false
_alt_down = false
_update_visible()
func _on_toggle_alt_pressed() -> void:
# Update toggle keys
_alt_down = not _alt_down
_shift_down = false
_caps_down = false
_update_visible()
# Update switching the visible case keys
func _update_visible() -> void:
# Ensure the control buttons are set correctly
$Background/Standard/ToggleShift.highlighted = _shift_down
$Background/Standard/ToggleCaps.highlighted = _caps_down
$Background/Standard/ToggleAlt.highlighted = _alt_down
# Evaluate the new mode
var new_mode: int
if _alt_down:
new_mode = KeyboardMode.ALTERNATE
elif _shift_down or _caps_down:
new_mode = KeyboardMode.UPPER_CASE
else:
new_mode = KeyboardMode.LOWER_CASE
# Skip if no change
if new_mode == _mode:
return
# Update the visible mode
_mode = new_mode
$Background/LowerCase.visible = _mode == KeyboardMode.LOWER_CASE
$Background/UpperCase.visible = _mode == KeyboardMode.UPPER_CASE
$Background/Alternate.visible = _mode == KeyboardMode.ALTERNATE

View File

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

View File

@@ -0,0 +1,724 @@
[gd_scene load_steps=4 format=3 uid="uid://lauwp8okd1vh"]
[ext_resource type="Script" uid="uid://phmvrwkudp7u" path="res://addons/godot-xr-tools/objects/keyboard/virtual_keyboard_2d.gd" id="1"]
[ext_resource type="Script" uid="uid://tae1jc8su0i6" path="res://addons/godot-xr-tools/objects/keyboard/virtual_key_char.gd" id="2_n0nlg"]
[ext_resource type="Script" uid="uid://bk7d6s38ejigt" path="res://addons/godot-xr-tools/objects/keyboard/virtual_key.gd" id="3_h05ve"]
[node name="VirtualKeyboard2D" type="CanvasLayer"]
script = ExtResource("1")
[node name="Background" type="ColorRect" parent="."]
offset_right = 400.0
offset_bottom = 200.0
color = Color(0.12549, 0.12549, 0.12549, 0.752941)
[node name="Standard" type="Control" parent="Background"]
layout_mode = 1
anchors_preset = 0
offset_left = 4.0
offset_top = 4.0
offset_right = 394.0
offset_bottom = 149.0
[node name="VirtualKey1" type="Node2D" parent="Background/Standard"]
script = ExtResource("2_n0nlg")
scan_code_text = "1"
unicode = 49
key_text = "1"
[node name="VirtualKey2" type="Node2D" parent="Background/Standard"]
position = Vector2(40, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "2"
unicode = 50
key_text = "2"
[node name="VirtualKey3" type="Node2D" parent="Background/Standard"]
position = Vector2(80, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "3"
unicode = 51
key_text = "3"
[node name="VirtualKey4" type="Node2D" parent="Background/Standard"]
position = Vector2(120, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "4"
unicode = 52
key_text = "4"
[node name="VirtualKey5" type="Node2D" parent="Background/Standard"]
position = Vector2(160, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "5"
unicode = 53
key_text = "5"
[node name="VirtualKey6" type="Node2D" parent="Background/Standard"]
position = Vector2(200, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "6"
unicode = 54
key_text = "6"
[node name="VirtualKey7" type="Node2D" parent="Background/Standard"]
position = Vector2(240, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "7"
unicode = 55
key_text = "7"
[node name="VirtualKey8" type="Node2D" parent="Background/Standard"]
position = Vector2(280, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "8"
unicode = 56
key_text = "8"
[node name="VirtualKey9" type="Node2D" parent="Background/Standard"]
position = Vector2(320, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "9"
unicode = 57
key_text = "9"
[node name="VirtualKey0" type="Node2D" parent="Background/Standard"]
position = Vector2(360, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "0"
unicode = 48
key_text = "0"
[node name="ToggleShift" type="Node2D" parent="Background/Standard"]
position = Vector2(0, 120)
script = ExtResource("3_h05ve")
key_size = Vector2(50, 32)
key_text = "SHIFT"
key_highlight = Color(1, 1, 1, 1)
[node name="ToggleCaps" type="Node2D" parent="Background/Standard"]
position = Vector2(0, 160)
script = ExtResource("3_h05ve")
key_size = Vector2(45, 32)
key_text = "CAPS"
key_highlight = Color(1, 1, 1, 1)
[node name="ToggleAlt" type="Node2D" parent="Background/Standard"]
position = Vector2(55, 160)
script = ExtResource("3_h05ve")
key_size = Vector2(35, 32)
key_text = "ALT"
key_highlight = Color(1, 1, 1, 1)
[node name="VirtualKeyBackspace" type="Node2D" parent="Background/Standard"]
position = Vector2(340, 120)
script = ExtResource("2_n0nlg")
scan_code_text = "BackSpace"
key_size = Vector2(52, 32)
key_text = "BKSP"
[node name="VirtualKeySpace" type="Node2D" parent="Background/Standard"]
position = Vector2(100, 160)
script = ExtResource("2_n0nlg")
scan_code_text = "Space"
unicode = 32
key_size = Vector2(190, 32)
key_text = "Space"
[node name="VirtualKeyPeriod" type="Node2D" parent="Background/Standard"]
position = Vector2(300, 160)
script = ExtResource("2_n0nlg")
scan_code_text = "Period"
unicode = 46
key_text = "."
[node name="VirtualKeyEnter" type="Node2D" parent="Background/Standard"]
position = Vector2(340, 160)
script = ExtResource("2_n0nlg")
scan_code_text = "Enter"
key_size = Vector2(52, 32)
key_text = "Enter"
[node name="LowerCase" type="Node2D" parent="Background"]
position = Vector2(4, 44)
[node name="VirtualKeyLowerQ" type="Node2D" parent="Background/LowerCase"]
script = ExtResource("2_n0nlg")
scan_code_text = "Q"
unicode = 113
key_text = "q"
[node name="VirtualKeyLowerW" type="Node2D" parent="Background/LowerCase"]
position = Vector2(40, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "W"
unicode = 119
key_text = "w"
[node name="VirtualKeyLowerE" type="Node2D" parent="Background/LowerCase"]
position = Vector2(80, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "E"
unicode = 101
key_text = "e"
[node name="VirtualKeyLowerR" type="Node2D" parent="Background/LowerCase"]
position = Vector2(120, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "R"
unicode = 114
key_text = "r"
[node name="VirtualKeyLowerT" type="Node2D" parent="Background/LowerCase"]
position = Vector2(160, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "T"
unicode = 116
key_text = "t"
[node name="VirtualKeyLowerY" type="Node2D" parent="Background/LowerCase"]
position = Vector2(200, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "Y"
unicode = 121
key_text = "y"
[node name="VirtualKeyLowerU" type="Node2D" parent="Background/LowerCase"]
position = Vector2(240, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "U"
unicode = 117
key_text = "u"
[node name="VirtualKeyLowerI" type="Node2D" parent="Background/LowerCase"]
position = Vector2(280, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "I"
unicode = 105
key_text = "i"
[node name="VirtualKeyLowerO" type="Node2D" parent="Background/LowerCase"]
position = Vector2(320, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "O"
unicode = 111
key_text = "o"
[node name="VirtualKeyLowerP" type="Node2D" parent="Background/LowerCase"]
position = Vector2(360, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "P"
unicode = 112
key_text = "p"
[node name="VirtualKeyLowerA" type="Node2D" parent="Background/LowerCase"]
position = Vector2(20, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "A"
unicode = 97
key_text = "a"
[node name="VirtualKeyLowerS" type="Node2D" parent="Background/LowerCase"]
position = Vector2(60, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "S"
unicode = 115
key_text = "s"
[node name="VirtualKeyLowerD" type="Node2D" parent="Background/LowerCase"]
position = Vector2(100, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "D"
unicode = 100
key_text = "d"
[node name="VirtualKeyLowerF" type="Node2D" parent="Background/LowerCase"]
position = Vector2(140, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "F"
unicode = 102
key_text = "f"
[node name="VirtualKeyLowerG" type="Node2D" parent="Background/LowerCase"]
position = Vector2(180, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "G"
unicode = 103
key_text = "g"
[node name="VirtualKeyLowerH" type="Node2D" parent="Background/LowerCase"]
position = Vector2(220, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "H"
unicode = 104
key_text = "h"
[node name="VirtualKeyLowerJ" type="Node2D" parent="Background/LowerCase"]
position = Vector2(260, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "J"
unicode = 106
key_text = "j"
[node name="VirtualKeyLowerK" type="Node2D" parent="Background/LowerCase"]
position = Vector2(300, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "K"
unicode = 107
key_text = "k"
[node name="VirtualKeyLowerL" type="Node2D" parent="Background/LowerCase"]
position = Vector2(340, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "L"
unicode = 108
key_text = "l"
[node name="VirtualKeyLowerZ" type="Node2D" parent="Background/LowerCase"]
position = Vector2(60, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "Z"
unicode = 122
key_text = "z"
[node name="VirtualKeyLowerX" type="Node2D" parent="Background/LowerCase"]
position = Vector2(100, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "X"
unicode = 120
key_text = "x"
[node name="VirtualKeyLowerC" type="Node2D" parent="Background/LowerCase"]
position = Vector2(140, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "C"
unicode = 99
key_text = "c"
[node name="VirtualKeyLowerV" type="Node2D" parent="Background/LowerCase"]
position = Vector2(180, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "V"
unicode = 118
key_text = "v"
[node name="VirtualKeyLowerB" type="Node2D" parent="Background/LowerCase"]
position = Vector2(220, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "B"
unicode = 98
key_text = "b"
[node name="VirtualKeyLowerN" type="Node2D" parent="Background/LowerCase"]
position = Vector2(260, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "N"
unicode = 110
key_text = "n"
[node name="VirtualKeyLowerM" type="Node2D" parent="Background/LowerCase"]
position = Vector2(300, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "M"
unicode = 109
key_text = "m"
[node name="UpperCase" type="Node2D" parent="Background"]
visible = false
position = Vector2(4, 44)
[node name="VirtualKeyUpperQ" type="Node2D" parent="Background/UpperCase"]
script = ExtResource("2_n0nlg")
scan_code_text = "Q"
unicode = 81
shift_modifier = true
key_text = "Q"
[node name="VirtualKeyUpperW" type="Node2D" parent="Background/UpperCase"]
position = Vector2(40, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "W"
unicode = 87
shift_modifier = true
key_text = "W"
[node name="VirtualKeyUpperE" type="Node2D" parent="Background/UpperCase"]
position = Vector2(80, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "E"
unicode = 69
shift_modifier = true
key_text = "E"
[node name="VirtualKeyUpperR" type="Node2D" parent="Background/UpperCase"]
position = Vector2(120, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "R"
unicode = 82
shift_modifier = true
key_text = "R"
[node name="VirtualKeyUpperT" type="Node2D" parent="Background/UpperCase"]
position = Vector2(160, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "T"
unicode = 84
shift_modifier = true
key_text = "T"
[node name="VirtualKeyUpperY" type="Node2D" parent="Background/UpperCase"]
position = Vector2(200, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "Y"
unicode = 89
shift_modifier = true
key_text = "Y"
[node name="VirtualKeyUpperU" type="Node2D" parent="Background/UpperCase"]
position = Vector2(240, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "U"
unicode = 85
shift_modifier = true
key_text = "U"
[node name="VirtualKeyUpperI" type="Node2D" parent="Background/UpperCase"]
position = Vector2(280, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "I"
unicode = 73
shift_modifier = true
key_text = "I"
[node name="VirtualKeyUpperO" type="Node2D" parent="Background/UpperCase"]
position = Vector2(320, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "O"
unicode = 79
shift_modifier = true
key_text = "O"
[node name="VirtualKeyUpperP" type="Node2D" parent="Background/UpperCase"]
position = Vector2(360, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "P"
unicode = 80
shift_modifier = true
key_text = "P"
[node name="VirtualKeyUpperA" type="Node2D" parent="Background/UpperCase"]
position = Vector2(20, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "A"
unicode = 65
shift_modifier = true
key_text = "A"
[node name="VirtualKeyUpperS" type="Node2D" parent="Background/UpperCase"]
position = Vector2(60, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "S"
unicode = 83
shift_modifier = true
key_text = "S"
[node name="VirtualKeyUpperD" type="Node2D" parent="Background/UpperCase"]
position = Vector2(100, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "D"
unicode = 68
shift_modifier = true
key_text = "D"
[node name="VirtualKeyUpperF" type="Node2D" parent="Background/UpperCase"]
position = Vector2(140, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "F"
unicode = 70
shift_modifier = true
key_text = "F"
[node name="VirtualKeyUpperG" type="Node2D" parent="Background/UpperCase"]
position = Vector2(180, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "G"
unicode = 71
shift_modifier = true
key_text = "G"
[node name="VirtualKeyUpperH" type="Node2D" parent="Background/UpperCase"]
position = Vector2(220, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "H"
unicode = 72
shift_modifier = true
key_text = "H"
[node name="VirtualKeyUpperJ" type="Node2D" parent="Background/UpperCase"]
position = Vector2(260, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "J"
unicode = 74
shift_modifier = true
key_text = "J"
[node name="VirtualKeyUpperK" type="Node2D" parent="Background/UpperCase"]
position = Vector2(300, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "K"
unicode = 75
shift_modifier = true
key_text = "K"
[node name="VirtualKeyUpperL" type="Node2D" parent="Background/UpperCase"]
position = Vector2(340, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "L"
unicode = 76
shift_modifier = true
key_text = "L"
[node name="VirtualKeyUpperZ" type="Node2D" parent="Background/UpperCase"]
position = Vector2(60, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "Z"
unicode = 90
shift_modifier = true
key_text = "Z"
[node name="VirtualKeyUpperX" type="Node2D" parent="Background/UpperCase"]
position = Vector2(100, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "X"
unicode = 88
shift_modifier = true
key_text = "X"
[node name="VirtualKeyUpperC" type="Node2D" parent="Background/UpperCase"]
position = Vector2(140, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "C"
unicode = 67
shift_modifier = true
key_text = "C"
[node name="VirtualKeyUpperV" type="Node2D" parent="Background/UpperCase"]
position = Vector2(180, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "V"
unicode = 86
shift_modifier = true
key_text = "V"
[node name="VirtualKeyUpperB" type="Node2D" parent="Background/UpperCase"]
position = Vector2(220, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "B"
unicode = 66
shift_modifier = true
key_text = "B"
[node name="VirtualKeyUpperN" type="Node2D" parent="Background/UpperCase"]
position = Vector2(260, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "N"
unicode = 78
shift_modifier = true
key_text = "N"
[node name="VirtualKeyUpperM" type="Node2D" parent="Background/UpperCase"]
position = Vector2(300, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "M"
unicode = 77
shift_modifier = true
key_text = "M"
[node name="Alternate" type="Node2D" parent="Background"]
visible = false
position = Vector2(4, 44)
[node name="VirtualKeyPlus" type="Node2D" parent="Background/Alternate"]
script = ExtResource("2_n0nlg")
scan_code_text = "Plus"
unicode = 43
key_text = "+"
[node name="VirtualKeyAsterisk" type="Node2D" parent="Background/Alternate"]
position = Vector2(40, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "Asterisk"
unicode = 42
key_text = "*"
[node name="VirtualKeyDivision" type="Node2D" parent="Background/Alternate"]
position = Vector2(80, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "Division"
unicode = 247
key_text = "÷"
[node name="VirtualKeyEqual" type="Node2D" parent="Background/Alternate"]
position = Vector2(120, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "Equal"
unicode = 61
key_text = "="
[node name="VirtualKeySlash" type="Node2D" parent="Background/Alternate"]
position = Vector2(160, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "Slash"
unicode = 47
key_text = "/"
[node name="VirtualKeyUnderScore" type="Node2D" parent="Background/Alternate"]
position = Vector2(200, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "UnderScore"
unicode = 95
key_text = "_"
[node name="VirtualKeyLess" type="Node2D" parent="Background/Alternate"]
position = Vector2(240, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "Less"
unicode = 60
key_text = "<"
[node name="VirtualKeyGreater" type="Node2D" parent="Background/Alternate"]
position = Vector2(280, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "Greater"
unicode = 62
key_text = ">"
[node name="VirtualKeyBracketLeft" type="Node2D" parent="Background/Alternate"]
position = Vector2(320, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "BracketLeft"
unicode = 91
key_text = "["
[node name="VirtualKeyBracketRight" type="Node2D" parent="Background/Alternate"]
position = Vector2(360, 0)
script = ExtResource("2_n0nlg")
scan_code_text = "BracketRight"
unicode = 93
key_text = "]"
[node name="VirtualKeyExclam" type="Node2D" parent="Background/Alternate"]
position = Vector2(20, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "Exclam"
unicode = 33
key_text = "!"
[node name="VirtualKeyAt" type="Node2D" parent="Background/Alternate"]
position = Vector2(60, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "At"
unicode = 64
key_text = "@"
[node name="VirtualKeyNumberSign" type="Node2D" parent="Background/Alternate"]
position = Vector2(100, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "NumberSign"
unicode = 35
key_text = "#"
[node name="VirtualKeyDollar" type="Node2D" parent="Background/Alternate"]
position = Vector2(140, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "Dollar"
unicode = 36
key_text = "$"
[node name="VirtualKeyPercent" type="Node2D" parent="Background/Alternate"]
position = Vector2(180, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "Percent"
unicode = 37
key_text = "%"
[node name="VirtualKeyCircumflex" type="Node2D" parent="Background/Alternate"]
position = Vector2(220, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "AsciiCircum"
unicode = 94
key_text = "^"
[node name="VirtualKeyAmpersand" type="Node2D" parent="Background/Alternate"]
position = Vector2(260, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "Ampersand"
unicode = 38
key_text = "&"
[node name="VirtualKeyParenLeft" type="Node2D" parent="Background/Alternate"]
position = Vector2(300, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "ParenLeft"
unicode = 40
key_text = "("
[node name="VirtualKeyParenRight" type="Node2D" parent="Background/Alternate"]
position = Vector2(340, 40)
script = ExtResource("2_n0nlg")
scan_code_text = "ParenRight"
unicode = 41
key_text = ")"
[node name="VirtualKeyMinus" type="Node2D" parent="Background/Alternate"]
position = Vector2(60, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "Minus"
unicode = 45
key_text = "-"
[node name="VirtualKeyApostrophe" type="Node2D" parent="Background/Alternate"]
position = Vector2(100, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "Apostrophe"
unicode = 39
key_text = "'"
[node name="VirtualKeyQuoteDbl" type="Node2D" parent="Background/Alternate"]
position = Vector2(140, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "QuoteDbl"
unicode = 34
key_text = "\""
[node name="VirtualKeyColon" type="Node2D" parent="Background/Alternate"]
position = Vector2(180, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "Colon"
unicode = 58
key_text = ":"
[node name="VirtualKeySemicolon" type="Node2D" parent="Background/Alternate"]
position = Vector2(220, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "Semicolon"
unicode = 59
key_text = ";"
[node name="VirtualKeyComma" type="Node2D" parent="Background/Alternate"]
position = Vector2(260, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "Comma"
unicode = 44
key_text = ","
[node name="VirtualKeyQuestion" type="Node2D" parent="Background/Alternate"]
position = Vector2(300, 80)
script = ExtResource("2_n0nlg")
scan_code_text = "Question"
unicode = 63
key_text = "?"
[connection signal="pressed" from="Background/Standard/ToggleShift" to="." method="_on_toggle_shift_pressed"]
[connection signal="pressed" from="Background/Standard/ToggleCaps" to="." method="_on_toggle_caps_pressed"]
[connection signal="pressed" from="Background/Standard/ToggleAlt" to="." method="_on_toggle_alt_pressed"]

View File

@@ -0,0 +1,439 @@
@tool
class_name XRToolsPickable
extends RigidBody3D
## XR Tools Pickable Object
##
## This script allows a [RigidBody3D] to be picked up by an
## [XRToolsFunctionPickup] attached to a players controller.
##
## Additionally pickable objects may support being snapped into
## [XRToolsSnapZone] areas.
##
## Grab-points can be defined by adding different types of [XRToolsGrabPoint]
## child nodes controlling hand and snap-zone grab locations.
# Signal emitted when this object is picked up (held by a player or snap-zone)
signal picked_up(pickable)
# Signal emitted when this object is dropped
signal dropped(pickable)
# Signal emitted when this object is grabbed (primary or secondary)
signal grabbed(pickable, by)
# Signal emitted when this object is released (primary or secondary)
signal released(pickable, by)
# Signal emitted when the user presses the action button while holding this object
signal action_pressed(pickable)
# Signal emitted when the user releases the action button while holding this object
signal action_released(pickable)
# Signal emitted when the highlight state changes
signal highlight_updated(pickable, enable)
## Method used to grab object at range
enum RangedMethod {
NONE, ## Ranged grab is not supported
SNAP, ## Object snaps to holder
LERP, ## Object lerps to holder
}
enum ReleaseMode {
ORIGINAL = -1, ## Preserve original mode when picked up
UNFROZEN = 0, ## Release and unfreeze
FROZEN = 1, ## Release and freeze
}
enum SecondHandGrab {
IGNORE, ## Ignore second grab
SWAP, ## Swap to second hand
SECOND, ## Second hand grab
}
# Default layer for held objects is 17:held-object
const DEFAULT_LAYER := 0b0000_0000_0000_0001_0000_0000_0000_0000
## If true, the pickable supports being picked up
@export var enabled : bool = true
## If true, the grip control must be held to keep the object picked up
@export var press_to_hold : bool = true
## Layer for this object while picked up
@export_flags_3d_physics var picked_up_layer : int = DEFAULT_LAYER
## Release mode to use when releasing the object
@export var release_mode : ReleaseMode = ReleaseMode.ORIGINAL
## Method used to perform a ranged grab
@export var ranged_grab_method : RangedMethod = RangedMethod.SNAP: set = _set_ranged_grab_method
## Second hand grab mode
@export var second_hand_grab : SecondHandGrab = SecondHandGrab.IGNORE
## Speed for ranged grab
@export var ranged_grab_speed : float = 20.0
## Refuse pick-by when in the specified group
@export var picked_by_exclude : String = ""
## Require pick-by to be in the specified group
@export var picked_by_require : String = ""
## If true, the object can be picked up at range
var can_ranged_grab: bool = true
## Frozen state to restore to when dropped
var restore_freeze : bool = false
# Count of 'is_closest' grabbers
var _closest_count: int = 0
# Grab Driver to control position while grabbed
var _grab_driver: XRToolsGrabDriver = null
# Array of grab points
var _grab_points : Array[XRToolsGrabPoint] = []
# Dictionary of nodes requesting highlight
var _highlight_requests : Dictionary = {}
# Is this node highlighted
var _highlighted : bool = false
# Remember some state so we can return to it when the user drops the object
@onready var original_collision_mask : int = collision_mask
@onready var original_collision_layer : int = collision_layer
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsPickable"
# Called when the node enters the scene tree for the first time.
func _ready():
# Get all grab points
for child in get_children():
var grab_point := child as XRToolsGrabPoint
if grab_point:
_grab_points.push_back(grab_point)
# Called when the node exits the tree
func _exit_tree():
# Skip if not picked up
if not is_instance_valid(_grab_driver):
return
# Release primary grab
if _grab_driver.primary:
_grab_driver.primary.release()
# Release secondary grab
if _grab_driver.secondary:
_grab_driver.secondary.release()
# Test if this object can be picked up
func can_pick_up(by: Node3D) -> bool:
# Refuse if not enabled
if not enabled:
return false
# Allow if not held by anything
if not is_picked_up():
return true
# Fail if second hand grabbing isn't allowed
if second_hand_grab == SecondHandGrab.IGNORE:
return false
# Fail if either pickup isn't by a hand
if not _grab_driver.primary.pickup or not by is XRToolsFunctionPickup:
return false
# Allow second hand grab
return true
# Test if this object is picked up
func is_picked_up() -> bool:
return _grab_driver and _grab_driver.primary
# action is called when user presses the action button while holding this object
func action():
# let interested parties know
action_pressed.emit(self)
func controller_action(controller : XRController3D):
# Let the grab points know about the action
if (
_grab_driver.primary and _grab_driver.primary.point
and _grab_driver.primary.controller == controller
):
_grab_driver.primary.point.action(self)
if (
_grab_driver.secondary and _grab_driver.secondary.point
and _grab_driver.secondary.controller == controller
):
_grab_driver.secondary.point.action(self)
# action_release is called when user releases the action button while holding this object
func action_release():
# let interested parties know
action_released.emit(self)
func controller_action_release(controller : XRController3D):
# Let the grab points know about the action release
if (
_grab_driver.primary and _grab_driver.primary.point
and _grab_driver.primary.controller == controller
):
_grab_driver.primary.point.action_release(self)
if (
_grab_driver.secondary and _grab_driver.secondary.point
and _grab_driver.secondary.controller == controller
):
_grab_driver.secondary.point.action_release(self)
## This method requests highlighting of the [XRToolsPickable].
## If [param from] is null then all highlighting requests are cleared,
## otherwise the highlight request is associated with the specified node.
func request_highlight(from : Node, on : bool = true) -> void:
# Save if we are highlighted
var old_highlighted := _highlighted
# Update the highlight requests dictionary
if not from:
_highlight_requests.clear()
elif on:
_highlight_requests[from] = from
else:
_highlight_requests.erase(from)
# Update the highlighted state
_highlighted = _highlight_requests.size() > 0
# Report any changes
if _highlighted != old_highlighted:
highlight_updated.emit(self, _highlighted)
func drop():
# Skip if not picked up
if not is_picked_up():
return
# Request secondary grabber to drop
if _grab_driver.secondary:
_grab_driver.secondary.by.drop_object()
# Request primary grabber to drop
_grab_driver.primary.by.drop_object()
func drop_and_free():
drop()
queue_free()
# Called when this object is picked up
func pick_up(by: Node3D) -> void:
# Skip if not enabled
if not enabled:
return
# Find the grabber information
var grabber := Grabber.new(by)
# Test if we're already picked up:
if is_picked_up():
# Ignore if we don't support second-hand grab
if second_hand_grab == SecondHandGrab.IGNORE:
print_verbose("%s> second-hand grab not enabled" % name)
return
# Ignore if either pickup isn't by a hand
if not _grab_driver.primary.pickup or not grabber.pickup:
return
# Construct the second grab
if second_hand_grab != SecondHandGrab.SWAP:
# Grab the object
var by_grab_point := _get_grab_point(by, _grab_driver.primary.point)
var grab := Grab.new(grabber, self, by_grab_point, true)
_grab_driver.add_grab(grab)
# Report the secondary grab
grabbed.emit(self, by)
return
# Swapping hands, let go with the primary grab
print_verbose("%s> letting go to swap hands" % name)
let_go(_grab_driver.primary.by, Vector3.ZERO, Vector3.ZERO)
# Remember the mode before pickup
match release_mode:
ReleaseMode.UNFROZEN:
restore_freeze = false
ReleaseMode.FROZEN:
restore_freeze = true
_:
restore_freeze = freeze
# turn off physics on our pickable object
freeze = true
collision_layer = picked_up_layer
collision_mask = 0
# Find a suitable primary hand grab
var by_grab_point := _get_grab_point(by, null)
# Construct the grab driver
if by.picked_up_ranged:
if ranged_grab_method == RangedMethod.LERP:
var grab := Grab.new(grabber, self, by_grab_point, false)
_grab_driver = XRToolsGrabDriver.create_lerp(self, grab, ranged_grab_speed)
else:
var grab := Grab.new(grabber, self, by_grab_point, false)
_grab_driver = XRToolsGrabDriver.create_snap(self, grab)
else:
var grab := Grab.new(grabber, self, by_grab_point, true)
_grab_driver = XRToolsGrabDriver.create_snap(self, grab)
# Report picked up and grabbed
picked_up.emit(self)
grabbed.emit(self, by)
# Called when this object is dropped
func let_go(by: Node3D, p_linear_velocity: Vector3, p_angular_velocity: Vector3) -> void:
# Skip if not picked up
if not is_picked_up():
return
# Get the grab information
var grab := _grab_driver.get_grab(by)
if not grab:
return
# Remove the grab from the driver and release the grab
_grab_driver.remove_grab(grab)
grab.release()
# Test if still grabbing
if _grab_driver.primary:
# Test if we need to swap grab-points
if is_instance_valid(_grab_driver.primary.hand_point):
# Verify the current primary grab point is a valid primary grab point
if _grab_driver.primary.hand_point.mode != XRToolsGrabPointHand.Mode.SECONDARY:
return
# Find a more suitable grab-point
var new_grab_point := _get_grab_point(_grab_driver.primary.by, null)
print_verbose("%s> held only by secondary, swapping grab points" % name)
switch_active_grab_point(new_grab_point)
# Grab is still good
return
# Drop the grab-driver
print_verbose("%s> dropping" % name)
_grab_driver.discard()
_grab_driver = null
# Restore RigidBody mode
freeze = restore_freeze
collision_mask = original_collision_mask
collision_layer = original_collision_layer
# Set velocity
linear_velocity = p_linear_velocity
angular_velocity = p_angular_velocity
# let interested parties know
dropped.emit(self)
## Get the node currently holding this object
func get_picked_up_by() -> Node3D:
# Skip if not picked up
if not is_picked_up():
return null
# Get the primary pickup
return _grab_driver.primary.by
## Get the controller currently holding this object
func get_picked_up_by_controller() -> XRController3D:
# Skip if not picked up
if not is_picked_up():
return null
# Get the primary pickup controller
return _grab_driver.primary.controller
## Get the active grab-point this object is held by
func get_active_grab_point() -> XRToolsGrabPoint:
# Skip if not picked up
if not is_picked_up():
return null
return _grab_driver.primary.point
## Switch the active grab-point for this object
func switch_active_grab_point(grab_point : XRToolsGrabPoint):
# Skip if not picked up
if not is_picked_up():
return null
# Apply the grab point
_grab_driver.primary.set_grab_point(grab_point)
## Find the most suitable grab-point for the grabber
func _get_grab_point(grabber : Node3D, current : XRToolsGrabPoint) -> XRToolsGrabPoint:
# Find the best grab-point
var fitness := 0.0
var point : XRToolsGrabPoint = null
for p in _grab_points:
var f := p.can_grab(grabber, current)
if f > fitness:
fitness = f
point = p
# Resolve redirection
while point is XRToolsGrabPointRedirect:
point = point.target
# Return the best grab point
print_verbose("%s> picked grab-point %s" % [name, point])
return point
func _set_ranged_grab_method(new_value: int) -> void:
ranged_grab_method = new_value
can_ranged_grab = new_value != RangedMethod.NONE

View File

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

View File

@@ -0,0 +1,11 @@
[gd_scene load_steps=2 format=3 uid="uid://c8l60rnugru40"]
[ext_resource type="Script" uid="uid://bfrlpbsqg5lqr" path="res://addons/godot-xr-tools/objects/pickable.gd" id="1"]
[node name="PickableObject" type="RigidBody3D"]
collision_layer = 4
collision_mask = 196615
freeze_mode = 1
script = ExtResource("1")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]

View File

@@ -0,0 +1,110 @@
@tool
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
class_name XRToolsReturnToSnapZone
extends Node
## XR Tools Return to Snap Zone
##
## This node can be added to an XRToolsPickable to make it return to a specified
## snap-zone when the object is dropped.
## Snap zone path
@export var snap_zone_path : NodePath
## Return delay
@export var return_delay : float = 1.0
# Pickable object to control
var _pickable : XRToolsPickable
# Snap zone to return to
var _snap_zone : XRToolsSnapZone
# Return counter
var _return_counter : float = 0.0
# Is the pickable held
var _held := false
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsReturnToSnapZone"
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# Get the pickable (parent of this node)
_pickable = get_parent() as XRToolsPickable
if _pickable:
_pickable.picked_up.connect(_on_picked_up)
_pickable.dropped.connect(_on_dropped)
# Get the optional snap-zone
_snap_zone = get_node_or_null(snap_zone_path)
if not _snap_zone:
set_process(false)
# Handle the return counter
func _process(delta : float) -> void:
if Engine.is_editor_hint():
return
# Update return time and skip if still waiting
_return_counter += delta
if _return_counter < return_delay:
return
# Stop counting
set_process(false)
# If the snap-zone is empty then snap to it
if not _snap_zone.has_snapped_object():
_snap_zone.pick_up_object(_pickable)
# Set the snap-zone
func set_snap_zone(snap_zone : XRToolsSnapZone) -> void:
# Set the snap zone
_snap_zone = snap_zone
_return_counter = 0.0
# Control counting
if _snap_zone and not _held:
set_process(true)
else:
set_process(false)
# Handle the object being picked up
func _on_picked_up(_pickable) -> void:
# Set held and stop counting
_held = true
set_process(false)
# Handle the object being dropped
func _on_dropped(_pickable) -> void:
# Clear held and reset counter
_held = false
_return_counter = 0.0
# Start counter if snap-zone specified
if _snap_zone:
set_process(true)
# This method verifies the pose area has a valid configuration.
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
# Verify this node is a child of a pickable
if not get_parent() is XRToolsPickable:
warnings.append("Must be a child of a pickable")
# Return warnings
return warnings

View File

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

View File

@@ -0,0 +1,176 @@
@tool
class_name XRToolsSnapPath
extends XRToolsSnapZone
## An [XRToolsSnapZone] that allows [XRToolsPickable] to be placed along a
## child [Path3D] node. They can either be placed along any point in the curve
## or at discrete intervals by setting "snap_interval" above '0.0'.
##
## Note: Attached [XRToolsPickable]s will face the +Z axis.
## Real world distance between intervals in Meters.
## Enabled when not 0
@export var snap_interval := 0.0:
set(v): snap_interval = absf(v)
@onready var path : Path3D
func _ready() -> void:
super._ready()
for c in get_children():
if c is Path3D:
path = c
break
func has_snap_interval() -> bool:
return !is_equal_approx(snap_interval, 0.0)
func _get_configuration_warnings() -> PackedStringArray:
# Check for Path3D child
for c in get_children():
if c is Path3D:
path = c
return[]
return["This node requires a Path3D child node to define its shape."]
# Called when a target in our grab area is dropped
func _on_target_dropped(target: Node3D) -> void:
# Skip if invalid
if !enabled or !path or !target.can_pick_up(self) or \
!is_instance_valid(target) or \
is_instance_valid(picked_up_object):
return
# Make a zone that will destruct once its object has left
var zone = _make_temp_zone()
var offset = _find_offset(path, target.global_position)
# if snap guide
if _has_snap_guide(target):
# comply with guide
offset = _find_closest_offset_with_length(path.curve, offset, _get_snap_guide(target).length)
# too large to place on path
if is_equal_approx(offset, -1.0):
return
# if snap_interval has been set, use it
if has_snap_interval():
offset = snappedf(offset, snap_interval)
# set position
zone.position = path.curve.sample_baked(offset)
# Add zone as a child
path.add_child(zone)
zone.owner = path
# Connect self-destruct with lambda
zone.has_dropped.connect(func(): zone.queue_free(), Object.ConnectFlags.CONNECT_ONE_SHOT)
# Use Pickable's Shapes as our Shapes
for c in target.get_children():
if c is CollisionShape3D:
PhysicsServer3D.area_add_shape(zone.get_rid(), c.shape.get_rid(), c.transform)
# Force pickup
zone.pick_up_object(target)
# Make a zone that dies on dropping objects
func _make_temp_zone():
var zone = XRToolsSnapZone.new()
# connect lambda to play stash sounds when temp zone picks up
if has_node("AudioStreamPlayer3D"):
zone.has_picked_up.connect(
func(object):
$AudioStreamPlayer3D.stream = stash_sound
$AudioStreamPlayer3D.play()
)
# XRToolsSnapZone manaul copy
zone.enabled = true
zone.stash_sound = stash_sound
zone.grab_distance = grab_distance
zone.snap_mode = snap_mode
zone.snap_require = snap_require
zone.snap_exclude = snap_exclude
zone.grab_require = grab_require
zone.grab_exclude = grab_exclude
zone.initial_object = NodePath()
# CollisionObject3D manual copy
zone.disable_mode = disable_mode
zone.collision_layer = collision_layer
zone.collision_mask = collision_mask
zone.collision_priority = collision_priority
return zone
func _has_snap_guide(target: Node3D) -> bool:
for c in target.get_children():
if c is XRToolsSnapPathGuide:
return true
return false
func _get_snap_guide(target: Node3D) -> Node3D:
for c in target.get_children():
if c is XRToolsSnapPathGuide:
return c
return null
# Returns -1 if invalid
# _offset should be in _curve's local coordinates
func _find_closest_offset_with_length(_curve: Curve3D, _offset: float, _length: float) -> float:
# p1 and p2 are the object's start and end respectively
var p1 = _offset
var p2 = _offset - _length
# a _curve's final point is its end, aka the furthest 'forward', which is why it is p1
# path_p1 and path_p2 are the curve's start and end respectively
var path_p1 := _curve.get_closest_offset(_curve.get_point_position(_curve.point_count-1))
var path_p2 := _curve.get_closest_offset(_curve.get_point_position(0))
# if at front (or beyond)
if is_equal_approx(p1, path_p1):
# if too large
if p2 < path_p2:
return -1
# if too far back
elif p2 < path_p2:
# check if snapping will over-extend
if has_snap_interval():
# snapping p1_new may move it further back, and out-of-bounds
# larger snaps move the object further forward
var p1_new = path_p2 + _length
var ideal_snap = snappedf(p1_new, snap_interval)
var more_snap = _snappedf_up(p1_new, snap_interval)
# if ideal snap fits, take that
if ideal_snap >= p1_new:
return ideal_snap
return more_snap
return path_p2 + _length
# otherwise: within bounds
return p1
## Round 'x' upwards to the nearest 'step'
func _snappedf_up(x, step) -> float:
return step * ceilf(x / step)
func _find_offset(_path: Path3D, _global_position: Vector3) -> float:
# transform target pos to local space
var local_pos: Vector3 = _global_position * _path.global_transform
return _path.curve.get_closest_offset(local_pos)

View File

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

View File

@@ -0,0 +1,32 @@
[gd_scene load_steps=4 format=3 uid="uid://dsstvanwd58r0"]
[ext_resource type="Script" uid="uid://bk0xen1ol7xb2" path="res://addons/godot-xr-tools/objects/snap_path.gd" id="1_m211o"]
[sub_resource type="BoxShape3D" id="BoxShape3D_pik8g"]
size = Vector3(0.1, 0.1, 1)
[sub_resource type="Curve3D" id="Curve3D_w68am"]
_data = {
"points": PackedVector3Array(0, 0, 0, 0, 0, 0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0.5),
"tilts": PackedFloat32Array(0, 0)
}
point_count = 2
[node name="SnapPath" type="Area3D"]
collision_layer = 4
collision_mask = 65540
script = ExtResource("1_m211o")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("BoxShape3D_pik8g")
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="."]
unit_size = 3.0
max_db = 1.0
max_distance = 100.0
[node name="Path3D" type="Path3D" parent="."]
curve = SubResource("Curve3D_w68am")
[connection signal="body_entered" from="." to="." method="_on_snap_zone_body_entered"]
[connection signal="body_exited" from="." to="." method="_on_snap_zone_body_exited"]

View File

@@ -0,0 +1,14 @@
@tool
class_name XRToolsSnapPathGuide
extends Marker3D
## XRToolsSnapRailGuide depicts a guide for [XRToolsSnapPath] to judge the
## length of an [XRToolsPickable], helping place pickables within its bounds.
## Add as a child node to any [XRToolsPickable], then move negatively along
## the Z-Axis to define a length.
var length : float:
get:
return abs(position.z)

View File

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

View File

@@ -0,0 +1,336 @@
@tool
class_name XRToolsSnapZone
extends Area3D
## Signal emitted when the snap-zone picks something up
signal has_picked_up(what)
## Signal emitted when the snap-zone drops something
signal has_dropped
# Signal emitted when the highlight state changes
signal highlight_updated(pickable, enable)
# Signal emitted when the highlight state changes
signal close_highlight_updated(pickable, enable)
## Enumeration of snap mode
enum SnapMode {
DROPPED, ## Snap only when the object is dropped
RANGE, ## Snap whenever an object is in range
}
## Enable or disable snap-zone
@export var enabled : bool = true: set = _set_enabled
## Optional audio stream to play when a object snaps to the zone
@export var stash_sound : AudioStream
## Grab distance
@export var grab_distance : float = 0.3: set = _set_grab_distance
## Snap mode
@export var snap_mode : SnapMode = SnapMode.DROPPED: set = _set_snap_mode
## Require snap items to be in specified group
@export var snap_require : String = ""
## Deny snapping items in the specified group
@export var snap_exclude : String = ""
## Require grab-by to be in the specified group
@export var grab_require : String = ""
## Deny grab-by
@export var grab_exclude : String= ""
## Initial object in snap zone
@export var initial_object : NodePath
# Public fields
var closest_object : Node3D = null
var picked_up_object : Node3D = null
var picked_up_ranged : bool = true
# Private fields
var _object_in_grab_area = Array()
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsSnapZone"
func _ready():
# Set collision shape radius
if has_node("CollisionShape3D") and "radius" in $CollisionShape3D.shape:
$CollisionShape3D.shape.radius = grab_distance
# Add important connections
if not body_entered.is_connected(_on_snap_zone_body_entered):
body_entered.connect(_on_snap_zone_body_entered)
if not body_exited.is_connected(_on_snap_zone_body_exited):
body_exited.connect(_on_snap_zone_body_exited)
# Perform updates
_update_snap_mode()
# Perform the initial object check when next idle
if not Engine.is_editor_hint():
_initial_object_check.call_deferred()
# Called on each frame to update the pickup
func _process(_delta):
# Skip if in editor or not enabled
if Engine.is_editor_hint() or not enabled:
return
# Skip if we aren't doing range-checking
if snap_mode != SnapMode.RANGE:
return
# Skip if already holding a valid object
if is_instance_valid(picked_up_object):
return
# Check for any object in range that can be grabbed
for o in _object_in_grab_area:
# skip objects that can not be picked up
if not o.can_pick_up(self):
continue
# pick up our target
pick_up_object(o)
return
# Pickable Method: snap-zone can be grabbed if holding object
func can_pick_up(by: Node3D) -> bool:
# Refuse if not enabled
if not enabled:
return false
# Refuse if no object is held
if not is_instance_valid(picked_up_object):
return false
# Refuse if the grab-by is not in the required group
if not grab_require.is_empty() and not by.is_in_group(grab_require):
return false
# Refuse if the grab-by is in the excluded group
if not grab_exclude.is_empty() and by.is_in_group(grab_exclude):
return false
# Grab is permitted
return true
# Pickable Method: Snap points can't be picked up
func is_picked_up() -> bool:
return false
# Pickable Method: Gripper-actions can't occur on snap zones
func action():
pass
# Ignore highlighting requests from XRToolsFunctionPickup
func request_highlight(from : Node, on : bool = true) -> void:
if is_instance_valid(picked_up_object):
picked_up_object.request_highlight(from, on)
# Pickable Method: Object being grabbed from this snap zone
func pick_up(_by: Node3D) -> void:
pass
# Pickable Method: Player never graps snap-zone
func let_go(_by: Node3D, _p_linear_velocity: Vector3, _p_angular_velocity: Vector3) -> void:
pass
# Pickup Method: Drop the currently picked up object
func drop_object() -> void:
if not is_instance_valid(picked_up_object):
return
# let go of this object
picked_up_object.let_go(self, Vector3.ZERO, Vector3.ZERO)
picked_up_object = null
has_dropped.emit()
highlight_updated.emit(self, enabled)
# Check for an initial object pickup
func _initial_object_check() -> void:
# Check for an initial object
if initial_object:
# Force pick-up the initial object
pick_up_object(get_node(initial_object))
else:
# Show highlight when empty and enabled
highlight_updated.emit(self, enabled)
# Stop any audio from initial pickup
var audio := get_node("AudioStreamPlayer3D") if has_node("AudioStreamPlayer3D") else null
# Only stop if the user doesn't intend to auto-play
if audio is AudioStreamPlayer3D and !audio.autoplay:
audio.stop()
# Called when a body enters the snap zone
func _on_snap_zone_body_entered(target: Node3D) -> void:
# Ignore objects already known about
if _object_in_grab_area.find(target) >= 0:
return
# Reject objects which don't support picking up
if not target.has_method('pick_up'):
return
# Reject objects not in the required snap group
if not snap_require.is_empty() and not target.is_in_group(snap_require):
return
# Reject objects in the excluded snap group
if not snap_exclude.is_empty() and target.is_in_group(snap_exclude):
return
# Reject climbable objects
if target is XRToolsClimbable:
return
# Add to the list of objects in grab area
_object_in_grab_area.push_back(target)
# If this snap zone is configured to snap objects that are dropped, then
# start listening for the objects dropped signal
if snap_mode == SnapMode.DROPPED and target.has_signal("dropped"):
target.connect("dropped", _on_target_dropped, CONNECT_DEFERRED)
# Show highlight when something could be snapped
if not is_instance_valid(picked_up_object):
close_highlight_updated.emit(self, enabled)
# Called when a body leaves the snap zone
func _on_snap_zone_body_exited(target: Node3D) -> void:
# Ensure the object is not in our list
_object_in_grab_area.erase(target)
# Stop listening for dropped signals
if target.has_signal("dropped") and target.is_connected("dropped", _on_target_dropped):
target.disconnect("dropped", _on_target_dropped)
# Hide highlight when nothing could be snapped
if _object_in_grab_area.is_empty():
close_highlight_updated.emit(self, false)
# Test if this snap zone has a picked up object
func has_snapped_object() -> bool:
return is_instance_valid(picked_up_object)
# Pick up the specified object
func pick_up_object(target: Node3D) -> void:
# check if already holding an object
if is_instance_valid(picked_up_object):
# skip if holding the target object
if picked_up_object == target:
return
# holding something else? drop it
drop_object()
# skip if target null or freed
if not is_instance_valid(target):
return
# Pick up our target. Note, target may do instant drop_and_free
picked_up_object = target
if has_node("AudioStreamPlayer3D"):
var player = get_node("AudioStreamPlayer3D")
if is_instance_valid(player):
if player.playing:
player.stop()
player.stream = stash_sound
player.play()
target.pick_up(self)
# If object picked up then emit signal
if is_instance_valid(picked_up_object):
has_picked_up.emit(picked_up_object)
highlight_updated.emit(self, false)
# Called when the enabled property has been modified
func _set_enabled(p_enabled: bool) -> void:
enabled = p_enabled
if is_inside_tree:
highlight_updated.emit(
self,
enabled and not is_instance_valid(picked_up_object))
# Called when the grab distance has been modified
func _set_grab_distance(new_value: float) -> void:
grab_distance = new_value
if is_inside_tree() and $CollisionShape3D:
$CollisionShape3D.shape.radius = grab_distance
# Called when the snap mode property has been modified
func _set_snap_mode(new_value: SnapMode) -> void:
snap_mode = new_value
if is_inside_tree():
_update_snap_mode()
# Handle changes to the snap mode
func _update_snap_mode() -> void:
match snap_mode:
SnapMode.DROPPED:
# Disable _process as we aren't using RANGE pickups
set_process(false)
# Start monitoring all objects in range for drop
for o in _object_in_grab_area:
o.connect("dropped", _on_target_dropped, CONNECT_DEFERRED)
SnapMode.RANGE:
# Enable _process to scan for RANGE pickups
set_process(true)
# Clear any dropped signal hooks
for o in _object_in_grab_area:
o.disconnect("dropped", _on_target_dropped)
# Called when a target in our grab area is dropped
func _on_target_dropped(target: Node3D) -> void:
# Skip if not enabled
if not enabled:
return
# Skip if already holding a valid object
if is_instance_valid(picked_up_object):
return
# Skip if the target is not valid
if not is_instance_valid(target):
return
# Pick up the target if we can
if target.can_pick_up(self):
pick_up_object(target)

View File

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

View File

@@ -0,0 +1,23 @@
[gd_scene load_steps=3 format=3 uid="uid://ce7vysyvondf8"]
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1"]
[sub_resource type="SphereShape3D" id="1"]
resource_local_to_scene = true
radius = 0.1
[node name="SnapZone" type="Area3D"]
collision_layer = 4
collision_mask = 65540
script = ExtResource("1")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("1")
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="."]
unit_size = 3.0
max_db = 1.0
max_distance = 100.0
[connection signal="body_entered" from="." to="." method="_on_snap_zone_body_entered"]
[connection signal="body_exited" from="." to="." method="_on_snap_zone_body_exited"]

View File

@@ -0,0 +1,29 @@
@tool
class_name XRToolsTeleportArea
extends Area3D
## Target node
@export var target : Node3D
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsTeleportArea"
# Called when the node enters the scene tree for the first time.
func _ready():
# Handle body entered
body_entered.connect(_on_body_entered)
# Handle body entering area
func _on_body_entered(body : Node3D) -> void:
# Test if the body is the player
var player_body := body as XRToolsPlayerBody
if not player_body:
return
# Teleport the player
player_body.teleport(target.global_transform)

View File

@@ -0,0 +1 @@
uid://153oneb2r3uq

View File

@@ -0,0 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://dpy1eg3i331se"]
[ext_resource type="Script" uid="uid://153oneb2r3uq" path="res://addons/godot-xr-tools/objects/teleport_area.gd" id="1_0awk1"]
[node name="TeleportArea" type="Area3D"]
collision_layer = 0
collision_mask = 524288
script = ExtResource("1_0awk1")

View File

@@ -0,0 +1,658 @@
@tool
class_name XRToolsViewport2DIn3D
extends Node3D
## XR ToolsViewport 2D in 3D
##
## This script manages a 2D scene rendered as a texture on a 3D quad.
##
## Pointer and keyboard input are mapped into the 2D scene.
## Signal for pointer events
signal pointer_event(event)
## Transparent property
enum TransparancyMode {
OPAQUE, ## Render opaque
TRANSPARENT, ## Render transparent
SCISSOR, ## Render using alpha-scissor
}
## Viewport Update Mode
enum UpdateMode {
UPDATE_ONCE, ## Update once (redraw triggered if set again to UPDATE_ONCE)
UPDATE_ALWAYS, ## Update on every frame
UPDATE_THROTTLED, ## Update at throttled rate
}
# The following dirty flags are private (leading _) to suppress them in the
# generated documentation. Unfortunately gdlint complaints on private constants
# (see https://github.com/Scony/godot-gdscript-toolkit/issues/223). Until this
# is fixed we suppress the rule.
# gdlint: disable=constant-name
# State dirty flags
const _DIRTY_NONE := 0x0000 # Everything up to date
const _DIRTY_MATERIAL := 0x0001 # Material needs update
const _DIRTY_SCENE := 0x0002 # Scene needs update
const _DIRTY_SIZE := 0x0004 # Viewport size needs update
const _DIRTY_ALBEDO := 0x0008 # Albedo texture needs update
const _DIRTY_UPDATE := 0x0010 # Update mode needs update
const _DIRTY_TRANSPARENCY := 0x0020 # Transparency needs update
const _DIRTY_ALPHA_SCISSOR := 0x0040 # Alpha scissor needs update
const _DIRTY_UNSHADED := 0x0080 # Shade mode needs update
const _DIRTY_FILTERED := 0x0100 # Filter mode needs update
const _DIRTY_SURFACE := 0x0200 # Surface material needs update
const _DIRTY_REDRAW := 0x0400 # Redraw required
const _DIRTY_ALL := 0x07FF # All dirty
# Default layer of 1:static-world, 21:pointable, 23:ui-objects
const DEFAULT_LAYER := 0b0000_0000_0101_0000_0000_0000_0000_0001
# Physics property group
@export_group("Physics")
## Physical screen size property
@export var screen_size : Vector2 = Vector2(3.0, 2.0): set = set_screen_size
## Viewport collision enabled property
@export var enabled : bool = true: set = set_enabled
## Collision layer
@export_flags_3d_physics var collision_layer : int = DEFAULT_LAYER: set = set_collision_layer
# Content property group
@export_group("Content")
## Scene property
@export var scene : PackedScene: set = set_scene
## Viewport size property
@export var viewport_size : Vector2 = Vector2(300.0, 200.0): set = set_viewport_size
## Update Mode property
@export var update_mode : UpdateMode = UpdateMode.UPDATE_ALWAYS: set = set_update_mode
## Update throttle property
@export var throttle_fps : float = 30.0
# Input property group
@export_group("Input")
## Allow physical keyboard input to viewport
@export var input_keyboard : bool = true
## Allow gamepad input to viewport
@export var input_gamepad : bool = false
# Rendering property group
@export_group("Rendering")
## Custom material template
@export var material : StandardMaterial3D = null: set = set_material
## Transparent property
var transparent : TransparancyMode = TransparancyMode.TRANSPARENT: set = set_transparent
## Alpha Scissor Threshold property (ignored when custom material provided)
var alpha_scissor_threshold : float = 0.25: set = set_alpha_scissor_threshold
## Unshaded flag (ignored when custom material provided)
var unshaded : bool = false: set = set_unshaded
## Filtering flag (ignored when custom material provided)
var filter : bool = true: set = set_filter
var is_ready : bool = false
var scene_node : Node
var scene_properties_keys: PackedStringArray = []
var scene_properties : Array[Dictionary] = []
# Needed to apply custom properties of the scene before it is instanced, as these are set on ready,
# But at this point in time the scene is not instanced yet
var scene_proxy_configuration: Dictionary = {}
var viewport_texture : ViewportTexture
var time_since_last_update : float = 0.0
var _screen_material : StandardMaterial3D
var _dirty := _DIRTY_ALL
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsViewport2DIn3D"
# Called when the node enters the scene tree for the first time.
func _ready():
is_ready = true
# Listen for pointer events on the screen body
$StaticBody3D.connect("pointer_event", _on_pointer_event)
# Update enabled based on visibility
visibility_changed.connect(_on_visibility_changed)
# Apply physics properties
_update_screen_size()
_update_enabled()
_update_collision_layer()
# Update the render objects
_update_render()
# Provide custom property information
func _get_property_list() -> Array[Dictionary]:
# Select visibility of properties
var show_transparency := not material
var show_alpha_scissor := not material and transparent == TransparancyMode.SCISSOR
var show_unshaded := not material
var show_filter := not material
var extra_properties : Array[Dictionary] = [
{
name = "Rendering",
type = TYPE_NIL,
usage = PROPERTY_USAGE_GROUP
},
{
name = "transparent",
type = TYPE_BOOL,
usage = PROPERTY_USAGE_DEFAULT if show_transparency else PROPERTY_USAGE_NO_EDITOR
},
{
name = "alpha_scissor_threshold",
type = TYPE_FLOAT,
usage = PROPERTY_USAGE_DEFAULT if show_alpha_scissor else PROPERTY_USAGE_NO_EDITOR,
hint = PROPERTY_HINT_RANGE,
hint_string = "0.0,1.0"
},
{
name = "unshaded",
type = TYPE_BOOL,
usage = PROPERTY_USAGE_DEFAULT if show_unshaded else PROPERTY_USAGE_NO_EDITOR
},
{
name = "filter",
type = TYPE_BOOL,
usage = PROPERTY_USAGE_DEFAULT if show_filter else PROPERTY_USAGE_NO_EDITOR
},
# Store the scene property keys on the disk, so that even before the scene is loaded we
# know about the custom properties
{
name = "scene_properties_keys",
type = TYPE_PACKED_STRING_ARRAY,
usage = PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_STORAGE
}
]
# Add all the custom properties of the subscene so they show up in the editor
if scene_properties_keys.size() > 0:
extra_properties.append_array(scene_properties)
return extra_properties
# Forward setting and getting of custom properties of the child scene
func _get(property: StringName) -> Variant:
if scene_properties_keys.has(property):
var return_value: Variant = null
# If our scene is already instanced then get the property directly
if is_instance_valid(scene_node):
return_value = scene_node.get(property)
# If it is not instanced, we use the proxy configuration
elif scene_proxy_configuration.has(property):
return_value = scene_proxy_configuration[property]
# Special handling is required for NodePaths, as they are relative to the scene
if return_value is NodePath and !return_value.is_absolute():
var path_string : String = str(return_value)
# Remove the additional leading ../../
return_value = NodePath(path_string.substr(6, -1))
return return_value
# Keep normal behaviour
return null
func _set(property: StringName, value: Variant):
if scene_properties_keys.has(property):
# Special handling is required for NodePaths, as they are relative to the scene
if value is NodePath and !value.is_absolute():
# Add the additional leading ../../
value = NodePath("../../" + str(value))
# If our scene is already instanced then set the property directly
if is_instance_valid(scene_node):
scene_node.set(property, value)
# If it is not instanced yet, store it to the proxy configuration,
# which will get applied on scene load
else:
scene_proxy_configuration[property] = value
return true
# Keep normal behaviour
return false
# Allow revert of custom properties
func _property_can_revert(property : StringName) -> bool:
match property:
"alpha_scissor_threshold":
return true
"unshaded":
return true
"filter":
return true
_:
return false
# Provide revert values for custom properties
func _property_get_revert(property : StringName): # Variant
match property:
"alpha_scissor_threshold":
return 0.25
"unshaded":
return false
"filter":
return true
# When the scene_node changes, update the property list
func _update_scene_property_list():
scene_properties = []
scene_properties_keys = []
if is_instance_valid(scene_node):
# If the scene is queued for deletion, clear the scene proxy configuration
if scene_node.is_queued_for_deletion():
scene_proxy_configuration = {}
else:
# Extract relevant properties of the provided scene to display in the editor (forwarded)
var node_script: Script = scene_node.get_script() as Script
if node_script:
var all_properties := node_script.get_script_property_list()
# Join this with the custom property list of the object created by the script
if scene_node.has_method("_get_property_list"):
all_properties.append_array(scene_node.call("_get_property_list"))
for property in all_properties:
# Filter out only the properties that are supposed to be stored, or are used for grouping
if property["usage"] & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_GROUP \
| PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_SUBGROUP):
scene_properties.append(property)
scene_properties_keys.append(property["name"])
notify_property_list_changed()
## Get the 2D scene instance
func get_scene_instance() -> Node:
return scene_node
## Connect a 2D scene signal
func connect_scene_signal(which : String, callback : Callable, flags : int = 0):
if scene_node:
scene_node.connect(which, callback, flags)
# Handle pointer event from screen-body
func _on_pointer_event(event : XRToolsPointerEvent) -> void:
pointer_event.emit(event)
# Handler for input events
func _input(event):
# Map keyboard events to the viewport if enabled
if input_keyboard and (event is InputEventKey or event is InputEventShortcut):
$Viewport.push_input(event)
return
# Map gamepad events to the viewport if enable
if input_gamepad and (event is InputEventJoypadButton or event is InputEventJoypadMotion):
$Viewport.push_input(event)
return
# Process event
func _process(delta):
# Process screen refreshing
if Engine.is_editor_hint():
# Perform periodic material refreshes to handle the user modifying the
# material properties in the editor
time_since_last_update += delta
if time_since_last_update > 1.0:
time_since_last_update = 0.0
# Trigger material refresh
_dirty = _DIRTY_MATERIAL
_update_render()
elif update_mode == UpdateMode.UPDATE_THROTTLED:
# Perform throttled updates of the viewport
var frame_time = 1.0 / throttle_fps
time_since_last_update += delta
if time_since_last_update > frame_time:
time_since_last_update = 0.0
# Trigger update
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
else:
# This is no longer needed
set_process(false)
# Handle visibility changed
func _on_visibility_changed() -> void:
# Fire visibility changed in scene
if scene_node:
scene_node.propagate_notification(
CanvasItem.NOTIFICATION_VISIBILITY_CHANGED)
# Update collision and rendering based on visibility
_update_enabled()
_dirty |= _DIRTY_UPDATE
_update_render()
## Set screen size property
func set_screen_size(new_size: Vector2) -> void:
screen_size = new_size
if is_ready:
_update_screen_size()
## Set enabled property
func set_enabled(is_enabled: bool) -> void:
enabled = is_enabled
if is_ready:
_update_enabled()
## Set collision layer property
func set_collision_layer(new_layer: int) -> void:
collision_layer = new_layer
if is_ready:
_update_collision_layer()
## Set scene property
func set_scene(new_scene: PackedScene) -> void:
scene = new_scene
_dirty |= _DIRTY_SCENE
if is_ready:
_update_render()
## Set viewport size property
func set_viewport_size(new_size: Vector2) -> void:
viewport_size = new_size
_dirty |= _DIRTY_SIZE
if is_ready:
_update_render()
## Set update mode property
func set_update_mode(new_update_mode: UpdateMode) -> void:
update_mode = new_update_mode
_dirty |= _DIRTY_UPDATE
if is_ready:
_update_render()
## Set material property
func set_material(new_material: StandardMaterial3D) -> void:
material = new_material
notify_property_list_changed()
# Discard our screen material, _update_render will create a new one.
_screen_material = null
_dirty |= _DIRTY_MATERIAL
if is_ready:
_update_render()
## Set transparent property
func set_transparent(new_transparent: TransparancyMode) -> void:
transparent = new_transparent
notify_property_list_changed()
_dirty |= _DIRTY_TRANSPARENCY
if is_ready:
_update_render()
## Set the alpha scisser threshold
func set_alpha_scissor_threshold(new_threshold: float) -> void:
alpha_scissor_threshold = new_threshold
_dirty |= _DIRTY_ALPHA_SCISSOR
if is_ready:
_update_render()
## Set the unshaded property
func set_unshaded(new_unshaded : bool) -> void:
unshaded = new_unshaded
_dirty |= _DIRTY_UNSHADED
if is_ready:
_update_render()
## Set filter property
func set_filter(new_filter: bool) -> void:
filter = new_filter
_dirty |= _DIRTY_FILTERED
if is_ready:
_update_render()
# Screen size update handler
func _update_screen_size() -> void:
$Screen.mesh.size = screen_size
$StaticBody3D.screen_size = screen_size
$StaticBody3D/CollisionShape3D.shape.size = Vector3(
screen_size.x,
screen_size.y,
0.02)
# Enabled update handler
func _update_enabled() -> void:
if Engine.is_editor_hint():
return
$StaticBody3D/CollisionShape3D.disabled = !enabled or not is_visible_in_tree()
# Collision layer update handler
func _update_collision_layer() -> void:
$StaticBody3D.collision_layer = collision_layer
# This complex function processes the render dirty flags and performs the
# minimal number of updates to get the render objects into the correct state.
func _update_render() -> void:
# Handle material change
if _dirty & _DIRTY_MATERIAL:
_dirty &= ~_DIRTY_MATERIAL
if material:
# We can't use our material directly because each instance uses
# it's own ViewportTexture. So we duplicate our material.
if not _screen_material:
_screen_material = material.duplicate()
else:
# We should only get here if we're in our editor.
# We can't detect when our material changes,
# so we need to check for changed properties.
for property in ClassDB.class_get_property_list("BaseMaterial3D", true):
# If any of the material properties we do not manage changed, update them.
if property.name != "albedo_texture":
var was_value = _screen_material.get(property.name)
var new_value = material.get(property.name)
if was_value != new_value:
_screen_material.set(property.name, new_value)
elif not _screen_material:
# Create new local material
_screen_material = StandardMaterial3D.new()
# Disable culling
_screen_material.params_cull_mode = StandardMaterial3D.CULL_DISABLED
# Ensure local material is configured
_dirty |= _DIRTY_TRANSPARENCY | \
_DIRTY_ALPHA_SCISSOR | \
_DIRTY_UNSHADED | \
_DIRTY_FILTERED
# Ensure new material renders viewport onto surface
_dirty |= _DIRTY_ALBEDO | _DIRTY_SURFACE
# If we have no screen material then skip everything else
if not _screen_material:
return
# Handle scene change
if _dirty & _DIRTY_SCENE:
_dirty &= ~_DIRTY_SCENE
# Out with the old
if is_instance_valid(scene_node):
if scene_node.property_list_changed.is_connected(_update_scene_property_list):
scene_node.property_list_changed.disconnect(_update_scene_property_list)
$Viewport.remove_child(scene_node)
scene_node.queue_free()
_update_scene_property_list()
# In with the new
if scene:
# Instantiate provided scene
scene_node = scene.instantiate()
_update_scene_property_list()
scene_node.property_list_changed.connect(_update_scene_property_list)
# Apply the scene proxy configuration on the first load
for key in scene_properties_keys:
if scene_proxy_configuration.has(key):
scene_node.set(key, scene_proxy_configuration[key])
# Finally add it to the scene, so values are available in _ready
$Viewport.add_child(scene_node)
elif $Viewport.get_child_count() == 1:
# Use already-provided scene
scene_node = $Viewport.get_child(0)
# Ensure the new scene is rendered at least once
_dirty |= _DIRTY_REDRAW
# Handle viewport size change
if _dirty & _DIRTY_SIZE:
_dirty &= ~_DIRTY_SIZE
# Set the viewport size
$Viewport.size = viewport_size
$StaticBody3D.viewport_size = viewport_size
# Perform redraw to let viewport texture update correctly after changing the viewport's size
_dirty |= _DIRTY_REDRAW
# Handle albedo change:
if _dirty & _DIRTY_ALBEDO:
_dirty &= ~_DIRTY_ALBEDO
# Set the screen material to use the viewport for the albedo channel
viewport_texture = $Viewport.get_texture()
_screen_material.albedo_texture = viewport_texture
# Handle update mode change
if _dirty & _DIRTY_UPDATE:
_dirty &= ~_DIRTY_UPDATE
# Apply update rules
if Engine.is_editor_hint():
# Update once. Process function used for editor refreshes
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
set_process(true)
elif update_mode == UpdateMode.UPDATE_ONCE or not is_visible_in_tree():
# Update once. Process function not used
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
set_process(false)
elif update_mode == UpdateMode.UPDATE_ALWAYS:
# Update always. Process function not used
$Viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
set_process(false)
elif update_mode == UpdateMode.UPDATE_THROTTLED:
# Update once. Process function triggers periodic refresh
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
set_process(true)
# Handle transparency update
if _dirty & _DIRTY_TRANSPARENCY:
_dirty &= ~_DIRTY_TRANSPARENCY
# If using a temporary material then update transparency
if _screen_material and not material:
# Set the transparancy mode
match transparent:
TransparancyMode.OPAQUE:
_screen_material.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED
TransparancyMode.TRANSPARENT:
_screen_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
TransparancyMode.SCISSOR:
_screen_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
# Set the viewport background transparency mode and force a redraw
$Viewport.transparent_bg = transparent != TransparancyMode.OPAQUE
_dirty |= _DIRTY_REDRAW
# Handle alpha scissor update
if _dirty & _DIRTY_ALPHA_SCISSOR:
_dirty &= ~_DIRTY_ALPHA_SCISSOR
# If using a temporary material with alpha-scissor then update
if _screen_material and not material and transparent == TransparancyMode.SCISSOR:
_screen_material.params_alpha_scissor_threshold = alpha_scissor_threshold
# Handle unshaded update
if _dirty & _DIRTY_UNSHADED:
_dirty &= ~_DIRTY_UNSHADED
# If using a temporary material then update the shading mode and force a redraw
if _screen_material and not material:
_screen_material.shading_mode = (
BaseMaterial3D.SHADING_MODE_UNSHADED if unshaded else
BaseMaterial3D.SHADING_MODE_PER_PIXEL)
#_dirty |= _DIRTY_REDRAW
# Handle filter update
if _dirty & _DIRTY_FILTERED:
_dirty &= ~_DIRTY_FILTERED
# If using a temporary material then update the filter mode and force a redraw
if _screen_material and not material:
_screen_material.texture_filter = (
BaseMaterial3D.TEXTURE_FILTER_LINEAR if filter else
BaseMaterial3D.TEXTURE_FILTER_NEAREST)
#_dirty |= _DIRTY_REDRAW
# Handle surface material update
if _dirty & _DIRTY_SURFACE:
_dirty &= ~_DIRTY_SURFACE
# Set the screen to render using the new screen material
$Screen.set_surface_override_material(0, _screen_material)
# Handle forced redraw of the viewport
if _dirty & _DIRTY_REDRAW:
_dirty &= ~_DIRTY_REDRAW
# Force a redraw of the viewport
if Engine.is_editor_hint() or update_mode == UpdateMode.UPDATE_ONCE:
$Viewport.render_target_update_mode = SubViewport.UPDATE_ONCE

View File

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

View File

@@ -0,0 +1,47 @@
[gd_scene load_steps=7 format=3 uid="uid://clujaf3u776a3"]
[ext_resource type="Script" uid="uid://eavk7267igp7" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.gd" id="1"]
[ext_resource type="Script" uid="uid://cpl5yiyf1ypft" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d_body.gd" id="2"]
[sub_resource type="QuadMesh" id="1"]
resource_local_to_scene = true
size = Vector2(3, 2)
[sub_resource type="ViewportTexture" id="ViewportTexture_ufe4n"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_fvml0"]
transparency = 1
cull_mode = 2
albedo_texture = SubResource("ViewportTexture_ufe4n")
texture_filter = 1
[sub_resource type="BoxShape3D" id="4"]
resource_local_to_scene = true
size = Vector3(3, 2, 0.02)
[node name="Viewport2Din3D" type="Node3D"]
script = ExtResource("1")
alpha_scissor_threshold = 0.25
unshaded = false
filter = true
[node name="Viewport" type="SubViewport" parent="."]
disable_3d = true
transparent_bg = true
gui_embed_subwindows = true
size = Vector2i(300, 200)
render_target_update_mode = 1
[node name="Screen" type="MeshInstance3D" parent="."]
mesh = SubResource("1")
surface_material_override/0 = SubResource("StandardMaterial3D_fvml0")
[node name="StaticBody3D" type="StaticBody3D" parent="."]
collision_layer = 5242881
collision_mask = 0
script = ExtResource("2")
viewport_size = Vector2(300, 200)
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.01)
shape = SubResource("4")

View File

@@ -0,0 +1,217 @@
extends XRToolsInteractableBody
## Screen size
@export var screen_size = Vector2(3.0, 2.0)
## Viewport size
@export var viewport_size = Vector2(100.0, 100.0)
# Current mouse mask
var _mouse_mask := 0
# Viewport node
var _viewport : Viewport
# Dictionary of pointers to touch-index
var _touches := {}
# Dictionary of pressed pointers
var _presses := {}
# Dominant pointer (index == 0)
var _dominant : Node3D
# Mouse pointer
var _mouse : Node3D
# Last mouse position
var _mouse_last := Vector2.ZERO
func _ready():
# Get viewport node
_viewport = get_node("../Viewport")
# Subscribe to pointer events
pointer_event.connect(_on_pointer_event)
## Convert intersection point to screen coordinate
func global_to_viewport(p_at : Vector3) -> Vector2:
var t = $CollisionShape3D.global_transform
var at = t.affine_inverse() * p_at
# Convert to screen space
at.x = ((at.x / screen_size.x) + 0.5) * viewport_size.x
at.y = (0.5 - (at.y / screen_size.y)) * viewport_size.y
return Vector2(at.x, at.y)
# Pointer event handler
func _on_pointer_event(event : XRToolsPointerEvent) -> void:
# Ignore if we have no viewport
if not is_instance_valid(_viewport):
return
# Get the pointer and event type
var pointer := event.pointer
var type := event.event_type
# Get the touch-index [0..]
var index : int = _touches.get(pointer, -1)
# Create a new touch-index if necessary
if index < 0 or type == XRToolsPointerEvent.Type.ENTERED:
# Clear any stale pointer information
_touches.erase(pointer)
_presses.erase(pointer)
# Assign a new touch-index for the pointer
index = _next_touch_index()
_touches[pointer] = index
# Detect dominant pointer
if index == 0:
_dominant = pointer
# Get the viewport positions
var at := global_to_viewport(event.position)
var last := global_to_viewport(event.last_position)
# Get/update pressed state
var pressed : bool
match type:
XRToolsPointerEvent.Type.PRESSED:
_presses[pointer] = true
pressed = true
XRToolsPointerEvent.Type.RELEASED:
_presses.erase(pointer)
pressed = false
_:
pressed = _presses.has(pointer)
# Dispatch touch events
match type:
XRToolsPointerEvent.Type.PRESSED:
_report_touch_down(index, at)
XRToolsPointerEvent.Type.RELEASED:
_report_touch_up(index, at)
XRToolsPointerEvent.Type.MOVED:
_report_touch_move(index, pressed, last, at)
# If the current mouse isn't pressed then consider switching to a new one
if not _presses.has(_mouse):
if type == XRToolsPointerEvent.Type.PRESSED and pointer is XRToolsFunctionPointer:
# Switch to pressed laser-pointer
_mouse = pointer
elif type == XRToolsPointerEvent.Type.EXITED and pointer == _mouse:
# Current mouse leaving, switch to dominant
_mouse = _dominant
elif not _mouse and _dominant:
# No mouse, pick the dominant
_mouse = _dominant
# Fire mouse events
if pointer == _mouse:
match type:
XRToolsPointerEvent.Type.PRESSED:
_report_mouse_down(at)
XRToolsPointerEvent.Type.RELEASED:
_report_mouse_up( at)
XRToolsPointerEvent.Type.MOVED:
_report_mouse_move(pressed, last, at)
# Clear pointer information on exit
if type == XRToolsPointerEvent.Type.EXITED:
# Clear pointer information
_touches.erase(pointer)
_presses.erase(pointer)
if pointer == _dominant:
_dominant = null
if pointer == _mouse:
_mouse = null
# Report touch-down event
func _report_touch_down(index : int, at : Vector2) -> void:
var event := InputEventScreenTouch.new()
event.index = index
event.position = at
event.pressed = true
_viewport.push_input(event)
# Report touch-up event
func _report_touch_up(index : int, at : Vector2) -> void:
var event := InputEventScreenTouch.new()
event.index = index
event.position = at
event.pressed = false
_viewport.push_input(event)
# Report touch-move event
func _report_touch_move(index : int, pressed : bool, from : Vector2, to : Vector2) -> void:
var event := InputEventScreenDrag.new()
event.index = index
event.position = to
event.pressure = 1.0 if pressed else 0.0
event.relative = to - from
_viewport.push_input(event)
# Report mouse-down event
func _report_mouse_down(at : Vector2) -> void:
var event := InputEventMouseButton.new()
event.button_index = 1
event.pressed = true
event.position = at
event.global_position = at
event.button_mask = 1
_viewport.push_input(event)
# Report mouse-up event
func _report_mouse_up(at : Vector2) -> void:
var event := InputEventMouseButton.new()
event.button_index = 1
event.pressed = false
event.position = at
event.global_position = at
event.button_mask = 0
_viewport.push_input(event)
# Report mouse-move event
func _report_mouse_move(pressed : bool, from : Vector2, to : Vector2) -> void:
var event := InputEventMouseMotion.new()
event.position = to
event.global_position = to
event.relative = to - from
event.button_mask = 1 if pressed else 0
event.pressure = 1.0 if pressed else 0.0
_viewport.push_input(event)
# Find the next free touch index
func _next_touch_index() -> int:
# Get the current touches
var current := _touches.values()
current.sort()
# Look for a hole
for touch in current.size():
if current[touch] != touch:
return touch
# No hole so add to end
return current.size()

View File

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

View File

@@ -0,0 +1,44 @@
[gd_scene load_steps=7 format=3 uid="uid://dgdb1texynduw"]
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="1"]
[ext_resource type="PackedScene" uid="uid://lauwp8okd1vh" path="res://addons/godot-xr-tools/objects/keyboard/virtual_keyboard_2d.tscn" id="2"]
[sub_resource type="QuadMesh" id="QuadMesh_iets3"]
resource_local_to_scene = true
size = Vector2(1.5, 0.75)
[sub_resource type="ViewportTexture" id="ViewportTexture_ncnoj"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_kjc12"]
cull_mode = 2
shading_mode = 0
albedo_texture = SubResource("ViewportTexture_ncnoj")
texture_filter = 1
[sub_resource type="BoxShape3D" id="BoxShape3D_twvoe"]
resource_local_to_scene = true
size = Vector3(1.5, 0.75, 0.02)
[node name="VirtualKeyboard" instance=ExtResource("1")]
screen_size = Vector2(1.5, 0.75)
scene = ExtResource("2")
viewport_size = Vector2(400, 200)
update_mode = 2
throttle_fps = 15.0
transparent = 0
unshaded = true
[node name="Viewport" parent="." index="0"]
transparent_bg = false
size = Vector2i(400, 200)
[node name="Screen" parent="." index="1"]
mesh = SubResource("QuadMesh_iets3")
surface_material_override/0 = SubResource("StandardMaterial3D_kjc12")
[node name="StaticBody3D" parent="." index="2"]
screen_size = Vector2(1.5, 0.75)
viewport_size = Vector2(400, 200)
[node name="CollisionShape3D" parent="StaticBody3D" index="0"]
shape = SubResource("BoxShape3D_twvoe")

View File

@@ -0,0 +1,14 @@
@tool
class_name XRToolsWindArea
extends Area3D
## Vector (direction and magnitude) of wind in this area
@export var wind_vector : Vector3 = Vector3.ZERO
## Wind drag factor
@export var drag : float = 1.0
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsWindArea"

View File

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

View File

@@ -0,0 +1,11 @@
[gd_scene load_steps=2 format=3 uid="uid://nack1qite6lx"]
[ext_resource type="Script" uid="uid://c7l0klrkdolyw" path="res://addons/godot-xr-tools/objects/wind_area.gd" id="1"]
[node name="WindArea" type="Area3D"]
collision_layer = 524288
collision_mask = 0
monitoring = false
script = ExtResource("1")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]

View File

@@ -0,0 +1,64 @@
@tool
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
class_name XRToolsWorldGrabArea
extends Area3D
## XR Tools World-Grab Area
##
## This script adds world-grab areas to an environment
##
## For world-grab to work, the player must have an [XRToolsMovementWorldGrab]
## node configured appropriately.
## If true, the grip control must be held to keep holding the climbable
var press_to_hold : bool = true
## Dictionary of temporary grab-handles indexed by the pickup node.
var grab_locations := {}
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsWorldGrabArea"
# Called by XRToolsFunctionPickup
func is_picked_up() -> bool:
return false
func can_pick_up(_by: Node3D) -> bool:
return true
# Called by XRToolsFunctionPickup when user presses the action button while holding this object
func action():
pass
# Ignore highlighting requests from XRToolsFunctionPickup
func request_highlight(_from, _on) -> void:
pass
# Called by XRToolsFunctionPickup when this is picked up by a controller
func pick_up(by: Node3D) -> void:
# Get the ID to save the grab handle under
var id = by.get_instance_id()
# Get or construct the grab handle
var handle = grab_locations.get(id)
if not handle:
handle = Node3D.new()
add_child(handle)
grab_locations[id] = handle
# Set the handles global transform. As it's a child of this
# climbable it will move as the climbable moves
handle.global_transform = by.global_transform
# Called by XRToolsFunctionPickup when this is let go by a controller
func let_go(_by: Node3D, _p_linear_velocity: Vector3, _p_angular_velocity: Vector3) -> void:
pass
# Get the grab handle
func get_grab_handle(p: Node3D) -> Node3D:
return grab_locations.get(p.get_instance_id())

View File

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

View File

@@ -0,0 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://57q7hhomocdh"]
[ext_resource type="Script" uid="uid://curcpmtyhxa1b" path="res://addons/godot-xr-tools/objects/world_grab_area.gd" id="1_uxhq5"]
[node name="WorldGrabArea" type="Area3D"]
collision_layer = 262144
collision_mask = 524288
script = ExtResource("1_uxhq5")