-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1080 from BastiaanOlij/openxr_composition_layers
OpenXR composition layer example
- Loading branch information
Showing
19 changed files
with
765 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.