OpenXR composition layer example

This commit is contained in:
Bastiaan Olij
2024-06-26 00:39:23 +10:00
parent 8fd41702c6
commit 98899881e7
19 changed files with 765 additions and 0 deletions

View File

@@ -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/

View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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>

After

Width:  |  Height:  |  Size: 949 B

View File

@@ -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

View File

@@ -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)

View File

@@ -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"]

View File

@@ -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")]

View File

@@ -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;
}

View File

@@ -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")

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

View File

@@ -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

View File

@@ -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()

View File

@@ -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 ]

View File

@@ -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"]