Browse Source

Add OpenXR movement demos (#977)

Bastiaan Olij 1 year ago
parent
commit
4a4b46cbe1
42 changed files with 1469 additions and 0 deletions
  1. 2 0
      xr/openxr_character_centric_movement/.gitattributes
  2. 5 0
      xr/openxr_character_centric_movement/.gitignore
  3. 54 0
      xr/openxr_character_centric_movement/README.md
  4. BIN
      xr/openxr_character_centric_movement/assets/pattern.png
  5. 36 0
      xr/openxr_character_centric_movement/assets/pattern.png.import
  6. 1 0
      xr/openxr_character_centric_movement/icon.svg
  7. 37 0
      xr/openxr_character_centric_movement/icon.svg.import
  8. 14 0
      xr/openxr_character_centric_movement/main.tscn
  9. 23 0
      xr/openxr_character_centric_movement/objects/black_out.gd
  10. 12 0
      xr/openxr_character_centric_movement/objects/black_out.gdshader
  11. 21 0
      xr/openxr_character_centric_movement/objects/black_out.tscn
  12. 22 0
      xr/openxr_character_centric_movement/objects/ramp.tscn
  13. 25 0
      xr/openxr_character_centric_movement/objects/wall.tscn
  14. 44 0
      xr/openxr_character_centric_movement/openxr_action_map.tres
  15. 136 0
      xr/openxr_character_centric_movement/player.gd
  16. 44 0
      xr/openxr_character_centric_movement/player.tscn
  17. 27 0
      xr/openxr_character_centric_movement/project.godot
  18. BIN
      xr/openxr_character_centric_movement/screenshots/character_movement_demo.png
  19. 34 0
      xr/openxr_character_centric_movement/screenshots/character_movement_demo.png.import
  20. 104 0
      xr/openxr_character_centric_movement/start_vr.gd
  21. 83 0
      xr/openxr_character_centric_movement/world.tscn
  22. 2 0
      xr/openxr_origin_centric_movement/.gitattributes
  23. 5 0
      xr/openxr_origin_centric_movement/.gitignore
  24. 53 0
      xr/openxr_origin_centric_movement/README.md
  25. BIN
      xr/openxr_origin_centric_movement/assets/pattern.png
  26. 36 0
      xr/openxr_origin_centric_movement/assets/pattern.png.import
  27. 1 0
      xr/openxr_origin_centric_movement/icon.svg
  28. 37 0
      xr/openxr_origin_centric_movement/icon.svg.import
  29. 14 0
      xr/openxr_origin_centric_movement/main.tscn
  30. 23 0
      xr/openxr_origin_centric_movement/objects/black_out.gd
  31. 12 0
      xr/openxr_origin_centric_movement/objects/black_out.gdshader
  32. 21 0
      xr/openxr_origin_centric_movement/objects/black_out.tscn
  33. 22 0
      xr/openxr_origin_centric_movement/objects/ramp.tscn
  34. 25 0
      xr/openxr_origin_centric_movement/objects/wall.tscn
  35. 44 0
      xr/openxr_origin_centric_movement/openxr_action_map.tres
  36. 157 0
      xr/openxr_origin_centric_movement/player.gd
  37. 45 0
      xr/openxr_origin_centric_movement/player.tscn
  38. 27 0
      xr/openxr_origin_centric_movement/project.godot
  39. BIN
      xr/openxr_origin_centric_movement/screenshots/origin_movement_demo.png
  40. 34 0
      xr/openxr_origin_centric_movement/screenshots/origin_movement_demo.png.import
  41. 104 0
      xr/openxr_origin_centric_movement/start_vr.gd
  42. 83 0
      xr/openxr_origin_centric_movement/world.tscn

+ 2 - 0
xr/openxr_character_centric_movement/.gitattributes

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

+ 5 - 0
xr/openxr_character_centric_movement/.gitignore

@@ -0,0 +1,5 @@
+# Godot 4+ specific ignores
+.godot/
+
+# Ignore our Android build folder, should be installed by user if needed
+android/

+ 54 - 0
xr/openxr_character_centric_movement/README.md

@@ -0,0 +1,54 @@
+# XR Character Body Centric Movement demo
+
+This is a demo for an OpenXR project where player movement is handled with a CharacterBody3D as a base node.
+This is based on the [Character body centric solution as explained in the room scale manual page](https://docs.godotengine.org/en/stable/tutorials/xr/xr_room_scale.html#character-body-centric-solution).
+
+Godot version: 4.1.x
+Language: GDScript
+Renderer: compatibility
+
+## How does it work?
+
+With modern VR equipment the user is able to move around a large playspace.
+This is often refered to as roomscale VR.
+The position of the headset and controllers are tracked in reference to a fixed point within this playspace.
+This is often a point on the ground at the center of the playspace mapped out by the user when setting up their guardian.
+
+In Godot the center of this playspace is represented by the `XROrigin3D` node with camera and controllers being tracked through resp. `XRCamera3D` and `XRController3D` child nodes which can thus not be positioned by the user.
+The misunderstandings this causes in handling player movement is described in detail in [the XR room scale manual page](https://docs.godotengine.org/en/stable/tutorials/xr/xr_room_scale.html), a highly recommended read before continuing with this demo.
+
+This demo implements the character body centric solution to the player movement problem.
+Virtual movement by the player (e.g. movement through controller input) in this demo is handled similarly to a non-XR Godot game.
+Physical movement by the player will result in the character body attempting to move to the players new location.
+If successful the XROrigin node is moved in the opposite direction of the players movement.
+If unsuccessful the character body stays behind, the further the player moves the more we black out the screen.
+
+## Action map
+
+This project does not use the default action map but instead configures an action map that just contains the actions required for this example to work. This so we remove any clutter and just focus on the functionality being demonstrated.
+
+There are only two actions needed for this example:
+- aim_pose is used to position the XR controllers
+- move is used as the input for our movement
+
+"Move" being the hero here. This action is only bound to one of the two controllers, by default making it a right hand option. Godot will always associate the move action with the controller that is bound to it.
+
+The code example assumes either controller could trigger the move action. Switching from right to left hand is a separate topic out of scope of this demonstration.
+
+Also following OpenXR guidelines only bindings for controllers with which the project has been tested are supplied. XR Runtimes should provide proper re-mapping however not all follow this guideline. You may need to add a binding for the platform you are using to the action map.
+
+## Running on PCVR
+
+This project can be run as normal for PCVR. Ensure that an OpenXR runtime has been installed.
+This project has been tested with the Oculus client and SteamVR OpenXR runtimes.
+Note that Godot currently can't run using the WMR OpenXR runtime. Install SteamVR with WMR support.
+
+## Running on standalone VR
+
+You must install the Android build templates and OpenXR loader plugin and configure an export template for your device.
+Please follow [the instructions for deploying on Android in the manual](https://docs.godotengine.org/en/stable/tutorials/xr/deploying_to_android.html).
+
+## Screenshots
+
+![Screenshot](screenshots/character_movement_demo.png)
+

BIN
xr/openxr_character_centric_movement/assets/pattern.png


+ 36 - 0
xr/openxr_character_centric_movement/assets/pattern.png.import

@@ -0,0 +1,36 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://rek0t7kubpx4"
+path.s3tc="res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.ctex"
+path.etc2="res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.etc2.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://assets/pattern.png"
+dest_files=["res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.ctex", "res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.etc2.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

+ 1 - 0
xr/openxr_character_centric_movement/icon.svg

@@ -0,0 +1 @@
+<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 813 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H447l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c3 34 55 34 58 0v-86c-3-34-55-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

+ 37 - 0
xr/openxr_character_centric_movement/icon.svg.import

@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://8i0kfiat0t8f"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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

+ 14 - 0
xr/openxr_character_centric_movement/main.tscn

@@ -0,0 +1,14 @@
+[gd_scene load_steps=4 format=3 uid="uid://gvmfk4owutdm"]
+
+[ext_resource type="Script" path="res://start_vr.gd" id="1_mbbqq"]
+[ext_resource type="PackedScene" uid="uid://cpj7vtj6gr3tv" path="res://player.tscn" id="2_igeja"]
+[ext_resource type="PackedScene" uid="uid://bymbq2ruecbhn" path="res://world.tscn" id="2_m075s"]
+
+[node name="Main" type="Node3D"]
+script = ExtResource("1_mbbqq")
+
+[node name="World" parent="." instance=ExtResource("2_m075s")]
+
+[node name="CharacterBody3D" parent="." instance=ExtResource("2_igeja")]
+
+[connection signal="pose_recentered" from="." to="CharacterBody3D" method="recenter"]

+ 23 - 0
xr/openxr_character_centric_movement/objects/black_out.gd

@@ -0,0 +1,23 @@
+@tool
+extends Node3D
+
+@export_range(0, 1, 0.1) var fade = 0.0:
+	set(value):
+		fade = value
+		if is_inside_tree():
+			_update_fade()
+
+var material : ShaderMaterial
+
+func _update_fade():
+	if fade == 0.0:
+		$MeshInstance3D.visible = false
+	else:
+		if material:
+			material.set_shader_parameter("albedo", Color(0.0, 0.0, 0.0, fade))
+		$MeshInstance3D.visible = true
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	material = $MeshInstance3D.material_override
+	_update_fade()

+ 12 - 0
xr/openxr_character_centric_movement/objects/black_out.gdshader

@@ -0,0 +1,12 @@
+shader_type spatial;
+render_mode blend_mix,depth_draw_opaque,cull_disabled,unshaded,depth_test_disabled;
+uniform vec4 albedo : source_color;
+
+void vertex() {
+	POSITION = vec4(VERTEX.xy * 2.0, -1.0, 1.0);
+}
+
+void fragment() {
+	ALBEDO = albedo.rgb;
+	ALPHA = albedo.a;
+}

+ 21 - 0
xr/openxr_character_centric_movement/objects/black_out.tscn

@@ -0,0 +1,21 @@
+[gd_scene load_steps=5 format=3 uid="uid://bbvciliw3xnf6"]
+
+[ext_resource type="Script" path="res://objects/black_out.gd" id="1_1r6dl"]
+[ext_resource type="Shader" path="res://objects/black_out.gdshader" id="2_xc5vy"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_ve0hd"]
+resource_local_to_scene = true
+render_priority = -99
+shader = ExtResource("2_xc5vy")
+shader_parameter/albedo = Color(1, 1, 1, 0.1)
+
+[sub_resource type="QuadMesh" id="QuadMesh_iv1ir"]
+
+[node name="BlackOut" type="Node3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1)
+script = ExtResource("1_1r6dl")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
+visible = false
+material_override = SubResource("ShaderMaterial_ve0hd")
+mesh = SubResource("QuadMesh_iv1ir")

+ 22 - 0
xr/openxr_character_centric_movement/objects/ramp.tscn

@@ -0,0 +1,22 @@
+[gd_scene load_steps=5 format=3 uid="uid://c3wwlb4huytmt"]
+
+[ext_resource type="Texture2D" uid="uid://rek0t7kubpx4" path="res://assets/pattern.png" id="1_qv1d7"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_v7wcs"]
+size = Vector3(5, 1, 3)
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n2enf"]
+albedo_color = Color(1, 0.658824, 0.321569, 1)
+albedo_texture = ExtResource("1_qv1d7")
+
+[sub_resource type="BoxMesh" id="BoxMesh_m8cmi"]
+size = Vector3(5, 1, 3)
+
+[node name="Ramp" type="StaticBody3D"]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+shape = SubResource("BoxShape3D_v7wcs")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
+material_override = SubResource("StandardMaterial3D_n2enf")
+mesh = SubResource("BoxMesh_m8cmi")

+ 25 - 0
xr/openxr_character_centric_movement/objects/wall.tscn

@@ -0,0 +1,25 @@
+[gd_scene load_steps=5 format=3 uid="uid://dpn6187qjqo75"]
+
+[ext_resource type="Texture2D" uid="uid://rek0t7kubpx4" path="res://assets/pattern.png" id="1_mflpj"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_r4qvg"]
+size = Vector3(5, 3, 0.5)
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_qi3if"]
+albedo_color = Color(0.690196, 0.556863, 0.909804, 1)
+albedo_texture = ExtResource("1_mflpj")
+uv1_scale = Vector3(3, 2, 1)
+
+[sub_resource type="BoxMesh" id="BoxMesh_ns54c"]
+material = SubResource("StandardMaterial3D_qi3if")
+size = Vector3(5, 3, 0.5)
+
+[node name="Wall" type="StaticBody3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+shape = SubResource("BoxShape3D_r4qvg")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
+mesh = SubResource("BoxMesh_ns54c")
+skeleton = NodePath("../..")

+ 44 - 0
xr/openxr_character_centric_movement/openxr_action_map.tres

@@ -0,0 +1,44 @@
+[gd_resource type="OpenXRActionMap" load_steps=9 format=3 uid="uid://dha3ympcdeka1"]
+
+[sub_resource type="OpenXRAction" id="OpenXRAction_f5o14"]
+resource_name = "aim_pose"
+localized_name = "Aim pose"
+action_type = 3
+toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right")
+
+[sub_resource type="OpenXRAction" id="OpenXRAction_xocvg"]
+resource_name = "haptic"
+localized_name = "Haptic"
+action_type = 4
+toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right", "/user/vive_tracker_htcx/role/left_foot", "/user/vive_tracker_htcx/role/right_foot", "/user/vive_tracker_htcx/role/left_shoulder", "/user/vive_tracker_htcx/role/right_shoulder", "/user/vive_tracker_htcx/role/left_elbow", "/user/vive_tracker_htcx/role/right_elbow", "/user/vive_tracker_htcx/role/left_knee", "/user/vive_tracker_htcx/role/right_knee", "/user/vive_tracker_htcx/role/waist", "/user/vive_tracker_htcx/role/chest", "/user/vive_tracker_htcx/role/camera", "/user/vive_tracker_htcx/role/keyboard")
+
+[sub_resource type="OpenXRAction" id="OpenXRAction_j46lb"]
+resource_name = "move"
+localized_name = "Move player"
+action_type = 2
+toplevel_paths = PackedStringArray("/user/hand/right")
+
+[sub_resource type="OpenXRActionSet" id="OpenXRActionSet_jpkx2"]
+resource_name = "godot"
+localized_name = "Godot action set"
+actions = [SubResource("OpenXRAction_f5o14"), SubResource("OpenXRAction_xocvg"), SubResource("OpenXRAction_j46lb")]
+
+[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_i1vot"]
+action = SubResource("OpenXRAction_f5o14")
+paths = PackedStringArray("/user/hand/left/input/aim/pose", "/user/hand/right/input/aim/pose")
+
+[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_6t5tt"]
+action = SubResource("OpenXRAction_xocvg")
+paths = PackedStringArray("/user/hand/left/output/haptic", "/user/hand/right/output/haptic")
+
+[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_a1aut"]
+action = SubResource("OpenXRAction_j46lb")
+paths = PackedStringArray("/user/hand/right/input/thumbstick")
+
+[sub_resource type="OpenXRInteractionProfile" id="OpenXRInteractionProfile_7f3ni"]
+interaction_profile_path = "/interaction_profiles/oculus/touch_controller"
+bindings = [SubResource("OpenXRIPBinding_i1vot"), SubResource("OpenXRIPBinding_6t5tt"), SubResource("OpenXRIPBinding_a1aut")]
+
+[resource]
+action_sets = [SubResource("OpenXRActionSet_jpkx2")]
+interaction_profiles = [SubResource("OpenXRInteractionProfile_7f3ni")]

+ 136 - 0
xr/openxr_character_centric_movement/player.gd

@@ -0,0 +1,136 @@
+extends CharacterBody3D
+
+# Settings to control the character
+@export var rotation_speed : float = 1.0
+@export var movement_speed : float = 5.0
+@export var movement_acceleration : float = 5.0
+
+# Get the gravity from the project settings to be synced with RigidBody nodes.
+var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
+
+# Helper variables to keep our code readable
+@onready var origin_node : XROrigin3D = $XROrigin3D
+@onready var camera_node : XRCamera3D = $XROrigin3D/XRCamera3D
+@onready var neck_position_node : Node3D = $XROrigin3D/XRCamera3D/Neck
+@onready var black_out : Node3D = $XROrigin3D/XRCamera3D/BlackOut
+
+# `recenter` is called when the user has requested their view to be recentered.
+# The code here assumes the player has walked into an area they shouldn't be
+# and we return the player back to the character body.
+# But other strategies can be applied here as well such as returning the player
+# to a starting position or a checkpoint.
+func recenter():
+	# Calculate where our camera should be, we start with our global transform
+	var new_camera_transform : Transform3D = global_transform
+
+	# Set to the height of our neck joint
+	new_camera_transform.origin.y = neck_position_node.global_position.y
+
+	# Apply transform our our next position to get our desired camera transform
+	new_camera_transform = new_camera_transform * neck_position_node.transform.inverse()
+
+	# Remove tilt from camera transform
+	var camera_transform : Transform3D = camera_node.transform
+	var forward_dir : Vector3 = camera_transform.basis.z
+	forward_dir.y = 0.0
+	camera_transform = camera_transform.looking_at(camera_transform.origin + forward_dir.normalized(), Vector3.UP, true)
+
+	# Update our XR location
+	origin_node.global_transform = new_camera_transform * camera_transform.inverse()
+
+# `_get_movement_input` returns our move input by querying the move action on each controller
+func _get_movement_input() -> Vector2:
+	var movement : Vector2 = Vector2()
+
+	# If move is not bound to one of our controllers,
+	# that controller will return a Vector2(0.0, 0.0)
+	movement += $XROrigin3D/LeftHand.get_vector2("move")
+	movement += $XROrigin3D/RightHand.get_vector2("move")
+
+	return movement
+
+# `_process_on_physical_movement` handles the physical movement of the player
+# adjusting our character body position to "catch up to" the player.
+# If the character body encounters an obstruction our view will black out
+# and we will stop further character movement until the player physically
+# moves back.
+func _process_on_physical_movement(delta) -> bool:
+	# Remember our current velocity, we'll apply that later
+	var current_velocity = velocity
+
+	# Start by rotating the player to face the same way our real player is
+	var camera_basis: Basis = origin_node.transform.basis * camera_node.transform.basis
+	var forward: Vector2 = Vector2(camera_basis.z.x, camera_basis.z.z)
+	var angle: float = forward.angle_to(Vector2(0.0, 1.0))
+
+	# Rotate our character body
+	transform.basis = transform.basis.rotated(Vector3.UP, angle)
+
+	# Reverse this rotation our origin node
+	origin_node.transform = Transform3D().rotated(Vector3.UP, -angle) * origin_node.transform
+
+	# Now apply movement, first move our player body to the right location
+	var org_player_body: Vector3 = global_transform.origin
+	var player_body_location: Vector3 = origin_node.transform * camera_node.transform * neck_position_node.transform.origin
+	player_body_location.y = 0.0
+	player_body_location = global_transform * player_body_location
+
+	velocity = (player_body_location - org_player_body) / delta
+	move_and_slide()
+
+	# Now move our XROrigin back
+	var delta_movement = global_transform.origin - org_player_body
+	origin_node.global_transform.origin -= delta_movement
+
+	# Negate any height change in local space due to player hitting ramps etc.
+	origin_node.transform.origin.y = 0.0
+
+	# Return our value
+	velocity = current_velocity
+
+	# Check if we managed to move where we wanted to
+	var location_offset = (player_body_location - global_transform.origin).length()
+	if location_offset > 0.1:
+		# We couldn't go where we wanted to, black out our screen
+		black_out.fade = clamp((location_offset - 0.1) / 0.1, 0.0, 1.0)
+
+		return true
+	else:
+		black_out.fade = 0.0
+		return false
+
+# `_process_movement_on_input` handles movement through controller input.
+# We first handle rotating the player and then apply movement.
+# We also apply the effects of gravity at this point.
+func _process_movement_on_input(is_colliding, delta):
+	if !is_colliding:
+		# Only handle input if we've not physically moved somewhere we shouldn't.
+		var movement_input = _get_movement_input()
+
+		# First handle rotation, to keep this example simple we are implementing
+		# "smooth" rotation here. This can lead to motion sickness.
+		# Adding a comfort option with "stepped" rotation is good practice but
+		# falls outside of the scope of this demonstration.
+		rotation.y += -movement_input.x * delta * rotation_speed
+
+		# Now handle forward/backwards movement.
+		# Straffing can be added by using the movement_input.x input
+		# and using a different input for rotational control.
+		# Straffing is more prone to motion sickness.
+		var direction = global_transform.basis * Vector3(0.0, 0.0, -movement_input.y) * movement_speed
+		if direction:
+			velocity.x = move_toward(velocity.x, direction.x, delta * movement_acceleration)
+			velocity.z = move_toward(velocity.z, direction.z, delta * movement_acceleration)
+		else:
+			velocity.x = move_toward(velocity.x, 0, delta * movement_acceleration)
+			velocity.z = move_toward(velocity.z, 0, delta * movement_acceleration)
+
+	# Always handle gravity
+	velocity.y -= gravity * delta
+
+	move_and_slide()
+
+# _physics_process handles our player movement.
+func _physics_process(delta):
+	var is_colliding = _process_on_physical_movement(delta)
+	_process_movement_on_input(is_colliding, delta)

+ 44 - 0
xr/openxr_character_centric_movement/player.tscn

@@ -0,0 +1,44 @@
+[gd_scene load_steps=5 format=3 uid="uid://cpj7vtj6gr3tv"]
+
+[ext_resource type="Script" path="res://player.gd" id="1_d31c7"]
+[ext_resource type="PackedScene" uid="uid://bbvciliw3xnf6" path="res://objects/black_out.tscn" id="2_ecfc5"]
+
+[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_xe2ly"]
+
+[sub_resource type="BoxMesh" id="BoxMesh_sldd7"]
+size = Vector3(0.05, 0.05, 0.05)
+
+[node name="CharacterBody3D" type="CharacterBody3D"]
+script = ExtResource("1_d31c7")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
+shape = SubResource("CapsuleShape3D_xe2ly")
+
+[node name="XROrigin3D" type="XROrigin3D" parent="."]
+
+[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -0.1)
+far = 1000.0
+
+[node name="Neck" type="Node3D" parent="XROrigin3D/XRCamera3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0.1)
+
+[node name="BlackOut" parent="XROrigin3D/XRCamera3D" instance=ExtResource("2_ecfc5")]
+
+[node name="LeftHand" type="XRController3D" parent="XROrigin3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.5, 1, -0.5)
+tracker = &"left_hand"
+pose = &"aim"
+
+[node name="PlaceholderHand" type="MeshInstance3D" parent="XROrigin3D/LeftHand"]
+mesh = SubResource("BoxMesh_sldd7")
+
+[node name="RightHand" type="XRController3D" parent="XROrigin3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5, 1, -0.5)
+tracker = &"right_hand"
+pose = &"aim"
+
+[node name="PlaceholderHand" type="MeshInstance3D" parent="XROrigin3D/RightHand"]
+mesh = SubResource("BoxMesh_sldd7")
+skeleton = NodePath("../../LeftHand")

+ 27 - 0
xr/openxr_character_centric_movement/project.godot

@@ -0,0 +1,27 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="OpenXR Character Centric Movement"
+run/main_scene="res://main.tscn"
+config/features=PackedStringArray("4.1", "GL Compatibility")
+config/icon="res://icon.svg"
+
+[rendering]
+
+renderer/rendering_method="gl_compatibility"
+renderer/rendering_method.mobile="gl_compatibility"
+textures/vram_compression/import_etc2_astc=true
+
+[xr]
+
+openxr/enabled=true
+shaders/enabled=true

BIN
xr/openxr_character_centric_movement/screenshots/character_movement_demo.png


+ 34 - 0
xr/openxr_character_centric_movement/screenshots/character_movement_demo.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dxvrb1uccxyom"
+path="res://.godot/imported/character_movement_demo.png-8a617b4191676e53bcbd85fa1edeb450.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://screenshots/character_movement_demo.png"
+dest_files=["res://.godot/imported/character_movement_demo.png-8a617b4191676e53bcbd85fa1edeb450.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

+ 104 - 0
xr/openxr_character_centric_movement/start_vr.gd

@@ -0,0 +1,104 @@
+extends Node3D
+
+signal focus_lost
+signal focus_gained
+signal pose_recentered
+
+@export var maximum_refresh_rate : int = 90
+
+var xr_interface : OpenXRInterface
+var xr_is_focussed = false
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	xr_interface = XRServer.find_interface("OpenXR")
+	if xr_interface and xr_interface.is_initialized():
+		print("OpenXR instantiated successfully.")
+		var vp : Viewport = get_viewport()
+
+		# Enable XR on our viewport
+		vp.use_xr = true
+
+		# Make sure v-sync is off, v-sync is handled by OpenXR
+		DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
+
+		# Connect the OpenXR events
+		xr_interface.connect("session_begun", _on_openxr_session_begun)
+		xr_interface.connect("session_visible", _on_openxr_visible_state)
+		xr_interface.connect("session_focussed", _on_openxr_focused_state)
+		xr_interface.connect("session_stopping", _on_openxr_stopping)
+		xr_interface.connect("pose_recentered", _on_openxr_pose_recentered)
+	else:
+		# We couldn't start OpenXR.
+		print("OpenXR not instantiated!")
+		get_tree().quit()
+
+
+# Handle OpenXR session ready
+func _on_openxr_session_begun() -> void:
+	# Get the reported refresh rate
+	var current_refresh_rate = xr_interface.get_display_refresh_rate()
+	if current_refresh_rate > 0:
+		print("OpenXR: Refresh rate reported as ", str(current_refresh_rate))
+	else:
+		print("OpenXR: No refresh rate given by XR runtime")
+
+	# See if we have a better refresh rate available
+	var new_rate = current_refresh_rate
+	var available_rates : Array = xr_interface.get_available_display_refresh_rates()
+	if available_rates.size() == 0:
+		print("OpenXR: Target does not support refresh rate extension")
+	elif available_rates.size() == 1:
+		# Only one available, so use it
+		new_rate = available_rates[0]
+	else:
+		for rate in available_rates:
+			if rate > new_rate and rate < maximum_refresh_rate:
+				new_rate = rate
+
+	# Did we find a better rate?
+	if current_refresh_rate != new_rate:
+		print("OpenXR: Setting refresh rate to ", str(new_rate))
+		xr_interface.set_display_refresh_rate(new_rate)
+		current_refresh_rate = new_rate
+
+	# Now match our physics rate
+	Engine.physics_ticks_per_second = current_refresh_rate
+
+
+# Handle OpenXR visible state
+func _on_openxr_visible_state() -> void:
+	# We always pass this state at startup,
+	# but the second time we get this it means our player took off their headset
+	if xr_is_focussed:
+		print("OpenXR lost focus")
+
+		xr_is_focussed = false
+
+		# pause our game
+		process_mode = Node.PROCESS_MODE_DISABLED
+
+		emit_signal("focus_lost")
+
+
+# Handle OpenXR focused state
+func _on_openxr_focused_state() -> void:
+	print("OpenXR gained focus")
+	xr_is_focussed = true
+
+	# unpause our game
+	process_mode = Node.PROCESS_MODE_INHERIT
+
+	emit_signal("focus_gained")
+
+# Handle OpenXR stopping state
+func _on_openxr_stopping() -> void:
+	# Our session is being stopped.
+	print("OpenXR is stopping")
+
+# Handle OpenXR pose recentered signal
+func _on_openxr_pose_recentered() -> void:
+	# User recentered view, we have to react to this by recentering the view.
+	# This is game implementation dependent.
+	emit_signal("pose_recentered")

+ 83 - 0
xr/openxr_character_centric_movement/world.tscn

@@ -0,0 +1,83 @@
+[gd_scene load_steps=10 format=3 uid="uid://bymbq2ruecbhn"]
+
+[ext_resource type="Texture2D" uid="uid://rek0t7kubpx4" path="res://assets/pattern.png" id="1_yguss"]
+[ext_resource type="PackedScene" uid="uid://dpn6187qjqo75" path="res://objects/wall.tscn" id="2_350v7"]
+[ext_resource type="PackedScene" uid="uid://c3wwlb4huytmt" path="res://objects/ramp.tscn" id="3_0sele"]
+
+[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_4mshr"]
+sky_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)
+ground_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)
+
+[sub_resource type="Sky" id="Sky_kwq44"]
+sky_material = SubResource("ProceduralSkyMaterial_4mshr")
+
+[sub_resource type="Environment" id="Environment_pcm4o"]
+background_mode = 2
+sky = SubResource("Sky_kwq44")
+tonemap_mode = 2
+
+[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_xavpm"]
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_0i7rc"]
+albedo_color = Color(0.368627, 1, 0.4, 1)
+albedo_texture = ExtResource("1_yguss")
+uv1_scale = Vector3(10, 10, 10)
+
+[sub_resource type="PlaneMesh" id="PlaneMesh_dxdgs"]
+material = SubResource("StandardMaterial3D_0i7rc")
+size = Vector2(100, 100)
+
+[node name="World" type="Node3D"]
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(-0.866025, -0.433013, 0.25, 0, 0.5, 0.866025, -0.5, 0.75, -0.433013, 0, 5, 0)
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
+environment = SubResource("Environment_pcm4o")
+
+[node name="Floor" type="StaticBody3D" parent="."]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor"]
+shape = SubResource("WorldBoundaryShape3D_xavpm")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor"]
+mesh = SubResource("PlaneMesh_dxdgs")
+
+[node name="Wall01" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -8)
+
+[node name="Wall02" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 1.5, -8)
+
+[node name="Wall03" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0.707107, 0, 0.707107, 0, 1, 0, -0.707107, 0, 0.707107, -9.24219, 1.5, -6.24219)
+
+[node name="Wall04" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0.707107, 0, 0.707107, 0, 1, 0, -0.707107, 0, 0.707107, -4, 1.5, 8)
+
+[node name="Wall05" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(-0.707107, 0, 0.707107, 0, 1, 0, -0.707107, 0, -0.707107, 4, 1.5, 8)
+
+[node name="Wall06" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0.707107, 0, 0.707107, 0, 1, 0, -0.707107, 0, 0.707107, 12, 1.5, 8)
+
+[node name="Wall07" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 8, 1.5, -10)
+
+[node name="Wall08" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 8, 1.5, -15)
+
+[node name="Wall10" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, -11, 1.5, -2)
+
+[node name="Wall09" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 8, 1.5, -20)
+
+[node name="Ramp01" parent="." instance=ExtResource("3_0sele")]
+transform = Transform3D(-4.2222e-08, 1.13133e-08, 1, 0.258819, 0.965926, 0, -0.965926, 0.258819, -4.37114e-08, 5, 0, -9)
+
+[node name="Ramp02" parent="." instance=ExtResource("3_0sele")]
+transform = Transform3D(8.9407e-08, -3.72529e-08, -1, 0.258819, 0.965926, -1.10534e-08, 0.965926, -0.258819, 2.98023e-08, 5, 0, -18.5646)
+
+[node name="Ramp03" parent="." instance=ExtResource("3_0sele")]
+transform = Transform3D(8.9407e-08, -3.72529e-08, -1, 0, 1, -1.83902e-08, 1, -2.98023e-08, 2.5926e-08, 5, 0.633461, -13.786)

+ 2 - 0
xr/openxr_origin_centric_movement/.gitattributes

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

+ 5 - 0
xr/openxr_origin_centric_movement/.gitignore

@@ -0,0 +1,5 @@
+# Godot 4+ specific ignores
+.godot/
+
+# Ignore our Android build folder, should be installed by user if needed
+android/

+ 53 - 0
xr/openxr_origin_centric_movement/README.md

@@ -0,0 +1,53 @@
+# XR Origin Centric Movement demo
+
+This is a demo for an OpenXR project where player movement is handled with a XRorigin3D as a base node.
+This is based on the [Origin centric solution as explained in the room scale manual page](https://docs.godotengine.org/en/stable/tutorials/xr/xr_room_scale.html#origin-centric-solution).
+
+Godot version: 4.1.x
+Language: GDScript
+Renderer: compatibility
+
+## How does it work?
+
+With modern VR equipment the user is able to move around a large playspace.
+This is often refered to as roomscale VR.
+The position of the headset and controllers are tracked in reference to a fixed point within this playspace.
+This is often a point on the ground at the center of the playspace mapped out by the user when setting up their guardian.
+
+In Godot the center of this playspace is represented by the `XROrigin3D` node with camera and controllers being tracked through resp. `XRCamera3D` and `XRController3D` child nodes which can thus not be positioned by the user.
+The misunderstandings this causes in handling player movement is described in detail in [the XR room scale manual page](https://docs.godotengine.org/en/stable/tutorials/xr/xr_room_scale.html), a highly recommended read before continuing with this demo.
+
+This demo implements the origin body centric solution to the player movement problem.
+This is an older approach to setting up movement in XR which keeps the origin point more clearly mapped in the virtual world.
+With this setup it is easier to see how elements are tracked and how the body is moving within the tracked environment.
+However moving the player through controller input requires a lot more math as we need to move the origin point to drive player movement.
+
+## Action map
+
+This project does not use the default action map but instead configures an action map that just contains the actions required for this example to work. This so we remove any clutter and just focus on the functionality being demonstrated.
+
+There are only two actions needed for this example:
+- aim_pose is used to position the XR controllers
+- move is used as the input for our movement
+
+"Move" being the hero here. This action is only bound to one of the two controllers, by default making it a right hand option. Godot will always associate the move action with the controller that is bound to it.
+
+The code example assumes either controller could trigger the move action. Switching from right to left hand is a separate topic out of scope of this demonstration.
+
+Also following OpenXR guidelines only bindings for controllers with which the project has been tested are supplied. XR Runtimes should provide proper re-mapping however not all follow this guideline. You may need to add a binding for the platform you are using to the action map.
+
+## Running on PCVR
+
+This project can be run as normal for PCVR. Ensure that an OpenXR runtime has been installed.
+This project has been tested with the Oculus client and SteamVR OpenXR runtimes.
+Note that Godot currently can't run using the WMR OpenXR runtime. Install SteamVR with WMR support.
+
+## Running on standalone VR
+
+You must install the Android build templates and OpenXR loader plugin and configure an export template for your device.
+Please follow [the instructions for deploying on Android in the manual](https://docs.godotengine.org/en/stable/tutorials/xr/deploying_to_android.html).
+
+## Screenshots
+
+![Screenshot](screenshots/origin_movement_demo.png)
+

BIN
xr/openxr_origin_centric_movement/assets/pattern.png


+ 36 - 0
xr/openxr_origin_centric_movement/assets/pattern.png.import

@@ -0,0 +1,36 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://rek0t7kubpx4"
+path.s3tc="res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.ctex"
+path.etc2="res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.etc2.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://assets/pattern.png"
+dest_files=["res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.ctex", "res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.etc2.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

+ 1 - 0
xr/openxr_origin_centric_movement/icon.svg

@@ -0,0 +1 @@
+<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 813 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H447l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c3 34 55 34 58 0v-86c-3-34-55-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

+ 37 - 0
xr/openxr_origin_centric_movement/icon.svg.import

@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://8i0kfiat0t8f"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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

+ 14 - 0
xr/openxr_origin_centric_movement/main.tscn

@@ -0,0 +1,14 @@
+[gd_scene load_steps=4 format=3 uid="uid://gvmfk4owutdm"]
+
+[ext_resource type="Script" path="res://start_vr.gd" id="1_mbbqq"]
+[ext_resource type="PackedScene" uid="uid://cpj7vtj6gr3tv" path="res://player.tscn" id="2_igeja"]
+[ext_resource type="PackedScene" uid="uid://bymbq2ruecbhn" path="res://world.tscn" id="2_m075s"]
+
+[node name="Main" type="Node3D"]
+script = ExtResource("1_mbbqq")
+
+[node name="World" parent="." instance=ExtResource("2_m075s")]
+
+[node name="XROrigin3D" parent="." instance=ExtResource("2_igeja")]
+
+[connection signal="pose_recentered" from="." to="XROrigin3D" method="recenter"]

+ 23 - 0
xr/openxr_origin_centric_movement/objects/black_out.gd

@@ -0,0 +1,23 @@
+@tool
+extends Node3D
+
+@export_range(0, 1, 0.1) var fade = 0.0:
+	set(value):
+		fade = value
+		if is_inside_tree():
+			_update_fade()
+
+var material : ShaderMaterial
+
+func _update_fade():
+	if fade == 0.0:
+		$MeshInstance3D.visible = false
+	else:
+		if material:
+			material.set_shader_parameter("albedo", Color(0.0, 0.0, 0.0, fade))
+		$MeshInstance3D.visible = true
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	material = $MeshInstance3D.material_override
+	_update_fade()

+ 12 - 0
xr/openxr_origin_centric_movement/objects/black_out.gdshader

@@ -0,0 +1,12 @@
+shader_type spatial;
+render_mode blend_mix,depth_draw_opaque,cull_disabled,unshaded,depth_test_disabled;
+uniform vec4 albedo : source_color;
+
+void vertex() {
+	POSITION = vec4(VERTEX.xy * 2.0, -1.0, 1.0);
+}
+
+void fragment() {
+	ALBEDO = albedo.rgb;
+	ALPHA = albedo.a;
+}

+ 21 - 0
xr/openxr_origin_centric_movement/objects/black_out.tscn

@@ -0,0 +1,21 @@
+[gd_scene load_steps=5 format=3 uid="uid://bbvciliw3xnf6"]
+
+[ext_resource type="Script" path="res://objects/black_out.gd" id="1_1r6dl"]
+[ext_resource type="Shader" path="res://objects/black_out.gdshader" id="2_xc5vy"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_ve0hd"]
+resource_local_to_scene = true
+render_priority = -99
+shader = ExtResource("2_xc5vy")
+shader_parameter/albedo = Color(1, 1, 1, 0.1)
+
+[sub_resource type="QuadMesh" id="QuadMesh_iv1ir"]
+
+[node name="BlackOut" type="Node3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1)
+script = ExtResource("1_1r6dl")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
+visible = false
+material_override = SubResource("ShaderMaterial_ve0hd")
+mesh = SubResource("QuadMesh_iv1ir")

+ 22 - 0
xr/openxr_origin_centric_movement/objects/ramp.tscn

@@ -0,0 +1,22 @@
+[gd_scene load_steps=5 format=3 uid="uid://c3wwlb4huytmt"]
+
+[ext_resource type="Texture2D" uid="uid://rek0t7kubpx4" path="res://assets/pattern.png" id="1_qv1d7"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_v7wcs"]
+size = Vector3(5, 1, 3)
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n2enf"]
+albedo_color = Color(1, 0.658824, 0.321569, 1)
+albedo_texture = ExtResource("1_qv1d7")
+
+[sub_resource type="BoxMesh" id="BoxMesh_m8cmi"]
+size = Vector3(5, 1, 3)
+
+[node name="Ramp" type="StaticBody3D"]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+shape = SubResource("BoxShape3D_v7wcs")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
+material_override = SubResource("StandardMaterial3D_n2enf")
+mesh = SubResource("BoxMesh_m8cmi")

+ 25 - 0
xr/openxr_origin_centric_movement/objects/wall.tscn

@@ -0,0 +1,25 @@
+[gd_scene load_steps=5 format=3 uid="uid://dpn6187qjqo75"]
+
+[ext_resource type="Texture2D" uid="uid://rek0t7kubpx4" path="res://assets/pattern.png" id="1_mflpj"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_r4qvg"]
+size = Vector3(5, 3, 0.5)
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_qi3if"]
+albedo_color = Color(0.690196, 0.556863, 0.909804, 1)
+albedo_texture = ExtResource("1_mflpj")
+uv1_scale = Vector3(3, 2, 1)
+
+[sub_resource type="BoxMesh" id="BoxMesh_ns54c"]
+material = SubResource("StandardMaterial3D_qi3if")
+size = Vector3(5, 3, 0.5)
+
+[node name="Wall" type="StaticBody3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+shape = SubResource("BoxShape3D_r4qvg")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
+mesh = SubResource("BoxMesh_ns54c")
+skeleton = NodePath("../..")

+ 44 - 0
xr/openxr_origin_centric_movement/openxr_action_map.tres

@@ -0,0 +1,44 @@
+[gd_resource type="OpenXRActionMap" load_steps=9 format=3 uid="uid://dha3ympcdeka1"]
+
+[sub_resource type="OpenXRAction" id="OpenXRAction_f5o14"]
+resource_name = "aim_pose"
+localized_name = "Aim pose"
+action_type = 3
+toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right")
+
+[sub_resource type="OpenXRAction" id="OpenXRAction_xocvg"]
+resource_name = "haptic"
+localized_name = "Haptic"
+action_type = 4
+toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right", "/user/vive_tracker_htcx/role/left_foot", "/user/vive_tracker_htcx/role/right_foot", "/user/vive_tracker_htcx/role/left_shoulder", "/user/vive_tracker_htcx/role/right_shoulder", "/user/vive_tracker_htcx/role/left_elbow", "/user/vive_tracker_htcx/role/right_elbow", "/user/vive_tracker_htcx/role/left_knee", "/user/vive_tracker_htcx/role/right_knee", "/user/vive_tracker_htcx/role/waist", "/user/vive_tracker_htcx/role/chest", "/user/vive_tracker_htcx/role/camera", "/user/vive_tracker_htcx/role/keyboard")
+
+[sub_resource type="OpenXRAction" id="OpenXRAction_j46lb"]
+resource_name = "move"
+localized_name = "Move player"
+action_type = 2
+toplevel_paths = PackedStringArray("/user/hand/right")
+
+[sub_resource type="OpenXRActionSet" id="OpenXRActionSet_jpkx2"]
+resource_name = "godot"
+localized_name = "Godot action set"
+actions = [SubResource("OpenXRAction_f5o14"), SubResource("OpenXRAction_xocvg"), SubResource("OpenXRAction_j46lb")]
+
+[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_i1vot"]
+action = SubResource("OpenXRAction_f5o14")
+paths = PackedStringArray("/user/hand/left/input/aim/pose", "/user/hand/right/input/aim/pose")
+
+[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_6t5tt"]
+action = SubResource("OpenXRAction_xocvg")
+paths = PackedStringArray("/user/hand/left/output/haptic", "/user/hand/right/output/haptic")
+
+[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_a1aut"]
+action = SubResource("OpenXRAction_j46lb")
+paths = PackedStringArray("/user/hand/right/input/thumbstick")
+
+[sub_resource type="OpenXRInteractionProfile" id="OpenXRInteractionProfile_7f3ni"]
+interaction_profile_path = "/interaction_profiles/oculus/touch_controller"
+bindings = [SubResource("OpenXRIPBinding_i1vot"), SubResource("OpenXRIPBinding_6t5tt"), SubResource("OpenXRIPBinding_a1aut")]
+
+[resource]
+action_sets = [SubResource("OpenXRActionSet_jpkx2")]
+interaction_profiles = [SubResource("OpenXRInteractionProfile_7f3ni")]

+ 157 - 0
xr/openxr_origin_centric_movement/player.gd

@@ -0,0 +1,157 @@
+extends XROrigin3D
+
+
+# Settings to control the character
+@export var rotation_speed : float = 1.0
+@export var movement_speed : float = 5.0
+@export var movement_acceleration : float = 5.0
+
+# Get the gravity from the project settings to be synced with RigidBody nodes.
+var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
+
+# Helper variables to keep our code readable
+@onready var character_body : CharacterBody3D = $CharacterBody3D
+@onready var camera_node : XRCamera3D = $XRCamera3D
+@onready var neck_position_node : Node3D = $XRCamera3D/Neck
+@onready var black_out : Node3D = $XRCamera3D/BlackOut
+
+# `recenter` is called when the user has requested their view to be recentered.
+# The code here assumes the player has walked into an area they shouldn't be
+# and we return the player back to the character body.
+# But other strategies can be applied here as well such as returning the player
+# to a starting position or a checkpoint.
+func recenter():
+	# Calculate where our camera should be, we start with our global transform
+	var new_camera_transform : Transform3D = character_body.global_transform
+
+	# Set to the height of our neck joint
+	new_camera_transform.origin.y = neck_position_node.global_position.y
+
+	# Apply transform our our next position to get our desired camera transform
+	new_camera_transform = new_camera_transform * neck_position_node.transform.inverse()
+
+	# Remove tilt from camera transform
+	var camera_transform : Transform3D = camera_node.transform
+	var forward_dir : Vector3 = camera_transform.basis.z
+	forward_dir.y = 0.0
+	camera_transform = camera_transform.looking_at(camera_transform.origin + forward_dir.normalized(), Vector3.UP, true)
+
+	# Update our XR location
+	global_transform = new_camera_transform * camera_transform.inverse()
+
+	# Recenter character body
+	character_body.transform = Transform3D()
+
+# `_get_movement_input` returns our move input by querying the move action on each controller
+func _get_movement_input() -> Vector2:
+	var movement : Vector2 = Vector2()
+
+	# If move is not bound to one of our controllers,
+	# that controller will return a Vector2(0.0, 0.0)
+	movement += $LeftHand.get_vector2("move")
+	movement += $RightHand.get_vector2("move")
+
+	return movement
+
+# `_process_on_physical_movement` handles the physical movement of the player
+# adjusting our character body position to "catch up to" the player.
+# If the character body encounters an obstruction our view will black out
+# and we will stop further character movement until the player physically
+# moves back.
+func _process_on_physical_movement(delta) -> bool:
+	# Remember our current velocity, we'll apply that later
+	var current_velocity = character_body.velocity
+
+	# Remember where our player body currently is
+	var org_player_body: Vector3 = character_body.global_transform.origin
+
+	# Determine where our player body should be
+	var player_body_location: Vector3 = camera_node.transform * neck_position_node.transform.origin
+	player_body_location.y = 0.0
+	player_body_location = global_transform * player_body_location
+
+	# Attempt to move our character
+	character_body.velocity = (player_body_location - org_player_body) / delta
+	character_body.move_and_slide()
+
+	# Set back to our current value
+	character_body.velocity = current_velocity
+
+	# Check if we managed to move all the way, ignoring height change
+	var movement_left = player_body_location - character_body.global_transform.origin
+	movement_left.y = 0.0
+
+	# Check if we managed to move where we wanted to
+	var location_offset = movement_left.length()
+	if location_offset > 0.1:
+		# We couldn't go where we wanted to, black out our screen
+		black_out.fade = clamp((location_offset - 0.1) / 0.1, 0.0, 1.0)
+
+		return true
+	else:
+		black_out.fade = 0.0
+		return false
+
+func _copy_player_rotation_to_character_body():
+	# We only copy our forward direction to our character body, we ignore tilt
+	var camera_forward: Vector3 = -camera_node.global_transform.basis.z
+	var body_forward: Vector3 = Vector3(camera_forward.x, 0.0, camera_forward.z)
+
+	character_body.global_transform.basis = Basis.looking_at(body_forward, Vector3.UP)
+
+# `_process_movement_on_input` handles movement through controller input.
+# We first handle rotating the player and then apply movement.
+# We also apply the effects of gravity at this point.
+func _process_movement_on_input(is_colliding, delta):
+	# Remember where our player body currently is
+	var org_player_body: Vector3 = character_body.global_transform.origin
+
+	if !is_colliding:
+		# Only handle input if we've not physically moved somewhere we shouldn't.
+		var movement_input = _get_movement_input()
+
+		# First handle rotation, to keep this example simple we are implementing
+		# "smooth" rotation here. This can lead to motion sickness.
+		# Adding a comfort option with "stepped" rotation is good practice but
+		# falls outside of the scope of this demonstration.
+
+		var t1 := Transform3D()
+		var t2 := Transform3D()
+		var rot := Transform3D()
+
+		# We are going to rotate the origin around the player
+		var player_position = character_body.global_transform.origin - global_transform.origin
+
+		t1.origin = -player_position
+		t2.origin = player_position
+		rot = rot.rotated(Vector3(0.0, 1.0, 0.0), -movement_input.x * delta * rotation_speed)
+		global_transform = (global_transform * t2 * rot * t1).orthonormalized()
+
+		# Now ensure our player body is facing the correct way as well
+		_copy_player_rotation_to_character_body()
+
+		# Now handle forward/backwards movement.
+		# Straffing can be added by using the movement_input.x input
+		# and using a different input for rotational control.
+		# Straffing is more prone to motion sickness.
+		var direction: Vector3 = (character_body.global_transform.basis * Vector3(0.0, 0.0, -movement_input.y)) * movement_speed
+		if direction:
+			character_body.velocity.x = move_toward(character_body.velocity.x, direction.x, delta * movement_acceleration)
+			character_body.velocity.z = move_toward(character_body.velocity.z, direction.z, delta * movement_acceleration)
+		else:
+			character_body.velocity.x = move_toward(character_body.velocity.x, 0, delta * movement_acceleration)
+			character_body.velocity.z = move_toward(character_body.velocity.z, 0, delta * movement_acceleration)
+
+	# Always handle gravity
+	character_body.velocity.y -= gravity * delta
+
+	# Attempt to move our player
+	character_body.move_and_slide()
+
+	# And now apply the actual movement to our origin
+	global_transform.origin += character_body.global_transform.origin - org_player_body
+
+# _physics_process handles our player movement.
+func _physics_process(delta):
+	var is_colliding = _process_on_physical_movement(delta)
+	_process_movement_on_input(is_colliding, delta)

+ 45 - 0
xr/openxr_origin_centric_movement/player.tscn

@@ -0,0 +1,45 @@
+[gd_scene load_steps=5 format=3 uid="uid://cpj7vtj6gr3tv"]
+
+[ext_resource type="Script" path="res://player.gd" id="1_d31c7"]
+[ext_resource type="PackedScene" uid="uid://bbvciliw3xnf6" path="res://objects/black_out.tscn" id="2_ecfc5"]
+
+[sub_resource type="BoxMesh" id="BoxMesh_sldd7"]
+size = Vector3(0.05, 0.05, 0.05)
+
+[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_xe2ly"]
+
+[node name="XROrigin3D" type="XROrigin3D"]
+current = true
+script = ExtResource("1_d31c7")
+
+[node name="XRCamera3D" type="XRCamera3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -0.1)
+far = 1000.0
+
+[node name="Neck" type="Node3D" parent="XRCamera3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0.1)
+
+[node name="BlackOut" parent="XRCamera3D" instance=ExtResource("2_ecfc5")]
+
+[node name="LeftHand" type="XRController3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.5, 1, -0.5)
+tracker = &"left_hand"
+pose = &"aim"
+
+[node name="PlaceholderHand" type="MeshInstance3D" parent="LeftHand"]
+mesh = SubResource("BoxMesh_sldd7")
+
+[node name="RightHand" type="XRController3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5, 1, -0.5)
+tracker = &"right_hand"
+pose = &"aim"
+
+[node name="PlaceholderHand" type="MeshInstance3D" parent="RightHand"]
+mesh = SubResource("BoxMesh_sldd7")
+skeleton = NodePath("../../LeftHand")
+
+[node name="CharacterBody3D" type="CharacterBody3D" parent="."]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="CharacterBody3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
+shape = SubResource("CapsuleShape3D_xe2ly")

+ 27 - 0
xr/openxr_origin_centric_movement/project.godot

@@ -0,0 +1,27 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="OpenXR Origin Centric Movement"
+run/main_scene="res://main.tscn"
+config/features=PackedStringArray("4.1", "GL Compatibility")
+config/icon="res://icon.svg"
+
+[rendering]
+
+renderer/rendering_method="gl_compatibility"
+renderer/rendering_method.mobile="gl_compatibility"
+textures/vram_compression/import_etc2_astc=true
+
+[xr]
+
+openxr/enabled=true
+shaders/enabled=true

BIN
xr/openxr_origin_centric_movement/screenshots/origin_movement_demo.png


+ 34 - 0
xr/openxr_origin_centric_movement/screenshots/origin_movement_demo.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d18iginxisy4f"
+path="res://.godot/imported/origin_movement_demo.png-11b60d4b031fd4625efb56215de01627.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://screenshots/origin_movement_demo.png"
+dest_files=["res://.godot/imported/origin_movement_demo.png-11b60d4b031fd4625efb56215de01627.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

+ 104 - 0
xr/openxr_origin_centric_movement/start_vr.gd

@@ -0,0 +1,104 @@
+extends Node3D
+
+signal focus_lost
+signal focus_gained
+signal pose_recentered
+
+@export var maximum_refresh_rate : int = 90
+
+var xr_interface : OpenXRInterface
+var xr_is_focussed = false
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready():
+	xr_interface = XRServer.find_interface("OpenXR")
+	if xr_interface and xr_interface.is_initialized():
+		print("OpenXR instantiated successfully.")
+		var vp : Viewport = get_viewport()
+
+		# Enable XR on our viewport
+		vp.use_xr = true
+
+		# Make sure v-sync is off, v-sync is handled by OpenXR
+		DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
+
+		# Connect the OpenXR events
+		xr_interface.connect("session_begun", _on_openxr_session_begun)
+		xr_interface.connect("session_visible", _on_openxr_visible_state)
+		xr_interface.connect("session_focussed", _on_openxr_focused_state)
+		xr_interface.connect("session_stopping", _on_openxr_stopping)
+		xr_interface.connect("pose_recentered", _on_openxr_pose_recentered)
+	else:
+		# We couldn't start OpenXR.
+		print("OpenXR not instantiated!")
+		get_tree().quit()
+
+
+# Handle OpenXR session ready
+func _on_openxr_session_begun() -> void:
+	# Get the reported refresh rate
+	var current_refresh_rate = xr_interface.get_display_refresh_rate()
+	if current_refresh_rate > 0:
+		print("OpenXR: Refresh rate reported as ", str(current_refresh_rate))
+	else:
+		print("OpenXR: No refresh rate given by XR runtime")
+
+	# See if we have a better refresh rate available
+	var new_rate = current_refresh_rate
+	var available_rates : Array = xr_interface.get_available_display_refresh_rates()
+	if available_rates.size() == 0:
+		print("OpenXR: Target does not support refresh rate extension")
+	elif available_rates.size() == 1:
+		# Only one available, so use it
+		new_rate = available_rates[0]
+	else:
+		for rate in available_rates:
+			if rate > new_rate and rate < maximum_refresh_rate:
+				new_rate = rate
+
+	# Did we find a better rate?
+	if current_refresh_rate != new_rate:
+		print("OpenXR: Setting refresh rate to ", str(new_rate))
+		xr_interface.set_display_refresh_rate(new_rate)
+		current_refresh_rate = new_rate
+
+	# Now match our physics rate
+	Engine.physics_ticks_per_second = current_refresh_rate
+
+
+# Handle OpenXR visible state
+func _on_openxr_visible_state() -> void:
+	# We always pass this state at startup,
+	# but the second time we get this it means our player took off their headset
+	if xr_is_focussed:
+		print("OpenXR lost focus")
+
+		xr_is_focussed = false
+
+		# pause our game
+		process_mode = Node.PROCESS_MODE_DISABLED
+
+		emit_signal("focus_lost")
+
+
+# Handle OpenXR focused state
+func _on_openxr_focused_state() -> void:
+	print("OpenXR gained focus")
+	xr_is_focussed = true
+
+	# unpause our game
+	process_mode = Node.PROCESS_MODE_INHERIT
+
+	emit_signal("focus_gained")
+
+# Handle OpenXR stopping state
+func _on_openxr_stopping() -> void:
+	# Our session is being stopped.
+	print("OpenXR is stopping")
+
+# Handle OpenXR pose recentered signal
+func _on_openxr_pose_recentered() -> void:
+	# User recentered view, we have to react to this by recentering the view.
+	# This is game implementation dependent.
+	emit_signal("pose_recentered")

+ 83 - 0
xr/openxr_origin_centric_movement/world.tscn

@@ -0,0 +1,83 @@
+[gd_scene load_steps=10 format=3 uid="uid://bymbq2ruecbhn"]
+
+[ext_resource type="Texture2D" uid="uid://rek0t7kubpx4" path="res://assets/pattern.png" id="1_yguss"]
+[ext_resource type="PackedScene" uid="uid://dpn6187qjqo75" path="res://objects/wall.tscn" id="2_350v7"]
+[ext_resource type="PackedScene" uid="uid://c3wwlb4huytmt" path="res://objects/ramp.tscn" id="3_0sele"]
+
+[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_4mshr"]
+sky_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)
+ground_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)
+
+[sub_resource type="Sky" id="Sky_kwq44"]
+sky_material = SubResource("ProceduralSkyMaterial_4mshr")
+
+[sub_resource type="Environment" id="Environment_pcm4o"]
+background_mode = 2
+sky = SubResource("Sky_kwq44")
+tonemap_mode = 2
+
+[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_xavpm"]
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_0i7rc"]
+albedo_color = Color(0.368627, 1, 0.4, 1)
+albedo_texture = ExtResource("1_yguss")
+uv1_scale = Vector3(10, 10, 10)
+
+[sub_resource type="PlaneMesh" id="PlaneMesh_dxdgs"]
+material = SubResource("StandardMaterial3D_0i7rc")
+size = Vector2(100, 100)
+
+[node name="World" type="Node3D"]
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(-0.866025, -0.433013, 0.25, 0, 0.5, 0.866025, -0.5, 0.75, -0.433013, 0, 5, 0)
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
+environment = SubResource("Environment_pcm4o")
+
+[node name="Floor" type="StaticBody3D" parent="."]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor"]
+shape = SubResource("WorldBoundaryShape3D_xavpm")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor"]
+mesh = SubResource("PlaneMesh_dxdgs")
+
+[node name="Wall01" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -8)
+
+[node name="Wall02" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 1.5, -8)
+
+[node name="Wall03" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0.707107, 0, 0.707107, 0, 1, 0, -0.707107, 0, 0.707107, -9.24219, 1.5, -6.24219)
+
+[node name="Wall04" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0.707107, 0, 0.707107, 0, 1, 0, -0.707107, 0, 0.707107, -4, 1.5, 8)
+
+[node name="Wall05" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(-0.707107, 0, 0.707107, 0, 1, 0, -0.707107, 0, -0.707107, 4, 1.5, 8)
+
+[node name="Wall06" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0.707107, 0, 0.707107, 0, 1, 0, -0.707107, 0, 0.707107, 12, 1.5, 8)
+
+[node name="Wall07" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 8, 1.5, -10)
+
+[node name="Wall08" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 8, 1.5, -15)
+
+[node name="Wall10" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, -11, 1.5, -2)
+
+[node name="Wall09" parent="." instance=ExtResource("2_350v7")]
+transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 8, 1.5, -20)
+
+[node name="Ramp01" parent="." instance=ExtResource("3_0sele")]
+transform = Transform3D(-4.2222e-08, 1.13133e-08, 1, 0.258819, 0.965926, 0, -0.965926, 0.258819, -4.37114e-08, 5, 0, -9)
+
+[node name="Ramp02" parent="." instance=ExtResource("3_0sele")]
+transform = Transform3D(8.9407e-08, -3.72529e-08, -1, 0.258819, 0.965926, -1.10534e-08, 0.965926, -0.258819, 2.98023e-08, 5, 0, -18.5646)
+
+[node name="Ramp03" parent="." instance=ExtResource("3_0sele")]
+transform = Transform3D(8.9407e-08, -3.72529e-08, -1, 0, 1, -1.83902e-08, 1, -2.98023e-08, 2.5926e-08, 5, 0.633461, -13.786)