Skip to content

Commit

Permalink
Merge pull request #1080 from BastiaanOlij/openxr_composition_layers
Browse files Browse the repository at this point in the history
OpenXR composition layer example
  • Loading branch information
akien-mga authored Aug 22, 2024
2 parents 6d5ded3 + 9889988 commit 6c635fe
Show file tree
Hide file tree
Showing 19 changed files with 765 additions and 0 deletions.
5 changes: 5 additions & 0 deletions xr/openxr_composition_layers/.gitignore
Original file line number Diff line number Diff line change
@@ -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 changes: 68 additions & 0 deletions xr/openxr_composition_layers/README.md
Original file line number Diff line number Diff line change
@@ -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 added xr/openxr_composition_layers/assets/pattern.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions xr/openxr_composition_layers/assets/pattern.png.import
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions xr/openxr_composition_layers/cursor.gdshader
Original file line number Diff line number Diff line change
@@ -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 changes: 80 additions & 0 deletions xr/openxr_composition_layers/handle_pointers.gd
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions xr/openxr_composition_layers/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions xr/openxr_composition_layers/icon.svg.import
Original file line number Diff line number Diff line change
@@ -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 changes: 64 additions & 0 deletions xr/openxr_composition_layers/main.gd
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 6c635fe

Please sign in to comment.