mirror of
https://github.com/godotengine/godot-demo-projects.git
synced 2025-12-15 13:00:07 +01:00
Add WebXR demo
Based on https://docs.godotengine.org/en/stable/classes/class_webxrinterface.html and https://www.snopekgames.com/tutorial/2023/how-make-vr-game-webxr-godot-4 Co-Authored-By: David Snopek <dsnopek@gmail.com>
This commit is contained in:
2
.github/dist/footer.html
vendored
2
.github/dist/footer.html
vendored
@@ -32,7 +32,7 @@
|
||||
<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
.github/workflows/export_web.yml
vendored
10
.github/workflows/export_web.yml
vendored
@@ -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="<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.
|
||||
|
||||
3
xr/webxr/.gitignore
vendored
Normal file
3
xr/webxr/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
21
xr/webxr/README.md
Normal file
21
xr/webxr/README.md
Normal file
@@ -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
|
||||
<script src="https://cdn.jsdelivr.net/npm/webxr-polyfill@latest/build/webxr-polyfill.min.js"></script>
|
||||
<script>
|
||||
var polyfill = new WebXRPolyfill();
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/webxr-layers-polyfill@latest/build/webxr-layers-polyfill.min.js"></script>
|
||||
<script>
|
||||
var layersPolyfill = new WebXRLayersPolyfill();
|
||||
</script>
|
||||
```
|
||||
|
||||
Language: GDScript
|
||||
|
||||
Renderer: Compatibility
|
||||
1
xr/webxr/icon.svg
Normal file
1
xr/webxr/icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
37
xr/webxr/icon.svg.import
Normal file
37
xr/webxr/icon.svg.import
Normal file
@@ -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
xr/webxr/main.gd
Normal file
133
xr/webxr/main.gd
Normal file
@@ -0,0 +1,133 @@
|
||||
extends Node3D
|
||||
|
||||
var webxr_interface: XRInterface
|
||||
var vr_supported: bool = false
|
||||
|
||||
|
||||
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 = $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))
|
||||
1
xr/webxr/main.gd.uid
Normal file
1
xr/webxr/main.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b71y6j7lamjqg
|
||||
66
xr/webxr/main.tscn
Normal file
66
xr/webxr/main.tscn
Normal file
@@ -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"
|
||||
30
xr/webxr/project.godot
Normal file
30
xr/webxr/project.godot
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user