Skip to content

Commit 11f92a5

Browse files
committed
Add demo for spectator view
1 parent 51a7d38 commit 11f92a5

21 files changed

+745
-0
lines changed
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Normalize EOL for all files that Git considers text files.
2+
* text=auto eol=lf

xr/openxr_spectator_view/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Godot 4+ specific ignores
2+
.godot/

xr/openxr_spectator_view/README.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# XR spectator view demo
2+
3+
This is a demo for an OpenXR project where the player sees a different view inside of the headset
4+
compared to what a spectator sees on screen.
5+
When deployed to a standalone XR device, only the player environment is exported.
6+
7+
Language: GDScript
8+
9+
Renderer: Compatibility
10+
11+
Check out this demo on the asset library: https://godotengine.org/asset-library/asset/????
12+
13+
## How does it work?
14+
15+
The VR game itself is contained within the `main.tscn` scene. When run on standalone VR headsets, that scene is loaded.
16+
17+
When run on desktop, we load the `construct.tscn` scene instead. This scene has the `main.tscn` scene as a child of a
18+
`SubViewport` which will be used to output the render result to the headset.
19+
20+
The construct scene also contains a `SubviewportContainer` with a `SubViewport` that renders the output that the user
21+
sees on the desktop screen.
22+
By default this will show a 3rd person camera that shows our player.
23+
24+
We've also configured our visual layers as follows:
25+
1. Layer 1 is visible both inside of the headset and by the 3rd person camera.
26+
2. Layer 2 is only visible inside of the headset.
27+
3. Layer 3 is only visible in the 3rd person camera.
28+
This is used to render the "head" of the player in spectator view only.
29+
30+
Finally, a dropdown also allows us to switch to showing either the left eye or right eye result the player is seeing.
31+
32+
## Action map
33+
34+
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.
35+
36+
There is only one action needed for this example:
37+
- hand_pose is used to position the XR controllers
38+
39+
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.
40+
41+
## Running on PCVR
42+
43+
This project is specifically designed for PCVR. Ensure that an OpenXR runtime has been installed.
44+
This project has been tested with the Oculus client and SteamVR OpenXR runtimes.
45+
Note that Godot currently can't run using the WMR OpenXR runtime. Install SteamVR with WMR support.
46+
47+
## Running on standalone VR
48+
49+
This project also shows how deploying to standalone skips the spectator view option.
50+
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.
51+
Please follow [the instructions for deploying on Android in the manual](https://docs.godotengine.org/en/stable/tutorials/xr/deploying_to_android.html).
52+
53+
## Screenshots
54+
55+
![Screenshot](screenshots/spectator_view_demo.png)
1.77 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="CompressedTexture2D"
5+
uid="uid://rek0t7kubpx4"
6+
path.s3tc="res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.ctex"
7+
metadata={
8+
"imported_formats": ["s3tc_bptc"],
9+
"vram_texture": true
10+
}
11+
12+
[deps]
13+
14+
source_file="res://assets/pattern.png"
15+
dest_files=["res://.godot/imported/pattern.png-cf6f03dfd1cdd4bc35da3414e912103d.s3tc.ctex"]
16+
17+
[params]
18+
19+
compress/mode=2
20+
compress/high_quality=false
21+
compress/lossy_quality=0.7
22+
compress/hdr_compression=1
23+
compress/normal_map=0
24+
compress/channel_pack=0
25+
mipmaps/generate=true
26+
mipmaps/limit=-1
27+
roughness/mode=0
28+
roughness/src_normal=""
29+
process/fix_alpha_border=true
30+
process/premult_alpha=false
31+
process/normal_map_invert_y=false
32+
process/hdr_as_srgb=false
33+
process/hdr_clamp_exposure=false
34+
process/size_limit=0
35+
detect_3d/compress_to=0

xr/openxr_spectator_view/construct.gd

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
extends Node2D
2+
3+
var vr_render_size : Vector2
4+
var window_size : Vector2
5+
var hmd_view_material : ShaderMaterial
6+
7+
@onready var spectator_camera_original_transform : Transform3D = %SpectatorCamera.global_transform
8+
9+
func _reposition_texture_rect():
10+
if window_size != Vector2() and vr_render_size != Vector2():
11+
%HMDView.size = vr_render_size
12+
%HMDView.position = (window_size - vr_render_size) * 0.5
13+
14+
15+
func _on_size_changed():
16+
# Get our hmd view material
17+
hmd_view_material = %HMDView.material
18+
19+
# Get the new size of our window
20+
window_size = get_tree().get_root().size
21+
22+
# Set our container to full screen, this should update our viewport
23+
$SubViewportContainer.size = window_size
24+
25+
_reposition_texture_rect()
26+
27+
28+
# Called when the node enters the scene tree for the first time.
29+
func _ready():
30+
# Get a signal when our window size changes
31+
get_tree().get_root().size_changed.connect(_on_size_changed)
32+
33+
# Call atleast once to initialise
34+
_on_size_changed()
35+
36+
# Select our default view mode
37+
_on_spectator_view_item_selected(%SpectatorView.selected)
38+
39+
# Setup our tracked camera
40+
_on_track_camera_toggled(%TrackCamera.button_pressed)
41+
42+
func _on_spectator_view_item_selected(index):
43+
match index:
44+
0: # Spectator camera
45+
%DesktopSubViewport.disable_3d = false
46+
%SpectatorCamera.current = true
47+
%HMDView.visible = false
48+
%TrackCamera.visible = true
49+
1: # Left eye
50+
%DesktopSubViewport.disable_3d = true
51+
%HMDView.visible = true
52+
%TrackCamera.visible = false
53+
if hmd_view_material:
54+
var vp_texture = $VRSubViewport.get_texture()
55+
hmd_view_material.set_shader_parameter("xr_texture", vp_texture)
56+
hmd_view_material.set_shader_parameter("layer", 0)
57+
2: # Right eye
58+
%DesktopSubViewport.disable_3d = true
59+
%HMDView.visible = true
60+
%TrackCamera.visible = false
61+
if hmd_view_material:
62+
var vp_texture = $VRSubViewport.get_texture()
63+
hmd_view_material.set_shader_parameter("xr_texture", vp_texture)
64+
hmd_view_material.set_shader_parameter("layer", 1)
65+
66+
67+
func _on_main_focus_gained():
68+
vr_render_size = %Main.get_vr_render_size()
69+
_reposition_texture_rect()
70+
71+
72+
func _on_track_camera_toggled(toggled_on):
73+
# TODO should detect if we have camera tracking available
74+
75+
if toggled_on:
76+
%Main.tracked_camera = %SpectatorCamera
77+
else:
78+
%Main.tracked_camera = null
79+
%SpectatorCamera.global_transform = spectator_camera_original_transform
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
[gd_scene load_steps=5 format=3 uid="uid://qb615fcqh8x0"]
2+
3+
[ext_resource type="Script" path="res://construct.gd" id="1_ktigi"]
4+
[ext_resource type="PackedScene" uid="uid://cn6s3kxlkt6ml" path="res://main.tscn" id="2_1qy5m"]
5+
[ext_resource type="Shader" path="res://shaders/eye_output.gdshader" id="2_ygl07"]
6+
7+
[sub_resource type="ShaderMaterial" id="ShaderMaterial_wue56"]
8+
shader = ExtResource("2_ygl07")
9+
shader_parameter/layer = null
10+
11+
[node name="Construct" type="Node2D"]
12+
script = ExtResource("1_ktigi")
13+
14+
[node name="SubViewportContainer" type="SubViewportContainer" parent="."]
15+
custom_minimum_size = Vector2(512, 512)
16+
offset_right = 512.0
17+
offset_bottom = 512.0
18+
stretch = true
19+
20+
[node name="DesktopSubViewport" type="SubViewport" parent="SubViewportContainer"]
21+
unique_name_in_owner = true
22+
handle_input_locally = false
23+
render_target_update_mode = 4
24+
25+
[node name="HMDView" type="ColorRect" parent="SubViewportContainer/DesktopSubViewport"]
26+
unique_name_in_owner = true
27+
visible = false
28+
material = SubResource("ShaderMaterial_wue56")
29+
offset_right = 40.0
30+
offset_bottom = 40.0
31+
32+
[node name="SpectatorView" type="OptionButton" parent="SubViewportContainer/DesktopSubViewport"]
33+
unique_name_in_owner = true
34+
offset_left = 10.0
35+
offset_top = 10.0
36+
offset_right = 194.0
37+
offset_bottom = 41.0
38+
item_count = 3
39+
selected = 0
40+
popup/item_0/text = "Spectator Camera"
41+
popup/item_0/id = 0
42+
popup/item_1/text = "Left eye"
43+
popup/item_1/id = 1
44+
popup/item_2/text = "Right eye"
45+
popup/item_2/id = 2
46+
47+
[node name="TrackCamera" type="CheckBox" parent="SubViewportContainer/DesktopSubViewport"]
48+
unique_name_in_owner = true
49+
offset_left = 10.0
50+
offset_top = 45.0
51+
offset_right = 145.0
52+
offset_bottom = 76.0
53+
text = "Track Camera"
54+
55+
[node name="SpectatorCamera" type="Camera3D" parent="SubViewportContainer/DesktopSubViewport"]
56+
unique_name_in_owner = true
57+
transform = Transform3D(0.428162, 0, -0.903702, 0, 1, 0, 0.903702, 0, 0.428162, -4.12546, 1.30569, 1.60112)
58+
cull_mask = 1048573
59+
60+
[node name="VRSubViewport" type="SubViewport" parent="."]
61+
62+
[node name="Main" parent="VRSubViewport" instance=ExtResource("2_1qy5m")]
63+
unique_name_in_owner = true
64+
65+
[connection signal="item_selected" from="SubViewportContainer/DesktopSubViewport/SpectatorView" to="." method="_on_spectator_view_item_selected"]
66+
[connection signal="toggled" from="SubViewportContainer/DesktopSubViewport/TrackCamera" to="." method="_on_track_camera_toggled"]
67+
[connection signal="focus_gained" from="VRSubViewport/Main" to="." method="_on_main_focus_gained"]

xr/openxr_spectator_view/icon.svg

+1
Loading
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="CompressedTexture2D"
5+
uid="uid://cqneypbjryrwv"
6+
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
7+
metadata={
8+
"vram_texture": false
9+
}
10+
11+
[deps]
12+
13+
source_file="res://icon.svg"
14+
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
15+
16+
[params]
17+
18+
compress/mode=0
19+
compress/high_quality=false
20+
compress/lossy_quality=0.7
21+
compress/hdr_compression=1
22+
compress/normal_map=0
23+
compress/channel_pack=0
24+
mipmaps/generate=false
25+
mipmaps/limit=-1
26+
roughness/mode=0
27+
roughness/src_normal=""
28+
process/fix_alpha_border=true
29+
process/premult_alpha=false
30+
process/normal_map_invert_y=false
31+
process/hdr_as_srgb=false
32+
process/hdr_clamp_exposure=false
33+
process/size_limit=0
34+
detect_3d/compress_to=1
35+
svg/scale=1.0
36+
editor/scale_with_editor_scale=false
37+
editor/convert_colors_with_editor_theme=false

xr/openxr_spectator_view/main.gd

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
extends "res://start_vr.gd"
2+
3+
@export var tracked_camera : Camera3D:
4+
set(value):
5+
tracked_camera = value
6+
if tracked_camera:
7+
%CameraRemoteTransform3D.remote_path = tracked_camera.get_path()
8+
else:
9+
%CameraRemoteTransform3D.remote_path = NodePath()

xr/openxr_spectator_view/main.tscn

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
[gd_scene load_steps=5 format=3 uid="uid://cn6s3kxlkt6ml"]
2+
3+
[ext_resource type="Script" path="res://main.gd" id="1_2eojn"]
4+
[ext_resource type="PackedScene" uid="uid://ckuw0ps7vjw7e" path="res://world/world.tscn" id="2_j3t0x"]
5+
6+
[sub_resource type="SphereMesh" id="SphereMesh_b2416"]
7+
radius = 0.1
8+
height = 0.2
9+
10+
[sub_resource type="BoxMesh" id="BoxMesh_go2t1"]
11+
size = Vector3(0.1, 0.1, 0.1)
12+
13+
[node name="Main" type="Node3D"]
14+
script = ExtResource("1_2eojn")
15+
maximum_refresh_rate = 120
16+
17+
[node name="XROrigin3D" type="XROrigin3D" parent="."]
18+
19+
[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"]
20+
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8, 0)
21+
cull_mask = 1048571
22+
23+
[node name="PlaceholderHead" type="MeshInstance3D" parent="XROrigin3D/XRCamera3D"]
24+
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0335748, 0.0557129)
25+
layers = 4
26+
mesh = SubResource("SphereMesh_b2416")
27+
28+
[node name="LeftHand" type="XRController3D" parent="XROrigin3D"]
29+
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.5, 1, -0.5)
30+
tracker = &"left_hand"
31+
pose = &"hand_pose"
32+
33+
[node name="PlaceholderHand" type="MeshInstance3D" parent="XROrigin3D/LeftHand"]
34+
mesh = SubResource("BoxMesh_go2t1")
35+
36+
[node name="RightHand" type="XRController3D" parent="XROrigin3D"]
37+
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5, 1, -0.5)
38+
tracker = &"right_hand"
39+
pose = &"hand_pose"
40+
41+
[node name="PlaceholderHand" type="MeshInstance3D" parent="XROrigin3D/RightHand"]
42+
mesh = SubResource("BoxMesh_go2t1")
43+
skeleton = NodePath("../../LeftHand")
44+
45+
[node name="CameraTracker" type="XRController3D" parent="XROrigin3D"]
46+
tracker = &"/user/vive_tracker_htcx/role/camera"
47+
pose = &"camera_pose"
48+
49+
[node name="CameraRemoteTransform3D" type="RemoteTransform3D" parent="XROrigin3D/CameraTracker"]
50+
unique_name_in_owner = true
51+
52+
[node name="World" parent="." instance=ExtResource("2_j3t0x")]

0 commit comments

Comments
 (0)