Эх сурвалжийг харах

OpenXR composition layer example

Bastiaan Olij 1 жил өмнө
parent
commit
98899881e7

+ 5 - 0
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/

+ 68 - 0
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)

BIN
xr/openxr_composition_layers/assets/pattern.png


+ 36 - 0
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

+ 9 - 0
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);
+}

+ 80 - 0
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

+ 1 - 0
xr/openxr_composition_layers/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 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 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z" fill="#478cbf"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-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_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

+ 64 - 0
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)

+ 120 - 0
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"]

+ 44 - 0
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")]

+ 16 - 0
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;
+}

+ 16 - 0
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")

+ 32 - 0
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

BIN
xr/openxr_composition_layers/screenshots/xr_composition_layer_demo.png


+ 34 - 0
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

+ 114 - 0
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()

+ 15 - 0
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 ]

+ 74 - 0
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"]