Browse Source

first commit

miziziziz 2 years ago
commit
8c99829e45
100 changed files with 4970 additions and 0 deletions
  1. 2 0
      .gitattributes
  2. 2 0
      .gitignore
  3. 5 0
      README.md
  4. 20 0
      addons/godot-xr-tools/CONTRIBUTORS.md
  5. 21 0
      addons/godot-xr-tools/LICENSE
  6. 152 0
      addons/godot-xr-tools/VERSIONS.md
  7. BIN
      addons/godot-xr-tools/assets/misc/Hold trigger to continue.png
  8. 35 0
      addons/godot-xr-tools/assets/misc/Hold trigger to continue.png.import
  9. BIN
      addons/godot-xr-tools/assets/misc/progress_bar.png
  10. 35 0
      addons/godot-xr-tools/assets/misc/progress_bar.png.import
  11. 32 0
      addons/godot-xr-tools/audio/surface_audio.gd
  12. 6 0
      addons/godot-xr-tools/audio/surface_audio.tscn
  13. 41 0
      addons/godot-xr-tools/audio/surface_audio_type.gd
  14. 121 0
      addons/godot-xr-tools/editor/icons/LICENSE
  15. 1 0
      addons/godot-xr-tools/editor/icons/body.svg
  16. 37 0
      addons/godot-xr-tools/editor/icons/body.svg.import
  17. 43 0
      addons/godot-xr-tools/editor/icons/foot.svg
  18. 37 0
      addons/godot-xr-tools/editor/icons/foot.svg.import
  19. 1 0
      addons/godot-xr-tools/editor/icons/function.svg
  20. 37 0
      addons/godot-xr-tools/editor/icons/function.svg.import
  21. 1 0
      addons/godot-xr-tools/editor/icons/hand.svg
  22. 37 0
      addons/godot-xr-tools/editor/icons/hand.svg.import
  23. 1 0
      addons/godot-xr-tools/editor/icons/movement_provider.svg
  24. 37 0
      addons/godot-xr-tools/editor/icons/movement_provider.svg.import
  25. 1 0
      addons/godot-xr-tools/editor/icons/node.svg
  26. 37 0
      addons/godot-xr-tools/editor/icons/node.svg.import
  27. 195 0
      addons/godot-xr-tools/effects/vignette.gd
  28. 34 0
      addons/godot-xr-tools/effects/vignette.gdshader
  29. 10 0
      addons/godot-xr-tools/effects/vignette.tres
  30. 13 0
      addons/godot-xr-tools/effects/vignette.tscn
  31. 92 0
      addons/godot-xr-tools/examples/fall_damage.gd
  32. 6 0
      addons/godot-xr-tools/examples/fall_damage.tscn
  33. 430 0
      addons/godot-xr-tools/functions/function_pickup.gd
  34. 6 0
      addons/godot-xr-tools/functions/function_pickup.tscn
  35. 363 0
      addons/godot-xr-tools/functions/function_pointer.gd
  36. 34 0
      addons/godot-xr-tools/functions/function_pointer.tscn
  37. 99 0
      addons/godot-xr-tools/functions/function_pose_detector.gd
  38. 19 0
      addons/godot-xr-tools/functions/function_pose_detector.tscn
  39. 392 0
      addons/godot-xr-tools/functions/function_teleport.gd
  40. 48 0
      addons/godot-xr-tools/functions/function_teleport.tscn
  41. 230 0
      addons/godot-xr-tools/functions/movement_climb.gd
  42. 6 0
      addons/godot-xr-tools/functions/movement_climb.tscn
  43. 92 0
      addons/godot-xr-tools/functions/movement_crouch.gd
  44. 6 0
      addons/godot-xr-tools/functions/movement_crouch.tscn
  45. 87 0
      addons/godot-xr-tools/functions/movement_direct.gd
  46. 6 0
      addons/godot-xr-tools/functions/movement_direct.tscn
  47. 228 0
      addons/godot-xr-tools/functions/movement_flight.gd
  48. 6 0
      addons/godot-xr-tools/functions/movement_flight.tscn
  49. 245 0
      addons/godot-xr-tools/functions/movement_footstep.gd
  50. 8 0
      addons/godot-xr-tools/functions/movement_footstep.tscn
  51. 234 0
      addons/godot-xr-tools/functions/movement_glide.gd
  52. 6 0
      addons/godot-xr-tools/functions/movement_glide.tscn
  53. 243 0
      addons/godot-xr-tools/functions/movement_grapple.gd
  54. 28 0
      addons/godot-xr-tools/functions/movement_grapple.tscn
  55. 52 0
      addons/godot-xr-tools/functions/movement_jump.gd
  56. 6 0
      addons/godot-xr-tools/functions/movement_jump.tscn
  57. 197 0
      addons/godot-xr-tools/functions/movement_physical_jump.gd
  58. 7 0
      addons/godot-xr-tools/functions/movement_physical_jump.tscn
  59. 93 0
      addons/godot-xr-tools/functions/movement_provider.gd
  60. 169 0
      addons/godot-xr-tools/functions/movement_sprint.gd
  61. 6 0
      addons/godot-xr-tools/functions/movement_sprint.tscn
  62. 111 0
      addons/godot-xr-tools/functions/movement_turn.gd
  63. 6 0
      addons/godot-xr-tools/functions/movement_turn.tscn
  64. 44 0
      addons/godot-xr-tools/functions/movement_wall_walk.gd
  65. 6 0
      addons/godot-xr-tools/functions/movement_wall_walk.tscn
  66. 135 0
      addons/godot-xr-tools/functions/movement_wind.gd
  67. 6 0
      addons/godot-xr-tools/functions/movement_wind.tscn
  68. 17 0
      addons/godot-xr-tools/hands/About.md
  69. 126 0
      addons/godot-xr-tools/hands/License.md
  70. 81 0
      addons/godot-xr-tools/hands/animations/left/AnimationPlayer.tscn
  71. BIN
      addons/godot-xr-tools/hands/animations/left/Cup.res
  72. BIN
      addons/godot-xr-tools/hands/animations/left/Default pose.res
  73. BIN
      addons/godot-xr-tools/hands/animations/left/Grip 1.res
  74. BIN
      addons/godot-xr-tools/hands/animations/left/Grip 2.res
  75. BIN
      addons/godot-xr-tools/hands/animations/left/Grip 3.res
  76. BIN
      addons/godot-xr-tools/hands/animations/left/Grip 4.res
  77. BIN
      addons/godot-xr-tools/hands/animations/left/Grip 5.res
  78. BIN
      addons/godot-xr-tools/hands/animations/left/Grip Shaft.res
  79. BIN
      addons/godot-xr-tools/hands/animations/left/Grip.res
  80. BIN
      addons/godot-xr-tools/hands/animations/left/Hold.res
  81. BIN
      addons/godot-xr-tools/hands/animations/left/Horns.res
  82. BIN
      addons/godot-xr-tools/hands/animations/left/Metal.res
  83. BIN
      addons/godot-xr-tools/hands/animations/left/Middle.res
  84. BIN
      addons/godot-xr-tools/hands/animations/left/OK.res
  85. BIN
      addons/godot-xr-tools/hands/animations/left/Peace.res
  86. BIN
      addons/godot-xr-tools/hands/animations/left/Pinch Flat.res
  87. BIN
      addons/godot-xr-tools/hands/animations/left/Pinch Large.res
  88. BIN
      addons/godot-xr-tools/hands/animations/left/Pinch Middle.res
  89. BIN
      addons/godot-xr-tools/hands/animations/left/Pinch Ring.res
  90. BIN
      addons/godot-xr-tools/hands/animations/left/Pinch Tight.res
  91. BIN
      addons/godot-xr-tools/hands/animations/left/Pinch Up.res
  92. BIN
      addons/godot-xr-tools/hands/animations/left/PingPong.res
  93. BIN
      addons/godot-xr-tools/hands/animations/left/Pinky.res
  94. BIN
      addons/godot-xr-tools/hands/animations/left/Pistol.res
  95. BIN
      addons/godot-xr-tools/hands/animations/left/Ring.res
  96. BIN
      addons/godot-xr-tools/hands/animations/left/Rounded.res
  97. BIN
      addons/godot-xr-tools/hands/animations/left/Sign 1.res
  98. BIN
      addons/godot-xr-tools/hands/animations/left/Sign 2.res
  99. BIN
      addons/godot-xr-tools/hands/animations/left/Sign 3.res
  100. BIN
      addons/godot-xr-tools/hands/animations/left/Sign 4.res

+ 2 - 0
.gitattributes

@@ -0,0 +1,2 @@
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+# Godot 4+ specific ignores
+.godot/

+ 5 - 0
README.md

@@ -0,0 +1,5 @@
+uses the OpenXR tools addon as a base, with some modifications to player_body.gd to adjust height to be short.
+
+new stuff is:
+	movement_hand_walk - handles moving the player body based on movement done by xr_tools_hand_pusher
+	xr_tools_hand_pusher - a physical object that pushes into the things and emits how much the player body needs to be offset by

+ 20 - 0
addons/godot-xr-tools/CONTRIBUTORS.md

@@ -0,0 +1,20 @@
+Contributors
+============
+
+The main author of this project is [Bastiaan Olij](https://github.com/BastiaanOlij) who manages the source repository found at:
+https://github.com/GodotVR/godot-xr-tools
+
+Other people who have helped out by submitting fixes, enhancements, etc are:
+- [Florian Jung](https://github.com/Windfisch)
+- [RMKD](https://github.com/RMKD)
+- [Alessandro Schillaci](https://github.com/silverslade)
+- [jtank4](https://github.com/jtank4)
+- [Malcolm Nixon](https://github.com/malcolmnixon)
+- [Sam Sarette](https://github.com/lunarcloud)
+- [Henodude](https://github.com/Henodude)
+- [Miodrag Sejic](https://github.com/DigitalN8m4r3)
+- [Carlos Padial](https://github.com/surreal6)
+- [Julian Todd](https://github.com/goatchurchprime)
+- [Kai Tödter](https://github.com/toedter)
+
+Want to be on this list? We would love your help.

+ 21 - 0
addons/godot-xr-tools/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018-2023 Bastiaan Olij and Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 152 - 0
addons/godot-xr-tools/VERSIONS.md

@@ -0,0 +1,152 @@
+# 4.1.0
+- Enhanced grappling to support collision and target layers
+- Added Godot Editor XR Tools menu for layers and openxr configuration
+- Improved gliding to support roll-turning while flapping
+
+# 4.0.0
+- Conversion to Godot 4
+- Fixed footstep resource leak and added jump sounds and footstep signal
+- Added grab-point switching to pickable objects
+- Added return-to-snap-zone feature
+
+# 3.4.0
+- Fixed footstep resource leak and added jump sounds and footstep signal
+- Added grab-point switching to pickable objects
+- Added return-to-snap-zone feature
+
+# 3.3.0
+- Added reset-scene and scene-control functions to scene-base
+- Fixed snap-zones stealing objects picked out of other near-by snap-zones
+- Improved player body so it can be used to child objects to
+- Updated scene/script default physics layers to match recommendations on website
+
+# 3.2.0
+- Minimum supported Godot version set to 3.5
+- Added glide option for turning with arm-roll
+- Added physics gravity effects on the player so they can walk around a planet
+- Added wall-walking movement provider
+- Cleaned the code to pass gdlint code checks
+- Modified to work with both WebXR and OpenXR
+- Added enable property to pickable objects
+- Added support for snap-on-drop to snap-zones
+- Added glide options for gaining altitude when flapping arms
+- Added option to disable snap-turn repeating by setting the delay to 0
+- Added capability for pointer function to auto-switch between controllers
+
+# 3.1.0
+- Improvements to our 2D in 3D viewport for filtering, unshaded, and transparency options
+- Fixed editor preview system for our 2D in 3D viewport
+- Use value based grip input with threshold
+- Improved pointer demo supporting left hand with switching
+- Enhanced pointer laser visibility options for colliding with targets
+- Implement poke feature (finger interaction)
+- Improvements to snap turning
+- Moved staging solution into plugin so it can be re-used
+- Allow setting different animations for hands
+- Added enable/disable to snap-zones
+- Added XR settings as Godot editor plugin and the ability to load and save the settings
+- Added crouching movement provider
+- Modified climbing to use the hand which most recently grabbed the climbing object
+- Added enable/disable to pickup function
+- Added ability to override hand material
+- Added realistic hand models and textures
+- Added ability to override hand animations
+- Added additional search functions to find nodes
+- Added support for viewport 2D in 3D to support 2D scenes instanced in the tree
+- Added sprinting movement provider
+- Added support for setting hand-poses when the hand enters an area
+- Added support for setting grab-points on objects, and the grab-points supporting different hand-poses
+
+# 3.0.0
+- Included demo project with test scenes to evaluate features
+- Standardized class naming convention for all scripts to "XRTools<PascalCaseName>"
+- Standardized file naming convention to "snake_case_name.ext"
+- Added many explicit type specifiers in preparation for GDScript 2.0
+- Renamed some functions to avoid name-collisions with Godot 4.0
+
+# 2.6.0
+- Fixed enforcement of direct-movement maximum speed
+- Added editor icons for all nodes
+- Added collision bouncing to PlayerBody
+
+# 2.5.0
+- Added advanced player height control
+- Modified climbing to collapse player to a sphere to allow mounting climbed objects
+- Added crouch movement provider
+- Added example fall damage detection
+- Added moving platform support to player body
+- Fixed player height-clamping to work in player-units
+- Fixed glide T-pose detection to work in player-units
+- Fixed jump detection to work in player-units
+- Added valid-layer checking to teleport movement
+- Modified hand meshes (blend and glb) to be scaled, so the hand scenes can be 1:1 scaled
+- Modified hands to scale with world_scale (required for godot-openxr 1.3.0 and later)
+- Added physics hands with PhysicsBody bones
+- Fixed disabling of `_process` in XRToolsPickable script
+
+# 2.4.1
+- Fixed grab distance
+- Fixed snap-zone instance drop and free issue
+- Movement provides react properly when disabled
+- Hiding grapple target when disabled
+
+# 2.4.0
+- Added configuration setting for head height in player body.
+- Added Function_JumpDetect_movement to detect jumping via the players body and/or arms
+- Improved responsiveness of snap-turning
+- Moved flight logic from Function_Direct_movement to Function_Flight_movement
+- Added option to disable player sliding on slopes
+- Added support for remote grabbing
+- Moved turning logic from Function_Direct_movement to Function_Turn_movement
+- Fixed movement provider servicing so disabled/bypassed providers can report their finished events
+- Added grappling movement provider
+- Added snap-zones
+
+# 2.3.0
+- Added vignette
+- Moved player physics into new PlayerBody asset (breaking change)
+- Moved Function_Direct_movement settings for player physics into PlayerBody
+- Added Function_Glide_movement to allow the player to glide
+- Added Function_Jump_movement to allow the player to jump
+- Added Function_Climb_movement to allow the player to climb
+- Redid the setup of the hands to make it easier to extend to other gestures
+- Improved pickup and throwing logic
+
+# 2.2
+- Changed default physics layers to make more sense (minor breaking change)
+- Replaced Center On Node property with PickupCenter node you can place
+- Made Object_pickable script work by itself and registers as class `XRToolsPickable`
+- New Object_interactable convenience script that registers as class `XRToolsInteractable` that reacts to our pointer function
+- Removed ducktype switch from pointer, pointer will use signals over ducktyping automatically (minor breaking change)
+
+# 2.1
+- added option to highlight object that can be picked up
+- added option to snap object to given location (if reset transform is true)
+- added callback when shader cache has finished
+- using proper UI for layers
+- added hand controllers that react on trigger and grip input
+- fixed delta on move and slide (breaking change!)
+- letting go of an object now adds angular velocity
+
+# 2.0
+- Renamed add on to **godot-xr-tools**
+- Add enums to our export variables
+- Add a switch on pickable objects to keep their current positioning when picked up
+- Move direct movement player collision slightly backwards based on player radius
+- Added switch between step turning and smooth turning
+- Fixed sizing issue with teleport
+- Added option to change pickup range
+
+# 1.2
+- Assign button to teleport function and no longer need to set origin
+- Added pickable object support
+- Fixed positioning of direct movement collision shape
+- Added strafe and fly mode for directional
+- Added ability to enable/disable the movement functions
+- Added 2D in 3D viewport for UI
+- Improved throwing by assigning linear velocity
+
+# 1.1*
+- previous versions were not tracked
+
+* Note that version history before 1.2 was not kept and is thus incomplete

BIN
addons/godot-xr-tools/assets/misc/Hold trigger to continue.png


+ 35 - 0
addons/godot-xr-tools/assets/misc/Hold trigger to continue.png.import

@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ocyj01x5mtt7"
+path.s3tc="res://.godot/imported/Hold trigger to continue.png-ce0a3a4de13c262f7015326bad2cb09d.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://addons/godot-xr-tools/assets/misc/Hold trigger to continue.png"
+dest_files=["res://.godot/imported/Hold trigger to continue.png-ce0a3a4de13c262f7015326bad2cb09d.s3tc.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0

BIN
addons/godot-xr-tools/assets/misc/progress_bar.png


+ 35 - 0
addons/godot-xr-tools/assets/misc/progress_bar.png.import

@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://clbtsf0ahb3fm"
+path.s3tc="res://.godot/imported/progress_bar.png-2ef3cbffca173889900be004fdeb1700.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://addons/godot-xr-tools/assets/misc/progress_bar.png"
+dest_files=["res://.godot/imported/progress_bar.png-2ef3cbffca173889900be004fdeb1700.s3tc.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0

+ 32 - 0
addons/godot-xr-tools/audio/surface_audio.gd

@@ -0,0 +1,32 @@
+@tool
+@icon("res://addons/godot-xr-tools/editor/icons/foot.svg")
+class_name XRToolsSurfaceAudio
+extends Node
+
+
+## XRTools Surface Audio Node
+##
+## This node is attached as a child of a StaticObject to give it a surface
+## audio type. This will cause the XRToolsMovementFootStep to play the correct
+## foot-step sounds when walking on the object.
+
+
+## XRToolsSurfaceAudioType to associate with this surface
+@export var surface_audio_type : XRToolsSurfaceAudioType
+
+
+# Add support for is_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsSurfaceAudio"
+
+
+# This method checks for configuration issues.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := PackedStringArray()
+
+	# Verify the camera
+	if !surface_audio_type:
+		warnings.append("Surface audio type not specified")
+
+	# Return warnings
+	return warnings

+ 6 - 0
addons/godot-xr-tools/audio/surface_audio.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://addons/godot-xr-tools/audio/surface_audio.gd" type="Script" id=1]
+
+[node name="SurfaceAudio" type="Node"]
+script = ExtResource( 1 )

+ 41 - 0
addons/godot-xr-tools/audio/surface_audio_type.gd

@@ -0,0 +1,41 @@
+@tool
+@icon("res://addons/godot-xr-tools/editor/icons/body.svg")
+class_name XRToolsSurfaceAudioType
+extends Resource
+
+
+## XRTools Surface Type Resource
+##
+## This resource defines a type of surface, and the audio streams to play when
+## the user steps on it
+
+
+## Surface name
+@export var name : String = ""
+
+## Optional audio stream to play when the player jumps on this surface
+@export var jump_sound : AudioStream
+
+## Optional audio stream to play when the player lands on this surface
+@export var hit_sound : AudioStream
+
+## Audio streams to play when the player walks on this surface
+@export var walk_sounds :Array[AudioStream] = []
+
+## Walking sound minimum pitch (to randomize steps)
+@export_range(0.5, 1.0) var walk_pitch_minimum : float = 0.8
+
+## Walking sound maximum pitch (to randomize steps)
+@export_range(1.0, 2.0) var walk_pitch_maximum : float = 1.2
+
+
+# This method checks for configuration issues.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := PackedStringArray()
+
+	# Verify the camera
+	if name == "":
+		warnings.append("Surface audio type must have a name")
+
+	# Return warnings
+	return warnings

+ 121 - 0
addons/godot-xr-tools/editor/icons/LICENSE

@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.

+ 1 - 0
addons/godot-xr-tools/editor/icons/body.svg

@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#fc7f7f"><path d="m5.640625.250438c-.75 0-.75 1-.75 1l.0028 1.7433557s-.0028.7623199.7656177.7594124l.7315823-.0027681v.75c-.9987826-.28732-2.3899024-.5553882-3.4887954-1.2601516-.4265336-.1870545-.9793234 1.1200236-.9793234 1.1200236 1.4123763 1.0085676 3.5979104.8866743 3.9681188 1.390128v1.75c-.5 1-1.5 2.75-2 3.5h2.25l1.5-2.75 1.25 2.75c.5 0 1.25 0 2 .000003-.5-1.000003-1.25-2.500006-1.75-3.500003v-1.75c.450387-.5452358 2.25264-.15852 3.39846-.4920078.282315-.5425377.182574-.6845166-.297031-1.5473061-1.253436.698062-1.535576.3459793-3.851429.7893139v-.75h.75c.25 0 .7631105-.245671.760209-.7445791l-.010209-1.7554209c0-.5-.25-1-1.25-1l-2.5.75h2.5v1.25c-.9605198.00114-1.745755-.00241-2.5 0v-1.25l2.5-.75z" fill-opacity=".99608" stroke-width=".762656" transform="translate(.609375 -.000438)"/><g fill="none" stroke="#ff8080" stroke-width="1.5"><path d="m3.7388367 3.4147117c-.9448159 1.3559779-.9992656 2.3269247-.4091983 3.6683904"/><path d="m12.545272 4.8349236c.597942-1.5043684.197251-2.2941014-.8486-3.1240651"/></g></g><g transform="translate(.5 -.000438)"><g fill="#fc7f7f"><path d="m12 11.999519c.55228 0 1 .448243 1 1.000481v1.000483.211639.786437 1.000479.0014h-1v-1c-.202186 0-.336627-.491203-.5-.5-1.452695-.07822-1.5-.503591-1.5-1h1 1v-.500438h-1-1c0-.552238.44772-1.000481 1-1.000481z" stroke-width=".999963"/><path d="m1.999832 12v1c0 .55228.44772 1 1 1-.55228 0-1 .44772-1 1v1h1v-1h1v1h1v-1c0-.55228-.44772-1-1-1 .55228 0 1-.44772 1-1v-1h-1v1h-1v-1z"/><path d="m5.999832 12v1 3h1v-1h1v1h1v-1c-.000834-.17579-.047991-.34825-.13672-.5.088728-.15175.13588-.32421.13672-.5v-1c0-.55228-.44772-1-1-1h-1zm1 1h1v1h-1z"/></g><g fill="#ff8080" stroke="#ff8080" stroke-width=".500002"><path d="m11.721643 12.70092h2"/><path d="m11.694618 13.781362h2"/></g></g></svg>

+ 37 - 0
addons/godot-xr-tools/editor/icons/body.svg.import

@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cyg33jxco0rh6"
+path="res://.godot/imported/body.svg-324e141d452c32f3136ca97c338025b4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot-xr-tools/editor/icons/body.svg"
+dest_files=["res://.godot/imported/body.svg-324e141d452c32f3136ca97c338025b4.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false

+ 43 - 0
addons/godot-xr-tools/editor/icons/foot.svg

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg40"
+   sodipodi:docname="foot.svg"
+   inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs44" />
+  <sodipodi:namedview
+     id="namedview42"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="22.627417"
+     inkscape:cx="25.654718"
+     inkscape:cy="13.412932"
+     inkscape:window-width="1920"
+     inkscape:window-height="1051"
+     inkscape:window-x="-9"
+     inkscape:window-y="-9"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg40" />
+  <path
+     style="fill:#fd8080;fill-opacity:1;stroke-width:0.0261891"
+     d="M 10.044819,9.931539 C 9.7799882,9.796189 9.3263332,9.204865 9.1824507,8.8074722 9.1150567,8.6213345 9.1130747,8.5988058 9.1126277,8.0135482 L 9.1121675,7.411198 9.2141197,6.952888 9.3160719,6.4945781 9.3280929,5.3815396 c 0.0083,-0.7685502 0.00101,-1.2913619 -0.02355,-1.6891995 C 9.2520989,2.8428903 9.2589719,1.9354066 9.3196159,1.7019654 9.3950705,1.4115129 9.4953072,1.2147127 9.6248763,1.1026311 l 0.1155004,-0.099912 0.4190263,0.012919 c 0.455795,0.014052 0.654131,0.053122 1.026564,0.2022222 0.54858,0.2196185 1.119659,0.6685668 1.275523,1.0027405 0.173923,0.3728916 0.239826,0.8748577 0.217049,1.6531852 -0.02249,0.7684764 -0.07101,1.0344577 -0.45261,2.4813776 -0.434952,1.6491948 -0.563063,2.0808353 -0.720937,2.4290176 -0.211941,0.4674268 -0.719089,1.0676688 -0.980889,1.1609458 -0.175215,0.06243 -0.339674,0.05776 -0.479284,-0.01359 z"
+     id="path856" />
+  <path
+     style="fill:#fd8080;fill-opacity:1;stroke-width:0.015625"
+     d="M 5.9752428,13.572626 C 5.825332,13.523052 5.6460658,13.38721 5.4790128,13.196601 4.9595621,12.603902 4.7383842,12.122706 4.5220532,11.114631 4.398494,10.538861 4.3604405,10.389768 4.0920785,9.429995 3.8150524,8.4392364 3.7160202,7.6471402 3.7726775,6.875308 3.8288625,6.1099036 3.9164565,5.844507 4.2308909,5.4869814 4.3533561,5.3477332 4.5254668,5.2179453 4.777465,5.0748125 5.38216,4.731351 5.8813362,4.5862455 6.4581809,4.5862455 c 0.231672,0 0.2836625,0.018714 0.4030472,0.1450784 0.1441095,0.1525344 0.1944306,0.2749974 0.2800073,0.6814355 0.036357,0.1726711 0.042378,0.2436393 0.048891,0.5762416 0.00833,0.4254889 -0.010196,0.7501784 -0.066116,1.1586762 -0.044668,0.3262878 -0.052009,0.4580547 -0.06274,1.1260683 -0.010129,0.6305528 0.012227,1.4086795 0.046016,1.6015625 0.032474,0.185381 0.0839,0.338966 0.2124984,0.634623 0.1235691,0.284096 0.1454697,0.352323 0.1871264,0.582953 0.037078,0.205281 0.038925,0.361544 0.0068,0.575496 -0.066421,0.442379 -0.1842582,0.717659 -0.5233293,1.222544 -0.2556581,0.380682 -0.4087131,0.548019 -0.5739267,0.627484 -0.104405,0.05022 -0.1346894,0.05743 -0.2601551,0.06198 -0.078098,0.0028 -0.1595736,-6.61e-4 -0.181058,-0.0078 z"
+     id="path902" />
+</svg>

+ 37 - 0
addons/godot-xr-tools/editor/icons/foot.svg.import

@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bfkcd3fkyahqu"
+path="res://.godot/imported/foot.svg-9e361563e010aa07be49bfb25fdb6639.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot-xr-tools/editor/icons/foot.svg"
+dest_files=["res://.godot/imported/foot.svg-9e361563e010aa07be49bfb25fdb6639.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false

+ 1 - 0
addons/godot-xr-tools/editor/icons/function.svg

@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#fc7f7f"><path d="m12 11.999519c.55228 0 1 .448243 1 1.000481v1.000483.211639.786437 1.000479.0014h-1v-1c-.202186 0-.336627-.491203-.5-.5-1.452695-.07822-1.5-.503591-1.5-1h1 1v-.500438h-1-1c0-.552238.44772-1.000481 1-1.000481z" stroke-width=".999963"/><path d="m1.999832 12v1c0 .55228.44772 1 1 1-.55228 0-1 .44772-1 1v1h1v-1h1v1h1v-1c0-.55228-.44772-1-1-1 .55228 0 1-.44772 1-1v-1h-1v1h-1v-1z"/><path d="m5.999832 12v1 3h1v-1h1v1h1v-1c-.000834-.17579-.047991-.34825-.13672-.5.088728-.15175.13588-.32421.13672-.5v-1c0-.55228-.44772-1-1-1h-1zm1 1h1v1h-1z"/></g><g stroke="#ff8080"><path d="m11.721643 12.70092h2" fill="#ff8080" stroke-width=".500002"/><path d="m11.694618 13.781362h2" fill="#ff8080" stroke-width=".500002"/><circle cx="-5.853771" cy="-6.970115" fill="none" r="3" stroke-width="1.42857" transform="scale(-1)"/><g fill="#ff8080" transform="matrix(1.5998367 0 0 1.4541212 -8.997551 -.227061)"><path d="m11 2.5h4"/><path d="m13 .5v4"/></g></g></svg>

+ 37 - 0
addons/godot-xr-tools/editor/icons/function.svg.import

@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b5vxil50s0ofi"
+path="res://.godot/imported/function.svg-52c5f936037e0f38a4da2b1e16ae67fe.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot-xr-tools/editor/icons/function.svg"
+dest_files=["res://.godot/imported/function.svg-52c5f936037e0f38a4da2b1e16ae67fe.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false

+ 1 - 0
addons/godot-xr-tools/editor/icons/hand.svg

@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m9.5532719 19.032065c.7515921 0 .8020221-.134249 1.3566411-1.096988 1.187157-1.391415 1.959866-2.115403 1.959866-2.115403.956259-1.186302.899466-1.493116.400204-2.10267-1.836473-.08397-.950366 1.07716-2.399132 1.067713-3.6654836.57138-5.8363537 3.182233-5.8211772 3.07349.2193447-1.571653 5.7194162-3.595593 5.7442212-3.670251.388057-1.167938 1.199163-2.981165 1.47376-3.928695.58339-1.7248573-.838773-1.8000802-1.724674-.5736257l-1.918979 4.8317897c-.0690625-.138458.3155477-4.449876.3998932-5.2980728.1599514-1.608499-2.0933718-1.6944154-2.1769669-.2258024-.0537649.944555-.1115382 6.0881672-.147506 4.8181392-.0302181-1.067011-1.3692069-2.513104-1.5668065-4.2277384-.1944963-1.7989751-2.5014442-1.2032339-1.7843238.6443794.4060542 2.461134.8612438 1.917287 1.3050931 3.81199l-1.5451335-2.033556c-.8083872-1.309984-2.2747721-.419846-1.466385.890138l1.7162803 2.711944c.1090999.892326.3001203 1.520805 1.3544509 2.699388.5134486.832581.8190766.71043 1.5706688.71043z" fill="#fc7f7f" fill-opacity=".99608" stroke-width="1.21993" transform="translate(.267457 -7.900915)"/><g fill="#fc7f7f"><path d="m12 11.999519c.55228 0 1 .448243 1 1.000481v1.000483.211639.786437 1.000479.0014h-1v-1c-.202186 0-.336627-.491203-.5-.5-1.452695-.07822-1.5-.503591-1.5-1h1 1v-.500438h-1-1c0-.552238.44772-1.000481 1-1.000481z" stroke-width=".999963"/><path d="m1.999832 12v1c0 .55228.44772 1 1 1-.55228 0-1 .44772-1 1v1h1v-1h1v1h1v-1c0-.55228-.44772-1-1-1 .55228 0 1-.44772 1-1v-1h-1v1h-1v-1z"/><path d="m5.999832 12v1 3h1v-1h1v1h1v-1c-.000834-.17579-.047991-.34825-.13672-.5.088728-.15175.13588-.32421.13672-.5v-1c0-.55228-.44772-1-1-1h-1zm1 1h1v1h-1z"/></g><g fill="#ff8080" stroke="#ff8080" stroke-width=".500002"><path d="m11.721643 12.70092h2"/><path d="m11.694618 13.781362h2"/></g></svg>

+ 37 - 0
addons/godot-xr-tools/editor/icons/hand.svg.import

@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://beko1qhyybx7e"
+path="res://.godot/imported/hand.svg-a05486d804ef16320d6cf54e06292b8f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot-xr-tools/editor/icons/hand.svg"
+dest_files=["res://.godot/imported/hand.svg-a05486d804ef16320d6cf54e06292b8f.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false

+ 1 - 0
addons/godot-xr-tools/editor/icons/movement_provider.svg

@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g stroke="#ff8080"><path d="m1.4374349 6.2407149c3.327012.02139 9.5648681-.0018 11.4589221-.028063" fill="#ff8080" stroke-width="3"/><g fill="none" stroke-width="2.29484"><path d="m9.324041 2.3242926 4.933526 4.498955"/><path d="m13.437147 5.859164c-1.047728 1.5267675-2.528193 2.753523-3.7347036 4.077989"/></g></g><g transform="translate(.000168)"><g fill="#fc7f7f"><path d="m12 11.999519c.55228 0 1 .448243 1 1.000481v1.000483.211639.786437 1.000479.0014h-1v-1c-.202186 0-.336627-.491203-.5-.5-1.452695-.07822-1.5-.503591-1.5-1h1 1v-.500438h-1-1c0-.552238.44772-1.000481 1-1.000481z" stroke-width=".999963"/><path d="m1.999832 12v1c0 .55228.44772 1 1 1-.55228 0-1 .44772-1 1v1h1v-1h1v1h1v-1c0-.55228-.44772-1-1-1 .55228 0 1-.44772 1-1v-1h-1v1h-1v-1z"/><path d="m5.999832 12v1 3h1v-1h1v1h1v-1c-.000834-.17579-.047991-.34825-.13672-.5.088728-.15175.13588-.32421.13672-.5v-1c0-.55228-.44772-1-1-1h-1zm1 1h1v1h-1z"/></g><g fill="#ff8080" stroke="#ff8080" stroke-width=".500002"><path d="m11.721643 12.70092h2"/><path d="m11.694618 13.781362h2"/></g></g></svg>

+ 37 - 0
addons/godot-xr-tools/editor/icons/movement_provider.svg.import

@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://04fn15h4x333"
+path="res://.godot/imported/movement_provider.svg-3c994cf0a3775c20f333be563d69fbf8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot-xr-tools/editor/icons/movement_provider.svg"
+dest_files=["res://.godot/imported/movement_provider.svg-3c994cf0a3775c20f333be563d69fbf8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false

+ 1 - 0
addons/godot-xr-tools/editor/icons/node.svg

@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#fc7f7f"><path d="m12 11.999519c.55228 0 1 .448243 1 1.000481v1.000483.211639.786437 1.000479.0014h-1v-1c-.202186 0-.336627-.491203-.5-.5-1.452695-.07822-1.5-.503591-1.5-1h1 1v-.500438h-1-1c0-.552238.44772-1.000481 1-1.000481z" stroke-width=".999963"/><path d="m1.999832 12v1c0 .55228.44772 1 1 1-.55228 0-1 .44772-1 1v1h1v-1h1v1h1v-1c0-.55228-.44772-1-1-1 .55228 0 1-.44772 1-1v-1h-1v1h-1v-1z"/><path d="m5.999832 12v1 3h1v-1h1v1h1v-1c-.000834-.17579-.047991-.34825-.13672-.5.088728-.15175.13588-.32421.13672-.5v-1c0-.55228-.44772-1-1-1h-1zm1 1h1v1h-1z"/></g><g stroke="#ff8080"><path d="m11.721643 12.70092h2" fill="#ff8080" stroke-width=".500002"/><path d="m11.694618 13.781362h2" fill="#ff8080" stroke-width=".500002"/><ellipse cx="-8" cy="-5.999998" fill="none" rx="4.285715" ry="4.285713" stroke-width="1.42857" transform="scale(-1)"/></g></svg>

+ 37 - 0
addons/godot-xr-tools/editor/icons/node.svg.import

@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b6gwa6o27pbry"
+path="res://.godot/imported/node.svg-37d53571b4a4459efefcc791c5402b4f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godot-xr-tools/editor/icons/node.svg"
+dest_files=["res://.godot/imported/node.svg-37d53571b4a4459efefcc791c5402b4f.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false

+ 195 - 0
addons/godot-xr-tools/effects/vignette.gd

@@ -0,0 +1,195 @@
+@tool
+class_name XRToolsVignette
+extends Node3D
+
+@export var radius : float = 1.0: set = set_radius
+@export var fade : float = 0.05: set = set_fade
+@export var steps : int = 32: set = set_steps
+
+@export var auto_adjust : bool = true: set = set_auto_adjust
+@export var auto_inner_radius : float = 0.35
+@export var auto_fade_out_factor : float = 1.5
+@export var auto_fade_delay : float = 1.0
+@export var auto_rotation_limit : float = 20.0: set = set_auto_rotation_limit
+@export var auto_velocity_limit : float = 10.0
+
+var material : ShaderMaterial = preload("res://addons/godot-xr-tools/effects/vignette.tres")
+
+var auto_first = true
+var fade_delay = 0.0
+var origin_node = null
+var last_origin_basis : Basis
+var last_location : Vector3
+@onready var auto_rotation_limit_rad = deg_to_rad(auto_rotation_limit)
+
+func set_radius(new_radius : float) -> void:
+	radius = new_radius
+	if is_inside_tree():
+		_update_radius()
+
+func _update_radius() -> void:
+	if radius < 1.0:
+		if material:
+			material.set_shader_parameter("radius", radius * sqrt(2))
+		$Mesh.visible = true
+	else:
+		$Mesh.visible = false
+
+func set_fade(new_fade : float) -> void:
+	fade = new_fade
+	if is_inside_tree():
+		_update_fade()
+
+func _update_fade() -> void:
+	if material:
+		material.set_shader_parameter("fade", fade)
+
+
+func set_steps(new_steps : int) -> void:
+	steps = new_steps
+	if is_inside_tree():
+		_update_mesh()
+
+func _update_mesh() -> void:
+	var vertices : PackedVector3Array
+	var indices : PackedInt32Array
+
+	vertices.resize(2 * steps)
+	indices.resize(6 * steps)
+	for i in steps:
+		var v : Vector3 = Vector3.RIGHT.rotated(Vector3.FORWARD, deg_to_rad((360.0 * i) / steps))
+		vertices[i] = v
+		vertices[steps+i] = v * 2.0
+
+		var off = i * 6
+		var i2 = ((i + 1) % steps)
+		indices[off + 0] = steps + i
+		indices[off + 1] = steps + i2
+		indices[off + 2] = i2
+		indices[off + 3] = steps + i
+		indices[off + 4] = i2
+		indices[off + 5] = i
+
+	# update our mesh
+	var arr_mesh = ArrayMesh.new()
+	var arr : Array
+	arr.resize(ArrayMesh.ARRAY_MAX)
+	arr[ArrayMesh.ARRAY_VERTEX] = vertices
+	arr[ArrayMesh.ARRAY_INDEX] = indices
+	arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr)
+	arr_mesh.custom_aabb = AABB(Vector3(-1.0, -1.0, -1.0), Vector3(1.0, 1.0, 1.0))
+
+	$Mesh.mesh = arr_mesh
+	$Mesh.set_surface_override_material(0, material)
+
+func set_auto_adjust(new_auto_adjust : bool) -> void:
+	auto_adjust = new_auto_adjust
+	if is_inside_tree() and !Engine.is_editor_hint():
+		_update_auto_adjust()
+
+func _update_auto_adjust() -> void:
+	# Turn process on if auto adjust is true.
+	# Note we don't turn it off here, we want to finish fading out the vignette if needed
+	if auto_adjust:
+		set_process(true)
+
+func set_auto_rotation_limit(new_auto_rotation_limit : float) -> void:
+	auto_rotation_limit = new_auto_rotation_limit
+	auto_rotation_limit_rad = deg_to_rad(auto_rotation_limit)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsVignette"
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	if !Engine.is_editor_hint():
+		origin_node = XRHelpers.get_xr_origin(self)
+		_update_mesh()
+		_update_radius()
+		_update_fade()
+		_update_auto_adjust()
+	else:
+		set_process(false)
+
+# Called on process
+func _process(delta):
+	if Engine.is_editor_hint():
+		return
+
+	if !origin_node:
+		return
+
+	if !auto_adjust:
+		# set to true for next time this is enabled
+		auto_first = true
+
+		# We are done, turn off process
+		set_process(false)
+
+		return
+
+	if auto_first:
+		# first time we run process since starting, just record transform
+		last_origin_basis = origin_node.global_transform.basis
+		last_location = global_transform.origin
+		auto_first = false
+		return
+
+	# Get our delta transform
+	var delta_b = origin_node.global_transform.basis * last_origin_basis.inverse()
+	var delta_v = global_transform.origin - last_location
+
+	# Adjust radius based on rotation speed of our origin point (not of head movement).
+	# We convert our delta rotation to a quaterion.
+	# A quaternion represents a rotation around an angle.
+	var q = delta_b.get_rotation_quaternion()
+
+	# We get our angle from our w component and then adjust to get a
+	# rotation speed per second by dividing by delta
+	var angle = (2 * acos(q.w)) / delta
+
+	# Calculate what our radius should be for our rotation speed
+	var target_radius = 1.0
+	if auto_rotation_limit > 0:
+		target_radius = 1.0 - (
+			clamp(angle / auto_rotation_limit_rad, 0.0, 1.0) * (1.0 - auto_inner_radius))
+
+	# Now do the same for speed, this includes players physical speed but there
+	# isn't much we can do there.
+	if auto_velocity_limit > 0:
+		var velocity = delta_v.length() / delta
+		target_radius = min(target_radius, 1.0 - (
+				clamp(velocity / auto_velocity_limit, 0.0, 1.0) * (1.0 - auto_inner_radius)))
+
+	# if our radius is small then our current we apply it
+	if target_radius < radius:
+		set_radius(target_radius)
+		fade_delay = auto_fade_delay
+	elif fade_delay > 0.0:
+		fade_delay -= delta
+	else:
+		set_radius(clamp(radius + delta / auto_fade_out_factor, 0.0, 1.0))
+
+	last_origin_basis = origin_node.global_transform.basis
+	last_location = global_transform.origin
+
+# This method verifies the vignette has a valid configuration.
+# Specifically it checks the following:
+# - XROrigin3D is a parent
+# - XRCamera3D is our parent
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := PackedStringArray()
+
+	# Check the origin node
+	if !XRHelpers.get_xr_origin(self):
+		warnings.append("Parent node must be in a branch from XROrigin3D")
+
+	# check camera node
+	var parent = get_parent()
+	if !parent or !parent is XRCamera3D:
+		warnings.append("Parent node must be an XRCamera3D")
+
+	return warnings

+ 34 - 0
addons/godot-xr-tools/effects/vignette.gdshader

@@ -0,0 +1,34 @@
+shader_type spatial;
+render_mode depth_test_disabled, skip_vertex_transform, unshaded, cull_disabled;
+
+uniform vec4 color : source_color = vec4(0.0, 0.0, 0.0, 1.0);
+uniform float radius = 1.0;
+uniform float fade = 0.05;
+
+varying float dist;
+
+void vertex() {
+	vec3 v = VERTEX;
+	dist = length(v);
+
+	// outer ring is 2.0, inner ring is 1.0, so this scales purely the inner ring
+	if (dist < 1.5) {
+		// Adjust by radius
+		dist = radius;
+		v *= dist;
+
+		// We don't know our eye center, projecting a center point in the distance gives us a good enough approximation
+		vec4 eye = PROJECTION_MATRIX * vec4(0.0, 0.0, 100.0, 1.0);
+
+		// and we offset our inner circle
+		v.xy += eye.xy / eye.z;
+	}
+	
+	// looks like this is broken in Godot 4...
+	POSITION = vec4(v, 1.0);
+}
+
+void fragment() {
+	ALBEDO = color.rgb;
+	ALPHA = clamp((dist - radius) / fade, 0.0, 1.0);
+}

+ 10 - 0
addons/godot-xr-tools/effects/vignette.tres

@@ -0,0 +1,10 @@
+[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://cesiqdvdfojle"]
+
+[ext_resource type="Shader" path="res://addons/godot-xr-tools/effects/vignette.gdshader" id="1_x02h0"]
+
+[resource]
+render_priority = 0
+shader = ExtResource("1_x02h0")
+shader_parameter/color = Color(0, 0, 0, 1)
+shader_parameter/radius = 0.2
+shader_parameter/fade = 0.05

File diff suppressed because it is too large
+ 13 - 0
addons/godot-xr-tools/effects/vignette.tscn


+ 92 - 0
addons/godot-xr-tools/examples/fall_damage.gd

@@ -0,0 +1,92 @@
+@tool
+class_name XRToolsFallDamage
+extends XRToolsMovementProvider
+
+
+## XR Tools Example Fall Damage Detector
+##
+## This example script detects the player falling to the ground and
+## optionally hitting walls.
+##
+## It works by tracking the player body velocity to detect velocity
+## changes (acceleration) exceeding a threshold.
+##
+## This doesn't use the usual Acceleration = dV / dT as it doesn't appear
+## to work too well considering the "instantaneous" nature of the
+## collision. Additionally all it would end up doing is multiplying the
+## change in velocity by the physics-frame-rate making it sensitive to
+## varying physics timing.
+##
+## Instead the threshold in terms of delta-velocity makes it easy to work
+## out natural values. For example if the player falls under regular gravity
+## (9.81 meters per second^2 for 1 second) then hits the ground, they will have fallen around
+## 4.9 meters, and will then encounter an instantaneous velocity-change of
+## 9.81 meters per second.
+##
+## This file can handle simple demonstrations, but games will most likely
+## want to modify it, for example to ignore damage on certain surfaces.
+
+
+## Signal invoked when the player takes fall damage
+signal player_fall_damage(damage)
+
+
+## Movement provider order
+@export var order : int = 1000
+
+## Ignore damage if player is launched up
+@export var ignore_launch : bool = true
+
+## Only take damage on ground
+@export var ground_only : bool = false
+
+## Acceleration limit
+@export var damage_threshold : float = 8.0
+
+
+## Previous velocity
+var _previous_velocity : Vector3 = Vector3.ZERO
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsFallDamage"
+
+
+func _ready():
+	# In Godot 4 we must now manually call our super class ready function
+	super()
+
+	# Set as always active
+	is_active = true
+
+
+func physics_movement(_delta: float, player_body: XRToolsPlayerBody, disabled: bool):
+	# Skip if not enabled
+	if disabled or !enabled:
+		_previous_velocity = player_body.velocity
+		return
+
+	# Calculate the instantaneous acceleration
+	var accel_vec := player_body.velocity - _previous_velocity
+	_previous_velocity = player_body.velocity
+
+	# Ignore launching the player
+	if ignore_launch:
+		# Forgive "up" acceleration equal to our "up" speed
+		var forgive : float = max(0, min(accel_vec.y, player_body.velocity.y))
+		accel_vec.y -= forgive
+
+	# Handle ground-only collisions
+	if ground_only:
+		# Ignore if not on ground
+		if not player_body.on_ground:
+			return
+
+		# Only consider vertical acceleration
+		accel_vec *= Vector3.UP
+
+	# Detect fall damage
+	var accel := accel_vec.length()
+	if accel > damage_threshold:
+		emit_signal("player_fall_damage", accel)

+ 6 - 0
addons/godot-xr-tools/examples/fall_damage.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://d2yejwiwab3wv"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/examples/fall_damage.gd" id="1"]
+
+[node name="FallDamage" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 430 - 0
addons/godot-xr-tools/functions/function_pickup.gd

@@ -0,0 +1,430 @@
+@tool
+@icon("res://addons/godot-xr-tools/editor/icons/function.svg")
+class_name XRToolsFunctionPickup
+extends Node3D
+
+
+## XR Tools Function Pickup Script
+##
+## This script implements picking up of objects. Most pickable
+## objects are instances of the [XRToolsPickable] class.
+##
+## Additionally this script can work in conjunction with the
+## [XRToolsMovementProvider] class support climbing. Most climbable objects are
+## instances of the [XRToolsClimbable] class.
+
+
+## Signal emitted when the pickup picks something up
+signal has_picked_up(what)
+
+## Signal emitted when the pickup drops something
+signal has_dropped
+
+
+# Default pickup collision mask of 3:pickable and 19:handle
+const DEFAULT_GRAB_MASK := 0b0000_0000_0000_0100_0000_0000_0000_0100
+
+# Default pickup collision mask of 3:pickable
+const DEFAULT_RANGE_MASK := 0b0000_0000_0000_0000_0000_0000_0000_0100
+
+# Constant for worst-case grab distance
+const MAX_GRAB_DISTANCE2: float = 1000000.0
+
+
+## Pickup enabled property
+@export var enabled : bool = true
+
+## Grip controller axis
+@export var pickup_axis_action : String = "grip"
+
+## Action controller button
+@export var action_button_action : String = "trigger_click"
+
+## Grab distance
+@export var grab_distance : float = 0.3: set = _set_grab_distance
+
+## Grab collision mask
+@export_flags_3d_physics \
+		var grab_collision_mask : int = DEFAULT_GRAB_MASK: set = _set_grab_collision_mask
+
+## If true, ranged-grabbing is enabled
+@export var ranged_enable : bool = true
+
+## Ranged-grab distance
+@export var ranged_distance : float = 5.0: set = _set_ranged_distance
+
+## Ranged-grab angle
+@export_range(0.0, 45.0) var ranged_angle : float = 5.0: set = _set_ranged_angle
+
+## Ranged-grab collision mask
+@export_flags_3d_physics \
+		var ranged_collision_mask : int = DEFAULT_RANGE_MASK: set = _set_ranged_collision_mask
+
+## Throw impulse factor
+@export var impulse_factor : float = 1.0
+
+## Throw velocity averaging
+@export var velocity_samples: int = 5
+
+
+# Public fields
+var closest_object : Node3D = null
+var picked_up_object : Node3D = null
+var picked_up_ranged : bool = false
+var grip_pressed : bool = false
+
+# Private fields
+var _object_in_grab_area := Array()
+var _object_in_ranged_area := Array()
+var _velocity_averager := XRToolsVelocityAverager.new(velocity_samples)
+var _grab_area : Area3D
+var _grab_collision : CollisionShape3D
+var _ranged_area : Area3D
+var _ranged_collision : CollisionShape3D
+
+
+## Controller
+@onready var _controller := XRHelpers.get_xr_controller(self)
+
+## Grip threshold (from configuration)
+@onready var _grip_threshold : float = XRTools.get_grip_threshold()
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsFunctionPickup"
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	# Skip creating grab-helpers if in the editor
+	if Engine.is_editor_hint():
+		return
+
+	# Create the grab collision shape
+	_grab_collision = CollisionShape3D.new()
+	_grab_collision.set_name("GrabCollisionShape")
+	_grab_collision.shape = SphereShape3D.new()
+	_grab_collision.shape.radius = grab_distance
+
+	# Create the grab area
+	_grab_area = Area3D.new()
+	_grab_area.set_name("GrabArea")
+	_grab_area.collision_layer = 0
+	_grab_area.collision_mask = grab_collision_mask
+	_grab_area.add_child(_grab_collision)
+	_grab_area.area_entered.connect(_on_grab_entered)
+	_grab_area.body_entered.connect(_on_grab_entered)
+	_grab_area.area_exited.connect(_on_grab_exited)
+	_grab_area.body_exited.connect(_on_grab_exited)
+	add_child(_grab_area)
+
+	# Create the ranged collision shape
+	_ranged_collision = CollisionShape3D.new()
+	_ranged_collision.set_name("RangedCollisionShape")
+	_ranged_collision.shape = CylinderShape3D.new()
+	_ranged_collision.transform.basis = Basis(Vector3.RIGHT, PI/2)
+
+	# Create the ranged area
+	_ranged_area = Area3D.new()
+	_ranged_area.set_name("RangedArea")
+	_ranged_area.collision_layer = 0
+	_ranged_area.collision_mask = ranged_collision_mask
+	_ranged_area.add_child(_ranged_collision)
+	_ranged_area.area_entered.connect(_on_ranged_entered)
+	_ranged_area.body_entered.connect(_on_ranged_entered)
+	_ranged_area.area_exited.connect(_on_ranged_exited)
+	_ranged_area.body_exited.connect(_on_ranged_exited)
+	add_child(_ranged_area)
+
+	# Update the colliders
+	_update_colliders()
+
+	# Monitor Grab Button
+	get_parent().connect("button_pressed", _on_button_pressed)
+	get_parent().connect("button_released", _on_button_released)
+
+
+# Called on each frame to update the pickup
+func _process(delta):
+	# Do not process if in the editor
+	if Engine.is_editor_hint():
+		return
+
+	# Skip if disabled, or the controller isn't active
+	if !enabled or !_controller.get_is_active():
+		return
+
+	# Handle our grip
+	var grip_value = _controller.get_float(pickup_axis_action)
+	if (grip_pressed and grip_value < (_grip_threshold - 0.1)):
+		grip_pressed = false
+		_on_grip_release()
+	elif (!grip_pressed and grip_value > (_grip_threshold + 0.1)):
+		grip_pressed = true
+		_on_grip_pressed()
+
+	# Calculate average velocity
+	if is_instance_valid(picked_up_object) and picked_up_object.is_picked_up():
+		# Average velocity of picked up object
+		_velocity_averager.add_transform(delta, picked_up_object.global_transform)
+	else:
+		# Average velocity of this pickup
+		_velocity_averager.add_transform(delta, global_transform)
+
+	_update_closest_object()
+
+
+## Find an [XRToolsFunctionPickup] node.
+##
+## This function searches from the specified node for an [XRToolsFunctionPickup]
+## assuming the node is a sibling of the pickup under an [XRController3D].
+static func find_instance(node : Node) -> XRToolsFunctionPickup:
+	return XRTools.find_xr_child(
+		XRHelpers.get_xr_controller(node),
+		"*",
+		"XRToolsFunctionPickup") as XRToolsFunctionPickup
+
+
+## Find the left [XRToolsFunctionPickup] node.
+##
+## This function searches from the specified node for the left controller
+## [XRToolsFunctionPickup] assuming the node is a sibling of the [XOrigin3D].
+static func find_left(node : Node) -> XRToolsFunctionPickup:
+	return XRTools.find_xr_child(
+		XRHelpers.get_left_controller(node),
+		"*",
+		"XRToolsFunctionPickup") as XRToolsFunctionPickup
+
+
+## Find the right [XRToolsFunctionPickup] node.
+##
+## This function searches from the specified node for the right controller
+## [XRToolsFunctionPickup] assuming the node is a sibling of the [XROrigin3D].
+static func find_right(node : Node) -> XRToolsFunctionPickup:
+	return XRTools.find_xr_child(
+		XRHelpers.get_right_controller(node),
+		"*",
+		"XRToolsFunctionPickup") as XRToolsFunctionPickup
+
+
+## Get the [XRController3D] driving this pickup.
+func get_controller() -> XRController3D:
+	return _controller
+
+
+# Called when the grab distance has been modified
+func _set_grab_distance(new_value: float) -> void:
+	grab_distance = new_value
+	if is_inside_tree():
+		_update_colliders()
+
+
+# Called when the grab collision mask has been modified
+func _set_grab_collision_mask(new_value: int) -> void:
+	grab_collision_mask = new_value
+	if is_inside_tree() and _grab_collision:
+		_grab_collision.collision_mask = new_value
+
+
+# Called when the ranged-grab distance has been modified
+func _set_ranged_distance(new_value: float) -> void:
+	ranged_distance = new_value
+	if is_inside_tree():
+		_update_colliders()
+
+
+# Called when the ranged-grab angle has been modified
+func _set_ranged_angle(new_value: float) -> void:
+	ranged_angle = new_value
+	if is_inside_tree():
+		_update_colliders()
+
+
+# Called when the ranged-grab collision mask has been modified
+func _set_ranged_collision_mask(new_value: int) -> void:
+	ranged_collision_mask = new_value
+	if is_inside_tree() and _ranged_collision:
+		_ranged_collision.collision_mask = new_value
+
+
+# Update the colliders geometry
+func _update_colliders() -> void:
+	# Update the grab sphere
+	if _grab_collision:
+		_grab_collision.shape.radius = grab_distance
+
+	# Update the ranged-grab cylinder
+	if _ranged_collision:
+		_ranged_collision.shape.radius = tan(deg_to_rad(ranged_angle)) * ranged_distance
+		_ranged_collision.shape.height = ranged_distance
+		_ranged_collision.transform.origin.z = -ranged_distance * 0.5
+
+
+# Called when an object enters the grab sphere
+func _on_grab_entered(target: Node3D) -> void:
+	# reject objects which don't support picking up
+	if not target.has_method('pick_up'):
+		return
+
+	# ignore objects already known
+	if _object_in_grab_area.find(target) >= 0:
+		return
+
+	# Add to the list of objects in grab area
+	_object_in_grab_area.push_back(target)
+
+
+# Called when an object enters the ranged-grab cylinder
+func _on_ranged_entered(target: Node3D) -> void:
+	# reject objects which don't support picking up rangedly
+	if not 'can_ranged_grab' in target or not target.can_ranged_grab:
+		return
+
+	# ignore objects already known
+	if _object_in_ranged_area.find(target) >= 0:
+		return
+
+	# Add to the list of objects in grab area
+	_object_in_ranged_area.push_back(target)
+
+
+# Called when an object exits the grab sphere
+func _on_grab_exited(target: Node3D) -> void:
+	_object_in_grab_area.erase(target)
+
+
+# Called when an object exits the ranged-grab cylinder
+func _on_ranged_exited(target: Node3D) -> void:
+	_object_in_ranged_area.erase(target)
+
+
+# Update the closest object field with the best choice of grab
+func _update_closest_object() -> void:
+	# Find the closest object we can pickup
+	var new_closest_obj: Node3D = null
+	if not picked_up_object:
+		# Find the closest in grab area
+		new_closest_obj = _get_closest_grab()
+		if not new_closest_obj and ranged_enable:
+			# Find closest in ranged area
+			new_closest_obj = _get_closest_ranged()
+
+	# Skip if no change
+	if closest_object == new_closest_obj:
+		return
+
+	# remove highlight on old object
+	if is_instance_valid(closest_object):
+		closest_object.decrease_is_closest()
+
+	# add highlight to new object
+	closest_object = new_closest_obj
+	if is_instance_valid(closest_object):
+		closest_object.increase_is_closest()
+
+
+# Find the pickable object closest to our hand's grab location
+func _get_closest_grab() -> Node3D:
+	var new_closest_obj: Node3D = null
+	var new_closest_distance := MAX_GRAB_DISTANCE2
+	for o in _object_in_grab_area:
+		# skip objects that can not be picked up
+		if not o.can_pick_up(self):
+			continue
+
+		# Save if this object is closer than the current best
+		var distance_squared := global_transform.origin.distance_squared_to(
+				o.global_transform.origin)
+		if distance_squared < new_closest_distance:
+			new_closest_obj = o
+			new_closest_distance = distance_squared
+
+	# Return best object
+	return new_closest_obj
+
+
+# Find the rangedly-pickable object closest to our hand's pointing direction
+func _get_closest_ranged() -> Node3D:
+	var new_closest_obj: Node3D = null
+	var new_closest_angle_dp := cos(deg_to_rad(ranged_angle))
+	var hand_forwards := -global_transform.basis.z
+	for o in _object_in_ranged_area:
+		# skip objects that can not be picked up
+		if not o.can_pick_up(self):
+			continue
+
+		# Save if this object is closer than the current best
+		var object_direction: Vector3 = o.global_transform.origin - global_transform.origin
+		object_direction = object_direction.normalized()
+		var angle_dp := hand_forwards.dot(object_direction)
+		if angle_dp > new_closest_angle_dp:
+			new_closest_obj = o
+			new_closest_angle_dp = angle_dp
+
+	# Return best object
+	return new_closest_obj
+
+
+## Drop the currently held object
+func drop_object() -> void:
+	if not is_instance_valid(picked_up_object):
+		return
+
+	# let go of this object
+	picked_up_object.let_go(
+		_velocity_averager.linear_velocity() * impulse_factor,
+		_velocity_averager.angular_velocity())
+	picked_up_object = null
+	emit_signal("has_dropped")
+
+
+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
+
+	# Handle snap-zone
+	var snap := target as XRToolsSnapZone
+	if snap:
+		target = snap.picked_up_object
+		snap.drop_object()
+
+	# Pick up our target. Note, target may do instant drop_and_free
+	picked_up_ranged = not _object_in_grab_area.has(target)
+	picked_up_object = target
+	target.pick_up(self, _controller)
+
+	# If object picked up then emit signal
+	if is_instance_valid(picked_up_object):
+		emit_signal("has_picked_up", picked_up_object)
+
+
+func _on_button_pressed(p_button) -> void:
+	if p_button == action_button_action:
+		if is_instance_valid(picked_up_object) and picked_up_object.has_method("action"):
+			picked_up_object.action()
+
+
+func _on_button_released(_p_button) -> void:
+	pass
+
+
+func _on_grip_pressed() -> void:
+	if is_instance_valid(picked_up_object) and !picked_up_object.press_to_hold:
+		drop_object()
+	elif is_instance_valid(closest_object):
+		_pick_up_object(closest_object)
+
+
+func _on_grip_release() -> void:
+	if is_instance_valid(picked_up_object) and picked_up_object.press_to_hold:
+		drop_object()

+ 6 - 0
addons/godot-xr-tools/functions/function_pickup.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://b4ysuy43poobf"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/function_pickup.gd" id="1"]
+
+[node name="FunctionPickup" type="Node3D"]
+script = ExtResource("1")

+ 363 - 0
addons/godot-xr-tools/functions/function_pointer.gd

@@ -0,0 +1,363 @@
+@tool
+@icon("res://addons/godot-xr-tools/editor/icons/function.svg")
+class_name XRToolsFunctionPointer
+extends Node3D
+
+
+## XR Tools Function Pointer Script
+##
+## This script implements a pointer function for a players controller. Pointer
+## events (entered, exited, pressed, release, and movement) are delivered by
+## invoking signals on the target node.
+##
+## Pointer target nodes commonly extend from [XRToolsInteractableArea] or
+## [XRToolsInteractableBody].
+
+
+## Enumeration of laser show modes
+enum LaserShow {
+	HIDE = 0,		## Hide laser
+	SHOW = 1,		## Show laser
+	COLLIDE = 2,	## Only show laser on collision
+}
+
+## Enumeration of laser length modes
+enum LaserLength {
+	FULL = 0,		## Full length
+	COLLIDE = 1		## Draw to collision
+}
+
+
+# Default pointer collision mask of 21:pointable
+const DEFAULT_MASK := 0b0000_0000_0001_0000_0000_0000_0000_0000
+
+
+
+## Pointer enabled property
+@export var enabled : bool = true: set = set_enabled
+
+## Show laser property
+@export var show_laser : LaserShow = LaserShow.SHOW: set = set_show_laser
+
+## Laser length property
+@export var laser_length : LaserLength = LaserLength.FULL
+
+## If true, the pointer target is shown
+@export var show_target : bool = false
+
+## Y Offset for pointer
+@export var y_offset : float = -0.05: set = set_y_offset
+
+## Pointer distance
+@export var distance : float = 10: set = set_distance
+
+## Pointer collision mask
+@export_flags_3d_physics var collision_mask : int = DEFAULT_MASK: set = set_collision_mask
+
+## Enable pointer collision with bodies
+@export var collide_with_bodies : bool = true: set = set_collide_with_bodies
+
+## Enable pointer collision with areas
+@export var collide_with_areas : bool = false: set = set_collide_with_areas
+
+## Active button action
+@export var active_button_action : String = "trigger_click"
+
+
+## Current target node
+var target : Node3D
+
+## Last target node
+var last_target : Node3D
+
+## Last collision point
+var last_collided_at : Vector3 = Vector3.ZERO
+
+# World scale
+var _world_scale : float = 1.0
+
+# Left controller node
+var _controller_left_node : XRController3D
+
+# Right controller node
+var _controller_right_node : XRController3D
+
+# Parent controller (if this pointer is childed to a specific controller)
+var _controller  : XRController3D
+
+# The currently active controller
+var _active_controller : XRController3D
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsFunctionPointer"
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	# Do not initialise if in the editor
+	if Engine.is_editor_hint():
+		return
+
+	# Read the initial world-scale
+	_world_scale = XRServer.world_scale
+
+	# Check for a parent controller
+	_controller = XRHelpers.get_xr_controller(self)
+	if _controller:
+		# Set as active on the parent controller
+		_active_controller = _controller
+
+		# Get button press feedback from our parent controller
+		_controller.button_pressed.connect(_on_button_pressed.bind(_controller))
+		_controller.button_released.connect(_on_button_released.bind(_controller))
+	else:
+		# Get the left and right controllers
+		_controller_left_node = XRHelpers.get_left_controller(self)
+		_controller_right_node = XRHelpers.get_right_controller(self)
+
+		# Start out right hand controller
+		_active_controller = _controller_right_node
+
+		# Get button press feedback from both left and right controllers
+		_controller_left_node.button_pressed.connect(
+				_on_button_pressed.bind(_controller_left_node))
+		_controller_left_node.button_released.connect(
+				_on_button_released.bind(_controller_left_node))
+		_controller_right_node.button_pressed.connect(
+				_on_button_pressed.bind(_controller_right_node))
+		_controller_right_node.button_released.connect(
+				_on_button_released.bind(_controller_right_node))
+
+	# init our state
+	_update_y_offset()
+	_update_distance()
+	_update_collision_mask()
+	_update_show_laser()
+	_update_collide_with_bodies()
+	_update_collide_with_areas()
+	_update_set_enabled()
+
+
+# Called on each frame to update the pickup
+func _process(_delta):
+	# Do not process if in the editor
+	if Engine.is_editor_hint() or !is_inside_tree():
+		return
+
+	# Track the active controller (if this pointer is not childed to a controller)
+	if _controller == null and _active_controller != null:
+		transform = _active_controller.transform
+
+	# Handle world-scale changes
+	var new_world_scale := XRServer.world_scale
+	if (_world_scale != new_world_scale):
+		_world_scale = new_world_scale
+		_update_y_offset()
+
+	if enabled and $RayCast.is_colliding():
+		var new_at = $RayCast.get_collision_point()
+
+		if is_instance_valid(target):
+			# if target is set our mouse must be down, we keep "focus" on our target
+			if new_at != last_collided_at:
+				if target.has_signal("pointer_moved"):
+					target.emit_signal("pointer_moved", last_collided_at, new_at)
+				elif target.has_method("pointer_moved"):
+					target.pointer_moved(last_collided_at, new_at)
+		else:
+			var new_target = $RayCast.get_collider()
+
+			# are we pointing to a new target?
+			if new_target != last_target:
+				# exit the old
+				if is_instance_valid(last_target):
+					if last_target.has_signal("pointer_exited"):
+						last_target.emit_signal("pointer_exited")
+					elif last_target.has_method("pointer_exited"):
+						last_target.pointer_exited()
+
+				# enter the new
+				if is_instance_valid(new_target):
+					if new_target.has_signal("pointer_entered"):
+						new_target.emit_signal("pointer_entered")
+					elif new_target.has_method("pointer_entered"):
+						new_target.pointer_entered()
+
+				last_target = new_target
+
+			if new_at != last_collided_at:
+				if new_target.has_signal("pointer_moved"):
+					new_target.emit_signal("pointer_moved", last_collided_at, new_at)
+				elif new_target.has_method("pointer_moved"):
+					new_target.pointer_moved(last_collided_at, new_at)
+
+		if last_target:
+			# Show target if configured
+			if show_target:
+				$Target.global_transform.origin = new_at
+				$Target.visible = true
+
+			# Show laser if set to show-on-collide
+			if show_laser == LaserShow.COLLIDE:
+				$Laser.visible = true
+
+			# Adjust laser length if set to collide-length
+			if laser_length == LaserLength.COLLIDE:
+				var collide_len : float = new_at.distance_to(global_transform.origin)
+				$Laser.mesh.size.z = collide_len
+				$Laser.position.z = collide_len * -0.5
+
+		# remember our new position
+		last_collided_at = new_at
+	else:
+		if is_instance_valid(last_target):
+			if last_target.has_signal("pointer_exited"):
+				last_target.emit_signal("pointer_exited")
+			elif last_target.has_method("pointer_exited"):
+				last_target.pointer_exited()
+
+		last_target = null
+
+		# Ensure target is hidden
+		$Target.visible = false
+
+		# Hide laser if set to show-on-collide
+		if show_laser == LaserShow.COLLIDE:
+			$Laser.visible = false
+
+		# Restore laser length if set to collide-length
+		if laser_length == LaserLength.COLLIDE:
+			$Laser.mesh.size.z = distance
+			$Laser.position.z = distance * -0.5
+
+
+# Set pointer enabled property
+func set_enabled(p_enabled : bool) -> void:
+	enabled = p_enabled
+
+	# this gets called before our scene is ready, we'll call this again in _ready to enable this
+	if is_inside_tree():
+		_update_set_enabled()
+
+
+# Set show-laser property
+func set_show_laser(p_show : LaserShow) -> void:
+	show_laser = p_show
+	if is_inside_tree():
+		_update_show_laser()
+
+
+# Set pointer Y offset property
+func set_y_offset(p_offset : float) -> void:
+	y_offset = p_offset
+	if is_inside_tree():
+		_update_y_offset()
+
+
+# Set pointer distance property
+func set_distance(p_new_value : float) -> void:
+	distance = p_new_value
+	if is_inside_tree():
+		_update_distance()
+
+
+# Set pointer collision mask property
+func set_collision_mask(p_new_mask : int) -> void:
+	collision_mask = p_new_mask
+	if is_inside_tree():
+		_update_collision_mask()
+
+
+# Set pointer collide-with-bodies property
+func set_collide_with_bodies(p_new_value : bool) -> void:
+	collide_with_bodies = p_new_value
+	if is_inside_tree():
+		_update_collide_with_bodies()
+
+
+# Set pointer collide-with-areas property
+func set_collide_with_areas(p_new_value : bool) -> void:
+	collide_with_areas = p_new_value
+	if is_inside_tree():
+		_update_collide_with_areas()
+
+
+# Pointer enabled update handler
+func _update_set_enabled() -> void:
+	$Laser.visible = enabled and show_laser
+	$RayCast.enabled = enabled
+
+
+# Pointer show-laser update handler
+func _update_show_laser() -> void:
+	$Laser.visible = enabled and show_laser == LaserShow.SHOW
+
+
+# Pointer Y offset update handler
+func _update_y_offset() -> void:
+	$Laser.position.y = y_offset * _world_scale
+	$RayCast.position.y = y_offset * _world_scale
+
+
+# Pointer distance update handler
+func _update_distance() -> void:
+	$Laser.mesh.size.z = distance
+	$Laser.position.z = distance * -0.5
+	$RayCast.target_position.z = -distance
+
+
+# Pointer collision mask update handler
+func _update_collision_mask() -> void:
+	$RayCast.collision_mask = collision_mask
+
+
+# Pointer collide-with-bodies update handler
+func _update_collide_with_bodies() -> void:
+	$RayCast.collide_with_bodies = collide_with_bodies
+
+
+# Pointer collide-with-areas update handler
+func _update_collide_with_areas() -> void:
+	$RayCast.collide_with_areas = collide_with_areas
+
+
+# Pointer-activation button pressed handler
+func _button_pressed() -> void:
+	if $RayCast.is_colliding():
+		target = $RayCast.get_collider()
+		last_collided_at = $RayCast.get_collision_point()
+
+		if target.has_signal("pointer_pressed"):
+			target.emit_signal("pointer_pressed", last_collided_at)
+		elif target.has_method("pointer_pressed"):
+			target.pointer_pressed(last_collided_at)
+
+
+# Pointer-activation button released handler
+func _button_released() -> void:
+	if target:
+		if target.has_signal("pointer_released"):
+			target.emit_signal("pointer_released", last_collided_at)
+		elif target.has_method("pointer_released"):
+			target.pointer_released(last_collided_at)
+
+		# unset target
+		target = null
+		last_collided_at = Vector3(0, 0, 0)
+
+
+# Button pressed handler
+func _on_button_pressed(p_button : String, controller : XRController3D) -> void:
+	if p_button == active_button_action and enabled:
+		if controller == _active_controller:
+			_button_pressed()
+		else:
+			_active_controller = controller
+
+
+# Button released handler
+func _on_button_released(p_button : String, _controller : XRController3D) -> void:
+	if p_button == active_button_action and target:
+		_button_released()

+ 34 - 0
addons/godot-xr-tools/functions/function_pointer.tscn

@@ -0,0 +1,34 @@
+[gd_scene load_steps=5 format=3 uid="uid://cqhw276realc"]
+
+[ext_resource type="Material" path="res://addons/godot-xr-tools/materials/pointer.tres" id="1"]
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/function_pointer.gd" id="2"]
+
+[sub_resource type="BoxMesh" id="1"]
+resource_local_to_scene = true
+size = Vector3(0.002, 0.002, 10)
+subdivide_depth = 20
+
+[sub_resource type="SphereMesh" id="2"]
+radius = 0.05
+height = 0.1
+radial_segments = 16
+rings = 8
+
+[node name="FunctionPointer" type="Node3D"]
+script = ExtResource("2")
+
+[node name="RayCast" type="RayCast3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
+target_position = Vector3(0, 0, 0)
+collision_mask = 1048576
+
+[node name="Laser" type="MeshInstance3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, -5)
+cast_shadow = 0
+mesh = SubResource("1")
+surface_material_override/0 = ExtResource("1")
+
+[node name="Target" type="MeshInstance3D" parent="."]
+visible = false
+mesh = SubResource("2")
+surface_material_override/0 = ExtResource("1")

+ 99 - 0
addons/godot-xr-tools/functions/function_pose_detector.gd

@@ -0,0 +1,99 @@
+@tool
+@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
+class_name XRToolsFunctionPoseDetector
+extends Node3D
+
+
+## XR Tools Function Pose Area
+##
+## This area works with the XRToolsHandPoseArea to control the pose
+## of the VR hands.
+
+
+# Default pose detector collision mask of 22:pose-area
+const DEFAULT_MASK := 0b0000_0000_0010_0000_0000_0000_0000_0000
+
+
+## Collision mask to detect hand pose areas
+@export_flags_3d_physics var collision_mask : int = DEFAULT_MASK: set = set_collision_mask
+
+
+## Hand controller
+@onready var _controller := XRHelpers.get_xr_controller(self)
+
+## Hand to control
+@onready var _hand := XRToolsHand.find_instance(self)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsFunctionPoseDetector"
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	# Connect signals (if controller and hand are valid)
+	if _controller and _hand:
+		if $SenseArea.area_entered.connect(_on_area_entered):
+			push_error("Unable to connect area_entered signal")
+		if $SenseArea.area_exited.connect(_on_area_exited):
+			push_error("Unable to connect area_exited signal")
+
+	# Update collision mask
+	_update_collision_mask()
+
+
+# This method verifies the pose area has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := PackedStringArray()
+
+	if !XRHelpers.get_xr_controller(self):
+		warnings.append("Node must be within a branch of an XRController3D node")
+
+	# Verify hand can be found
+	if !XRToolsHand.find_instance(self):
+		warnings.append("Node must be a within a branch of an XRController node with a hand")
+
+	# Pass basic validation
+	return warnings
+
+
+func set_collision_mask(mask : int) -> void:
+	collision_mask = mask
+	if is_inside_tree():
+		_update_collision_mask()
+
+
+func _update_collision_mask() -> void:
+	$SenseArea.collision_mask = collision_mask
+
+
+## Signal handler called when this XRToolsFunctionPoseArea enters an area
+func _on_area_entered(area : Area3D) -> void:
+	# Igjnore if the area is not a hand-pose area
+	var pose_area := area as XRToolsHandPoseArea
+	if !pose_area:
+		return
+
+	# Set the appropriate poses
+	if _controller.tracker == "left_hand" and pose_area.left_pose:
+		_hand.add_pose_override(
+				pose_area,
+				pose_area.pose_priority,
+				pose_area.left_pose)
+	elif _controller.tracker == "right_hand" and pose_area.right_pose:
+		_hand.add_pose_override(
+				pose_area,
+				pose_area.pose_priority,
+				pose_area.right_pose)
+
+
+## Signal handler called when this XRToolsFunctionPoseArea leaves an area
+func _on_area_exited(area : Area3D) -> void:
+	# Ignore if the area is not a hand-pose area
+	var pose_area := area as XRToolsHandPoseArea
+	if !pose_area:
+		return
+
+	# Remove any overrides set from this hand-pose area
+	_hand.remove_pose_override(pose_area)

+ 19 - 0
addons/godot-xr-tools/functions/function_pose_detector.tscn

@@ -0,0 +1,19 @@
+[gd_scene load_steps=3 format=3 uid="uid://bft3xyxs31ci3"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/function_pose_detector.gd" id="1"]
+
+[sub_resource type="CapsuleShape3D" id="1"]
+radius = 0.08
+height = 0.24
+
+[node name="FunctionPoseDetector" type="Node3D"]
+script = ExtResource("1")
+
+[node name="SenseArea" type="Area3D" parent="."]
+collision_layer = 0
+collision_mask = 2097152
+monitorable = false
+
+[node name="CollisionShape" type="CollisionShape3D" parent="SenseArea"]
+transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, -0.04, 0.08)
+shape = SubResource("1")

+ 392 - 0
addons/godot-xr-tools/functions/function_teleport.gd

@@ -0,0 +1,392 @@
+@tool
+@icon("res://addons/godot-xr-tools/editor/icons/function.svg")
+class_name XRToolsFunctionTeleport
+extends CharacterBody3D
+
+# should really change this to Node3D once #17401 is resolved
+
+
+## XR Tools Function Teleport Script
+##
+## This script provides teleport functionality.
+##
+## Add this scene as a sub scene of your ARVRController node to implement
+## a teleport function on that controller.
+
+
+# Default teleport collision mask of all
+const DEFAULT_MASK := 0b1111_1111_1111_1111_1111_1111_1111_1111
+
+
+## If true, teleporting is enabled
+@export var enabled : bool = true: set = set_enabled
+
+## Teleport allowed color property
+@export var can_teleport_color : Color = Color(0.0, 1.0, 0.0, 1.0)
+
+## Teleport denied color property
+@export var cant_teleport_color : Color = Color(1.0, 0.0, 0.0, 1.0)
+
+## Teleport no-collision color property
+@export var no_collision_color : Color = Color(45.0 / 255.0, 80.0 / 255.0, 220.0 / 255.0, 1.0)
+
+## Player height property
+@export var player_height : float = 1.8: set = set_player_height
+
+## Player radius property
+@export var player_radius : float = 0.4: set = set_player_radius
+
+## Teleport-arc strength
+@export var strength : float = 5.0
+
+## Maximum floor slope
+@export var max_slope : float = 20.0
+
+## Valid teleport layer mask
+@export_flags_3d_physics var valid_teleport_mask : int = DEFAULT_MASK
+
+# once this is no longer a kinematic body, we'll need this..
+# export (int, LAYERS_3D_PHYSICS) var collision_mask = 1
+
+## Teleport button action
+@export var teleport_button_action : String = "trigger_click"
+
+## Teleport rotation action
+@export var rotation_action : String = "primary"
+
+
+var is_on_floor : bool = true
+var is_teleporting : bool = false
+var can_teleport : bool = true
+var teleport_rotation : float = 0.0;
+var floor_normal : Vector3 = Vector3.UP
+var last_target_transform : Transform3D = Transform3D()
+var collision_shape : Shape3D
+var step_size : float = 0.5
+
+
+# World scale
+@onready var ws : float = XRServer.world_scale
+
+# By default we show a capsule to indicate where the player lands.
+# Turn on editable children,
+# hide the capsule,
+# and add your own player character as child.
+@onready var capsule : MeshInstance3D = get_node("Target/Player_figure/Capsule")
+
+## [XROrigin3D] node.
+@onready var origin_node := XRHelpers.get_xr_origin(self)
+
+## [XRCamera3D] node.
+@onready var camera_node := XRHelpers.get_xr_camera(self)
+
+## [XRController3D] node.
+@onready var controller := XRHelpers.get_xr_controller(self)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsFunctionTeleport"
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	# Do not initialise if in the editor
+	if Engine.is_editor_hint():
+		return
+
+	# It's inactive when we start
+	$Teleport.visible = false
+	$Target.visible = false
+
+	# Scale to our world scale
+	$Teleport.mesh.size = Vector2(0.05 * ws, 1.0)
+	$Target.mesh.size = Vector2(ws, ws)
+	$Target/Player_figure.scale = Vector3(ws, ws, ws)
+
+	# get our capsule shape
+	collision_shape = $CollisionShape3D.shape
+	$CollisionShape3D.shape = null
+
+	# now remove our collision shape, we are not using our kinematic body
+	remove_child($CollisionShape3D)
+
+	# call set player to ensure our collision shape is sized
+	_update_player_height()
+	_update_player_radius()
+
+
+func _physics_process(delta):
+	# Do not process physics if in the editor
+	if Engine.is_editor_hint():
+		return
+
+	# Skip if required nodes are missing
+	if !origin_node or !camera_node or !controller:
+		return
+
+	# if we're not enabled no point in doing mode
+	if !enabled:
+		# reset these
+		is_teleporting = false;
+		$Teleport.visible = false
+		$Target.visible = false
+
+		# and stop this from running until we enable again
+		set_physics_process(false)
+		return
+
+	# check if our world scale has changed..
+	var new_ws = XRServer.world_scale
+	if ws != new_ws:
+		ws = new_ws
+		$Teleport.mesh.size = Vector2(0.05 * ws, 1.0)
+		$Target.mesh.size = Vector2(ws, ws)
+		$Target/Player_figure.scale = Vector3(ws, ws, ws)
+
+	if controller and controller.get_is_active() and \
+			controller.is_button_pressed(teleport_button_action):
+		if !is_teleporting:
+			is_teleporting = true
+			$Teleport.visible = true
+			$Target.visible = true
+			teleport_rotation = 0.0
+
+		# get our physics engine state
+		var space = PhysicsServer3D.body_get_space(self.get_rid())
+		var state = PhysicsServer3D.space_get_direct_state(space)
+		var query = PhysicsShapeQueryParameters3D.new()
+
+		# init stuff about our query that doesn't change
+		query.collision_mask = collision_mask
+		query.margin = get_safe_margin()
+		query.shape_rid = collision_shape.get_rid()
+
+		# make a transform for offsetting our shape, it's always
+		# lying on its side by default...
+		var shape_transform = Transform3D(
+				Basis(),
+				Vector3(0.0, player_height / 2.0, 0.0))
+
+		# update location
+		var teleport_global_transform = $Teleport.global_transform
+		var target_global_origin = teleport_global_transform.origin
+		var down = Vector3(0.0, -1.0 / ws, 0.0)
+
+		############################################################
+		# New teleport logic
+		# We're going to use test move in steps to find out where we hit something...
+		# This can be optimised loads by determining the lenght based on the angle
+		# between sections extending the length when we're in a flat part of the arch
+		# Where we do get a collission we may want to fine tune the collision
+		var cast_length = 0.0
+		var fine_tune = 1.0
+		var hit_something = false
+		var max_slope_cos = cos(deg_to_rad(max_slope))
+		for i in range(1,26):
+			var new_cast_length = cast_length + (step_size / fine_tune)
+			var global_target = Vector3(0.0, 0.0, -new_cast_length)
+
+			# our quadratic values
+			var t = global_target.z / strength
+			var t2 = t * t
+
+			# target to world space
+			global_target = teleport_global_transform * global_target
+
+			# adjust for gravity
+			global_target += down * t2
+
+			# test our new location for collisions
+			query.transform = Transform3D(Basis(), global_target) * shape_transform
+			var cast_result = state.collide_shape(query, 10)
+			if cast_result.is_empty():
+				# we didn't collide with anything so check our next section...
+				cast_length = new_cast_length
+				target_global_origin = global_target
+			elif (fine_tune <= 16.0):
+				# try again with a small step size
+				fine_tune *= 2.0
+			else:
+				# if we don't collide make sure we keep using our current origin point
+				var collided_at = target_global_origin
+
+				# check for collision
+				if global_target.y > target_global_origin.y:
+					# if we're moving up, we hit the ceiling of something, we
+					# don't really care what
+					is_on_floor = false
+				else:
+					# now we cast a ray downwards to see if we're on a surface
+					var ray_query = PhysicsRayQueryParameters3D.new()
+					ray_query.from = target_global_origin + (Vector3.UP * 0.5 * player_height)
+					ray_query.to = target_global_origin - (Vector3.UP * 1.1 * player_height)
+					ray_query.collision_mask = collision_mask
+
+					var intersects = state.intersect_ray(ray_query)
+					if intersects.is_empty():
+						is_on_floor = false
+					else:
+						# did we collide with a floor or a wall?
+						floor_normal = intersects["normal"]
+						var dot = floor_normal.dot(Vector3.UP)
+
+						if dot > max_slope_cos:
+							is_on_floor = true
+						else:
+							is_on_floor = false
+
+						# Update our collision point if it's moved enough, this
+						# solves a little bit of jittering
+						var diff = collided_at - intersects["position"]
+
+						if diff.length() > 0.1:
+							collided_at = intersects["position"]
+
+						# Fail if the hit target isn't in our valid mask
+						var collider_mask = intersects["collider"].collision_layer
+						if not valid_teleport_mask & collider_mask:
+							is_on_floor = false
+
+				# we are colliding, find our if we're colliding on a wall or
+				# floor, one we can do, the other nope...
+				cast_length += (collided_at - target_global_origin).length()
+				target_global_origin = collided_at
+				hit_something = true
+				break
+
+		# and just update our shader
+		$Teleport.get_surface_override_material(0).set_shader_parameter("scale_t", 1.0 / strength)
+		$Teleport.get_surface_override_material(0).set_shader_parameter("ws", ws)
+		$Teleport.get_surface_override_material(0).set_shader_parameter("length", cast_length)
+		if hit_something:
+			var color = can_teleport_color
+			var normal = Vector3.UP
+			if is_on_floor:
+				# if we're on the floor we'll reorientate our target to match.
+				normal = floor_normal
+				can_teleport = true
+			else:
+				can_teleport = false
+				color = cant_teleport_color
+
+			# check our axis to see if we need to rotate
+			teleport_rotation += (delta * controller.get_vector2(rotation_action).x * -4.0)
+
+			# update target and colour
+			var target_basis = Basis()
+			target_basis.z = Vector3(
+					teleport_global_transform.basis.z.x,
+					0.0,
+					teleport_global_transform.basis.z.z).normalized()
+			target_basis.y = normal
+			target_basis.x = target_basis.y.cross(target_basis.z)
+			target_basis.z = target_basis.x.cross(target_basis.y)
+
+			target_basis = target_basis.rotated(normal, teleport_rotation)
+			last_target_transform.basis = target_basis
+			last_target_transform.origin = target_global_origin + Vector3(0.0, 0.001, 0.0)
+			$Target.global_transform = last_target_transform
+
+			$Teleport.get_surface_override_material(0).set_shader_parameter("mix_color", color)
+			$Target.get_surface_override_material(0).albedo_color = color
+			$Target.visible = can_teleport
+		else:
+			can_teleport = false
+			$Target.visible = false
+			$Teleport.get_surface_override_material(0).set_shader_parameter("mix_color", no_collision_color)
+	elif is_teleporting:
+		if can_teleport:
+
+			# make our target horizontal again
+			var new_transform = last_target_transform
+			new_transform.basis.y = Vector3(0.0, 1.0, 0.0)
+			new_transform.basis.x = new_transform.basis.y.cross(new_transform.basis.z).normalized()
+			new_transform.basis.z = new_transform.basis.x.cross(new_transform.basis.y).normalized()
+
+			# Find out our user's feet's transformation.
+			# The feet are on the ground, but have the same X,Z as the camera
+			var cam_transform = camera_node.transform
+			var user_feet_transform = Transform3D()
+			user_feet_transform.origin = cam_transform.origin
+			user_feet_transform.origin.y = 0
+
+			# ensure this transform is upright
+			user_feet_transform.basis.y = Vector3(0.0, 1.0, 0.0)
+			user_feet_transform.basis.x = user_feet_transform.basis.y.cross(
+					cam_transform.basis.z).normalized()
+			user_feet_transform.basis.z = user_feet_transform.basis.x.cross(
+					user_feet_transform.basis.y).normalized()
+
+			# now move the origin such that the new global user_feet_transform
+			# would be == new_transform
+			origin_node.global_transform = new_transform * user_feet_transform.inverse()
+
+		# and disable
+		is_teleporting = false;
+		$Teleport.visible = false
+		$Target.visible = false
+
+
+# This method verifies the teleport has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := PackedStringArray()
+
+	# Verify we can find the XROrigin3D
+	if !XRHelpers.get_xr_origin(self):
+		warnings.append("This node must be within a branch of an XROrigin3D node")
+
+	# Verify we can find the XRCamera3D
+	if !XRHelpers.get_xr_camera(self):
+		warnings.append("Unable to find XRCamera3D node")
+
+	# Verify we can find the XRController3D
+	if !XRHelpers.get_xr_controller(self):
+		warnings.append("This node must be within a branch of an XRController3D node")
+
+	# Return warnings
+	return warnings
+
+
+# Set enabled property
+func set_enabled(new_value : bool) -> void:
+	enabled = new_value
+	if enabled:
+		# make sure our physics process is on
+		set_physics_process(true)
+	else:
+		# we turn this off in physics process just in case we want to do some cleanup
+		pass
+
+
+# Set player height property
+func set_player_height(p_height : float) -> void:
+	player_height = p_height
+	_update_player_height()
+
+
+# Set player radius property
+func set_player_radius(p_radius : float) -> void:
+	player_radius = p_radius
+	_update_player_radius()
+
+
+# Player height update handler
+func _update_player_height() -> void:
+	if collision_shape:
+		collision_shape.height = player_height - (2.0 * player_radius)
+
+	if capsule:
+		capsule.mesh.height = player_height
+		capsule.position = Vector3(0.0, player_height/2.0, 0.0)
+
+
+# Player radius update handler
+func _update_player_radius():
+	if collision_shape:
+		collision_shape.height = player_height
+		collision_shape.radius = player_radius
+
+	if capsule:
+		capsule.mesh.height = player_height
+		capsule.mesh.radius = player_radius

+ 48 - 0
addons/godot-xr-tools/functions/function_teleport.tscn

@@ -0,0 +1,48 @@
+[gd_scene load_steps=9 format=3 uid="uid://fiul51tsyoop"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/function_teleport.gd" id="1"]
+[ext_resource type="Material" uid="uid://bk72wfw25ff0v" path="res://addons/godot-xr-tools/materials/teleport.tres" id="2"]
+[ext_resource type="Material" path="res://addons/godot-xr-tools/materials/target.tres" id="3"]
+[ext_resource type="Material" path="res://addons/godot-xr-tools/materials/capsule.tres" id="4"]
+
+[sub_resource type="PlaneMesh" id="1"]
+size = Vector2(0.05, 1)
+subdivide_depth = 40
+
+[sub_resource type="PlaneMesh" id="2"]
+size = Vector2(1, 1)
+
+[sub_resource type="CapsuleMesh" id="3"]
+radius = 0.4
+height = 1.8
+
+[sub_resource type="CapsuleShape3D" id="4"]
+radius = 0.05
+height = 0.1
+
+[node name="FunctionTeleport" type="CharacterBody3D"]
+collision_layer = 524288
+collision_mask = 1023
+input_ray_pickable = false
+script = ExtResource("1")
+no_collision_color = Color(0.176471, 0.313726, 0.862745, 1)
+
+[node name="Teleport" type="MeshInstance3D" parent="."]
+mesh = SubResource("1")
+surface_material_override/0 = ExtResource("2")
+
+[node name="Target" type="MeshInstance3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, -4.92359)
+mesh = SubResource("2")
+surface_material_override/0 = ExtResource("3")
+
+[node name="Player_figure" type="Marker3D" parent="Target"]
+
+[node name="Capsule" type="MeshInstance3D" parent="Target/Player_figure"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
+mesh = SubResource("3")
+surface_material_override/0 = ExtResource("4")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, -1.62921e-07, -1, 0, 1, -1.62921e-07, 0, 0, 0)
+shape = SubResource("4")

+ 230 - 0
addons/godot-xr-tools/functions/movement_climb.gd

@@ -0,0 +1,230 @@
+@tool
+class_name XRToolsMovementClimb
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Climbing
+##
+## This script provides climbing movement for the player. To add climbing
+## support, the player must also have [XRToolsFunctionPickup] nodes attached
+## to the left and right controllers, and an [XRToolsPlayerBody] under the
+## [XROrigin3D].
+##
+## Climbable objects can inherit from the climbable scene, or be [StaticBody]
+## objects with the [XRToolsClimbable] script attached to them.
+##
+## When climbing, the global velocity of the [XRToolsPlayerBody] is averaged,
+## and upon release the velocity is applied to the [XRToolsPlayerBody] with an
+## optional fling multiplier, so the player can fling themselves up walls if
+## desired.
+
+
+## Signal invoked when the player starts climing
+signal player_climb_start
+
+## Signal invoked when the player ends climbing
+signal player_climb_end
+
+
+## Distance at which grabs snap
+const SNAP_DISTANCE : float = 1.0
+
+
+## Movement provider order
+@export var order : int = 15
+
+## Push forward when flinging
+@export var forward_push : float = 1.0
+
+## Velocity multiplier when flinging up walls
+@export var fling_multiplier : float = 1.0
+
+## Averages for velocity measurement
+@export var velocity_averages : int = 5
+
+
+## Left climbable
+var _left_climbable : XRToolsClimbable
+
+## Right climbable
+var _right_climbable : XRToolsClimbable
+
+## Dominant pickup (moving the player)
+var _dominant : XRToolsFunctionPickup
+
+
+# Velocity averager
+@onready var _averager := XRToolsVelocityAveragerLinear.new(velocity_averages)
+
+# Left pickup node
+@onready var _left_pickup_node := XRToolsFunctionPickup.find_left(self)
+
+# Right pickup node
+@onready var _right_pickup_node := XRToolsFunctionPickup.find_right(self)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementClimb" or super(name)
+
+
+## Called when the node enters the scene tree for the first time.
+func _ready():
+	# In Godot 4 we must now manually call our super class ready function
+	super()
+
+	# Connect pickup funcitons
+	if _left_pickup_node.connect("has_picked_up", _on_left_picked_up):
+		push_error("Unable to connect left picked up signal")
+	if _right_pickup_node.connect("has_picked_up", _on_right_picked_up):
+		push_error("Unable to connect right picked up signal")
+	if _left_pickup_node.connect("has_dropped", _on_left_dropped):
+		push_error("Unable to connect left dropped signal")
+	if _right_pickup_node.connect("has_dropped", _on_right_dropped):
+		push_error("Unable to connect right dropped signal")
+
+
+## Perform player physics movement
+func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
+	# Disable climbing if requested
+	if disabled or !enabled:
+		_set_climbing(false, player_body)
+		return
+
+	# Snap grabs if too far
+	if is_instance_valid(_left_climbable):
+		var left_pickup_pos := _left_pickup_node.global_transform.origin
+		var left_grab_pos := _left_climbable.get_grab_location(_left_pickup_node)
+		if left_pickup_pos.distance_to(left_grab_pos) > SNAP_DISTANCE:
+			_left_pickup_node.drop_object()
+	if is_instance_valid(_right_climbable):
+		var right_pickup_pos := _right_pickup_node.global_transform.origin
+		var right_grab_pos := _right_climbable.get_grab_location(_right_pickup_node)
+		if right_pickup_pos.distance_to(right_grab_pos) > SNAP_DISTANCE:
+			_right_pickup_node.drop_object()
+
+	# Update climbing
+	_set_climbing(_dominant != null, player_body)
+
+	# Skip if not actively climbing
+	if !is_active:
+		return
+
+	# Calculate how much the player has moved
+	var offset := Vector3.ZERO
+	if _dominant == _left_pickup_node:
+		var left_pickup_pos := _left_pickup_node.global_transform.origin
+		var left_grab_pos := _left_climbable.get_grab_location(_left_pickup_node)
+		offset = left_pickup_pos - left_grab_pos
+	elif _dominant == _right_pickup_node:
+		var right_pickup_pos := _right_pickup_node.global_transform.origin
+		var right_grab_pos := _right_climbable.get_grab_location(_right_pickup_node)
+		offset = right_pickup_pos - right_grab_pos
+
+	# Move the player by the offset
+	var old_position := player_body.global_transform.origin
+	player_body.move_and_collide(-offset)
+	player_body.velocity = Vector3.ZERO
+
+	# Update the players average-velocity data
+	var distance := player_body.global_transform.origin - old_position
+	_averager.add_distance(delta, distance)
+
+	# Report exclusive motion performed (to bypass gravity)
+	return true
+
+
+## Start or stop climbing
+func _set_climbing(active: bool, player_body: XRToolsPlayerBody) -> void:
+	# Skip if no change
+	if active == is_active:
+		return
+
+	# Update state
+	is_active = active
+
+	# Handle state change
+	if is_active:
+		_averager.clear()
+		player_body.override_player_height(self, 0.0)
+		emit_signal("player_climb_start")
+	else:
+		# Calculate the forward direction (based on camera-forward)
+		var dir_forward = -player_body.up_player_plane.project(
+			player_body.camera_node.global_transform.basis.z).normalized()
+
+		# Set player velocity based on averaged velocity, fling multiplier,
+		# and a forward push
+		var velocity := _averager.velocity()
+		player_body.velocity = (velocity * fling_multiplier) + (dir_forward * forward_push)
+
+		player_body.override_player_height(self)
+		emit_signal("player_climb_end")
+
+
+## Handler for left controller picked up
+func _on_left_picked_up(what : Node3D) -> void:
+	# Get the climbable
+	_left_climbable = what as XRToolsClimbable
+
+	# Transfer climb dominance
+	if is_instance_valid(_left_climbable):
+		_dominant = _left_pickup_node
+	else:
+		_left_climbable = null
+
+
+## Handler for right controller picked up
+func _on_right_picked_up(what : Node3D) -> void:
+	# Get the climbable
+	_right_climbable = what as XRToolsClimbable
+
+	# Transfer climb dominance
+	if is_instance_valid(_right_climbable):
+		_dominant = _right_pickup_node
+	else:
+		_right_climbable = null
+
+
+## Handler for left controller dropped
+func _on_left_dropped() -> void:
+	# Release climbable
+	_left_climbable = null
+
+	# Transfer climb dominance
+	if is_instance_valid(_right_climbable):
+		_dominant = _right_pickup_node
+	else:
+		_dominant = null
+
+
+## Handler for righ controller dropped
+func _on_right_dropped() -> void:
+	# Release climbable
+	_right_climbable = null
+
+	# Transfer climb dominance
+	if is_instance_valid(_left_climbable):
+		_dominant = _left_pickup_node
+	else:
+		_dominant = null
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Verify the left controller pickup
+	if !XRToolsFunctionPickup.find_left(self):
+		warnings.append("Unable to find left XRToolsFunctionPickup node")
+
+	# Verify the right controller pickup
+	if !XRToolsFunctionPickup.find_right(self):
+		warnings.append("Unable to find right XRToolsFunctionPickup node")
+
+	# Verify velocity averages
+	if velocity_averages < 2:
+		warnings.append("Minimum of 2 velocity averages needed")
+
+	# Return warnings
+	return warnings

+ 6 - 0
addons/godot-xr-tools/functions/movement_climb.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://bxm1ply47vaan"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_climb.gd" id="1"]
+
+[node name="MovementClimb" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 92 - 0
addons/godot-xr-tools/functions/movement_crouch.gd

@@ -0,0 +1,92 @@
+@tool
+class_name XRToolsMovementCrouch
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Crouching
+##
+## This script works with the [XRToolsPlayerBody] attached to the players
+## [XROrigin3D].
+##
+## While the player presses the crounch button, the height is overridden to
+## the specified crouch height.
+
+
+## Enumeration of crouching modes
+enum CrouchType {
+	HOLD_TO_CROUCH,	## Hold button to crouch
+	TOGGLE_CROUCH,	## Toggle crouching on button press
+}
+
+
+## Movement provider order
+@export var order : int = 10
+
+## Crouch height
+@export var crouch_height : float = 1.0
+
+## Crouch button
+@export var crouch_button_action : String = "primary_click"
+
+## Type of crouching
+@export var crouch_type : CrouchType = CrouchType.HOLD_TO_CROUCH
+
+
+## Crouching flag
+var _crouching : bool = false
+
+## Crouch button down state
+var _crouch_button_down : bool = false
+
+
+# Controller node
+@onready var _controller := XRHelpers.get_xr_controller(self)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementCrouch" or super(name)
+
+
+# Perform jump movement
+func physics_movement(_delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
+	# Skip if the controller isn't active
+	if !_controller.get_is_active():
+		return
+
+	# Detect crouch button down and pressed states
+	var crouch_button_down := _controller.is_button_pressed(crouch_button_action)
+	var crouch_button_pressed := crouch_button_down and !_crouch_button_down
+	_crouch_button_down = crouch_button_down
+
+	# Calculate new crouching state
+	var crouching := _crouching
+	match crouch_type:
+		CrouchType.HOLD_TO_CROUCH:
+			# Crouch when button down
+			crouching = crouch_button_down
+
+		CrouchType.TOGGLE_CROUCH:
+			# Toggle when button pressed
+			if crouch_button_pressed:
+				crouching = !crouching
+
+	# Update crouching state
+	if crouching != _crouching:
+		_crouching = crouching
+		if crouching:
+			player_body.override_player_height(self, crouch_height)
+		else:
+			player_body.override_player_height(self)
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Check the controller node
+	if !XRHelpers.get_xr_controller(self):
+		warnings.append("This node must be within a branch of an XRController3D node")
+
+	# Return warnings
+	return warnings

+ 6 - 0
addons/godot-xr-tools/functions/movement_crouch.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://clt88d5d1dje4"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_crouch.gd" id="1"]
+
+[node name="MovementCrouch" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 87 - 0
addons/godot-xr-tools/functions/movement_direct.gd

@@ -0,0 +1,87 @@
+@tool
+class_name XRToolsMovementDirect
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Direct Movement
+##
+## This script provides direct movement for the player. This script works
+## with the [XRToolsPlayerBody] attached to the players [XROrigin3D].
+##
+## The player may have multiple [XRToolsMovementDirect] nodes attached to
+## different controllers to provide different types of direct movement.
+
+
+## Movement provider order
+@export var order : int = 10
+
+## Movement speed
+@export var max_speed : float = 3.0
+
+## If true, the player can strafe
+@export var strafe : bool = false
+
+## Input action for movement direction
+@export var input_action : String = "primary"
+
+
+# Controller node
+@onready var _controller := XRHelpers.get_xr_controller(self)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementDirect" or super(name)
+
+
+# Perform jump movement
+func physics_movement(_delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
+	# Skip if the controller isn't active
+	if !_controller.get_is_active():
+		return
+
+	# Apply forwards/backwards ground control
+	player_body.ground_control_velocity.y += _controller.get_vector2(input_action).y * max_speed
+
+	# Apply left/right ground control
+	if strafe:
+		player_body.ground_control_velocity.x += _controller.get_vector2(input_action).x * max_speed
+
+	# Clamp ground control
+	var length := player_body.ground_control_velocity.length()
+	if length > max_speed:
+		player_body.ground_control_velocity *= max_speed / length
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Check the controller node
+	if !XRHelpers.get_xr_controller(self):
+		warnings.append("This node must be within a branch of an XRController3D node")
+
+	# Return warnings
+	return warnings
+
+
+## Find the left [XRToolsMovementDirect] node.
+##
+## This function searches from the specified node for the left controller
+## [XRToolsMovementDirect] assuming the node is a sibling of the [XROrigin3D].
+static func find_left(node : Node) -> XRToolsMovementDirect:
+	return XRTools.find_xr_child(
+		XRHelpers.get_left_controller(node),
+		"*",
+		"XRToolsMovementDirect") as XRToolsMovementDirect
+
+
+## Find the right [XRToolsMovementDirect] node.
+##
+## This function searches from the specified node for the right controller
+## [XRToolsMovementDirect] assuming the node is a sibling of the [XROrigin3D].
+static func find_right(node : Node) -> XRToolsMovementDirect:
+	return XRTools.find_xr_child(
+		XRHelpers.get_right_controller(node),
+		"*",
+		"XRToolsMovementDirect") as XRToolsMovementDirect

+ 6 - 0
addons/godot-xr-tools/functions/movement_direct.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://bl2nuu3qhlb5k"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_direct.gd" id="1"]
+
+[node name="MovementDirect" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 228 - 0
addons/godot-xr-tools/functions/movement_flight.gd

@@ -0,0 +1,228 @@
+@tool
+class_name XRToolsMovementFlight
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Flying
+##
+## This script provides flying movement for the player. The control parameters
+## are intended to support a wide variety of flight mechanics.
+##
+## Pitch and Bearing input devices are selected which produce a "forwards"
+## reference frame. The player controls (forwards/backwards and
+## left/right) are applied in relation to this reference frame.
+##
+## The Speed Scale and Traction parameters allow primitive flight where
+## the player is in direct control of their speed (in the reference frame).
+## This produces an effect described as the "Mary Poppins Flying Umbrella".
+##
+## The Acceleration, Drag, and Guidance parameters allow for slightly more
+## realisitic flying where the player can accelerate in their reference
+## frame. The drag is applied against the global reference and can be used
+## to construct a terminal velocity.
+##
+## The Guidance property attempts to lerp the players velocity into flight
+## forwards direction as if the player had guide-fins or wings.
+##
+## The Exclusive property specifies whether flight is exclusive (no further
+## physics effects after flying) or whether additional effects such as
+## the default player gravity are applied.
+
+
+## Signal emitted when flight starts
+signal flight_started()
+
+## Signal emitted when flight finishes
+signal flight_finished()
+
+
+## Enumeration of controller to use for flight
+enum FlightController {
+	LEFT,		## Use left controller
+	RIGHT,		## Use right controler
+}
+
+## Enumeration of pitch control input
+enum FlightPitch {
+	HEAD,		## Head controls pitch
+	CONTROLLER,	## Controller controls pitch
+}
+
+## Enumeration of bearing control input
+enum FlightBearing {
+	HEAD,		## Head controls bearing
+	CONTROLLER,	## Controller controls bearing
+	BODY,		## Body controls bearing
+}
+
+
+## Movement provider order
+@export var order : int = 30
+
+## Flight controller
+@export var controller : FlightController = FlightController.LEFT
+
+## Flight toggle button
+@export var flight_button : String = "by_button"
+
+## Flight pitch control
+@export var pitch : FlightPitch = FlightPitch.CONTROLLER
+
+## Flight bearing control
+@export var bearing : FlightBearing = FlightBearing.CONTROLLER
+
+## Flight speed from control
+@export var speed_scale : float = 5.0
+
+## Flight traction pulling flight velocity towards the controlled speed
+@export var speed_traction : float = 3.0
+
+## Flight acceleration from control
+@export var acceleration_scale : float = 0.0
+
+## Flight drag
+@export var drag : float = 0.1
+
+## Guidance effect (virtual fins/wings)
+@export var guidance : float = 0.0
+
+## If true, flight movement is exclusive preventing further movement functions
+@export var exclusive : bool = true
+
+
+## Flight button state
+var _flight_button : bool = false
+
+## Flight controller
+var _controller : XRController3D
+
+
+# Node references
+@onready var _camera := XRHelpers.get_xr_camera(self)
+@onready var _left_controller := XRHelpers.get_left_controller(self)
+@onready var _right_controller := XRHelpers.get_right_controller(self)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementFlight" or super(name)
+
+
+func _ready():
+	# In Godot 4 we must now manually call our super class ready function
+	super()
+
+	# Get the flight controller
+	if controller == FlightController.LEFT:
+		_controller = _left_controller
+	else:
+		_controller = _right_controller
+
+
+# Process physics movement for flight
+func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
+	# Disable flying if requested, or if no controller
+	if disabled or !enabled or !_controller.get_is_active():
+		set_flying(false)
+		return
+
+	# Detect press of flight button
+	var old_flight_button = _flight_button
+	_flight_button = _controller.is_button_pressed(flight_button)
+	if _flight_button and !old_flight_button:
+		set_flying(!is_active)
+
+	# Skip if not flying
+	if !is_active:
+		return
+
+	# Select the pitch vector
+	var pitch_vector: Vector3
+	if pitch == FlightPitch.HEAD:
+		# Use the vertical part of the 'head' forwards vector
+		pitch_vector = -_camera.transform.basis.z.y * player_body.up_player_vector
+	else:
+		# Use the vertical part of the 'controller' forwards vector
+		pitch_vector = -_controller.transform.basis.z.y * player_body.up_player_vector
+
+	# Select the bearing vector
+	var bearing_vector: Vector3
+	if bearing == FlightBearing.HEAD:
+		# Use the horizontal part of the 'head' forwards vector
+		bearing_vector = -player_body.up_player_plane.project(
+				_camera.global_transform.basis.z)
+	elif bearing == FlightBearing.CONTROLLER:
+		# Use the horizontal part of the 'controller' forwards vector
+		bearing_vector = -player_body.up_player_plane.project(
+				_controller.global_transform.basis.z)
+	else:
+		# Use the horizontal part of the 'body' forwards vector
+		var left := _left_controller.global_transform.origin
+		var right := _right_controller.global_transform.origin
+		var left_to_right := right - left
+		bearing_vector = player_body.up_player_plane.project(
+				left_to_right.rotated(player_body.up_player_vector, PI/2))
+
+	# Construct the flight bearing
+	var forwards := (bearing_vector.normalized() + pitch_vector).normalized()
+	var side := forwards.cross(player_body.up_player_vector)
+
+	# Construct the target velocity
+	var joy_forwards := _controller.get_vector2("primary").y
+	var joy_side := _controller.get_vector2("primary").x
+	var heading := forwards * joy_forwards + side * joy_side
+
+	# Calculate the flight velocity
+	var flight_velocity := player_body.velocity
+	flight_velocity *= 1.0 - drag * delta
+	flight_velocity = flight_velocity.lerp(heading * speed_scale, speed_traction * delta)
+	flight_velocity += heading * acceleration_scale * delta
+
+	# Apply virtual guidance effect
+	if guidance > 0.0:
+		var velocity_forwards := forwards * flight_velocity.length()
+		flight_velocity = flight_velocity.lerp(velocity_forwards, guidance * delta)
+
+	# If exclusive then perform the exclusive move-and-slide
+	if exclusive:
+		player_body.velocity = player_body.move_body(flight_velocity)
+		return true
+
+	# Update velocity and return for additional effects
+	player_body.velocity = flight_velocity
+	return
+
+
+func set_flying(active: bool) -> void:
+	# Skip if no change
+	if active == is_active:
+		return
+
+	# Update state
+	is_active = active
+
+	# Handle state change
+	if is_active:
+		emit_signal("flight_started")
+	else:
+		emit_signal("flight_finished")
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Verify the camera
+	if !XRHelpers.get_xr_camera(self):
+		warnings.append("Unable to find XRCamera3D")
+
+	# Verify the left controller
+	if !XRHelpers.get_left_controller(self):
+		warnings.append("Unable to find left XRController3D node")
+
+	# Verify the right controller
+	if !XRHelpers.get_right_controller(self):
+		warnings.append("Unable to find left XRController3D node")
+
+	# Return warnings
+	return warnings

+ 6 - 0
addons/godot-xr-tools/functions/movement_flight.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://kyhaogt0a4q8"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_flight.gd" id="1"]
+
+[node name="MovementFlight" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 245 - 0
addons/godot-xr-tools/functions/movement_footstep.gd

@@ -0,0 +1,245 @@
+@tool
+class_name XRToolsMovementFootstep
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Footsteps
+##
+## This movement provider detects walking on different surfaces.
+## It plays audio sounds associated with the surface the player is
+## currently walking on.
+
+
+## Signal emitted when a footstep is generated
+signal footstep(name)
+
+
+# Number of audio players to pool
+const AUDIO_POOL_SIZE := 3
+
+
+## Movement provider order
+@export var order : int = 1001
+
+## Default XRToolsSurfaceAudioType when not overridden
+@export var default_surface_audio_type : XRToolsSurfaceAudioType
+
+## Speed at which the player is considered walking
+@export var walk_speed := 0.4
+
+## Step per meter by time
+@export var steps_per_meter = 1.0
+
+
+# step time
+var step_time = 0.0
+
+# Last on_ground state of the player
+var _old_on_ground := true
+
+# Node representing the location of the players foot
+var _foot_spatial : Node3D
+
+# Pool of idle AudioStreamPlayer3D nodes
+var _audio_pool_idle : Array[AudioStreamPlayer3D]
+
+# Last ground node
+var _ground_node : Node
+
+# Surface audio type associated with last ground node
+var _ground_node_audio_type : XRToolsSurfaceAudioType
+
+
+## PlayerBody - Player Physics Body Script
+@onready var player_body := XRToolsPlayerBody.find_instance(self)
+
+
+# Add support for is_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementFootstep" or super(name)
+
+
+func _ready():
+	# In Godot 4 we must now manually call our super class ready function
+	super()
+
+	# Construct the foot spatial - we will move it around as the player moves.
+	_foot_spatial = Node3D.new()
+	_foot_spatial.name = "FootSpatial"
+	add_child(_foot_spatial)
+
+	# Make the array of players in _audio_pool_idle
+	for i in AUDIO_POOL_SIZE:
+		var player = $PlayerSettings.duplicate()
+		player.name = "PlayerCopy%d" % (i + 1)
+		_foot_spatial.add_child(player)
+		_audio_pool_idle.append(player)
+		player.finished.connect(_on_player_finished.bind(player))
+
+	# Set as always active
+	is_active = true
+
+	# Listen for the player jumping
+	player_body.player_jumped.connect(_on_player_jumped)
+
+
+# This method checks for configuration issues.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Verify player settings node exists
+	if not $PlayerSettings:
+		warnings.append("Missing player settings node")
+
+	# Return warnings
+	return warnings
+
+
+func physics_movement(_delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
+	# Update the spatial location of the foot
+	_update_foot_spatial()
+
+	# Update the ground audio information
+	_update_ground_audio()
+
+	# Skip if footsteps have been disabled
+	if not enabled:
+		step_time = 0
+		return
+
+	# Detect landing on ground
+	if not _old_on_ground and player_body.on_ground:
+		# Play the ground hit sound
+		_play_ground_hit()
+
+	# Update the old on_ground state
+	_old_on_ground = player_body.on_ground
+	if not player_body.on_ground:
+		step_time = 0 	# Reset when not on ground
+		return
+
+	# Handle slow/stopped
+	if player_body.ground_control_velocity.length() < walk_speed:
+		step_time = 0	# Reset when slow/stopped
+		return
+
+	# Count up the step timer, and skip if not take a step yet
+	step_time += _delta * player_body.ground_control_velocity.length()
+	if step_time > steps_per_meter:
+		_play_step_sound()
+		step_time = 0
+
+
+# Update the foot spatial to be where the players foot is
+func _update_foot_spatial() -> void:
+	# Project the players camera down to the XZ plane (real-world space)
+	var local_foot := Plane.PLANE_XZ.project(player_body.camera_node.position)
+
+	# Move the foot_spatial to the local foot in the global origin space
+	_foot_spatial.global_position = player_body.origin_node.global_transform * local_foot
+
+
+# Update the ground audio information
+func _update_ground_audio() -> void:
+	# Skip if no change
+	if player_body.ground_node == _ground_node:
+		return
+
+	# Save the new ground node
+	_ground_node = player_body.ground_node
+
+	# Handle no ground
+	if not _ground_node:
+		_ground_node_audio_type = null
+		return
+
+	# Find the surface audio for the ground (if any)
+	var ground_audio : XRToolsSurfaceAudio = XRTools.find_xr_child(
+		_ground_node, "*", "XRToolsSurfaceAudio")
+	if ground_audio:
+		_ground_node_audio_type = ground_audio.surface_audio_type
+	else:
+		_ground_node_audio_type = default_surface_audio_type
+
+
+# Called when the player jumps
+func _on_player_jumped() -> void:
+	# Skip if no jump sound
+	if not _ground_node_audio_type:
+		return
+
+	# Play the jump sound
+	_play_sound(
+			_ground_node_audio_type.name,
+			_ground_node_audio_type.jump_sound)
+
+
+# Play the hit sound made when the player lands on the ground
+func _play_ground_hit() -> void:
+	# Skip if no hit sound
+	if not _ground_node_audio_type:
+		return
+
+	# Play the hit sound
+	_play_sound(
+			_ground_node_audio_type.name,
+			_ground_node_audio_type.hit_sound)
+
+
+# Play a step sound for the current ground
+func _play_step_sound() -> void:
+	# Skip if no walk audio
+	if not _ground_node_audio_type or _ground_node_audio_type.walk_sounds.size() == 0:
+		return
+
+	# Pick the sound index
+	var idx := randi() % _ground_node_audio_type.walk_sounds.size()
+
+	# Pick the playback pitck
+	var pitch := randf_range(
+			_ground_node_audio_type.walk_pitch_minimum,
+			_ground_node_audio_type.walk_pitch_maximum)
+
+	# Play the walk sound
+	_play_sound(
+			_ground_node_audio_type.name,
+			_ground_node_audio_type.walk_sounds[idx],
+			pitch)
+
+
+# Play the specified audio stream at the requested pitch using an
+# AudioStreamPlayer3D in the idle pool of players.
+func _play_sound(name : String, stream : AudioStream, pitch : float = 1.0) -> void:
+	# Skip if no stream provided
+	if not stream:
+		return
+
+	# Emit the footstep signal
+	footstep.emit(name)
+
+	# Verify we have an audio player
+	if _audio_pool_idle.size() == 0:
+		push_warning("XRToolsMovementFootstep idle audio pool empty")
+		return
+
+	# Play the sound
+	var player : AudioStreamPlayer3D = _audio_pool_idle.pop_front()
+	player.stream = stream
+	player.pitch_scale = pitch
+	player.play()
+
+
+# Called when an AudioStreamPlayer3D in our pool finishes playing its sound
+func _on_player_finished(player : AudioStreamPlayer3D) -> void:
+	_audio_pool_idle.append(player)
+
+
+## Find an [XRToolsMovementFootstep] node.
+##
+## This function searches from the specified node for an [XRToolsMovementFootstep]
+## assuming the node is a sibling of the body under an [ARVROrigin].
+static func find_instance(node: Node) -> XRToolsMovementFootstep:
+	return XRTools.find_xr_child(
+		XRHelpers.get_xr_origin(node),
+		"*",
+		"XRToolsMovementFootstep") as XRToolsMovementFootstep

+ 8 - 0
addons/godot-xr-tools/functions/movement_footstep.tscn

@@ -0,0 +1,8 @@
+[gd_scene load_steps=2 format=3 uid="uid://0xlsitpu17r1"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_footstep.gd" id="1"]
+
+[node name="MovementFootstep" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")
+
+[node name="PlayerSettings" type="AudioStreamPlayer3D" parent="."]

+ 234 - 0
addons/godot-xr-tools/functions/movement_glide.gd

@@ -0,0 +1,234 @@
+@tool
+class_name XRToolsMovementGlide
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Gliding
+##
+## This script provides glide mechanics for the player. This script works
+## with the [XRToolsPlayerBody] attached to the players [XROrigin3D].
+##
+## The player enables flying by moving the controllers apart further than
+## 'glide_detect_distance'.
+##
+## When gliding, the players fall speed will slew to 'glide_fall_speed' and
+## the velocity will slew to 'glide_forward_speed' in the direction the
+## player is facing.
+##
+## Gliding is an exclusive motion operation, and so gliding should be ordered
+## after any Direct movement providers responsible for turning.
+
+
+## Signal invoked when the player starts gliding
+signal player_glide_start
+
+## Signal invoked when the player ends gliding
+signal player_glide_end
+
+## Signal invoked when the player flaps
+signal player_flapped
+
+## Movement provider order
+@export var order : int = 35
+
+## Controller separation distance to register as glide
+@export var glide_detect_distance : float = 1.0
+
+## Minimum falling speed to be considered gliding
+@export var glide_min_fall_speed : float = -1.5
+
+## Glide falling speed
+@export var glide_fall_speed : float = -2.0
+
+## Glide forward speed
+@export var glide_forward_speed : float = 12.0
+
+## Slew rate to transition to gliding
+@export var horizontal_slew_rate : float = 1.0
+
+## Slew rate to transition to gliding
+@export var vertical_slew_rate : float = 2.0
+
+## glide rotate with roll angle
+@export var turn_with_roll : bool = false
+
+## Smooth turn speed in radians per second
+@export var roll_turn_speed : float = 1
+
+## Add vertical impulse by flapping controllers
+@export var wings_impulse : bool = false
+
+## Minimum velocity for flapping
+@export var flap_min_speed : float = 0.3
+
+## Flapping force multiplier
+@export var wings_force : float = 1.0
+
+## Minimum distance from controllers to ARVRCamera to rearm flaps.
+## if set to 0, you need to reach head level with hands to rearm flaps
+@export var rearm_distance_offset : float = 0.2
+
+
+## Flap activated (when both controllers are near the ARVRCamera height)
+var flap_armed : bool = false
+
+## Last controllers position to calculate flapping velocity
+var last_local_left_position : Vector3
+var last_local_right_position : Vector3
+
+# True if the controller positions are valid
+var _has_controller_positions : bool = false
+
+
+# Left controller
+@onready var _left_controller := XRHelpers.get_left_controller(self)
+
+# Right controller
+@onready var _right_controller := XRHelpers.get_right_controller(self)
+
+# ARVRCamera
+@onready var _camera_node := XRHelpers.get_xr_camera(self)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementGlide" or super(name)
+
+
+func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
+	# Skip if disabled or either controller is off
+	if disabled or !enabled or \
+		!_left_controller.get_is_active() or \
+		!_right_controller.get_is_active():
+		_set_gliding(false)
+		return
+
+	# If on the ground, then not gliding
+	if player_body.on_ground:
+		_set_gliding(false)
+		return
+
+	# Get the controller left and right global horizontal positions
+	var left_position := _left_controller.global_transform.origin
+	var right_position := _right_controller.global_transform.origin
+
+	# Set default wings impulse to zero
+	var wings_impulse_velocity := 0.0
+
+	# If wings impulse is active, calculate flapping impulse
+	if wings_impulse:
+		# Check controllers position relative to head
+		var cam_local_y := _camera_node.position.y
+		var left_hand_over_head = cam_local_y < _left_controller.position.y + rearm_distance_offset
+		var right_hand_over_head = cam_local_y < _right_controller.position.y + rearm_distance_offset
+		if left_hand_over_head && right_hand_over_head:
+			flap_armed = true
+
+		if flap_armed:
+			# Get controller local positions
+			var local_left_position := _left_controller.position
+			var local_right_position := _right_controller.position
+
+			# Store last frame controller positions for the first step
+			if not _has_controller_positions:
+				_has_controller_positions = true
+				last_local_left_position = local_left_position
+				last_local_right_position = local_right_position
+
+			# Calculate controllers velocity only when flapping downwards
+			var left_wing_velocity = 0.0
+			var right_wing_velocity = 0.0
+			if local_left_position.y < last_local_left_position.y:
+				left_wing_velocity = local_left_position.distance_to(last_local_left_position) / delta
+			if local_right_position.y < last_local_right_position.y:
+				right_wing_velocity = local_right_position.distance_to(last_local_right_position) / delta
+
+			# Calculate wings impulse
+			if left_wing_velocity > flap_min_speed && right_wing_velocity > flap_min_speed:
+				wings_impulse_velocity = (left_wing_velocity + right_wing_velocity) / 2
+				wings_impulse_velocity = wings_impulse_velocity * wings_force * delta * 50
+				emit_signal("player_flapped")
+				flap_armed = false
+
+			# Store controller position for next frame
+			last_local_left_position = local_left_position
+			last_local_right_position = local_right_position
+
+	# Calculate global left to right controller vector
+	var left_to_right := right_position - left_position
+
+	if turn_with_roll:
+		var angle = -left_to_right.dot(player_body.up_player_vector)
+		player_body.rotate_player(roll_turn_speed * delta * angle)
+
+	# If not falling, then not gliding
+	var vertical_velocity := player_body.velocity.dot(player_body.up_gravity_vector)
+	vertical_velocity += wings_impulse_velocity
+	if vertical_velocity >= glide_min_fall_speed && wings_impulse_velocity == 0.0:
+		_set_gliding(false)
+		return
+
+	# Set gliding based on hand separation
+	var separation := left_to_right.length() / XRServer.world_scale
+	_set_gliding(separation >= glide_detect_distance)
+
+	# Skip if not gliding
+	if !is_active:
+		return
+
+	# Lerp the vertical velocity to glide_fall_speed
+	vertical_velocity = lerp(vertical_velocity, glide_fall_speed, vertical_slew_rate * delta)
+
+	# Lerp the horizontal velocity towards forward_speed
+	var horizontal_velocity := player_body.up_gravity_plane.project(player_body.velocity)
+	var dir_forward := player_body.up_gravity_plane.project(
+			left_to_right.rotated(player_body.up_gravity_vector, PI/2)).normalized()
+	var forward_velocity := dir_forward * glide_forward_speed
+	horizontal_velocity = horizontal_velocity.lerp(forward_velocity, horizontal_slew_rate * delta)
+
+	# Perform the glide
+	var glide_velocity := horizontal_velocity + vertical_velocity * player_body.up_gravity_vector
+	player_body.velocity = player_body.move_body(glide_velocity)
+
+	# Report exclusive motion performed (to bypass gravity)
+	return true
+
+
+# Set the gliding state and fire any signals
+func _set_gliding(active: bool) -> void:
+	# Skip if no change
+	if active == is_active:
+		return
+
+	# Update the is_gliding flag
+	is_active = active;
+
+	# Report transition
+	if is_active:
+		emit_signal("player_glide_start")
+	else:
+		emit_signal("player_glide_end")
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Verify the left controller
+	if !XRHelpers.get_left_controller(self):
+		warnings.append("Unable to find left XRController3D node")
+
+	# Verify the right controller
+	if !XRHelpers.get_right_controller(self):
+		warnings.append("Unable to find right XRController3D node")
+
+	# Check glide parameters
+	if glide_min_fall_speed > 0:
+		warnings.append("Glide minimum fall speed must be zero or less")
+	if glide_fall_speed > 0:
+		warnings.append("Glide fall speed must be zero or less")
+	if glide_min_fall_speed < glide_fall_speed:
+		warnings.append("Glide fall speed must be faster than minimum fall speed")
+
+	# Return warnings
+	return warnings

+ 6 - 0
addons/godot-xr-tools/functions/movement_glide.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://cvokcudrffkgc"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_glide.gd" id="1"]
+
+[node name="MovementGlide" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 243 - 0
addons/godot-xr-tools/functions/movement_grapple.gd

@@ -0,0 +1,243 @@
+@tool
+class_name XRToolsMovementGrapple
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Grapple Movement
+##
+## This script provide simple grapple based movement - "bat hook" style
+## where the player flings a rope to the target and swings on it.
+## This script works with the [XRToolsPlayerBody] attached to the players
+## [XROrigin3D].
+
+
+## Signal emitted when grapple starts
+signal grapple_started()
+
+## Signal emitted when grapple finishes
+signal grapple_finished()
+
+
+## Grapple state
+enum GrappleState {
+	IDLE,			## Grapple is idle
+	FIRED,			## Grapple is fired
+	WINCHING,		## Grapple is winching
+}
+
+
+# Default grapple collision mask of 1-5 (world)
+const DEFAULT_COLLISION_MASK := 0b0000_0000_0000_0000_0000_0000_0001_1111
+
+# Default grapple enable mask of 5:grapple-target
+const DEFAULT_ENABLE_MASK := 0b0000_0000_0000_0000_0000_0000_0001_0000
+
+
+## Movement provider order
+@export var order : int = 20
+
+## Grapple length - use to adjust maximum distance for possible grapple hooking.
+@export var grapple_length : float = 15.0
+
+## Grapple collision mask
+@export_flags_3d_physics var grapple_collision_mask : int = DEFAULT_COLLISION_MASK:
+	set = _set_grapple_collision_mask
+
+## Grapple enable mask
+@export_flags_3d_physics var grapple_enable_mask : int = DEFAULT_ENABLE_MASK
+
+## Impulse speed applied to the player on first grapple
+@export var impulse_speed : float = 10.0
+
+## Winch speed applied to the player while the grapple is held
+@export var winch_speed : float = 2.0
+
+## Probably need to add export variables for line size, maybe line material at
+## some point so dev does not need to make children editable to do this.
+## For now, right click on grapple node and make children editable to edit these
+## facets.
+@export var rope_width : float = 0.02
+
+## Air friction while grappling
+@export var friction : float = 0.1
+
+## Grapple button (triggers grappling movement).  Be sure this button does not
+## conflict with other functions.
+@export var grapple_button_action : String = "trigger_click"
+
+# Hook related variables
+var hook_object : Node3D = null
+var hook_local := Vector3(0,0,0)
+var hook_point := Vector3(0,0,0)
+
+# Grapple button state
+var _grapple_button := false
+
+# Get line creation nodes
+@onready var _line_helper : Node3D = $LineHelper
+@onready var _line : CSGCylinder3D = $LineHelper/Line
+
+# Get Controller node - consider way to universalize this if user wanted to
+# attach this to a gun instead of player's hand.  Could consider variable to
+# select controller instead.
+@onready var _controller := XRHelpers.get_xr_controller(self)
+
+# Get Raycast node
+@onready var _grapple_raycast : RayCast3D = $Grapple_RayCast
+
+# Get Grapple Target Node
+@onready var _grapple_target : Node3D = $Grapple_Target
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementGrapple" or super(name)
+
+
+# Function run when node is added to scene
+func _ready():
+	# In Godot 4 we must now manually call our super class ready function
+	super()
+
+	# Skip if running in the editor
+	if Engine.is_editor_hint():
+		return
+
+	# Ensure grapple length is valid
+	var min_hook_length := 1.5 * XRServer.world_scale
+	if grapple_length < min_hook_length:
+		grapple_length = min_hook_length
+
+	# Set ray-cast
+	_grapple_raycast.target_position = Vector3(0, 0, -grapple_length) * XRServer.world_scale
+	_grapple_raycast.collision_mask = grapple_collision_mask
+
+	# Deal with line
+	_line.radius = rope_width
+	_line.hide()
+
+
+# Update grapple display objects
+func _process(_delta: float):
+	# Skip if running in the editor
+	if Engine.is_editor_hint():
+		return
+
+	# Update grapple line
+	if is_active:
+		var line_length := (hook_point - _controller.global_transform.origin).length()
+		_line_helper.look_at(hook_point, Vector3.UP)
+		_line.height = line_length
+		_line.position.z = line_length / -2
+		_line.visible = true
+	else:
+		_line.visible = false
+
+	# Update grapple target
+	if enabled and !is_active and _is_raycast_valid():
+		_grapple_target.global_transform.origin  = _grapple_raycast.get_collision_point()
+		_grapple_target.global_transform = _grapple_target.global_transform.orthonormalized()
+		_grapple_target.visible = true
+	else:
+		_grapple_target.visible = false
+
+
+# Perform grapple movement
+func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
+	# Disable if requested
+	if disabled or !enabled or !_controller.get_is_active():
+		_set_grappling(false)
+		return
+
+	# Update grapple button
+	var old_grapple_button := _grapple_button
+	_grapple_button = _controller.is_button_pressed(grapple_button_action)
+
+	# Enable/disable grappling
+	var do_impulse := false
+	if is_active and !_grapple_button:
+		_set_grappling(false)
+	elif _grapple_button and !old_grapple_button and _is_raycast_valid():
+		hook_object = _grapple_raycast.get_collider()
+		hook_point = _grapple_raycast.get_collision_point()
+		hook_local = hook_point * hook_object.global_transform
+		do_impulse = true
+		_set_grappling(true)
+
+	# Skip if not grappling
+	if !is_active:
+		return
+
+	# Get hook direction
+	hook_point = hook_object.global_transform * hook_local
+	var hook_vector := hook_point - _controller.global_transform.origin
+	var hook_length := hook_vector.length()
+	var hook_direction := hook_vector / hook_length
+
+	# Apply gravity
+	player_body.velocity += player_body.gravity * delta
+
+	# Select the grapple speed
+	var speed := impulse_speed if do_impulse else winch_speed
+	if hook_length < 1.0:
+		speed = 0.0
+
+	# Ensure velocity is at least winch_speed towards hook
+	var vdot = player_body.velocity.dot(hook_direction)
+	if vdot < speed:
+		player_body.velocity += hook_direction * (speed - vdot)
+
+	# Scale down velocity
+	player_body.velocity *= 1.0 - friction * delta
+
+	# Perform exclusive movement as we have dealt with gravity
+	player_body.velocity = player_body.move_body(player_body.velocity)
+	return true
+
+
+# Called when the grapple collision mask has been modified
+func _set_grapple_collision_mask(new_value: int) -> void:
+	grapple_collision_mask = new_value
+	if is_inside_tree() and _grapple_raycast:
+		_grapple_raycast.collision_mask = new_value
+
+
+# Set the grappling state and fire any signals
+func _set_grappling(active: bool) -> void:
+	# Skip if no change
+	if active == is_active:
+		return
+
+	# Update the is_active flag
+	is_active = active;
+
+	# Report transition
+	if is_active:
+		emit_signal("grapple_started")
+	else:
+		emit_signal("grapple_finished")
+
+
+# Test if the raycast is striking a valid target
+func _is_raycast_valid() -> bool:
+	# Fail if raycast not colliding
+	if not _grapple_raycast.is_colliding():
+		return false
+
+	# Get the target of the raycast
+	var target : CollisionObject3D = _grapple_raycast.get_collider()
+
+	# Check tartget layer
+	return true if target.collision_layer & grapple_enable_mask else false
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Check the controller node
+	if !XRHelpers.get_xr_controller(self):
+		warnings.append("This node must be within a branch of an XRController3D node")
+
+	# Return warnings
+	return warnings

+ 28 - 0
addons/godot-xr-tools/functions/movement_grapple.tscn

@@ -0,0 +1,28 @@
+[gd_scene load_steps=4 format=3 uid="uid://c78tjrtiyqna8"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_grapple.gd" id="1"]
+[ext_resource type="Material" path="res://addons/godot-xr-tools/materials/pointer.tres" id="2_n6olo"]
+
+[sub_resource type="BoxMesh" id="1"]
+resource_local_to_scene = true
+size = Vector3(0.05, 0.05, 0.05)
+subdivide_depth = 20
+
+[node name="MovementGrapple" type="Node3D" groups=["movement_providers"]]
+script = ExtResource("1")
+
+[node name="Grapple_RayCast" type="RayCast3D" parent="."]
+collision_mask = 3
+debug_shape_custom_color = Color(0.862745, 0.278431, 0.278431, 1)
+debug_shape_thickness = 1
+
+[node name="Grapple_Target" type="MeshInstance3D" parent="."]
+visible = false
+mesh = SubResource("1")
+surface_material_override/0 = ExtResource("2_n6olo")
+
+[node name="LineHelper" type="Node3D" parent="."]
+
+[node name="Line" type="CSGCylinder3D" parent="LineHelper"]
+transform = Transform3D(1.91069e-15, 4.37114e-08, 1, 1, -4.37114e-08, 0, 4.37114e-08, 1, -4.37114e-08, 0, 0, 0)
+radius = 0.02

+ 52 - 0
addons/godot-xr-tools/functions/movement_jump.gd

@@ -0,0 +1,52 @@
+@tool
+class_name XRToolsMovementJump
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Jumping
+##
+## This script provides jumping mechanics for the player. This script works
+## with the [XRToolsPlayerBody] attached to the players [XROrigin3D].
+##
+## The player enables jumping by attaching an [XRToolsMovementJump] as a
+## child of the appropriate [XRController3D], then configuring the jump button
+## and jump velocity.
+
+
+## Movement provider order
+@export var order : int = 20
+
+## Button to trigger jump
+@export var jump_button_action : String = "trigger_click"
+
+
+# Node references
+@onready var _controller := XRHelpers.get_xr_controller(self)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementJump" or super(name)
+
+
+# Perform jump movement
+func physics_movement(_delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
+	# Skip if the jump controller isn't active
+	if !_controller.get_is_active():
+		return
+
+	# Request jump if the button is pressed
+	if _controller.is_button_pressed(jump_button_action):
+		player_body.request_jump()
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Check the controller node
+	if !XRHelpers.get_xr_controller(self):
+		warnings.append("This node must be within a branch of an XRController3D node")
+
+	# Return warnings
+	return warnings

+ 6 - 0
addons/godot-xr-tools/functions/movement_jump.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://c2q5phg8w08o"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_jump.gd" id="1"]
+
+[node name="MovementJump" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 197 - 0
addons/godot-xr-tools/functions/movement_physical_jump.gd

@@ -0,0 +1,197 @@
+@tool
+class_name XRToolsMovementPhysicalJump
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Player Physical Jump Detection
+##
+## This script can detect jumping based on either the players body jumping,
+## or by the player swinging their arms up.
+##
+## The player body jumping is detected by putting the cameras instantaneous
+## Y velocity (in the tracking space) into a sliding-window averager. If the
+## average Y velocity exceeds a threshold parameter then the player has
+## jumped.
+##
+## The player arms jumping is detected by putting both controllers instantaneous
+## Y velocity (in the tracking space) into a sliding-window averager. If both
+## average Y velocities exceed a threshold parameter then the player has
+## jumped.
+
+
+## Movement provider order
+@export var order : int = 20
+
+## If true, jumps are detected via the players body (through the camera)
+@export var body_jump_enable : bool = true
+
+## If true, the player jump is as high as the physical jump(no ground physics)
+@export var body_jump_player_only : bool = false
+
+## Body jump detection threshold (M/S^2)
+@export var body_jump_threshold : float = 2.5
+
+## If true, jumps are detected via the players arms (through the controllers)
+@export var arms_jump_enable : bool = false
+
+## Arms jump detection threshold (M/S^2)
+@export var arms_jump_threshold : float = 5.0
+
+
+# Node Positions
+var _camera_position : float = 0.0
+var _controller_left_position : float = 0.0
+var _controller_right_position : float = 0.0
+
+# Node Velocities
+var _camera_velocity : SlidingAverage = SlidingAverage.new(5)
+var _controller_left_velocity : SlidingAverage = SlidingAverage.new(5)
+var _controller_right_velocity : SlidingAverage = SlidingAverage.new(5)
+
+
+# Node references
+@onready var _origin_node := XRHelpers.get_xr_origin(self)
+@onready var _camera_node := XRHelpers.get_xr_camera(self)
+@onready var _controller_left_node := XRHelpers.get_left_controller(self)
+@onready var _controller_right_node := XRHelpers.get_right_controller(self)
+
+
+# Sliding Average class
+class SlidingAverage:
+	# Sliding window size
+	var _size: int
+
+	# Sum of items in the window
+	var _sum := 0.0
+
+	# Position
+	var _pos := 0
+
+	# Data window
+	var _data := Array()
+
+	# Constructor
+	func _init(size: int):
+		# Set the size and fill the array
+		_size = size
+		for i in size:
+			_data.push_back(0.0)
+
+	# Update the average
+	func update(entry: float) -> float:
+		# Add the new entry and subtract the old
+		_sum += entry
+		_sum -= _data[_pos]
+
+		# Store the new entry in the array and circularly advance the index
+		_data[_pos] = entry;
+		_pos = (_pos + 1) % _size
+
+		# Return the average
+		return _sum / _size
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementPhysicalJump" or super(name)
+
+
+# Perform jump detection
+func physics_movement(delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
+	# Handle detecting body jump
+	if body_jump_enable:
+		_detect_body_jump(delta, player_body)
+
+	# Handle detecting arms jump
+	if arms_jump_enable:
+		_detect_arms_jump(delta, player_body)
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Verify the camera
+	if !XRHelpers.get_xr_origin(self):
+		warnings.append("This node must be within a branch of an XROrigin3D node")
+
+	# Verify the camera
+	if !XRHelpers.get_xr_camera(self):
+		warnings.append("Unable to find XRCamera3D")
+
+	# Verify the left controller
+	if !XRHelpers.get_left_controller(self):
+		warnings.append("Unable to find left XRController3D node")
+
+	# Verify the right controller
+	if !XRHelpers.get_right_controller(self):
+		warnings.append("Unable to find left XRController3D node")
+
+	# Return warnings
+	return warnings
+
+
+# Detect the player jumping with their body (using the headset camera)
+func _detect_body_jump(delta: float, player_body: XRToolsPlayerBody) -> void:
+	# Get the camera instantaneous velocity
+	var new_camera_pos := _camera_node.transform.origin.y
+	var camera_vel := (new_camera_pos - _camera_position) / delta
+	_camera_position = new_camera_pos
+
+	# Ignore zero moves (either not tracking, or no update since last physics)
+	if abs(camera_vel) < 0.001:
+		return;
+
+	# Correct for world-scale (convert to player units)
+	camera_vel /= XRServer.world_scale
+
+	# Clamp the camera instantaneous velocity to +/- 2x the jump threshold
+	camera_vel = clamp(camera_vel, -2.0 * body_jump_threshold, 2.0 * body_jump_threshold)
+
+	# Get the averaged velocity
+	camera_vel = _camera_velocity.update(camera_vel)
+
+	# Detect a jump
+	if camera_vel >= body_jump_threshold:
+		player_body.request_jump(body_jump_player_only)
+
+
+# Detect the player jumping with their arms (using the controllers)
+func _detect_arms_jump(delta: float, player_body: XRToolsPlayerBody) -> void:
+	# Skip if either of the controllers is disabled
+	if !_controller_left_node.get_is_active() or !_controller_right_node.get_is_active():
+		return
+
+	# Get the controllers instantaneous velocity
+	var new_controller_left_pos := _controller_left_node.transform.origin.y
+	var new_controller_right_pos := _controller_right_node.transform.origin.y
+	var controller_left_vel := (new_controller_left_pos - _controller_left_position) / delta
+	var controller_right_vel := (new_controller_right_pos - _controller_right_position) / delta
+	_controller_left_position = new_controller_left_pos
+	_controller_right_position = new_controller_right_pos
+
+	# Ignore zero moves (either not tracking, or no update since last physics)
+	if abs(controller_left_vel) <= 0.001 and abs(controller_right_vel) <= 0.001:
+		return
+
+	# Correct for world-scale (convert to player units)
+	controller_left_vel /= XRServer.world_scale
+	controller_right_vel /= XRServer.world_scale
+
+	# Clamp the controller instantaneous velocity to +/- 2x the jump threshold
+	controller_left_vel = clamp(
+			controller_left_vel,
+			-2.0 * arms_jump_threshold,
+			2.0 * arms_jump_threshold)
+	controller_right_vel = clamp(
+			controller_right_vel,
+			-2.0 * arms_jump_threshold,
+			2.0 * arms_jump_threshold)
+
+	# Get the averaged velocity
+	controller_left_vel = _controller_left_velocity.update(controller_left_vel)
+	controller_right_vel = _controller_right_velocity.update(controller_right_vel)
+
+	# Detect a jump
+	if controller_left_vel >= arms_jump_threshold and controller_right_vel >= arms_jump_threshold:
+		player_body.request_jump()

+ 7 - 0
addons/godot-xr-tools/functions/movement_physical_jump.tscn

@@ -0,0 +1,7 @@
+[gd_scene load_steps=2 format=3 uid="uid://ckt118vcpmr6q"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_physical_jump.gd" id="1"]
+
+[node name="MovementPhysicalJump" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")
+arms_jump_enable = true

+ 93 - 0
addons/godot-xr-tools/functions/movement_provider.gd

@@ -0,0 +1,93 @@
+@tool
+@icon("res://addons/godot-xr-tools/editor/icons/movement_provider.svg")
+class_name XRToolsMovementProvider
+extends Node
+
+
+## XR Tools Movement Provider base class
+##
+## This movement provider class is the base class of all movement providers.
+## Movement providers are invoked by the [XRToolsPlayerBody] object in order
+## to apply motion to the player.
+##
+## Movement provider implementations should:
+##  - Export an 'order' integer to control order of processing
+##  - Override the physics_movement method to impelment motion
+
+
+## Player body scene
+const PLAYER_BODY := preload("res://addons/godot-xr-tools/player/player_body.tscn")
+
+
+## Enable movement provider
+@export var enabled : bool = true
+
+
+## If true, the movement provider is actively performing a move
+var is_active := false
+
+
+# If missing we need to add our [XRToolsPlayerBody]
+func _create_player_body_node():
+	# get our origin node
+	var xr_origin = XRHelpers.get_xr_origin(self)
+	if !xr_origin:
+		return
+
+	# Double check if it hasn't already been created by another movement function
+	var player_body := XRToolsPlayerBody.find_instance(self)
+	if !player_body:
+		# create our XRToolsPlayerBody node and add it into our tree
+		player_body = PLAYER_BODY.instantiate()
+		player_body.set_name("PlayerBody")
+		xr_origin.add_child(player_body)
+		player_body.set_owner(get_tree().get_edited_scene_root())
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementProvider"
+
+
+# Function run when node is added to scene
+func _ready():
+	# If we're in the editor, help the user out by creating our XRToolsPlayerBody node
+	# automatically when needed.
+	if Engine.is_editor_hint():
+		var player_body = XRToolsPlayerBody.find_instance(self)
+		if !player_body:
+			# This call needs to be deferred, we can't add nodes during scene construction
+			call_deferred("_create_player_body_node")
+
+
+## Override this method to perform pre-movement updates to the PlayerBody
+func physics_pre_movement(_delta: float, _player_body: XRToolsPlayerBody):
+	pass
+
+
+## Override this method to apply motion to the PlayerBody
+func physics_movement(_delta: float, _player_body: XRToolsPlayerBody, _disabled: bool):
+	pass
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := PackedStringArray()
+
+	# Verify we're within the tree of an XROrigin3D node
+	if !XRHelpers.get_xr_origin(self):
+		warnings.append("This node must be within a branch on an XROrigin3D node")
+
+	if !XRToolsPlayerBody.find_instance(self):
+		warnings.append("Missing PlayerBody node on the XROrigin3D")
+
+	# Verify movement provider is in the correct group
+	if !is_in_group("movement_providers"):
+		warnings.append("Movement provider not in 'movement_providers' group")
+
+	# Verify order property exists
+	if !"order" in self:
+		warnings.append("Movement provider does not expose an order property")
+
+	# Return warnings
+	return warnings

+ 169 - 0
addons/godot-xr-tools/functions/movement_sprint.gd

@@ -0,0 +1,169 @@
+@tool
+class_name XRToolsMovementSprint
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Sprinting
+##
+## This script provides sprinting movement for the player. It assumes there is
+## a direct movement node in the scene otherwise it will not be functional.
+##
+## There will not be an error, there just will not be any reason for it to
+## have any impact on the player.  This node should be a direct child of
+## the [XROrigin3D] node rather than to a specific [XRController3D].
+
+
+## Signal emitted when sprinting starts
+signal sprinting_started()
+
+## Signal emitted when sprinting finishes
+signal sprinting_finished()
+
+
+## Enumeration of controller to use for triggering sprinting.  This allows the
+## developer to assign the sprint button to either controller.
+enum SprintController {
+	LEFT,		## Use left controller
+	RIGHT,		## Use right controler
+}
+
+## Enumeration of sprinting modes - toggle or hold button
+enum SprintType {
+	HOLD_TO_SPRINT,	## Hold button to sprint
+	TOGGLE_SPRINT,	## Toggle sprinting on button press
+}
+
+
+## Type of sprinting
+@export var sprint_type : SprintType = SprintType.HOLD_TO_SPRINT
+
+## Sprint speed multiplier (multiplier from speed set by direct movement node(s))
+@export_range(1.0, 4.0) var sprint_speed_multiplier : float = 2.0
+
+## Movement provider order
+@export var order : int = 11
+
+## Sprint controller
+@export var controller : SprintController = SprintController.LEFT
+
+## Sprint button
+@export var sprint_button : String = "primary_click"
+
+
+# Sprint controller
+var _controller : XRController3D
+
+# Sprint button down state
+var _sprint_button_down : bool = false
+
+# Variable to hold left controller direct movement node original max speed
+var _left_controller_original_max_speed : float = 0.0
+
+# Variable to hold right controller direct movement node original max speed
+var _right_controller_original_max_speed : float = 0.0
+
+
+# Variable used to cache left controller direct movement function, if any
+@onready var _left_controller_direct_move := XRToolsMovementDirect.find_left(self)
+
+# Variable used to cache right controller direct movement function, if any
+@onready var _right_controller_direct_move := XRToolsMovementDirect.find_right(self)
+
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementSprint" or super(name)
+
+
+func _ready():
+	# In Godot 4 we must now manually call our super class ready function
+	super()
+
+	# Get the sprinting controller
+	if controller == SprintController.LEFT:
+		_controller = XRHelpers.get_left_controller(self)
+	else:
+		_controller = XRHelpers.get_right_controller(self)
+
+
+# Perform sprinting
+func physics_movement(_delta: float, _player_body: XRToolsPlayerBody, disabled: bool):
+	# Skip if the controller isn't active or is not enabled
+	if !_controller.get_is_active() or disabled == true or !enabled:
+		set_sprinting(false)
+		return
+
+	# Detect sprint button down and pressed states
+	var sprint_button_down := _controller.is_button_pressed(sprint_button)
+	var sprint_button_pressed := sprint_button_down and !_sprint_button_down
+	_sprint_button_down = sprint_button_down
+
+	# Calculate new sprinting state
+	var sprinting := is_active
+	match sprint_type:
+		SprintType.HOLD_TO_SPRINT:
+			# Sprint when button down
+			sprinting = sprint_button_down
+
+		SprintType.TOGGLE_SPRINT:
+			# Toggle when button pressed
+			if sprint_button_pressed:
+				sprinting = !sprinting
+
+	# Update sprinting state
+	if sprinting != is_active:
+		set_sprinting(sprinting)
+
+
+# Public function used to set sprinting active or not active
+func set_sprinting(active: bool) -> void:
+	# Skip if no change
+	if active == is_active:
+		return
+
+	# Update state
+	is_active = active
+
+	# Handle state change
+	if is_active:
+		# We are sprinting
+		emit_signal("sprinting_started")
+
+		# Since max speeds could be changed while game is running, check
+		# now for original max speeds of left and right nodes
+		if _left_controller_direct_move:
+			_left_controller_original_max_speed = _left_controller_direct_move.max_speed
+		if _right_controller_direct_move:
+			_right_controller_original_max_speed = _right_controller_direct_move.max_speed
+
+		# Set both controllers' direct movement functions, if appliable, to
+		# the sprinting speed
+		if _left_controller_direct_move:
+			_left_controller_direct_move.max_speed = \
+					_left_controller_original_max_speed * sprint_speed_multiplier
+		if _right_controller_direct_move:
+			_right_controller_direct_move.max_speed = \
+					_right_controller_original_max_speed * sprint_speed_multiplier
+	else:
+		# We are not sprinting
+		emit_signal("sprinting_finished")
+
+		# Set both controllers' direct movement functions, if applicable, to
+		# their original speeds
+		if _left_controller_direct_move:
+			_left_controller_direct_move.max_speed = _left_controller_original_max_speed
+		if _right_controller_direct_move:
+			_right_controller_direct_move.max_speed = _right_controller_original_max_speed
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Make sure player has at least one direct movement node
+	if !XRToolsMovementDirect.find_left(self) and !XRToolsMovementDirect.find_right(self):
+		warnings.append("Player missing XRToolsMovementDirect nodes")
+
+	# Return warnings
+	return warnings

+ 6 - 0
addons/godot-xr-tools/functions/movement_sprint.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://drs4eeq721ojn"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_sprint.gd" id="1"]
+
+[node name="MovementSprint" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 111 - 0
addons/godot-xr-tools/functions/movement_turn.gd

@@ -0,0 +1,111 @@
+@tool
+class_name XRToolsMovementTurn
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Turning
+##
+## This script provides turning support for the player. This script works
+## with the PlayerBody attached to the players XROrigin3D.
+
+
+## Movement mode
+enum TurnMode {
+	DEFAULT,	## Use turn mode from project/user settings
+	SNAP,		## Use snap-turning
+	SMOOTH		## Use smooth-turning
+}
+
+
+## Movement provider order
+@export var order : int = 5
+
+## Movement mode property
+@export var turn_mode : TurnMode = TurnMode.DEFAULT
+
+## Smooth turn speed in radians per second
+@export var smooth_turn_speed : float = 2.0
+
+## Seconds per step (at maximum turn rate)
+@export var step_turn_delay : float = 0.2
+
+## Step turn angle in degrees
+@export var step_turn_angle : float = 20.0
+
+## Our directional input
+@export var input_action : String = "primary"
+
+# Turn step accumulator
+var _turn_step : float = 0.0
+
+
+# Controller node
+@onready var _controller := XRHelpers.get_xr_controller(self)
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementTurn" or super(name)
+
+
+# Perform jump movement
+func physics_movement(delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
+	# Skip if the controller isn't active
+	if !_controller.get_is_active():
+		return
+
+	var deadzone = 0.1
+	if _snap_turning():
+		deadzone = XRTools.get_snap_turning_deadzone()
+
+	# Read the left/right joystick axis
+	var left_right := _controller.get_vector2(input_action).x
+	if abs(left_right) <= deadzone:
+		# Not turning
+		_turn_step = 0.0
+		return
+
+	# Handle smooth rotation
+	if !_snap_turning():
+		left_right -= deadzone * sign(left_right)
+		player_body.rotate_player(smooth_turn_speed * delta * left_right)
+		return
+
+	# Disable repeat snap turning if delay is zero
+	if step_turn_delay == 0.0 and _turn_step < 0.0:
+		return
+
+	# Update the next turn-step delay
+	_turn_step -= abs(left_right) * delta
+	if _turn_step >= 0.0:
+		return
+
+	# Turn one step in the requested direction
+	if step_turn_delay != 0.0:
+		_turn_step = step_turn_delay
+	player_body.rotate_player(deg_to_rad(step_turn_angle) * sign(left_right))
+
+
+# This method verifies the movement provider has a valid configuration.
+func _get_configuration_warnings() -> PackedStringArray:
+	var warnings := super()
+
+	# Check the controller node
+	if !XRHelpers.get_xr_controller(self):
+		warnings.append("Unable to find XRController3D node")
+
+	# Return warnings
+	return warnings
+
+
+# Test if snap turning should be used
+func _snap_turning():
+	match turn_mode:
+		TurnMode.SNAP:
+			return true
+
+		TurnMode.SMOOTH:
+			return false
+
+		_:
+			return XRToolsUserSettings.snap_turning

+ 6 - 0
addons/godot-xr-tools/functions/movement_turn.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://b6bk2pj8vbj28"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_turn.gd" id="1"]
+
+[node name="MovementTurn" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 44 - 0
addons/godot-xr-tools/functions/movement_wall_walk.gd

@@ -0,0 +1,44 @@
+@tool
+class_name XRToolsMovementWallWalk
+extends XRToolsMovementProvider
+
+
+# Default wall-walk mask of 4:wall-walk
+const DEFAULT_MASK := 0b0000_0000_0000_0000_0000_0000_0000_1000
+
+
+## Wall walking provider order
+@export var order : int = 25
+
+## Set our follow layer mask
+@export_flags_3d_physics var follow_mask : int = DEFAULT_MASK
+
+## Wall stick distance
+@export var stick_distance : float = 1.0
+
+## Wall stick strength
+@export var stick_strength : float = 9.8
+
+
+func physics_pre_movement(_delta: float, player_body: XRToolsPlayerBody):
+	# Test for collision with wall under feet
+	var wall_collision := player_body.move_and_collide(
+		player_body.up_player_vector * -stick_distance, true, true, true)
+	if !wall_collision:
+		return
+
+	# Get the wall information
+	var wall_node := wall_collision.get_collider()
+	var wall_normal := wall_collision.get_normal()
+
+	# Skip if the wall node doesn't have a collision layer
+	if not "collision_layer" in wall_node:
+		return
+
+	# Skip if the wall doesn't match the follow layer
+	var wall_layer : int = wall_node.collision_layer
+	if (wall_layer & follow_mask) == 0:
+		return
+
+	# Modify the player gravity
+	player_body.gravity = -wall_normal * stick_strength

+ 6 - 0
addons/godot-xr-tools/functions/movement_wall_walk.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://bk6ban0hctyym"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_wall_walk.gd" id="1"]
+
+[node name="MovementWallWalk" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 135 - 0
addons/godot-xr-tools/functions/movement_wind.gd

@@ -0,0 +1,135 @@
+@tool
+class_name XRToolsMovementWind
+extends XRToolsMovementProvider
+
+
+## XR Tools Movement Provider for Wind
+##
+## This script provides wind mechanics for the player. This script works
+## with the [XRToolsPlayerBody] attached to the players [XROrigin3D].
+##
+## When the player enters an [XRToolsWindArea], the wind pushes the player
+## around, and can even lift the player into the air.
+
+
+## Signal invoked when changing active wind areas
+signal wind_area_changed(active_wind_area)
+
+
+# Default wind area collision mask of 20:player-body
+const DEFAULT_MASK := 0b0000_0000_0000_1000_0000_0000_0000_0000
+
+
+## Movement provider order
+@export var order : int = 25
+
+## Drag multiplier for the player
+@export var drag_multiplier : float = 1.0
+
+# Set our collision mask
+@export_flags_3d_physics var collision_mask : int = DEFAULT_MASK: set = set_collision_mask
+
+
+# Wind detection area
+var _sense_area : Area3D
+
+# Array of wind areas the player is in
+var _in_wind_areas := Array()
+
+# Currently active wind area
+var _active_wind_area : XRToolsWindArea
+
+
+# Add support for is_xr_class on XRTools classes
+func is_xr_class(name : String) -> bool:
+	return name == "XRToolsMovementWind" or super(name)
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	# In Godot 4 we must now manually call our super class ready function
+	super()
+
+	# Skip if running in the editor
+	if Engine.is_editor_hint():
+		return
+
+	# Skip if we don't have a camera
+	var camera := XRHelpers.get_xr_camera(self)
+	if !camera:
+		return
+
+	# Construct the sphere shape
+	var sphere_shape := SphereShape3D.new()
+	sphere_shape.radius = 0.3
+
+	# Construct the collision shape
+	var collision_shape := CollisionShape3D.new()
+	collision_shape.set_name("WindSensorShape")
+	collision_shape.shape = sphere_shape
+
+	# Construct the sense area
+	_sense_area = Area3D.new()
+	_sense_area.set_name("WindSensorArea")
+	_sense_area.collision_mask = collision_mask
+	_sense_area.add_child(collision_shape)
+
+	# Add the sense area to the camera
+	camera.add_child(_sense_area)
+
+	# Subscribe to area notifications
+	_sense_area.area_entered.connect(_on_area_entered)
+	_sense_area.area_exited.connect(_on_area_exited)
+
+
+func set_collision_mask(new_mask: int) -> void:
+	collision_mask = new_mask
+	if is_inside_tree() and _sense_area:
+		_sense_area.collision_mask = collision_mask
+
+
+func _on_area_entered(area: Area3D):
+	# Skip if not wind area
+	var wind_area = area as XRToolsWindArea
+	if !wind_area:
+		return
+
+	# Save area and set active
+	_in_wind_areas.push_front(wind_area)
+	_active_wind_area = wind_area
+
+	# Report the wind area change
+	emit_signal("wind_area_changed", _active_wind_area)
+
+
+func _on_area_exited(area: Area3D):
+	# Erase from the wind area
+	_in_wind_areas.erase(area)
+
+	# If we didn't leave the active wind area then we're done
+	if area != _active_wind_area:
+		return
+
+	# Select a new active wind area
+	if _in_wind_areas.is_empty():
+		_active_wind_area = null
+	else:
+		_active_wind_area = _in_wind_areas.front()
+
+	# Report the wind area change
+	emit_signal("wind_area_changed", _active_wind_area)
+
+
+# Perform wind movement
+func physics_movement(delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
+	# Skip if no active wind area
+	if !_active_wind_area:
+		return
+
+	# Calculate the global wind velocity of the wind area
+	var wind_velocity := _active_wind_area.global_transform.basis * _active_wind_area.wind_vector
+
+	# Drag the player into the wind
+	var drag_factor := _active_wind_area.drag * drag_multiplier * delta
+	drag_factor = clamp(drag_factor, 0.0, 1.0)
+	player_body.velocity = player_body.velocity.lerp(wind_velocity, drag_factor)

+ 6 - 0
addons/godot-xr-tools/functions/movement_wind.tscn

@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://bgts3vpmjn6bb"]
+
+[ext_resource type="Script" path="res://addons/godot-xr-tools/functions/movement_wind.gd" id="1"]
+
+[node name="MovementWind" type="Node" groups=["movement_providers"]]
+script = ExtResource("1")

+ 17 - 0
addons/godot-xr-tools/hands/About.md

@@ -0,0 +1,17 @@
+The Hand Mesh was made using the Makehuman Community Edition
+It was slightly modified inside Blender 3D and textured inside Substance Painter
+
+If there should be concerns regarding License issues
+https://github.com/makehumancommunity/makehuman/blob/master/LICENSE.md
+
+under point D of the makehuman communtiy edition license
+https://github.com/makehumancommunity/makehuman/blob/master/LICENSE.md#d-concerning-the-output-from-makehuman
+As the assets have been released under CC0, there is no limitation on what you can do with this combined output.
+The MakeHuman project makes no claim whatsoever over output such as:
+
+    Exports to files (FBX, OBJ, DAE, MHX2...)
+    Exports via direct integration (import via MPFB)
+    Graphical data generated via scripting or plugins
+    Renderings
+    Screenshots
+    Saved model files

+ 126 - 0
addons/godot-xr-tools/hands/License.md

@@ -0,0 +1,126 @@
+To the extent possible under law, DigitalN8m4r3 aka Miodrag Sejic
+has waived all copyright and related or neighboring rights to Hand Models for the Godot XR Tools.
+This work is published from: Austria.
+Date: 7th November 2022
+
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.

+ 81 - 0
addons/godot-xr-tools/hands/animations/left/AnimationPlayer.tscn

@@ -0,0 +1,81 @@
+[gd_scene load_steps=37 format=3 uid="uid://the6y7swe6j0"]
+
+[ext_resource type="Animation" uid="uid://dgfeikrugfewi" path="res://addons/godot-xr-tools/hands/animations/left/Cup.res" id="1_7svyl"]
+[ext_resource type="Animation" uid="uid://dlxa6f6hwurka" path="res://addons/godot-xr-tools/hands/animations/left/Default pose.res" id="2_ykhfd"]
+[ext_resource type="Animation" uid="uid://dqa0h82y3qn1t" path="res://addons/godot-xr-tools/hands/animations/left/Grip 1.res" id="3_y80ds"]
+[ext_resource type="Animation" uid="uid://di384xtde8ydf" path="res://addons/godot-xr-tools/hands/animations/left/Grip 2.res" id="4_1nbr8"]
+[ext_resource type="Animation" uid="uid://dd67rufxwj2u" path="res://addons/godot-xr-tools/hands/animations/left/Grip 3.res" id="5_28lp1"]
+[ext_resource type="Animation" uid="uid://db62hs5s4n2b3" path="res://addons/godot-xr-tools/hands/animations/left/Grip 4.res" id="6_uj648"]
+[ext_resource type="Animation" uid="uid://bediglpx0rj7i" path="res://addons/godot-xr-tools/hands/animations/left/Grip 5.res" id="7_yxjeh"]
+[ext_resource type="Animation" uid="uid://nq3xh1olqipq" path="res://addons/godot-xr-tools/hands/animations/left/Grip Shaft.res" id="8_i0ro5"]
+[ext_resource type="Animation" uid="uid://plad1r85f7ws" path="res://addons/godot-xr-tools/hands/animations/left/Grip.res" id="9_811qe"]
+[ext_resource type="Animation" uid="uid://bi1l6lre2w2lp" path="res://addons/godot-xr-tools/hands/animations/left/Hold.res" id="10_o5u3q"]
+[ext_resource type="Animation" uid="uid://c3e6h0rv2uw2d" path="res://addons/godot-xr-tools/hands/animations/left/Horns.res" id="11_i2b8g"]
+[ext_resource type="Animation" uid="uid://dfekure1r6q13" path="res://addons/godot-xr-tools/hands/animations/left/Metal.res" id="12_yoig6"]
+[ext_resource type="Animation" uid="uid://b0rhk4r0r0t32" path="res://addons/godot-xr-tools/hands/animations/left/Middle.res" id="13_kvhwh"]
+[ext_resource type="Animation" uid="uid://f5k0gh4qnmv5" path="res://addons/godot-xr-tools/hands/animations/left/OK.res" id="14_ofab3"]
+[ext_resource type="Animation" uid="uid://1nlkfvitq7ku" path="res://addons/godot-xr-tools/hands/animations/left/Peace.res" id="15_a4q3y"]
+[ext_resource type="Animation" uid="uid://dhjb0e334tfwl" path="res://addons/godot-xr-tools/hands/animations/left/Pinch Flat.res" id="16_ixe2d"]
+[ext_resource type="Animation" uid="uid://dkjsnihi81b7p" path="res://addons/godot-xr-tools/hands/animations/left/Pinch Large.res" id="17_oifug"]
+[ext_resource type="Animation" uid="uid://bn0fdhe2jwq3h" path="res://addons/godot-xr-tools/hands/animations/left/Pinch Middle.res" id="18_2ic2e"]
+[ext_resource type="Animation" uid="uid://bo1b8w0s4ci81" path="res://addons/godot-xr-tools/hands/animations/left/Pinch Ring.res" id="19_4a8v4"]
+[ext_resource type="Animation" uid="uid://m5x2m8x3tcel" path="res://addons/godot-xr-tools/hands/animations/left/Pinch Tight.res" id="20_ugc01"]
+[ext_resource type="Animation" uid="uid://fi23m6i7orhw" path="res://addons/godot-xr-tools/hands/animations/left/Pinch Up.res" id="21_l3kgg"]
+[ext_resource type="Animation" uid="uid://c8qmcuyaltdnw" path="res://addons/godot-xr-tools/hands/animations/left/PingPong.res" id="22_j83er"]
+[ext_resource type="Animation" uid="uid://bqnoubqq7ogwu" path="res://addons/godot-xr-tools/hands/animations/left/Pinky.res" id="23_sm4sd"]
+[ext_resource type="Animation" uid="uid://ddbo6ioa282en" path="res://addons/godot-xr-tools/hands/animations/left/Pistol.res" id="24_r4kss"]
+[ext_resource type="Animation" uid="uid://brkptjihht3ae" path="res://addons/godot-xr-tools/hands/animations/left/Ring.res" id="25_2ncut"]
+[ext_resource type="Animation" uid="uid://cnng6xumhw7cx" path="res://addons/godot-xr-tools/hands/animations/left/Rounded.res" id="26_02lfv"]
+[ext_resource type="Animation" uid="uid://cevirj0eagdrq" path="res://addons/godot-xr-tools/hands/animations/left/Sign 1.res" id="27_o5si3"]
+[ext_resource type="Animation" uid="uid://cc6phxovf1ban" path="res://addons/godot-xr-tools/hands/animations/left/Sign 2.res" id="28_ble5k"]
+[ext_resource type="Animation" uid="uid://ohthjp8qbcc4" path="res://addons/godot-xr-tools/hands/animations/left/Sign 3.res" id="29_twq1x"]
+[ext_resource type="Animation" uid="uid://dmx42g64577g5" path="res://addons/godot-xr-tools/hands/animations/left/Sign 4.res" id="30_eyeuv"]
+[ext_resource type="Animation" uid="uid://dhsoxntrktx0p" path="res://addons/godot-xr-tools/hands/animations/left/Sign 5.res" id="31_50jrs"]
+[ext_resource type="Animation" uid="uid://c0u2a3yc2vhg8" path="res://addons/godot-xr-tools/hands/animations/left/Sign_Point.res" id="32_7v122"]
+[ext_resource type="Animation" uid="uid://4g211my0hoiw" path="res://addons/godot-xr-tools/hands/animations/left/Straight.res" id="33_m3lif"]
+[ext_resource type="Animation" uid="uid://d06l7hygl4qt3" path="res://addons/godot-xr-tools/hands/animations/left/Surfer.res" id="34_b3p10"]
+[ext_resource type="Animation" uid="uid://bxei4oebd4hu3" path="res://addons/godot-xr-tools/hands/animations/left/Thumb.res" id="35_huikq"]
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_kw48d"]
+_data = {
+"Cup": ExtResource("1_7svyl"),
+"Default pose": ExtResource("2_ykhfd"),
+"Grip": ExtResource("9_811qe"),
+"Grip 1": ExtResource("3_y80ds"),
+"Grip 2": ExtResource("4_1nbr8"),
+"Grip 3": ExtResource("5_28lp1"),
+"Grip 4": ExtResource("6_uj648"),
+"Grip 5": ExtResource("7_yxjeh"),
+"Grip Shaft": ExtResource("8_i0ro5"),
+"Hold": ExtResource("10_o5u3q"),
+"Horns": ExtResource("11_i2b8g"),
+"Metal": ExtResource("12_yoig6"),
+"Middle": ExtResource("13_kvhwh"),
+"OK": ExtResource("14_ofab3"),
+"Peace": ExtResource("15_a4q3y"),
+"Pinch Flat": ExtResource("16_ixe2d"),
+"Pinch Large": ExtResource("17_oifug"),
+"Pinch Middle": ExtResource("18_2ic2e"),
+"Pinch Ring": ExtResource("19_4a8v4"),
+"Pinch Tight": ExtResource("20_ugc01"),
+"Pinch Up": ExtResource("21_l3kgg"),
+"PingPong": ExtResource("22_j83er"),
+"Pinky": ExtResource("23_sm4sd"),
+"Pistol": ExtResource("24_r4kss"),
+"Ring": ExtResource("25_2ncut"),
+"Rounded": ExtResource("26_02lfv"),
+"Sign 1": ExtResource("27_o5si3"),
+"Sign 2": ExtResource("28_ble5k"),
+"Sign 3": ExtResource("29_twq1x"),
+"Sign 4": ExtResource("30_eyeuv"),
+"Sign 5": ExtResource("31_50jrs"),
+"Sign_Point": ExtResource("32_7v122"),
+"Straight": ExtResource("33_m3lif"),
+"Surfer": ExtResource("34_b3p10"),
+"Thumb": ExtResource("35_huikq")
+}
+
+[node name="AnimationPlayer" type="AnimationPlayer"]
+libraries = {
+"": SubResource("AnimationLibrary_kw48d")
+}

BIN
addons/godot-xr-tools/hands/animations/left/Cup.res


BIN
addons/godot-xr-tools/hands/animations/left/Default pose.res


BIN
addons/godot-xr-tools/hands/animations/left/Grip 1.res


BIN
addons/godot-xr-tools/hands/animations/left/Grip 2.res


BIN
addons/godot-xr-tools/hands/animations/left/Grip 3.res


BIN
addons/godot-xr-tools/hands/animations/left/Grip 4.res


BIN
addons/godot-xr-tools/hands/animations/left/Grip 5.res


BIN
addons/godot-xr-tools/hands/animations/left/Grip Shaft.res


BIN
addons/godot-xr-tools/hands/animations/left/Grip.res


BIN
addons/godot-xr-tools/hands/animations/left/Hold.res


BIN
addons/godot-xr-tools/hands/animations/left/Horns.res


BIN
addons/godot-xr-tools/hands/animations/left/Metal.res


BIN
addons/godot-xr-tools/hands/animations/left/Middle.res


BIN
addons/godot-xr-tools/hands/animations/left/OK.res


BIN
addons/godot-xr-tools/hands/animations/left/Peace.res


BIN
addons/godot-xr-tools/hands/animations/left/Pinch Flat.res


BIN
addons/godot-xr-tools/hands/animations/left/Pinch Large.res


BIN
addons/godot-xr-tools/hands/animations/left/Pinch Middle.res


BIN
addons/godot-xr-tools/hands/animations/left/Pinch Ring.res


BIN
addons/godot-xr-tools/hands/animations/left/Pinch Tight.res


BIN
addons/godot-xr-tools/hands/animations/left/Pinch Up.res


BIN
addons/godot-xr-tools/hands/animations/left/PingPong.res


BIN
addons/godot-xr-tools/hands/animations/left/Pinky.res


BIN
addons/godot-xr-tools/hands/animations/left/Pistol.res


BIN
addons/godot-xr-tools/hands/animations/left/Ring.res


BIN
addons/godot-xr-tools/hands/animations/left/Rounded.res


BIN
addons/godot-xr-tools/hands/animations/left/Sign 1.res


BIN
addons/godot-xr-tools/hands/animations/left/Sign 2.res


BIN
addons/godot-xr-tools/hands/animations/left/Sign 3.res


BIN
addons/godot-xr-tools/hands/animations/left/Sign 4.res


Some files were not shown because too many files changed in this diff