Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add XR demo for spectator view #1144

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions xr/openxr_spectator_view/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
2 changes: 2 additions & 0 deletions xr/openxr_spectator_view/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Godot 4+ specific ignores
.godot/
71 changes: 71 additions & 0 deletions xr/openxr_spectator_view/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# XR spectator view demo

This is a demo for an OpenXR project where the player sees a different view inside of the headset
compared to what a spectator sees on screen.
When deployed to a standalone XR device, only the player environment is exported.

Language: GDScript

Renderer: Compatibility

Check out this demo on the asset library: https://godotengine.org/asset-library/asset/????

## How does it work?

The VR game itself is contained within the `main.tscn` scene. This is similar to the other XR demos found on this repo.
This demo has a bare bones example as to not distract from the solution we're presenting here.

When run on standalone VR headsets, that scene is loaded.
![Project setup](screenshots/project_setup_main_scene.png)

When run on desktop, we load the `construct.tscn` scene instead. This scene has the `main.tscn` scene as a child of a
`SubViewport` which will be used to output the render result to the headset.

![Construct scene](screenshots/construct_scene.png)

The construct scene also contains a `SubviewportContainer` with a `SubViewport` that renders the output that the user
sees on the desktop screen.
By default this will show a 3rd person camera that shows our player.

We've also configured our visual layers as follows:
1. Layer 1 is visible both inside of the headset and by the 3rd person camera.
2. Layer 2 is only visible inside of the headset.
3. Layer 3 is only visible in the 3rd person camera.
This is used to render the "head" of the player in spectator view only.

Finally, a dropdown also allows us to switch to showing either the left eye or right eye result the player is seeing.

## Tracked camera

There is also an option in the demo to enable camera tracking.
This is currently only supported on SteamVR together with a properly configured HTC Vive Tracker.

When properly setup, this allows you to use a Vive tracker to position the 3rd person camera.
Attaching the Vive tracker to a physical camera, and setting the correct offset would allow
implementation of mixed reality capture by combining the 3rd person render result,
with a green screened captured camera.

## 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 is only one action needed for this example:
- hand_pose is used to position the XR controllers

Also following OpenXR guidelines only bindings for controllers with which the project has been tested are supplied. XR Runtimes should provide proper re-mapping however not all follow this guideline. You may need to add a binding for the platform you are using to the action map.

## Running on PCVR

This project is specifically designed 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

This project also shows how deploying to standalone skips the spectator view option.
You must install the Android build templates and [OpenXR vendors plugin](https://github.com/GodotVR/godot_openxr_vendors/releases) 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](screenshots/spectator_view_demo.png)
Binary file added xr/openxr_spectator_view/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.
35 changes: 35 additions & 0 deletions xr/openxr_spectator_view/assets/pattern.png.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://rek0t7kubpx4"
path.s3tc="res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}

[deps]

source_file="res://assets/pattern.png"
dest_files=["res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.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
79 changes: 79 additions & 0 deletions xr/openxr_spectator_view/construct.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
extends Node2D

var vr_render_size : Vector2
var window_size : Vector2
var hmd_view_material : ShaderMaterial

@onready var tracked_camera_original_transform : Transform3D = %TrackedCamera.global_transform

func _reposition_texture_rect():
if window_size != Vector2() and vr_render_size != Vector2():
%HMDView.size = vr_render_size
%HMDView.position = (window_size - vr_render_size) * 0.5


func _on_size_changed():
# Get our hmd view material
hmd_view_material = %HMDView.material

# Get the new size of our window
window_size = get_tree().get_root().size

# Set our container to full screen, this should update our viewport
$SubViewportContainer.size = window_size

_reposition_texture_rect()


# Called when the node enters the scene tree for the first time.
func _ready():
# Get a signal when our window size changes
get_tree().get_root().size_changed.connect(_on_size_changed)

# Call atleast once to initialise
_on_size_changed()

# Select our default view mode
_on_spectator_view_item_selected(%SpectatorView.selected)

# Setup our tracked camera
_on_track_camera_toggled(%TrackCamera.button_pressed)

func _on_spectator_view_item_selected(index):
match index:
0: # Spectator camera
%DesktopSubViewport.disable_3d = false
%SpectatorCamera.current = true
%HMDView.visible = false
%TrackCamera.visible = true
1: # Left eye
%DesktopSubViewport.disable_3d = true
%HMDView.visible = true
%TrackCamera.visible = false
if hmd_view_material:
var vp_texture = $VRSubViewport.get_texture()
hmd_view_material.set_shader_parameter("xr_texture", vp_texture)
hmd_view_material.set_shader_parameter("layer", 0)
2: # Right eye
%DesktopSubViewport.disable_3d = true
%HMDView.visible = true
%TrackCamera.visible = false
if hmd_view_material:
var vp_texture = $VRSubViewport.get_texture()
hmd_view_material.set_shader_parameter("xr_texture", vp_texture)
hmd_view_material.set_shader_parameter("layer", 1)


func _on_main_focus_gained():
vr_render_size = %Main.get_vr_render_size()
_reposition_texture_rect()


func _on_track_camera_toggled(toggled_on):
# TODO should detect if we have camera tracking available

if toggled_on:
%Main.tracked_camera = %TrackedCamera
else:
%Main.tracked_camera = null
%TrackedCamera.global_transform = tracked_camera_original_transform
70 changes: 70 additions & 0 deletions xr/openxr_spectator_view/construct.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
[gd_scene load_steps=5 format=3 uid="uid://qb615fcqh8x0"]

[ext_resource type="Script" path="res://construct.gd" id="1_ktigi"]
[ext_resource type="PackedScene" uid="uid://cn6s3kxlkt6ml" path="res://main.tscn" id="2_1qy5m"]
[ext_resource type="Shader" path="res://shaders/eye_output.gdshader" id="2_ygl07"]

[sub_resource type="ShaderMaterial" id="ShaderMaterial_wue56"]
shader = ExtResource("2_ygl07")
shader_parameter/layer = null

[node name="Construct" type="Node2D"]
script = ExtResource("1_ktigi")

[node name="SubViewportContainer" type="SubViewportContainer" parent="."]
custom_minimum_size = Vector2(512, 512)
offset_right = 512.0
offset_bottom = 512.0
stretch = true

[node name="DesktopSubViewport" type="SubViewport" parent="SubViewportContainer"]
unique_name_in_owner = true
handle_input_locally = false
render_target_update_mode = 4

[node name="HMDView" type="ColorRect" parent="SubViewportContainer/DesktopSubViewport"]
unique_name_in_owner = true
visible = false
material = SubResource("ShaderMaterial_wue56")
offset_right = 40.0
offset_bottom = 40.0

[node name="UI" type="VBoxContainer" parent="SubViewportContainer/DesktopSubViewport"]
offset_left = 10.0
offset_top = 10.0
offset_right = 194.0
offset_bottom = 76.0

[node name="SpectatorView" type="OptionButton" parent="SubViewportContainer/DesktopSubViewport/UI"]
unique_name_in_owner = true
layout_mode = 2
item_count = 3
selected = 0
popup/item_0/text = "Spectator Camera"
popup/item_0/id = 0
popup/item_1/text = "Left eye"
popup/item_1/id = 1
popup/item_2/text = "Right eye"
popup/item_2/id = 2

[node name="TrackCamera" type="CheckBox" parent="SubViewportContainer/DesktopSubViewport/UI"]
unique_name_in_owner = true
layout_mode = 2
text = "Track Camera"

[node name="TrackedCamera" type="Node3D" parent="SubViewportContainer/DesktopSubViewport"]
unique_name_in_owner = true
transform = Transform3D(0.428162, 0, -0.903702, 0, 1, 0, 0.903702, 0, 0.428162, -4.12546, 1.30569, 1.60112)

[node name="SpectatorCamera" type="Camera3D" parent="SubViewportContainer/DesktopSubViewport/TrackedCamera"]
unique_name_in_owner = true
cull_mask = 1048573

[node name="VRSubViewport" type="SubViewport" parent="."]

[node name="Main" parent="VRSubViewport" instance=ExtResource("2_1qy5m")]
unique_name_in_owner = true

[connection signal="item_selected" from="SubViewportContainer/DesktopSubViewport/UI/SpectatorView" to="." method="_on_spectator_view_item_selected"]
[connection signal="toggled" from="SubViewportContainer/DesktopSubViewport/UI/TrackCamera" to="." method="_on_track_camera_toggled"]
[connection signal="focus_gained" from="VRSubViewport/Main" to="." method="_on_main_focus_gained"]
1 change: 1 addition & 0 deletions xr/openxr_spectator_view/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_spectator_view/icon.svg.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://cqneypbjryrwv"
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
9 changes: 9 additions & 0 deletions xr/openxr_spectator_view/main.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extends "res://start_vr.gd"

@export var tracked_camera : Node3D:
set(value):
tracked_camera = value
if tracked_camera:
%CameraRemoteTransform3D.remote_path = tracked_camera.get_path()
else:
%CameraRemoteTransform3D.remote_path = NodePath()
52 changes: 52 additions & 0 deletions xr/openxr_spectator_view/main.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[gd_scene load_steps=5 format=3 uid="uid://cn6s3kxlkt6ml"]

[ext_resource type="Script" path="res://main.gd" id="1_2eojn"]
[ext_resource type="PackedScene" uid="uid://ckuw0ps7vjw7e" path="res://world/world.tscn" id="2_j3t0x"]

[sub_resource type="SphereMesh" id="SphereMesh_b2416"]
radius = 0.1
height = 0.2

[sub_resource type="BoxMesh" id="BoxMesh_go2t1"]
size = Vector3(0.1, 0.1, 0.1)

[node name="Main" type="Node3D"]
script = ExtResource("1_2eojn")
maximum_refresh_rate = 120

[node name="XROrigin3D" type="XROrigin3D" parent="."]

[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8, 0)
cull_mask = 1048571

[node name="PlaceholderHead" type="MeshInstance3D" parent="XROrigin3D/XRCamera3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0335748, 0.0557129)
layers = 4
mesh = SubResource("SphereMesh_b2416")

[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 = &"hand_pose"

[node name="PlaceholderHand" type="MeshInstance3D" parent="XROrigin3D/LeftHand"]
mesh = SubResource("BoxMesh_go2t1")

[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 = &"hand_pose"

[node name="PlaceholderHand" type="MeshInstance3D" parent="XROrigin3D/RightHand"]
mesh = SubResource("BoxMesh_go2t1")
skeleton = NodePath("../../LeftHand")

[node name="CameraTracker" type="XRController3D" parent="XROrigin3D"]
tracker = &"/user/vive_tracker_htcx/role/camera"
pose = &"camera_pose"

[node name="CameraRemoteTransform3D" type="RemoteTransform3D" parent="XROrigin3D/CameraTracker"]
unique_name_in_owner = true

[node name="World" parent="." instance=ExtResource("2_j3t0x")]
Loading