Skip to content

Add WebXR demo #1168

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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: 1 addition & 1 deletion .github/dist/footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ <h2>Unavailable demos</h2>
<li><code>mono/*</code>: Not available yet (requires Mono-enabled HTML5 build).</li>
<li><code>networking/*</code>: Doesn't make sense to be hosted on a static host, as the server must be hosted on the same origin due to the browser's same-origin policy.</li>
<li><code>plugins/*</code>: Only effective within the editor.</li>
<li><code>xr/*</code>: Not functional on the web platform, as these demos are not designed for WebXR.</li>
<li><code>xr/openxr_*</code>: Not functional on the web platform, as these demos are not designed for WebXR.</li>
</ul>
</body>
</html>
10 changes: 9 additions & 1 deletion .github/workflows/export_web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ jobs:
mono/ \
networking/ \
plugins/ \
xr/
xr/openxr_character_centric_movement \
xr/openxr_composition_layers \
xr/openxr_hand_tracking_demo \
xr/openxr_origin_centric_movement

for panorama in 3d/material_testers/backgrounds/*.hdr; do
# Decrease the resolution to get below the 100 MB PCK size limit.
Expand Down Expand Up @@ -88,6 +91,11 @@ jobs:
# Enable ETC2 texture importing, which is disabled by default (but required for web exports to work on mobile platforms).
echo "[rendering]\n\ntextures/vram_compression/import_etc2_astc=true" >> project.godot

# Enable WebXR Polyfill and WebXR Layers Polyfill for the WebXR demo.
if [ "$demo" == "xr/webxr/" ]; then
sed -i 's~^html/head_include=""$~html/head_include="<script src=\\"https://cdn.jsdelivr.net/npm/webxr-polyfill@latest/build/webxr-polyfill.min.js\\"></script>\n<script>\nvar polyfill = new WebXRPolyfill();\n</script>\n<script src=\\"https://cdn.jsdelivr.net/npm/webxr-layers-polyfill@latest/build/webxr-layers-polyfill.min.js\\"></script>\n<script>\nvar layersPolyfill = new WebXRLayersPolyfill();\n</script>"~g' export_presets.cfg
fi

godot --verbose --headless --export-release "Web" "$BASEDIR/.github/dist/$demo/index.html"

# Replace the WASM file with a symbolic link to avoid duplicating files in the pushed branch.
Expand Down
3 changes: 3 additions & 0 deletions xr/webxr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/
7 changes: 7 additions & 0 deletions xr/webxr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# WebXR demo

This is a minimalist demo of WebXR rendering and controller support.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This is a minimalist demo of WebXR rendering and controller support.
This is a minimal demo of WebXR rendering and controller support.

Minimalist implies more than minimal


Language: GDScript

Renderer: Compatibility
1 change: 1 addition & 0 deletions xr/webxr/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/webxr/icon.svg.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[remap]

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

var webxr_interface: XRInterface
var vr_supported: bool = false


func _ready() -> void:
$CanvasLayer/EnterVRButton.pressed.connect(self._on_enter_vr_button_pressed)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$CanvasLayer/EnterVRButton.pressed.connect(self._on_enter_vr_button_pressed)
$CanvasLayer/EnterVRButton.pressed.connect(_on_enter_vr_button_pressed)

Redundant, and also unsafe


webxr_interface = XRServer.find_interface("WebXR")
if webxr_interface:
# WebXR uses a lot of asynchronous callbacks, so we connect to various
# signals in order to receive them.
webxr_interface.session_supported.connect(self._webxr_session_supported)
webxr_interface.session_started.connect(self._webxr_session_started)
webxr_interface.session_ended.connect(self._webxr_session_ended)
webxr_interface.session_failed.connect(self._webxr_session_failed)

webxr_interface.select.connect(self._webxr_on_select)
webxr_interface.selectstart.connect(self._webxr_on_select_start)
webxr_interface.selectend.connect(self._webxr_on_select_end)

webxr_interface.squeeze.connect(self._webxr_on_squeeze)
webxr_interface.squeezestart.connect(self._webxr_on_squeeze_start)
webxr_interface.squeezeend.connect(self._webxr_on_squeeze_end)
Comment on lines +14 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
webxr_interface.session_supported.connect(self._webxr_session_supported)
webxr_interface.session_started.connect(self._webxr_session_started)
webxr_interface.session_ended.connect(self._webxr_session_ended)
webxr_interface.session_failed.connect(self._webxr_session_failed)
webxr_interface.select.connect(self._webxr_on_select)
webxr_interface.selectstart.connect(self._webxr_on_select_start)
webxr_interface.selectend.connect(self._webxr_on_select_end)
webxr_interface.squeeze.connect(self._webxr_on_squeeze)
webxr_interface.squeezestart.connect(self._webxr_on_squeeze_start)
webxr_interface.squeezeend.connect(self._webxr_on_squeeze_end)
webxr_interface.session_supported.connect(_webxr_session_supported)
webxr_interface.session_started.connect(_webxr_session_started)
webxr_interface.session_ended.connect(_webxr_session_ended)
webxr_interface.session_failed.connect(_webxr_session_failed)
webxr_interface.select.connect(_webxr_on_select)
webxr_interface.selectstart.connect(_webxr_on_select_start)
webxr_interface.selectend.connect(_webxr_on_select_end)
webxr_interface.squeeze.connect(_webxr_on_squeeze)
webxr_interface.squeezestart.connect(_webxr_on_squeeze_start)
webxr_interface.squeezeend.connect(_webxr_on_squeeze_end)


# This returns immediately - our _webxr_session_supported() method
# (which we connected to the "session_supported" signal above) will
# be called sometime later to let us know if it's supported or not.
webxr_interface.is_session_supported("immersive-vr")

$XROrigin3D/LeftController.button_pressed.connect(self._on_left_controller_button_pressed)
$XROrigin3D/LeftController.button_released.connect(self._on_left_controller_button_released)
Comment on lines +32 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$XROrigin3D/LeftController.button_pressed.connect(self._on_left_controller_button_pressed)
$XROrigin3D/LeftController.button_released.connect(self._on_left_controller_button_released)
$XROrigin3D/LeftController.button_pressed.connect(_on_left_controller_button_pressed)
$XROrigin3D/LeftController.button_released.connect(_on_left_controller_button_released)



func _webxr_session_supported(session_mode: String, supported: bool) -> void:
if session_mode == 'immersive-vr':
vr_supported = supported


func _on_enter_vr_button_pressed() -> void:
if not vr_supported:
OS.alert("Your browser doesn't support VR")
return

# We want an immersive VR session, as opposed to AR ('immersive-ar') or a
# simple 3DoF viewer ('viewer').
webxr_interface.session_mode = 'immersive-vr'
# 'bounded-floor' is room scale, 'local-floor' is a standing or sitting
# experience (it puts you 1.6m above the ground if you have 3DoF headset),
# whereas as 'local' puts you down at the XROrigin3D.
# This list means it'll first try to request 'bounded-floor', then
# fallback on 'local-floor' and ultimately 'local', if nothing else is
# supported.
Comment on lines +49 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A first pass at rewriting this, but I'm not super happy with it:

Suggested change
# 'bounded-floor' is room scale, 'local-floor' is a standing or sitting
# experience (it puts you 1.6m above the ground if you have 3DoF headset),
# whereas as 'local' puts you down at the XROrigin3D.
# This list means it'll first try to request 'bounded-floor', then
# fallback on 'local-floor' and ultimately 'local', if nothing else is
# supported.
# The reference space affects what the XROrigin3D position represents, and
# consequently what the positions of the XRCamera3D and XRController3D
# nodes are relative to:
# - 'bounded-floor' is for room-scale, where the XROrigin3D will be positioned
# in the center of your play space
# - 'local-floor' is for most standing or sitting experiences, where you want the
# XROrigin3D to positioned at your real world floor, but the X and Z position
# will start where you were standing when entering XR
# - 'local' puts the XROrigin3D where your head was when entering XR, and is
# best suited for experiences that don't have any relation to the real world floor
# like driving or flight simulators, or where the character you embody is of a different
# height than the real player
# This list means it'll first try to request 'bounded-floor', then fallback on 'local-floor'
# and ultimately 'local', if nothing else is supported.

... or, maybe the whole list of reference spaces should be removed, and we should just have the last part "This list means it'll first try..." and have a link to some other documentation where we describe them individually?

webxr_interface.requested_reference_space_types = 'bounded-floor, local-floor, local'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the number of people that have copy-pasted this code wholesale over the years, I've been regretting including 'bounded-floor' in here. It's a good example to show both optional and required features, but I suspect most developers would actually have a better experience with 'local-floor'.

But I'm not entirely sure we should change this, and if we do, it should be changed in the WebXRInterface docs too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I would just keep the configuration as is for this demo (because as you say it's a good example of optional and required features), but we could modify the comments or add to them, here and/or in the WebXRInterface documentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense! The descriptions of the reference spaces in the comment also aren't that great.

I'll take a stab at rewriting the comment, but we don't really need to fix this here and now - we can iterate on it elsewhere

# In order to use 'local-floor' or 'bounded-floor' we must also
# mark the features as required or optional.
webxr_interface.required_features = 'local-floor'
webxr_interface.optional_features = 'bounded-floor'

# This will return false if we're unable to even request the session,
# however, it can still fail asynchronously later in the process, so we
# only know if it's really succeeded or failed when our
# _webxr_session_started() or _webxr_session_failed() methods are called.
if not webxr_interface.initialize():
OS.alert("Failed to initialize WebXR")
return


func _webxr_session_started() -> void:
$CanvasLayer.visible = false
# This tells Godot to start rendering to the headset.
get_viewport().use_xr = true
# This will be the reference space type you ultimately got, out of the
# types that you requested above. This is useful if you want the game to
# work a little differently in 'bounded-floor' versus 'local-floor'.
print ("Reference space type: " + webxr_interface.reference_space_type)
# This will be the list of features that were successfully enabled
# (except on browsers that don't support this property).
print("Enabled features: ", webxr_interface.enabled_features)


func _webxr_session_ended() -> void:
$CanvasLayer.visible = true
# If the user exits immersive mode, then we tell Godot to render to the web
# page again.
get_viewport().use_xr = false


func _webxr_session_failed(message: String) -> void:
OS.alert("Failed to initialize: " + message)


func _on_left_controller_button_pressed(button: String) -> void:
print ("Button pressed: " + button)


func _on_left_controller_button_released(button: String) -> void:
print ("Button release: " + button)


func _process(_delta: float) -> void:
var thumbstick_vector: Vector2 = $XROrigin3D/LeftController.get_vector2("thumbstick")
if thumbstick_vector != Vector2.ZERO:
print ("Left thumbstick position: " + str(thumbstick_vector))


func _webxr_on_select(input_source_id: int) -> void:
print("Select: " + str(input_source_id))

var tracker: XRPositionalTracker = webxr_interface.get_input_source_tracker(input_source_id)
var xform = tracker.get_pose('default').transform
print (xform.origin)


func _webxr_on_select_start(input_source_id: int) -> void:
print("Select Start: " + str(input_source_id))


func _webxr_on_select_end(input_source_id: int) -> void:
print("Select End: " + str(input_source_id))


func _webxr_on_squeeze(input_source_id: int) -> void:
print("Squeeze: " + str(input_source_id))


func _webxr_on_squeeze_start(input_source_id: int) -> void:
print("Squeeze Start: " + str(input_source_id))


func _webxr_on_squeeze_end(input_source_id: int) -> void:
print("Squeeze End: " + str(input_source_id))
66 changes: 66 additions & 0 deletions xr/webxr/main.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
[gd_scene load_steps=7 format=3 uid="uid://dismxfxe7wvdn"]

[ext_resource type="Script" path="res://main.gd" id="1_ig7tw"]

[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_lins3"]
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_wiqav"]
sky_material = SubResource("ProceduralSkyMaterial_lins3")

[sub_resource type="Environment" id="Environment_6ff2h"]
background_mode = 2
sky = SubResource("Sky_wiqav")
tonemap_mode = 2

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

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

[node name="Main" type="Node3D"]
script = ExtResource("1_ig7tw")

[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_6ff2h")

[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(-0.866025, -0.433013, 0.25, 0, 0.5, 0.866025, -0.5, 0.75, -0.433013, 0, 0, 0)
shadow_enabled = true

[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.7, 0)

[node name="LeftController" type="XRController3D" parent="XROrigin3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.5, 1, 0)
tracker = &"left_hand"

[node name="MeshInstance3D" type="MeshInstance3D" parent="XROrigin3D/LeftController"]
mesh = SubResource("BoxMesh_gv5m4")

[node name="RightController" type="XRController3D" parent="XROrigin3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5, 1, 0)
tracker = &"right_hand"

[node name="MeshInstance3D" type="MeshInstance3D" parent="XROrigin3D/RightController"]
mesh = SubResource("BoxMesh_f3sb7")

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

[node name="EnterVRButton" type="Button" parent="CanvasLayer"]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -25.0
offset_right = 50.0
offset_bottom = 25.0
grow_horizontal = 2
grow_vertical = 2
text = "Enter VR"
30 changes: 30 additions & 0 deletions xr/webxr/project.godot
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
; 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="WebXR demo"
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.3", "GL Compatibility")
config/icon="res://icon.svg"

[physics]

common/enable_object_picking=false

[rendering]

renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
textures/vram_compression/import_etc2_astc=true

[xr]

shaders/enabled=true