diff --git a/.github/dist/footer.html b/.github/dist/footer.html index 7f955d9904f..748e1d37f69 100644 --- a/.github/dist/footer.html +++ b/.github/dist/footer.html @@ -32,7 +32,7 @@

Unavailable demos

  • mono/*: Not available yet (requires Mono-enabled HTML5 build).
  • networking/*: 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.
  • plugins/*: Only effective within the editor.
  • -
  • xr/*: Not functional on the web platform, as these demos are not designed for WebXR.
  • +
  • xr/openxr_*: Not functional on the web platform, as these demos are not designed for WebXR.
  • diff --git a/.github/workflows/export_web.yml b/.github/workflows/export_web.yml index 8660727fd2c..be07cc27047 100644 --- a/.github/workflows/export_web.yml +++ b/.github/workflows/export_web.yml @@ -61,7 +61,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. @@ -89,6 +92,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="\n\n\n"~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. diff --git a/xr/webxr/README.md b/xr/webxr/README.md new file mode 100644 index 00000000000..0be658a4ee6 --- /dev/null +++ b/xr/webxr/README.md @@ -0,0 +1,21 @@ +# WebXR demo + +This is a minimal demo of WebXR rendering and controller support. + +When exporting to the Web platform, make sure to include the WebXR Polyfill and WebXR Layers Polyfill which will fill holes in web browsers' WebXR support. +To include these polyfills, open the **Export** window and copy the following code into the `Head Include` field of the Web export preset: + +```html + + + + +``` + +Language: GDScript + +Renderer: Compatibility diff --git a/xr/webxr/icon.svg b/xr/webxr/icon.svg new file mode 100644 index 00000000000..c6bbb7d820d --- /dev/null +++ b/xr/webxr/icon.svg @@ -0,0 +1 @@ + diff --git a/xr/webxr/icon.svg.import b/xr/webxr/icon.svg.import new file mode 100644 index 00000000000..2b538a27437 --- /dev/null +++ b/xr/webxr/icon.svg.import @@ -0,0 +1,43 @@ +[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/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/xr/webxr/main.gd b/xr/webxr/main.gd new file mode 100644 index 00000000000..e99b4af29cb --- /dev/null +++ b/xr/webxr/main.gd @@ -0,0 +1,136 @@ +extends Node3D + + +var webxr_interface: WebXRInterface +var vr_supported: bool = false + +@onready var left_controller = $XROrigin3D/LeftController + + +func _ready() -> void: + $CanvasLayer/EnterVRButton.pressed.connect(_on_enter_vr_button_pressed) + + 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(_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(_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. + webxr_interface.requested_reference_space_types = "bounded-floor, local-floor, local" + # 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 = left_controller.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: XRControllerTracker = webxr_interface.get_input_source_tracker(input_source_id) + var xform: Transform3D = 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)) diff --git a/xr/webxr/main.gd.uid b/xr/webxr/main.gd.uid new file mode 100644 index 00000000000..44a2beaf92d --- /dev/null +++ b/xr/webxr/main.gd.uid @@ -0,0 +1 @@ +uid://b71y6j7lamjqg diff --git a/xr/webxr/main.tscn b/xr/webxr/main.tscn new file mode 100644 index 00000000000..2a9706f0215 --- /dev/null +++ b/xr/webxr/main.tscn @@ -0,0 +1,66 @@ +[gd_scene load_steps=7 format=3 uid="uid://dismxfxe7wvdn"] + +[ext_resource type="Script" uid="uid://b71y6j7lamjqg" 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" diff --git a/xr/webxr/project.godot b/xr/webxr/project.godot new file mode 100644 index 00000000000..084fe8c3f50 --- /dev/null +++ b/xr/webxr/project.godot @@ -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.5", "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