diff --git a/xr/openxr_spectator_view/.gitattributes b/xr/openxr_spectator_view/.gitattributes new file mode 100644 index 0000000000..8ad74f78d9 --- /dev/null +++ b/xr/openxr_spectator_view/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/xr/openxr_spectator_view/.gitignore b/xr/openxr_spectator_view/.gitignore new file mode 100644 index 0000000000..4709183670 --- /dev/null +++ b/xr/openxr_spectator_view/.gitignore @@ -0,0 +1,2 @@ +# Godot 4+ specific ignores +.godot/ diff --git a/xr/openxr_spectator_view/README.md b/xr/openxr_spectator_view/README.md new file mode 100644 index 0000000000..86c386e7a5 --- /dev/null +++ b/xr/openxr_spectator_view/README.md @@ -0,0 +1,74 @@ +# XR spectator view demo + +This is a demo for an OpenXR project where the player sees a different view inside of the headset +compared to what a spectator sees on screen. +When deployed to a standalone XR device, only the player environment is exported. + +Language: GDScript + +Renderer: Compatibility + +Check out this demo on the asset library: https://godotengine.org/asset-library/asset/???? + +## How does it work? + +The VR game itself is contained within the `main.tscn` scene. This is similar to the other XR demos found on this repo. +This demo has a bare bones example as to not distract from the solution we're presenting here. + +When run on standalone VR headsets, that scene is loaded. +![Project setup](screenshots/project_setup_main_scene.png) + +When run on desktop, we load the `spectator.tscn` scene instead (used to be called `construct.tscn`). This scene has the `main.tscn` scene as a child of a +`SubViewport` which will be used to output the render result to the headset. + +![Construct scene](screenshots/construct_scene.png) + +The spectator scene also contains a `SubviewportContainer` with a `SubViewport` that renders the output that the user +sees on the desktop screen. +By default this will show a 3rd person camera that shows our player. + +We've also configured our visual layers as follows: +1. Layer 1 is visible both inside of the headset and by the 3rd person camera. +2. Layer 2 is only visible inside of the headset. +3. Layer 3 is only visible in the 3rd person camera. +This is used to render the "head" of the player in spectator view only. + +Finally, a dropdown also allows us to switch to: +- show the 3rd person camera view, +- show a 1st person camera view but with a stabilized camera, +- showing either the left eye or right eye result the player is seeing. + +## Tracked camera + +There is also an option in the demo to enable camera tracking. +This is currently only supported on SteamVR together with a properly configured HTC Vive Tracker. + +When properly setup, this allows you to use a Vive tracker to position the 3rd person camera. +Attaching the Vive tracker to a physical camera, and setting the correct offset would allow +implementation of mixed reality capture by combining the 3rd person render result, +with a green screened captured camera. + +## 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 is only one action needed for this example: +- hand_pose is used to position the XR controllers + +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 is specifically designed 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 + +This project also shows how deploying to standalone skips the spectator view option. +You must install the Android build templates and [OpenXR vendors plugin](https://github.com/GodotVR/godot_openxr_vendors/releases) 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/spectator_view_demo.png) diff --git a/xr/openxr_spectator_view/assets/pattern.png b/xr/openxr_spectator_view/assets/pattern.png new file mode 100644 index 0000000000..8bf420b0d5 Binary files /dev/null and b/xr/openxr_spectator_view/assets/pattern.png differ diff --git a/xr/openxr_spectator_view/assets/pattern.png.import b/xr/openxr_spectator_view/assets/pattern.png.import new file mode 100644 index 0000000000..8dfd0a82bb --- /dev/null +++ b/xr/openxr_spectator_view/assets/pattern.png.import @@ -0,0 +1,41 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://rek0t7kubpx4" +path.s3tc="res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://assets/pattern.png" +dest_files=["res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/xr/openxr_spectator_view/icon.svg b/xr/openxr_spectator_view/icon.svg new file mode 100644 index 0000000000..3fe4f4ae8c --- /dev/null +++ b/xr/openxr_spectator_view/icon.svg @@ -0,0 +1 @@ + diff --git a/xr/openxr_spectator_view/icon.svg.import b/xr/openxr_spectator_view/icon.svg.import new file mode 100644 index 0000000000..c81c687abc --- /dev/null +++ b/xr/openxr_spectator_view/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cqneypbjryrwv" +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/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/xr/openxr_spectator_view/main.gd b/xr/openxr_spectator_view/main.gd new file mode 100644 index 0000000000..9e4b42999f --- /dev/null +++ b/xr/openxr_spectator_view/main.gd @@ -0,0 +1,9 @@ +extends "res://start_vr.gd" + +@export var tracked_camera : Node3D: + set(value): + tracked_camera = value + if tracked_camera: + %CameraRemoteTransform3D.remote_path = tracked_camera.get_path() + else: + %CameraRemoteTransform3D.remote_path = NodePath() diff --git a/xr/openxr_spectator_view/main.gd.uid b/xr/openxr_spectator_view/main.gd.uid new file mode 100644 index 0000000000..bca23702ed --- /dev/null +++ b/xr/openxr_spectator_view/main.gd.uid @@ -0,0 +1 @@ +uid://dknfswmyiegk8 diff --git a/xr/openxr_spectator_view/main.tscn b/xr/openxr_spectator_view/main.tscn new file mode 100644 index 0000000000..21501afc0d --- /dev/null +++ b/xr/openxr_spectator_view/main.tscn @@ -0,0 +1,52 @@ +[gd_scene load_steps=5 format=3 uid="uid://cn6s3kxlkt6ml"] + +[ext_resource type="Script" path="res://main.gd" id="1_2eojn"] +[ext_resource type="PackedScene" uid="uid://ckuw0ps7vjw7e" path="res://world/world.tscn" id="2_j3t0x"] + +[sub_resource type="SphereMesh" id="SphereMesh_b2416"] +radius = 0.1 +height = 0.2 + +[sub_resource type="BoxMesh" id="BoxMesh_go2t1"] +size = Vector3(0.1, 0.1, 0.1) + +[node name="Main" type="Node3D"] +script = ExtResource("1_2eojn") +maximum_refresh_rate = 120 + +[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.8, 0) +cull_mask = 1048571 + +[node name="PlaceholderHead" type="MeshInstance3D" parent="XROrigin3D/XRCamera3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0335748, 0.0557129) +layers = 4 +mesh = SubResource("SphereMesh_b2416") + +[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 = &"hand_pose" + +[node name="PlaceholderHand" type="MeshInstance3D" parent="XROrigin3D/LeftHand"] +mesh = SubResource("BoxMesh_go2t1") + +[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 = &"hand_pose" + +[node name="PlaceholderHand" type="MeshInstance3D" parent="XROrigin3D/RightHand"] +mesh = SubResource("BoxMesh_go2t1") +skeleton = NodePath("../../LeftHand") + +[node name="CameraTracker" type="XRController3D" parent="XROrigin3D"] +tracker = &"/user/vive_tracker_htcx/role/camera" +pose = &"camera_pose" + +[node name="CameraRemoteTransform3D" type="RemoteTransform3D" parent="XROrigin3D/CameraTracker"] +unique_name_in_owner = true + +[node name="World" parent="." instance=ExtResource("2_j3t0x")] diff --git a/xr/openxr_spectator_view/openxr_action_map.tres b/xr/openxr_spectator_view/openxr_action_map.tres new file mode 100644 index 0000000000..bcb9c46b6c --- /dev/null +++ b/xr/openxr_spectator_view/openxr_action_map.tres @@ -0,0 +1,102 @@ +[gd_resource type="OpenXRActionMap" load_steps=24 format=3 uid="uid://dhxuabt5tjwbt"] + +[sub_resource type="OpenXRAction" id="OpenXRAction_bmp3j"] +resource_name = "hand_pose" +localized_name = "Hand pose" +action_type = 3 +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", "/user/eyes_ext") + +[sub_resource type="OpenXRAction" id="OpenXRAction_ei2gi"] +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="OpenXRActionSet" id="OpenXRActionSet_43ynn"] +resource_name = "godot" +localized_name = "Godot action set" +actions = [SubResource("OpenXRAction_bmp3j"), SubResource("OpenXRAction_ei2gi")] + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_6ivru"] +action = SubResource("OpenXRAction_bmp3j") +binding_path = "/user/hand/left/input/aim/pose" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_vfhwq"] +action = SubResource("OpenXRAction_bmp3j") +binding_path = "/user/hand/right/input/aim/pose" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_5w03k"] +action = SubResource("OpenXRAction_ei2gi") +binding_path = "/user/hand/left/output/haptic" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_typ1r"] +action = SubResource("OpenXRAction_ei2gi") +binding_path = "/user/hand/right/output/haptic" + +[sub_resource type="OpenXRInteractionProfile" id="OpenXRInteractionProfile_3e47y"] +interaction_profile_path = "/interaction_profiles/khr/simple_controller" +bindings = [SubResource("OpenXRIPBinding_6ivru"), SubResource("OpenXRIPBinding_vfhwq"), SubResource("OpenXRIPBinding_5w03k"), SubResource("OpenXRIPBinding_typ1r")] + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_clvbf"] +action = SubResource("OpenXRAction_bmp3j") +binding_path = "/user/hand/left/input/aim/pose" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_5bppb"] +action = SubResource("OpenXRAction_bmp3j") +binding_path = "/user/hand/right/input/aim/pose" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_3k6la"] +action = SubResource("OpenXRAction_ei2gi") +binding_path = "/user/hand/left/output/haptic" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_i8esw"] +action = SubResource("OpenXRAction_ei2gi") +binding_path = "/user/hand/right/output/haptic" + +[sub_resource type="OpenXRInteractionProfile" id="OpenXRInteractionProfile_ucbdf"] +interaction_profile_path = "/interaction_profiles/oculus/touch_controller" +bindings = [SubResource("OpenXRIPBinding_clvbf"), SubResource("OpenXRIPBinding_5bppb"), SubResource("OpenXRIPBinding_3k6la"), SubResource("OpenXRIPBinding_i8esw")] + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_um1hv"] +action = SubResource("OpenXRAction_bmp3j") +binding_path = "/user/hand/left/input/aim/pose" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_oqnsu"] +action = SubResource("OpenXRAction_bmp3j") +binding_path = "/user/hand/right/input/aim/pose" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_r5bl7"] +action = SubResource("OpenXRAction_ei2gi") +binding_path = "/user/hand/left/output/haptic" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_ytptc"] +action = SubResource("OpenXRAction_ei2gi") +binding_path = "/user/hand/right/output/haptic" + +[sub_resource type="OpenXRInteractionProfile" id="OpenXRInteractionProfile_oejwx"] +interaction_profile_path = "/interaction_profiles/bytedance/pico4_controller" +bindings = [SubResource("OpenXRIPBinding_um1hv"), SubResource("OpenXRIPBinding_oqnsu"), SubResource("OpenXRIPBinding_r5bl7"), SubResource("OpenXRIPBinding_ytptc")] + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_iphn4"] +action = SubResource("OpenXRAction_bmp3j") +binding_path = "/user/hand/left/input/aim/pose" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_3p2as"] +action = SubResource("OpenXRAction_bmp3j") +binding_path = "/user/hand/right/input/aim/pose" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_wdehm"] +action = SubResource("OpenXRAction_ei2gi") +binding_path = "/user/hand/left/output/haptic" + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_clfly"] +action = SubResource("OpenXRAction_ei2gi") +binding_path = "/user/hand/right/output/haptic" + +[sub_resource type="OpenXRInteractionProfile" id="OpenXRInteractionProfile_ggrkr"] +interaction_profile_path = "/interaction_profiles/valve/index_controller" +bindings = [SubResource("OpenXRIPBinding_iphn4"), SubResource("OpenXRIPBinding_3p2as"), SubResource("OpenXRIPBinding_wdehm"), SubResource("OpenXRIPBinding_clfly")] + +[resource] +action_sets = [SubResource("OpenXRActionSet_43ynn")] +interaction_profiles = [SubResource("OpenXRInteractionProfile_3e47y"), SubResource("OpenXRInteractionProfile_ucbdf"), SubResource("OpenXRInteractionProfile_oejwx"), SubResource("OpenXRInteractionProfile_ggrkr")] diff --git a/xr/openxr_spectator_view/openxr_spectator_view_demo.zip b/xr/openxr_spectator_view/openxr_spectator_view_demo.zip new file mode 100644 index 0000000000..8f3438f37e Binary files /dev/null and b/xr/openxr_spectator_view/openxr_spectator_view_demo.zip differ diff --git a/xr/openxr_spectator_view/project.godot b/xr/openxr_spectator_view/project.godot new file mode 100644 index 0000000000..5dda6b8e3a --- /dev/null +++ b/xr/openxr_spectator_view/project.godot @@ -0,0 +1,36 @@ +; 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 Spectator View Demo" +run/main_scene="res://spectator.tscn" +config/features=PackedStringArray("4.5", "Mobile") +config/icon="res://icon.svg" +run/main_scene.android="res://main.tscn" + +[layer_names] + +3d_render/layer_1="Default" +3d_render/layer_2="VR only" +3d_render/layer_3="Spectator only" + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" + +[xr] + +openxr/enabled=true +openxr/reference_space=2 +openxr/foveation_level=3 +openxr/foveation_dynamic=true +shaders/enabled=true diff --git a/xr/openxr_spectator_view/screenshots/construct_scene.png b/xr/openxr_spectator_view/screenshots/construct_scene.png new file mode 100644 index 0000000000..e80612447a Binary files /dev/null and b/xr/openxr_spectator_view/screenshots/construct_scene.png differ diff --git a/xr/openxr_spectator_view/screenshots/construct_scene.png.import b/xr/openxr_spectator_view/screenshots/construct_scene.png.import new file mode 100644 index 0000000000..ad6f95722e --- /dev/null +++ b/xr/openxr_spectator_view/screenshots/construct_scene.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://4n4djsp5wkx6" +path="res://.godot/imported/construct_scene.png-b40474c81a420909f762371c9d92bd2c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://screenshots/construct_scene.png" +dest_files=["res://.godot/imported/construct_scene.png-b40474c81a420909f762371c9d92bd2c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/xr/openxr_spectator_view/screenshots/project_setup_main_scene.png b/xr/openxr_spectator_view/screenshots/project_setup_main_scene.png new file mode 100644 index 0000000000..0ce9f6e938 Binary files /dev/null and b/xr/openxr_spectator_view/screenshots/project_setup_main_scene.png differ diff --git a/xr/openxr_spectator_view/screenshots/project_setup_main_scene.png.import b/xr/openxr_spectator_view/screenshots/project_setup_main_scene.png.import new file mode 100644 index 0000000000..2a3dd695af --- /dev/null +++ b/xr/openxr_spectator_view/screenshots/project_setup_main_scene.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dgkva8hbcd64c" +path="res://.godot/imported/project_setup_main_scene.png-f2d83dfe429af2460fa447bcc5d0e660.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://screenshots/project_setup_main_scene.png" +dest_files=["res://.godot/imported/project_setup_main_scene.png-f2d83dfe429af2460fa447bcc5d0e660.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/xr/openxr_spectator_view/screenshots/spectator_view_demo.png b/xr/openxr_spectator_view/screenshots/spectator_view_demo.png new file mode 100644 index 0000000000..d32915337e Binary files /dev/null and b/xr/openxr_spectator_view/screenshots/spectator_view_demo.png differ diff --git a/xr/openxr_spectator_view/screenshots/spectator_view_demo.png.import b/xr/openxr_spectator_view/screenshots/spectator_view_demo.png.import new file mode 100644 index 0000000000..1a0f483881 --- /dev/null +++ b/xr/openxr_spectator_view/screenshots/spectator_view_demo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://df68vh8mudk6y" +path="res://.godot/imported/spectator_view_demo.png-a41f97f6853703bdb6be6e05bfdf887b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://screenshots/spectator_view_demo.png" +dest_files=["res://.godot/imported/spectator_view_demo.png-a41f97f6853703bdb6be6e05bfdf887b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/xr/openxr_spectator_view/shaders/eye_output.gdshader b/xr/openxr_spectator_view/shaders/eye_output.gdshader new file mode 100644 index 0000000000..17a97b6357 --- /dev/null +++ b/xr/openxr_spectator_view/shaders/eye_output.gdshader @@ -0,0 +1,24 @@ +shader_type canvas_item; + +uniform sampler2DArray xr_texture : source_color; +uniform float layer; + +vec3 linear_to_srgb(vec3 color) { + // Approximation from http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html + return max(vec3(1.055) * pow(color, vec3(0.416666667)) - vec3(0.055), vec3(0.0)); +} + +void vertex() { +#if CURRENT_RENDERER == RENDERER_COMPATIBILITY + UV.y = 1.0 - UV.y; +#endif +} + +void fragment() { + vec3 color = texture(xr_texture, vec3(UV, layer)).rgb; +#if CURRENT_RENDERER == RENDERER_COMPATIBILITY + color = linear_to_srgb(color); +#endif + COLOR.rgb = color; + COLOR.a = 1.0; +} diff --git a/xr/openxr_spectator_view/shaders/eye_output.gdshader.uid b/xr/openxr_spectator_view/shaders/eye_output.gdshader.uid new file mode 100644 index 0000000000..25a73b222a --- /dev/null +++ b/xr/openxr_spectator_view/shaders/eye_output.gdshader.uid @@ -0,0 +1 @@ +uid://cp4n0jfpkl816 diff --git a/xr/openxr_spectator_view/shaders/textures.gdshader b/xr/openxr_spectator_view/shaders/textures.gdshader new file mode 100644 index 0000000000..3d6faf0d03 --- /dev/null +++ b/xr/openxr_spectator_view/shaders/textures.gdshader @@ -0,0 +1,26 @@ +// NOTE: Shader automatically converted from Godot Engine 4.2.2.stable's StandardMaterial3D. + +shader_type spatial; +render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx; + +uniform vec4 albedo : source_color = vec4(1.0, 1.0, 1.0, 1.0); +uniform sampler2D texture_albedo : source_color,filter_linear_mipmap,repeat_enable; +uniform float roughness : hint_range(0,1) = 1.0; +uniform float specular : hint_range(0,1) = 0.5; +uniform float metallic : hint_range(0,1) = 0.0; +uniform vec3 uv1_scale = vec3(1.0, 1.0, 1.0); +uniform vec3 uv1_offset = vec3(0.0, 0.0, 0.0); + + +void vertex() { + UV=UV*uv1_scale.xy+uv1_offset.xy; +} + + +void fragment() { + vec4 albedo_tex = texture(texture_albedo,UV); + ALBEDO = albedo.rgb * albedo_tex.rgb; + METALLIC = metallic; + ROUGHNESS = roughness; + SPECULAR = specular; +} diff --git a/xr/openxr_spectator_view/shaders/textures.gdshader.uid b/xr/openxr_spectator_view/shaders/textures.gdshader.uid new file mode 100644 index 0000000000..04570b7fbd --- /dev/null +++ b/xr/openxr_spectator_view/shaders/textures.gdshader.uid @@ -0,0 +1 @@ +uid://j8dcfmfbgojp diff --git a/xr/openxr_spectator_view/spectator.gd b/xr/openxr_spectator_view/spectator.gd new file mode 100644 index 0000000000..0f74795080 --- /dev/null +++ b/xr/openxr_spectator_view/spectator.gd @@ -0,0 +1,93 @@ +extends Node2D + +var vr_render_size : Vector2 = Vector2() +var window_size : Vector2 = Vector2() +var hmd_view_material : ShaderMaterial + +@onready var tracked_camera_original_transform : Transform3D = %TrackedCamera.global_transform + +func _reposition_texture_rect(): + if window_size != Vector2() and vr_render_size != Vector2(): + var size = vr_render_size * %ZoomSlider.value + %HMDView.size = size + %HMDView.position = (window_size - size) * 0.5 + + +func _on_size_changed(): + # Get our hmd view material + hmd_view_material = %HMDView.material + + # Get the new size of our window + window_size = get_tree().get_root().size + + # Set our container to full screen, this should update our viewport + $SubViewportContainer.size = window_size + + _reposition_texture_rect() + + +# Called when the node enters the scene tree for the first time. +func _ready(): + # Get a signal when our window size changes + get_tree().get_root().size_changed.connect(_on_size_changed) + + # Call atleast once to initialise + _on_size_changed() + + # Select our default view mode + _on_spectator_view_item_selected(%SpectatorView.selected) + + # Setup our tracked camera + _on_track_camera_toggled(%TrackCamera.button_pressed) + +func _on_spectator_view_item_selected(index): + match index: + 0: # Spectator camera + %DesktopSubViewport.disable_3d = false + %SpectatorCamera.current = true + %HMDView.visible = false + %TrackCamera.visible = true + %ZoomSlider.visible = false + 1: # Stabilized + %DesktopSubViewport.disable_3d = false + %StabilizedCamera.current = true + %HMDView.visible = false + %TrackCamera.visible = false + %ZoomSlider.visible = false + 2: # Left eye + %DesktopSubViewport.disable_3d = true + %HMDView.visible = true + %TrackCamera.visible = false + %ZoomSlider.visible = true + if hmd_view_material: + var vp_texture = $VRSubViewport.get_texture() + hmd_view_material.set_shader_parameter("xr_texture", vp_texture) + hmd_view_material.set_shader_parameter("layer", 0) + 3: # Right eye + %DesktopSubViewport.disable_3d = true + %HMDView.visible = true + %TrackCamera.visible = false + %ZoomSlider.visible = true + if hmd_view_material: + var vp_texture = $VRSubViewport.get_texture() + hmd_view_material.set_shader_parameter("xr_texture", vp_texture) + hmd_view_material.set_shader_parameter("layer", 1) + + +func _on_main_session_begun(): + vr_render_size = %Main.get_vr_render_size() + _reposition_texture_rect() + + +func _on_track_camera_toggled(toggled_on): + # TODO should detect if we have camera tracking available + + if toggled_on: + %Main.tracked_camera = %TrackedCamera + else: + %Main.tracked_camera = null + %TrackedCamera.global_transform = tracked_camera_original_transform + + +func _on_zoom_slider_value_changed(value): + _reposition_texture_rect() diff --git a/xr/openxr_spectator_view/spectator.gd.uid b/xr/openxr_spectator_view/spectator.gd.uid new file mode 100644 index 0000000000..1870453528 --- /dev/null +++ b/xr/openxr_spectator_view/spectator.gd.uid @@ -0,0 +1 @@ +uid://wq3v75egxo18 diff --git a/xr/openxr_spectator_view/spectator.tscn b/xr/openxr_spectator_view/spectator.tscn new file mode 100644 index 0000000000..e9d2d40748 --- /dev/null +++ b/xr/openxr_spectator_view/spectator.tscn @@ -0,0 +1,89 @@ +[gd_scene load_steps=6 format=3 uid="uid://qb615fcqh8x0"] + +[ext_resource type="Script" uid="uid://wq3v75egxo18" path="res://spectator.gd" id="1_ktigi"] +[ext_resource type="PackedScene" uid="uid://cn6s3kxlkt6ml" path="res://main.tscn" id="2_1qy5m"] +[ext_resource type="Shader" uid="uid://cp4n0jfpkl816" path="res://shaders/eye_output.gdshader" id="2_ygl07"] +[ext_resource type="Script" uid="uid://b2qld1m6kikku" path="res://stabilized_camera.gd" id="3_6skb3"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_wue56"] +shader = ExtResource("2_ygl07") +shader_parameter/layer = 0.0 + +[node name="Spectator" type="Node2D"] +script = ExtResource("1_ktigi") + +[node name="SubViewportContainer" type="SubViewportContainer" parent="."] +custom_minimum_size = Vector2(512, 512) +offset_right = 512.0 +offset_bottom = 512.0 +stretch = true + +[node name="DesktopSubViewport" type="SubViewport" parent="SubViewportContainer"] +unique_name_in_owner = true +handle_input_locally = false +render_target_update_mode = 4 + +[node name="HMDView" type="ColorRect" parent="SubViewportContainer/DesktopSubViewport"] +unique_name_in_owner = true +visible = false +material = SubResource("ShaderMaterial_wue56") +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="UI" type="VBoxContainer" parent="SubViewportContainer/DesktopSubViewport"] +offset_left = 10.0 +offset_top = 10.0 +offset_right = 194.0 +offset_bottom = 76.0 + +[node name="SpectatorView" type="OptionButton" parent="SubViewportContainer/DesktopSubViewport/UI"] +unique_name_in_owner = true +layout_mode = 2 +selected = 0 +item_count = 4 +popup/item_0/text = "Spectator Camera" +popup/item_0/id = 0 +popup/item_1/text = "Stabilized Camera" +popup/item_1/id = 1 +popup/item_2/text = "Left eye" +popup/item_2/id = 2 +popup/item_3/text = "Right eye" +popup/item_3/id = 3 + +[node name="TrackCamera" type="CheckBox" parent="SubViewportContainer/DesktopSubViewport/UI"] +unique_name_in_owner = true +layout_mode = 2 +text = "Track Camera" + +[node name="ZoomSlider" type="HSlider" parent="SubViewportContainer/DesktopSubViewport/UI"] +unique_name_in_owner = true +layout_mode = 2 +min_value = 0.1 +max_value = 5.0 +step = 0.1 +value = 1.0 + +[node name="TrackedCamera" type="Node3D" parent="SubViewportContainer/DesktopSubViewport"] +unique_name_in_owner = true +transform = Transform3D(0.428162, 0, -0.903702, 0, 1, 0, 0.903702, 0, 0.428162, -4.12546, 1.30569, 1.60112) + +[node name="SpectatorCamera" type="Camera3D" parent="SubViewportContainer/DesktopSubViewport/TrackedCamera"] +unique_name_in_owner = true +cull_mask = 1048573 + +[node name="StabilizedCamera" type="Camera3D" parent="SubViewportContainer/DesktopSubViewport" node_paths=PackedStringArray("xr_camera")] +unique_name_in_owner = true +script = ExtResource("3_6skb3") +xr_camera = NodePath("../../../VRSubViewport/Main/XROrigin3D/XRCamera3D") + +[node name="VRSubViewport" type="SubViewport" parent="."] + +[node name="Main" parent="VRSubViewport" instance=ExtResource("2_1qy5m")] +unique_name_in_owner = true + +[connection signal="item_selected" from="SubViewportContainer/DesktopSubViewport/UI/SpectatorView" to="." method="_on_spectator_view_item_selected"] +[connection signal="toggled" from="SubViewportContainer/DesktopSubViewport/UI/TrackCamera" to="." method="_on_track_camera_toggled"] +[connection signal="value_changed" from="SubViewportContainer/DesktopSubViewport/UI/ZoomSlider" to="." method="_on_zoom_slider_value_changed"] +[connection signal="session_begun" from="VRSubViewport/Main" to="." method="_on_main_session_begun"] + +[editable path="VRSubViewport/Main"] diff --git a/xr/openxr_spectator_view/stabilized_camera.gd b/xr/openxr_spectator_view/stabilized_camera.gd new file mode 100644 index 0000000000..bdf3696266 --- /dev/null +++ b/xr/openxr_spectator_view/stabilized_camera.gd @@ -0,0 +1,48 @@ +extends Camera3D + +## This script allows us to place a camera in the world that provides +## a spectator view where we see what the player sees. +## The positioning is smoothed to create a steadycam type of effect. + +## Specify the XRCamera3D node we're replicating the view for. +@export var xr_camera : XRCamera3D + +## Remove pitch from our camera orientation? +@export var remove_pitch : bool = true + +## Speed at which we lerp, the lower the more stable our camera gets +## at the cost of introducing more lag. +@export_range(1.0, 60.0, 1.0) var lerp_speed : float = 10.0 + +var prev_camera_transform : Transform3D +var first_frame : bool = true + +func _process(delta): + if current and xr_camera: + # Note, we apply our smoothing in the local space of our XROrigin3D node. + # This way we don't apply smoothing when game logic moves the XROrigin3D node. + # We only apply smoothing to the physical movement of the player. + # This makes the spectator view easier to watch by a 3rd party. + + # We smooth out the camera. + var camera_transform : Transform3D = xr_camera.transform + + if remove_pitch: + # Remove pitch from camera. + camera_transform.basis = Basis.looking_at(camera_transform.basis.z, Vector3.UP, true) + + if first_frame: + first_frame = false + else: + # We (s)lerp our physical camera movement to smooth things out + camera_transform.basis = prev_camera_transform.basis.slerp(camera_transform.basis, delta * lerp_speed) + camera_transform.origin = prev_camera_transform.origin.lerp(camera_transform.origin, delta * lerp_speed) + + # Update our first person view. + global_transform = xr_camera.get_parent().global_transform * camera_transform + + # Store camera transform for next frame + prev_camera_transform = camera_transform + else: + # Make sure next time we run through this we don't lerp. + first_frame = true diff --git a/xr/openxr_spectator_view/stabilized_camera.gd.uid b/xr/openxr_spectator_view/stabilized_camera.gd.uid new file mode 100644 index 0000000000..bcdeb7bac0 --- /dev/null +++ b/xr/openxr_spectator_view/stabilized_camera.gd.uid @@ -0,0 +1 @@ +uid://b2qld1m6kikku diff --git a/xr/openxr_spectator_view/start_vr.gd b/xr/openxr_spectator_view/start_vr.gd new file mode 100644 index 0000000000..40617bd654 --- /dev/null +++ b/xr/openxr_spectator_view/start_vr.gd @@ -0,0 +1,120 @@ +extends Node3D + +signal session_begun +signal focus_lost +signal focus_gained +signal pose_recentered + +@export var maximum_refresh_rate: int = 90 + +var xr_interface: OpenXRInterface +var xr_is_focused := false + + +func get_vr_render_size(): + if xr_interface and xr_interface.is_initialized(): + return xr_interface.get_render_target_size() + else: + return Vector2(0.0, 0.0) + + +func _ready() -> void: + 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, as V-Sync is handled by OpenXR. + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) + + # Enable variable rate shading. + if RenderingServer.get_rendering_device(): + vp.vrs_mode = Viewport.VRS_XR + elif int(ProjectSettings.get_setting("xr/openxr/foveation_level")) == 0: + push_warning("OpenXR: Recommend setting Foveation level to High in Project Settings") + + # Connect the OpenXR events. + xr_interface.session_begun.connect(_on_openxr_session_begun) + xr_interface.session_visible.connect(_on_openxr_visible_state) + xr_interface.session_focussed.connect(_on_openxr_focused_state) + xr_interface.session_stopping.connect(_on_openxr_stopping) + xr_interface.pose_recentered.connect(_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.is_empty(): + 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. This is currently needed to avoid jittering, + # due to physics interpolation not being used. + Engine.physics_ticks_per_second = roundi(current_refresh_rate) + + session_begun.emit() + + +# 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_focused: + print("OpenXR lost focus") + + xr_is_focused = false + + # Pause our game. + process_mode = Node.PROCESS_MODE_DISABLED + + focus_lost.emit() + + +# Handle OpenXR focused state +func _on_openxr_focused_state() -> void: + print("OpenXR gained focus") + xr_is_focused = true + + # Unpause our game. + process_mode = Node.PROCESS_MODE_INHERIT + + focus_gained.emit() + +# 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. + pose_recentered.emit() diff --git a/xr/openxr_spectator_view/start_vr.gd.uid b/xr/openxr_spectator_view/start_vr.gd.uid new file mode 100644 index 0000000000..ca7f1e0c5f --- /dev/null +++ b/xr/openxr_spectator_view/start_vr.gd.uid @@ -0,0 +1 @@ +uid://i1xei4qhq1xq diff --git a/xr/openxr_spectator_view/world/floor.tscn b/xr/openxr_spectator_view/world/floor.tscn new file mode 100644 index 0000000000..e08c21897e --- /dev/null +++ b/xr/openxr_spectator_view/world/floor.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=6 format=3 uid="uid://c18kajc4sbkqv"] + +[ext_resource type="Shader" uid="uid://j8dcfmfbgojp" path="res://shaders/textures.gdshader" id="1_slppi"] +[ext_resource type="Texture2D" uid="uid://rek0t7kubpx4" path="res://assets/pattern.png" id="2_0scsl"] + +[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_h7spe"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_v1io3"] +render_priority = 0 +shader = ExtResource("1_slppi") +shader_parameter/albedo = Color(0.14753789, 0.41322553, 0.28724656, 1) +shader_parameter/texture_albedo = ExtResource("2_0scsl") +shader_parameter/roughness = 1.0 +shader_parameter/specular = 0.5 +shader_parameter/metallic = 0.0 +shader_parameter/uv1_scale = Vector3(100, 100, 100) +shader_parameter/uv1_offset = Vector3(0, 0, 0) + +[sub_resource type="PlaneMesh" id="PlaneMesh_2x6s1"] +material = SubResource("ShaderMaterial_v1io3") +size = Vector2(1000, 1000) + +[node name="Floor" type="StaticBody3D"] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +shape = SubResource("WorldBoundaryShape3D_h7spe") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="."] +mesh = SubResource("PlaneMesh_2x6s1") diff --git a/xr/openxr_spectator_view/world/wall.tscn b/xr/openxr_spectator_view/world/wall.tscn new file mode 100644 index 0000000000..a18da8190b --- /dev/null +++ b/xr/openxr_spectator_view/world/wall.tscn @@ -0,0 +1,32 @@ +[gd_scene load_steps=6 format=3 uid="uid://dr051k6vi2hg6"] + +[ext_resource type="Shader" path="res://shaders/textures.gdshader" id="1_0276j"] +[ext_resource type="Texture2D" uid="uid://rek0t7kubpx4" path="res://assets/pattern.png" id="2_m6l35"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_5arfh"] +size = Vector3(2, 2, 0.1) + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_oy0dr"] +render_priority = 0 +shader = ExtResource("1_0276j") +shader_parameter/albedo = Color(0.442791, 0.316045, 0.911513, 1) +shader_parameter/roughness = 1.0 +shader_parameter/specular = 0.5 +shader_parameter/metallic = 0.0 +shader_parameter/uv1_scale = Vector3(1, 1, 1) +shader_parameter/uv1_offset = Vector3(0, 0, 0) +shader_parameter/texture_albedo = ExtResource("2_m6l35") + +[sub_resource type="BoxMesh" id="BoxMesh_3bsdh"] +material = SubResource("ShaderMaterial_oy0dr") +size = Vector3(2, 2, 0.1) + +[node name="Wall" type="StaticBody3D"] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("BoxShape3D_5arfh") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +mesh = SubResource("BoxMesh_3bsdh") diff --git a/xr/openxr_spectator_view/world/world.tscn b/xr/openxr_spectator_view/world/world.tscn new file mode 100644 index 0000000000..4d2bbf7633 --- /dev/null +++ b/xr/openxr_spectator_view/world/world.tscn @@ -0,0 +1,53 @@ +[gd_scene load_steps=6 format=3 uid="uid://ckuw0ps7vjw7e"] + +[ext_resource type="PackedScene" uid="uid://c18kajc4sbkqv" path="res://world/floor.tscn" id="1_ghfsf"] +[ext_resource type="PackedScene" uid="uid://dr051k6vi2hg6" path="res://world/wall.tscn" id="2_a3wlv"] + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_qamop"] +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_qs8p5"] +sky_material = SubResource("ProceduralSkyMaterial_qamop") + +[sub_resource type="Environment" id="Environment_duone"] +background_mode = 2 +sky = SubResource("Sky_qs8p5") +tonemap_mode = 2 + +[node name="World" type="Node3D"] + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_duone") + +[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, 0, 0) +shadow_enabled = true +directional_shadow_mode = 0 +directional_shadow_max_distance = 10.0 + +[node name="Floor" parent="." instance=ExtResource("1_ghfsf")] + +[node name="Wall01" parent="." instance=ExtResource("2_a3wlv")] +transform = Transform3D(0.812592, 0, -0.582832, 0, 1, 0, 0.582832, 0, 0.812592, 0, 0, -2.64147) + +[node name="Wall08" parent="." instance=ExtResource("2_a3wlv")] +transform = Transform3D(0.872956, 0, 0.487799, 0, 1, 0, -0.487799, 0, 0.872956, 1.62242, 0, -2.55675) + +[node name="Wall02" parent="." instance=ExtResource("2_a3wlv")] +transform = Transform3D(0.882078, 0, 0.471104, 0, 1, 0, -0.471104, 0, 0.882078, -3.87147, 0, -0.457518) + +[node name="Wall05" parent="." instance=ExtResource("2_a3wlv")] +transform = Transform3D(0.295525, 0, 0.955335, 0, 1, 0, -0.955335, 0, 0.295525, -2.72498, 0, -1.88187) + +[node name="Wall06" parent="." instance=ExtResource("2_a3wlv")] +transform = Transform3D(0.625502, 0, 0.780223, 0, 1, 0, -0.780223, 0, 0.625502, -1.812, 0, -3.62771) + +[node name="Wall03" parent="." instance=ExtResource("2_a3wlv")] +transform = Transform3D(-0.390571, 0, -0.920573, 0, 1, 0, 0.920573, 0, -0.390571, -0.74192, 0, 3.08859) + +[node name="Wall07" parent="." instance=ExtResource("2_a3wlv")] +transform = Transform3D(0.911001, 0, -0.412405, 0, 1, 0, 0.412405, 0, 0.911001, -0.190205, 0, 4.36401) + +[node name="Wall04" parent="." instance=ExtResource("2_a3wlv")] +transform = Transform3D(0.138931, 0, -0.990302, 0, 1, 0, 0.990302, 0, 0.138931, 2.32413, 0, 0.0712545)