Is there a WebXR working example? (Godot 4.4.1 2025)

Godot Version

4.4.1

Question

I followed the tutorial from the docs
https://www.snopekgames.com/tutorial/2023/how-make-vr-game-webxr-godot-4/

and get an empty scene and then non-stop errors and no reaction from moving the headset or the controllers. (see end of post)

Are there any up to date step by step tutorials/instructions to get a basic scene working?

What I have tried

Followed the snopekgames tutorial 2023 to the letter being pedantic about ensuring I have followed ever step and that all screen shots match the interface exactly.

But before that I tried that I followed the VirtualRook video Getting Started With XR in Godot 4.3 Tutorial!
Which is much more involved and crucially not WebXR focused (I thought export to Web would magically work).

I then edited the main.gd script to include the script from snopekgames and managed to get the exported project to work in Firefox with the WebXR API emulator.
But the scene only contained the hand objects. The headset and the controllers were movable via the API GUI, but none of the rest of the scene, the table and pickable objects could not be seen.
There were also a continuous flow of errors “ERROR: The axis Vector3 (0.0, 0.0, 0.0) must be normalized.” Which I understand is non-fatal but made trying to see any actual issues impossible.

The output I get for the snopekgames tutorial are

WebXR emulator extension overrides native WebXR API with polyfill. xr2:37274:27
Injected Layers Polyfill webxr-layers-polyfill.min.js:204:14329
Godot Engine v4.4.1.stable.official.49a5bc7b6 - https://godotengine.org index.js:459:16
OpenGL API OpenGL ES 3.0 (WebGL 2.0) - Compatibility - Using Device: Mozilla - NVIDIA GeForce GTX 980, or similar index.js:459:16
An AudioContext was prevented from starting automatically. It must be created or resumed after a user gesture on the page. index.js:9:155485
The optional feature 'bounded-floor' is not supported xr2:938:33
Reference space type: local-floor index.js:459:16
Button release: trigger_click index.js:459:16
Button release: grip_click index.js:459:16
Button release: touchpad_click index.js:459:16
Button release: thumbstick_click index.js:459:16
Button release: ax_button index.js:459:16
Button release: by_button index.js:459:16
Button release: button_6 index.js:459:16
WARNING: Object picking can't be used when stereo rendering, this will be turned off! index.js:474:18
   at: _process_picking (scene/main/viewport.cpp:715) index.js:474:18
texture-array layers are supported...questionably in the polyfill at the moment. Use at your own risk. 2 webxr-layers-polyfill.min.js:42:8481
ERROR: Condition "p_right <= p_left" is true. index.js:474:18
   at: set_frustum (core/math/projection.cpp:365) index.js:474:18
1: #version 300 es index.js:459:16
2: #define USE_MULTIVIEW index.js:459:16
3: #define APPLY_TONEMAPPING index.js:459:16
4: #define MATERIAL_UNIFORMS_USED index.js:459:16
5: #define SKY_CODE_USED index.js:459:16
6: index.js:459:16
7: #define MAX_GLOBAL_SHADER_UNIFORMS 256 index.js:459:16
8: index.js:459:16
9: #define MAX_DIRECTIONAL_LIGHT_DATA_STRUCTS 4 index.js:459:16
10: index.js:459:16
11: #define USE_DEBANDING index.js:459:16
12: index.js:459:16
13: #define samplerExternalOES sampler2D index.js:459:16
14: #ifdef USE_MULTIVIEW index.js:459:16
15: #if defined(GL_OVR_multiview2) index.js:459:16
16: #extension GL_OVR_multiview2 : require index.js:459:16
17: #elif defined(GL_OVR_multiview) index.js:459:16
18: #extension GL_OVR_multiview : require index.js:459:16
19: #endif index.js:459:16
20: layout(num_views=2) in; index.js:459:16
21: #define ViewIndex gl_ViewID_OVR index.js:459:16
22: #define MAX_VIEWS 2 index.js:459:16
23: #else index.js:459:16
24: #define ViewIndex uint(0) index.js:459:16
25: #define MAX_VIEWS 1 index.js:459:16
26: #endif index.js:459:16
27: precision highp float; index.js:459:16
28: precision highp int; index.js:459:16
29: precision highp sampler2D; index.js:459:16
30: precision highp samplerCube; index.js:459:16
31: precision highp sampler2DArray; index.js:459:16
32: precision highp sampler3D; index.js:459:16
33: index.js:459:16
34: layout(location = 0) in vec2 vertex_attrib; index.js:459:16
35: index.js:459:16
36: out vec2 uv_interp; index.js:459:16
37: /* clang-format on */ index.js:459:16
38: index.js:459:16
39: void main() { index.js:459:16
40: #ifdef USE_INVERTED_Y index.js:459:16
41: 	uv_interp = vertex_attrib; index.js:459:16
42: #else index.js:459:16
43: 	// We're doing clockwise culling so flip the order index.js:459:16
44: 	uv_interp = vec2(vertex_attrib.x, vertex_attrib.y * -1.0); index.js:459:16
45: #endif index.js:459:16
46: 	gl_Position = vec4(uv_interp, -1.0, 1.0); index.js:459:16
47: } index.js:459:16
48: index.js:459:16
49: /* clang-format off */ index.js:459:16
50: index.js:459:16
51: index.js:459:16
ERROR: SkyShaderGLES3: Vertex shader compilation failed: index.js:474:18
0(3) : error C0210: extension GL_OVR_multiview2 not supported for version 150 index.js:474:18
<empty string> index.js:474:18
   at: _display_error_with_code (drivers/gles3/shader_gles3.cpp:265) index.js:474:18
ERROR: Method/function failed. index.js:474:18
   at: _compile_specialization (drivers/gles3/shader_gles3.cpp:359) index.js:474:18
WARNING: shader failed to compile, unable to bind shader. index.js:474:18
   at: _version_bind_shader (./drivers/gles3/shader_gles3.h:218) index.js:474:18
ERROR: Condition "p_right <= p_left" is true. index.js:474:18
   at: set_frustum (core/math/projection.cpp:365) index.js:474:18
ERROR: Condition "p_right <= p_left" is true. index.js:474:18
   at: set_frustum (core/math/projection.cpp:365) index.js:474:18
ERROR: Condition "p_right <= p_left" is true. index.js:474:18
   at: set_frustum (core/math/projection.cpp:365) index.js:474:18
ERROR: Condition "p_right <= p_left" is true. index.js:474:18
   at: set_frustum (core/math/projection.cpp:365) index.js:474:18

any help much appreciated

I just tried the older Malcolm Nixon Godot 4.0.1 Web-XR Tutorial video.

It does not make you enter any code, instead it uses the start_xr.tscn

It never enters VR.
But I get more errors and repeating ERROR: Condition "p_right <= p_left" is true. index.js:474:18 at: set_frustum (core/math/projection.cpp:365) index.js:474:18

This page does fully work in the Firefox emulator https://immersive-web.github.io/webxr-samples/inline-session.html, but it is not godot based

I have found out how I was able to get the WebXR API emulator working, but it does not make any sense.

If I comment out the line get_viewport().use_xr = true in the “_webxr_session_started” function then it works. Which really does not make any sense as it is noted as

# This tells Godot to start rendering to the headset.

I am going to try some of these on the Quest2 headset and see if this is a Firefox issue.

Unsurprisingly I just get a black screen running it either Meta Browser or Wolvic. But what was weird was that quitting them from the Meta browser leaves them running in the 2D browser window and then the visuals matched the headset movement, no controllers.

Well this does not make any sense. Re-enabling that line does allow me to see and operate my controllers in the bastardised OpenXR+WebXR project that I cobbled together, but I get a black screen on the snopekgames official tutorial.

I tried all the WebXR entries from the last three game jams and none of the them worked. Either would not load, would load but no controllers and no interaction.
Sadly itch.io blocks Wolvic stating it is not a compatible browser.

Not sure where I go from here. I thought that WebXR was done deal, with Firefox being the reference implementation and the WebXR API emulator being the default way everyone was developing.

Am I missing something?

I happy to revisit this if someone has something to add and I am not adverse to the answers being
Quest2 is not very powerful
The Meta browser is not very good
Firefox is not a reference implementation of WebXR
The Firefox add-on WebXR API emulator is not up to date.

but for now I have found that I can use WiVRn to test directly from Godot to the Quest 2(OpenXR not WebXR). Which seems pretty magical, especially as I run Gentoo linux :sweat_smile: