diff --git a/xr/openxr_composition_layers/.gitignore b/xr/openxr_composition_layers/.gitignore new file mode 100644 index 00000000..de8bf8f8 --- /dev/null +++ b/xr/openxr_composition_layers/.gitignore @@ -0,0 +1,5 @@ +# Ignore our Android build folder, should be installed by user if needed +android/ + +# Ignore our vendors addon, users need to download the vendor plugin separate +addons/godotopenxrvendors/ diff --git a/xr/openxr_composition_layers/README.md b/xr/openxr_composition_layers/README.md new file mode 100644 index 00000000..687a4e8d --- /dev/null +++ b/xr/openxr_composition_layers/README.md @@ -0,0 +1,68 @@ +# OpenXR compositor layer demo + +This is a demo for an OpenXR project where we showcase the new compositor layer functionality. +This is a companion to the [OpenXR composition layers manual page](https://docs.godotengine.org/en/latest/tutorials/xr/openxr_composition_layers.html). + +Language: GDScript +Renderer: Compatibility +Minimum Godot Version: 4.3 + +## How does it work? + +Compositor layers allow us to present additional content on a headset outside of our normal 3D rendered results. +With XR we render our 3D image at a higher resolution after which its lens distorted before it's displayed on the headset. +This to counter the natural barrel distortion caused by the lenses in most XR headsets. + +When we look at things like rendered text or other mostly 2D elements that are presented on a virtual screen, +this causes a double whammy when it comes to sampling that data. +The subsequent quality loss often renders text unreadable or at the least ugly looking. + +It turns out however that when 2D interfaces are presented on a virtual screen in front of the user, +often as a rectangle or slightly curved screen, +that rendering this content ontop of the lens distorted 3D rendering, +and simply curving this 2D plane, +results in a high quality render. + +OpenXR supports three such shapes that when used appropriately leads to crisp 2D visuals. +This demo shows one such shape, the equirect, a curved display. + +The only downside of this approach is that compositing happens in the XR runtime, +so any spectator view shown on screen will omit these layers. + +> Note, if composition layers aren't supported by the XR runtime, +> Godot falls back to rendering the content within the normal 3D rendered result. + +## 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 three actions needed for this example: +- aim_pose is used to position the XR controllers, +- select is used as a way to interact with the UI, it reacts to the trigger, +- haptic is used to emit a pulse on the controller when the player presses the trigger. + +Aiming at the 2D UI will mimic mouse movement based on where you point. +Only one controller will interact with the UI at any given time seeing we can only mimic one mouse cursor. +You can switch between the left and right controller by pressing the trigger on the controller you wish to use. + +Seeing the simplicity of this example we only supply bindings for the simple controller. +XR runtimes should provide proper re-mapping and as support for the simple controller is mandatory when controllers are used, +this should work on any XR runtime. +On some system the simple controller is also supported with hand tracking and on those you can use a pinch gesture +(touch your thumb and index finger together) to interact with the UI. + +## 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 vendors 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](xr_composition_layer_demo.png) diff --git a/xr/openxr_composition_layers/assets/pattern.png b/xr/openxr_composition_layers/assets/pattern.png new file mode 100644 index 00000000..8bf420b0 Binary files /dev/null and b/xr/openxr_composition_layers/assets/pattern.png differ diff --git a/xr/openxr_composition_layers/assets/pattern.png.import b/xr/openxr_composition_layers/assets/pattern.png.import new file mode 100644 index 00000000..88219f68 --- /dev/null +++ b/xr/openxr_composition_layers/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 diff --git a/xr/openxr_composition_layers/cursor.gdshader b/xr/openxr_composition_layers/cursor.gdshader new file mode 100644 index 00000000..5fe61287 --- /dev/null +++ b/xr/openxr_composition_layers/cursor.gdshader @@ -0,0 +1,9 @@ +shader_type canvas_item; + +uniform vec3 color : source_color = vec3(1.0, 1.0, 1.0); + +void fragment() { + // Called for every pixel the material is visible on. + float dist = length(UV - vec2(0.5, 0.5)); + COLOR.a = 1.0 - clamp(abs(0.4 - dist)/0.1, 0.0, 1.0); +} diff --git a/xr/openxr_composition_layers/handle_pointers.gd b/xr/openxr_composition_layers/handle_pointers.gd new file mode 100644 index 00000000..fe97ed2b --- /dev/null +++ b/xr/openxr_composition_layers/handle_pointers.gd @@ -0,0 +1,80 @@ +extends OpenXRCompositionLayerEquirect + +const NO_INTERSECTION = Vector2(-1.0, -1.0) + +@export var controller : XRController3D +@export var button_action : String = "select" + +var was_pressed : bool = false +var was_intersect : Vector2 = NO_INTERSECTION + + +# Pass input events on to viewport. +func _input(event): + if not layer_viewport: + return + + if event is InputEventMouse: + # Desktop mouse events do not translate so ignore. + return + + # Anything else, just pass on! + layer_viewport.push_input(event) + + +# Convert the intersect point reurned by intersects_ray to local coords in the viewport. +func _intersect_to_viewport_pos(intersect : Vector2) -> Vector2i: + if layer_viewport and intersect != NO_INTERSECTION: + var pos : Vector2 = intersect * Vector2(layer_viewport.size) + return Vector2i(pos) + else: + return Vector2i(-1, -1) + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(_delta): + if not controller: + return + if not layer_viewport: + return + + var controller_t : Transform3D = controller.global_transform + var intersect : Vector2 = intersects_ray(controller_t.origin, -controller_t.basis.z) + + if intersect != NO_INTERSECTION: + var is_pressed : bool = controller.is_button_pressed(button_action) + + if was_intersect != NO_INTERSECTION and intersect != was_intersect: + # Pointer moved + var event : InputEventMouseMotion = InputEventMouseMotion.new() + var from : Vector2 = _intersect_to_viewport_pos(was_intersect) + var to : Vector2 = _intersect_to_viewport_pos(intersect) + if was_pressed: + event.button_mask = MOUSE_BUTTON_MASK_LEFT + event.relative = to - from + event.position = to + layer_viewport.push_input(event) + + if not is_pressed and was_pressed: + # Button was let go? + var event : InputEventMouseButton = InputEventMouseButton.new() + event.button_index = MOUSE_BUTTON_LEFT + event.pressed = false + event.position = _intersect_to_viewport_pos(intersect) + layer_viewport.push_input(event) + + elif is_pressed and not was_pressed: + # Button was pressed? + var event : InputEventMouseButton = InputEventMouseButton.new() + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = MOUSE_BUTTON_MASK_LEFT + event.pressed = true + event.position = _intersect_to_viewport_pos(intersect) + layer_viewport.push_input(event) + + was_pressed = is_pressed + was_intersect = intersect + + else: + was_pressed = false + was_intersect = NO_INTERSECTION diff --git a/xr/openxr_composition_layers/icon.svg b/xr/openxr_composition_layers/icon.svg new file mode 100644 index 00000000..3fe4f4ae --- /dev/null +++ b/xr/openxr_composition_layers/icon.svg @@ -0,0 +1 @@ + diff --git a/xr/openxr_composition_layers/icon.svg.import b/xr/openxr_composition_layers/icon.svg.import new file mode 100644 index 00000000..cc75cf79 --- /dev/null +++ b/xr/openxr_composition_layers/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bmk2i75noe1ih" +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 diff --git a/xr/openxr_composition_layers/main.gd b/xr/openxr_composition_layers/main.gd new file mode 100644 index 00000000..0cfea72b --- /dev/null +++ b/xr/openxr_composition_layers/main.gd @@ -0,0 +1,64 @@ +extends Node3D + +var tween : Tween +var active_hand : XRController3D + + +# Called when the node enters the scene tree for the first time. +func _ready(): + $XROrigin3D/LeftHand/Pointer.visible = false + $XROrigin3D/RightHand/Pointer.visible = true + active_hand = $XROrigin3D/RightHand + + +# Callback for our tween to set the energy level on our active pointer. +func _update_energy(new_value : float): + var pointer = active_hand.get_node("Pointer") + var material : ShaderMaterial = pointer.material_override + if material: + material.set_shader_parameter("energy", new_value) + + +# Start our tween to show a pulse on our click. +func _do_tween_energy(): + if tween: + tween.kill() + + tween = create_tween() + tween.tween_method(_update_energy, 5.0, 1.0, 0.5) + + +# Called if left hand trigger is pressed. +func _on_left_hand_button_pressed(action_name): + if action_name == "select": + # Make the left hand the active pointer. + $XROrigin3D/LeftHand/Pointer.visible = true + $XROrigin3D/RightHand/Pointer.visible = false + + active_hand = $XROrigin3D/LeftHand + $XROrigin3D/OpenXRCompositionLayerEquirect.controller = active_hand + + # Make a visual pulse. + _do_tween_energy() + + # And make us feel it. + # Note: frequence == 0.0 => XR runtime chooses optimal frequency for a given controller. + active_hand.trigger_haptic_pulse("haptic", 0.0, 1.0, 0.5, 0.0) + + +# Called if right hand trigger is pressed. +func _on_right_hand_button_pressed(action_name): + if action_name == "select": + # Make the right hand the active pointer. + $XROrigin3D/LeftHand/Pointer.visible = false + $XROrigin3D/RightHand/Pointer.visible = true + + active_hand = $XROrigin3D/RightHand + $XROrigin3D/OpenXRCompositionLayerEquirect.controller = active_hand + + # Make a visual pulse. + _do_tween_energy() + + # And make us feel it. + # Note: frequence == 0.0 => XR runtime chooses optimal frequency for a given controller. + active_hand.trigger_haptic_pulse("haptic", 0.0, 1.0, 0.5, 0.0) diff --git a/xr/openxr_composition_layers/main.tscn b/xr/openxr_composition_layers/main.tscn new file mode 100644 index 00000000..797884a4 --- /dev/null +++ b/xr/openxr_composition_layers/main.tscn @@ -0,0 +1,120 @@ +[gd_scene load_steps=16 format=3 uid="uid://gybusi3kmss"] + +[ext_resource type="Script" path="res://main.gd" id="1_oboy8"] +[ext_resource type="Script" path="res://start_vr.gd" id="1_xxyg6"] +[ext_resource type="PackedScene" uid="uid://cenb0bfok13vx" path="res://ui.tscn" id="2_ee2ui"] +[ext_resource type="Texture2D" uid="uid://rek0t7kubpx4" path="res://assets/pattern.png" id="3_l16dp"] +[ext_resource type="Script" path="res://handle_pointers.gd" id="4_211j6"] +[ext_resource type="PackedScene" uid="uid://cl6m21y2uldtf" path="res://pointer.tscn" id="4_qvtse"] +[ext_resource type="Shader" path="res://pointer.gdshader" id="5_gtvna"] + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_401xc"] +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_v0f0v"] +sky_material = SubResource("ProceduralSkyMaterial_401xc") + +[sub_resource type="Environment" id="Environment_niqal"] +background_mode = 2 +sky = SubResource("Sky_v0f0v") +tonemap_mode = 2 + +[sub_resource type="SphereMesh" id="SphereMesh_078nk"] +radius = 0.02 +height = 0.04 + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_j0iib"] +resource_local_to_scene = true +resource_name = "Left hand pointer material" +render_priority = 0 +shader = ExtResource("5_gtvna") +shader_parameter/color = Color(1, 0, 0, 0.5) +shader_parameter/energy = 1.0 + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_yobup"] +resource_local_to_scene = true +resource_name = "Right hand pointer material" +render_priority = 0 +shader = ExtResource("5_gtvna") +shader_parameter/color = Color(1, 0, 0, 0.5) +shader_parameter/energy = 1.0 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_2jnxs"] +albedo_color = Color(0.012593, 0.294147, 0, 1) +albedo_texture = ExtResource("3_l16dp") +uv1_scale = Vector3(100, 100, 100) + +[sub_resource type="PlaneMesh" id="PlaneMesh_leufb"] +material = SubResource("StandardMaterial3D_2jnxs") +size = Vector2(1000, 1000) +subdivide_width = 15 +subdivide_depth = 15 + +[node name="Main" type="Node3D"] +script = ExtResource("1_oboy8") + +[node name="StartVR" type="Node3D" parent="."] +script = ExtResource("1_xxyg6") + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_niqal") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(-0.866023, -0.433016, 0.250001, 0, 0.499998, 0.866027, -0.500003, 0.749999, -0.43301, 0, 0, 0) + +[node name="UIViewport" type="SubViewport" parent="."] +disable_3d = true +transparent_bg = true +size = Vector2i(1024, 512) +render_target_update_mode = 4 + +[node name="UI" parent="UIViewport" instance=ExtResource("2_ee2ui")] + +[node name="XROrigin3D" type="XROrigin3D" parent="."] +current = true + +[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7, 0) +current = true + +[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" +show_when_tracked = true + +[node name="HandMesh" type="MeshInstance3D" parent="XROrigin3D/LeftHand"] +mesh = SubResource("SphereMesh_078nk") + +[node name="Pointer" parent="XROrigin3D/LeftHand" instance=ExtResource("4_qvtse")] +visible = false +material_override = SubResource("ShaderMaterial_j0iib") + +[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" +show_when_tracked = true + +[node name="HandMesh" type="MeshInstance3D" parent="XROrigin3D/RightHand"] +mesh = SubResource("SphereMesh_078nk") + +[node name="Pointer" parent="XROrigin3D/RightHand" instance=ExtResource("4_qvtse")] +material_override = SubResource("ShaderMaterial_yobup") + +[node name="OpenXRCompositionLayerEquirect" type="OpenXRCompositionLayerEquirect" parent="XROrigin3D" node_paths=PackedStringArray("layer_viewport", "controller")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) +layer_viewport = NodePath("../../UIViewport") +alpha_blend = true +radius = 2.0 +upper_vertical_angle = 0.436332 +lower_vertical_angle = 0.436332 +script = ExtResource("4_211j6") +controller = NodePath("../RightHand") + +[node name="Floor" type="MeshInstance3D" parent="."] +mesh = SubResource("PlaneMesh_leufb") + +[connection signal="button_pressed" from="XROrigin3D/LeftHand" to="." method="_on_left_hand_button_pressed"] +[connection signal="button_pressed" from="XROrigin3D/RightHand" to="." method="_on_right_hand_button_pressed"] diff --git a/xr/openxr_composition_layers/openxr_action_map.tres b/xr/openxr_composition_layers/openxr_action_map.tres new file mode 100644 index 00000000..07796ee9 --- /dev/null +++ b/xr/openxr_composition_layers/openxr_action_map.tres @@ -0,0 +1,44 @@ +[gd_resource type="OpenXRActionMap" load_steps=9 format=3 uid="uid://cv3fftnsowiud"] + +[sub_resource type="OpenXRAction" id="OpenXRAction_75o1n"] +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_8d7jj"] +resource_name = "select" +localized_name = "Select" +action_type = 0 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRAction" id="OpenXRAction_t2cd0"] +resource_name = "haptic" +localized_name = "Haptic" +action_type = 4 +toplevel_paths = PackedStringArray("/user/hand/left", "/user/hand/right") + +[sub_resource type="OpenXRActionSet" id="OpenXRActionSet_ke8tl"] +resource_name = "godot" +localized_name = "Godot Action Set" +actions = [SubResource("OpenXRAction_75o1n"), SubResource("OpenXRAction_8d7jj"), SubResource("OpenXRAction_t2cd0")] + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_wqxq3"] +action = SubResource("OpenXRAction_75o1n") +paths = PackedStringArray("/user/hand/left/input/aim/pose", "/user/hand/right/input/aim/pose") + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_cdhbm"] +action = SubResource("OpenXRAction_8d7jj") +paths = PackedStringArray("/user/hand/left/input/select/click", "/user/hand/right/input/select/click") + +[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_em1q7"] +action = SubResource("OpenXRAction_t2cd0") +paths = PackedStringArray("/user/hand/left/output/haptic", "/user/hand/right/output/haptic") + +[sub_resource type="OpenXRInteractionProfile" id="OpenXRInteractionProfile_cvu6o"] +interaction_profile_path = "/interaction_profiles/khr/simple_controller" +bindings = [SubResource("OpenXRIPBinding_wqxq3"), SubResource("OpenXRIPBinding_cdhbm"), SubResource("OpenXRIPBinding_em1q7")] + +[resource] +action_sets = [SubResource("OpenXRActionSet_ke8tl")] +interaction_profiles = [SubResource("OpenXRInteractionProfile_cvu6o")] diff --git a/xr/openxr_composition_layers/pointer.gdshader b/xr/openxr_composition_layers/pointer.gdshader new file mode 100644 index 00000000..1cdfa0a4 --- /dev/null +++ b/xr/openxr_composition_layers/pointer.gdshader @@ -0,0 +1,16 @@ +shader_type spatial; + +uniform vec4 color : source_color = vec4(1.0, 0.0, 0.0, 0.5); +uniform float energy = 1.0; + +varying float f; + +void vertex() { + f = VERTEX.z + 0.5; +} + +void fragment() { + // Called for every pixel the material is visible on. + ALBEDO = color.rgb; + ALPHA = color.a * f * energy; +} diff --git a/xr/openxr_composition_layers/pointer.tscn b/xr/openxr_composition_layers/pointer.tscn new file mode 100644 index 00000000..91925e0c --- /dev/null +++ b/xr/openxr_composition_layers/pointer.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=4 format=3 uid="uid://cl6m21y2uldtf"] + +[ext_resource type="Shader" path="res://pointer.gdshader" id="1_u1f3u"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_pwb3i"] +render_priority = 0 +shader = ExtResource("1_u1f3u") +shader_parameter/color = Color(1, 0, 0, 0.5) + +[sub_resource type="BoxMesh" id="BoxMesh_1je57"] +size = Vector3(0.01, 0.01, 1) + +[node name="Pointer" type="MeshInstance3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.5) +material_override = SubResource("ShaderMaterial_pwb3i") +mesh = SubResource("BoxMesh_1je57") diff --git a/xr/openxr_composition_layers/project.godot b/xr/openxr_composition_layers/project.godot new file mode 100644 index 00000000..21cfbb3e --- /dev/null +++ b/xr/openxr_composition_layers/project.godot @@ -0,0 +1,32 @@ +; 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 Composition Layers" +run/main_scene="res://main.tscn" +config/features=PackedStringArray("4.3", "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 +anti_aliasing/quality/msaa_3d=1 + +[xr] + +openxr/enabled=true +openxr/reference_space=2 +openxr/foveation_level=3 +openxr/foveation_dynamic=true +openxr/extensions/hand_tracking=false +shaders/enabled=true diff --git a/xr/openxr_composition_layers/screenshots/xr_composition_layer_demo.png b/xr/openxr_composition_layers/screenshots/xr_composition_layer_demo.png new file mode 100644 index 00000000..a74db939 Binary files /dev/null and b/xr/openxr_composition_layers/screenshots/xr_composition_layer_demo.png differ diff --git a/xr/openxr_composition_layers/screenshots/xr_composition_layer_demo.png.import b/xr/openxr_composition_layers/screenshots/xr_composition_layer_demo.png.import new file mode 100644 index 00000000..41cd6520 --- /dev/null +++ b/xr/openxr_composition_layers/screenshots/xr_composition_layer_demo.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://mskdgk6h6hfj" +path="res://.godot/imported/xr_composition_layer_demo.png-ac3464258296848faf11aa855f57e16c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://screenshots/xr_composition_layer_demo.png" +dest_files=["res://.godot/imported/xr_composition_layer_demo.png-ac3464258296848faf11aa855f57e16c.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 diff --git a/xr/openxr_composition_layers/start_vr.gd b/xr/openxr_composition_layers/start_vr.gd new file mode 100644 index 00000000..3aa78712 --- /dev/null +++ b/xr/openxr_composition_layers/start_vr.gd @@ -0,0 +1,114 @@ +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_focused := false + + +# Called when the node enters the scene tree for the first time. +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[float] + available_rates.assign(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) + + +# 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_composition_layers/ui.gd b/xr/openxr_composition_layers/ui.gd new file mode 100644 index 00000000..150df22a --- /dev/null +++ b/xr/openxr_composition_layers/ui.gd @@ -0,0 +1,15 @@ +extends Control + +var button_count : int = 0 + + +func _input(event): + if event is InputEventMouseMotion: + # Move our cursor + var mouse_motion : InputEventMouseMotion = event + $Cursor.position = mouse_motion.position - Vector2(16, 16) + + +func _on_button_pressed(): + button_count = button_count + 1 + $CountLabel.text = "The button has been pressed %d times!" % [ button_count ] diff --git a/xr/openxr_composition_layers/ui.tscn b/xr/openxr_composition_layers/ui.tscn new file mode 100644 index 00000000..3a6e25c2 --- /dev/null +++ b/xr/openxr_composition_layers/ui.tscn @@ -0,0 +1,74 @@ +[gd_scene load_steps=5 format=3 uid="uid://cenb0bfok13vx"] + +[ext_resource type="Script" path="res://ui.gd" id="1_wnf2v"] +[ext_resource type="Shader" path="res://cursor.gdshader" id="2_hngl5"] + +[sub_resource type="LabelSettings" id="LabelSettings_cnxo1"] +font_size = 64 + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_84eui"] +shader = ExtResource("2_hngl5") +shader_parameter/color = Color(1, 1, 1, 1) + +[node name="UI" type="Control"] +custom_minimum_size = Vector2(1024, 512) +layout_mode = 3 +anchors_preset = 0 +offset_right = 40.0 +offset_bottom = 40.0 +script = ExtResource("1_wnf2v") + +[node name="ColorRect" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.638554, 0.499437, 0.164002, 0.384314) + +[node name="TopLabel" type="Label" parent="."] +layout_mode = 0 +offset_left = 25.0 +offset_top = 17.0 +offset_right = 125.0 +offset_bottom = 40.0 +text = "This is a test!" +label_settings = SubResource("LabelSettings_cnxo1") + +[node name="Button" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -249.0 +offset_top = -48.0 +offset_right = 249.0 +offset_bottom = 48.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_font_sizes/font_size = 64 +text = "Press this button" + +[node name="CountLabel" type="Label" parent="."] +layout_mode = 0 +offset_left = 39.0 +offset_top = 316.0 +offset_right = 965.0 +offset_bottom = 495.0 +text = "The button has not been pressed." +label_settings = SubResource("LabelSettings_cnxo1") +horizontal_alignment = 1 +autowrap_mode = 3 + +[node name="Cursor" type="ColorRect" parent="."] +process_mode = 4 +material = SubResource("ShaderMaterial_84eui") +layout_mode = 0 +offset_right = 32.0 +offset_bottom = 32.0 +mouse_filter = 2 + +[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]