mirror of
https://github.com/godotengine/godot-demo-projects.git
synced 2025-12-16 13:30:07 +01:00
OpenXR composition layer example
This commit is contained in:
5
xr/openxr_composition_layers/.gitignore
vendored
Normal file
5
xr/openxr_composition_layers/.gitignore
vendored
Normal 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/
|
||||
68
xr/openxr_composition_layers/README.md
Normal file
68
xr/openxr_composition_layers/README.md
Normal 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
|
||||
|
||||

|
||||
BIN
xr/openxr_composition_layers/assets/pattern.png
Normal file
BIN
xr/openxr_composition_layers/assets/pattern.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
36
xr/openxr_composition_layers/assets/pattern.png.import
Normal file
36
xr/openxr_composition_layers/assets/pattern.png.import
Normal 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
|
||||
9
xr/openxr_composition_layers/cursor.gdshader
Normal file
9
xr/openxr_composition_layers/cursor.gdshader
Normal 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);
|
||||
}
|
||||
80
xr/openxr_composition_layers/handle_pointers.gd
Normal file
80
xr/openxr_composition_layers/handle_pointers.gd
Normal 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
|
||||
1
xr/openxr_composition_layers/icon.svg
Normal file
1
xr/openxr_composition_layers/icon.svg
Normal 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 |
37
xr/openxr_composition_layers/icon.svg.import
Normal file
37
xr/openxr_composition_layers/icon.svg.import
Normal 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
|
||||
64
xr/openxr_composition_layers/main.gd
Normal file
64
xr/openxr_composition_layers/main.gd
Normal 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)
|
||||
120
xr/openxr_composition_layers/main.tscn
Normal file
120
xr/openxr_composition_layers/main.tscn
Normal 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"]
|
||||
44
xr/openxr_composition_layers/openxr_action_map.tres
Normal file
44
xr/openxr_composition_layers/openxr_action_map.tres
Normal 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")]
|
||||
16
xr/openxr_composition_layers/pointer.gdshader
Normal file
16
xr/openxr_composition_layers/pointer.gdshader
Normal 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;
|
||||
}
|
||||
16
xr/openxr_composition_layers/pointer.tscn
Normal file
16
xr/openxr_composition_layers/pointer.tscn
Normal 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")
|
||||
32
xr/openxr_composition_layers/project.godot
Normal file
32
xr/openxr_composition_layers/project.godot
Normal 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 |
@@ -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
xr/openxr_composition_layers/start_vr.gd
Normal file
114
xr/openxr_composition_layers/start_vr.gd
Normal 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()
|
||||
15
xr/openxr_composition_layers/ui.gd
Normal file
15
xr/openxr_composition_layers/ui.gd
Normal 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 ]
|
||||
74
xr/openxr_composition_layers/ui.tscn
Normal file
74
xr/openxr_composition_layers/ui.tscn
Normal 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"]
|
||||
Reference in New Issue
Block a user