diff options
author | David Snopek <dsnopek@gmail.com> | 2022-11-18 20:43:11 -0600 |
---|---|---|
committer | David Snopek <dsnopek@gmail.com> | 2022-12-01 21:46:30 -0600 |
commit | 310bf39cd3f8c0e9ac6602e37fa9d65ca0d1c2e1 (patch) | |
tree | 0bca28bf11f672052a182e9cbe8c26c1ad23e44d /modules | |
parent | 84c404f6bcce9ba112118d77afd6bd70a92774d1 (diff) |
Get WebXR fully working in Godot 4!
Diffstat (limited to 'modules')
-rw-r--r-- | modules/webxr/doc_classes/WebXRInterface.xml | 105 | ||||
-rw-r--r-- | modules/webxr/godot_webxr.h | 41 | ||||
-rw-r--r-- | modules/webxr/native/library_godot_webxr.js | 460 | ||||
-rw-r--r-- | modules/webxr/native/webxr.externs.js | 682 | ||||
-rw-r--r-- | modules/webxr/webxr_interface.cpp | 23 | ||||
-rw-r--r-- | modules/webxr/webxr_interface.h | 14 | ||||
-rw-r--r-- | modules/webxr/webxr_interface_js.cpp | 487 | ||||
-rw-r--r-- | modules/webxr/webxr_interface_js.h | 156 |
8 files changed, 1560 insertions, 408 deletions
diff --git a/modules/webxr/doc_classes/WebXRInterface.xml b/modules/webxr/doc_classes/WebXRInterface.xml index 49dd9f7318..f5964eb4d1 100644 --- a/modules/webxr/doc_classes/WebXRInterface.xml +++ b/modules/webxr/doc_classes/WebXRInterface.xml @@ -1,13 +1,13 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="WebXRInterface" inherits="XRInterface" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> - AR/VR interface using WebXR. + XR interface using WebXR. </brief_description> <description> WebXR is an open standard that allows creating VR and AR applications that run in the web browser. As such, this interface is only available when running in Web exports. WebXR supports a wide range of devices, from the very capable (like Valve Index, HTC Vive, Oculus Rift and Quest) down to the much less capable (like Google Cardboard, Oculus Go, GearVR, or plain smartphones). - Since WebXR is based on JavaScript, it makes extensive use of callbacks, which means that [WebXRInterface] is forced to use signals, where other AR/VR interfaces would instead use functions that return a result immediately. This makes [WebXRInterface] quite a bit more complicated to initialize than other AR/VR interfaces. + Since WebXR is based on JavaScript, it makes extensive use of callbacks, which means that [WebXRInterface] is forced to use signals, where other XR interfaces would instead use functions that return a result immediately. This makes [WebXRInterface] quite a bit more complicated to initialize than other XR interfaces. Here's the minimum code required to start an immersive VR session: [codeblock] extends Node3D @@ -69,7 +69,7 @@ func _webxr_session_started(): $Button.visible = false # This tells Godot to start rendering to the headset. - get_viewport().xr = true + 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'. @@ -79,28 +79,35 @@ $Button.visible = true # If the user exits immersive mode, then we tell Godot to render to the web # page again. - get_viewport().xr = false + get_viewport().use_xr = false func _webxr_session_failed(message): OS.alert("Failed to initialize: " + message) [/codeblock] - There are several ways to handle "controller" input: - - Using [XRController3D] nodes and their [signal XRController3D.button_pressed] and [signal XRController3D.button_released] signals. This is how controllers are typically handled in AR/VR apps in Godot, however, this will only work with advanced VR controllers like the Oculus Touch or Index controllers, for example. The buttons codes are defined by [url=https://immersive-web.github.io/webxr-gamepads-module/#xr-standard-gamepad-mapping]Section 3.3 of the WebXR Gamepads Module[/url]. - - Using [method Node._unhandled_input] and [InputEventJoypadButton] or [InputEventJoypadMotion]. This works the same as normal joypads, except the [member InputEvent.device] starts at 100, so the left controller is 100 and the right controller is 101, and the button codes are also defined by [url=https://immersive-web.github.io/webxr-gamepads-module/#xr-standard-gamepad-mapping]Section 3.3 of the WebXR Gamepads Module[/url]. - - Using the [signal select], [signal squeeze] and related signals. This method will work for both advanced VR controllers, and non-traditional "controllers" like a tap on the screen, a spoken voice command or a button press on the device itself. - You can use one or all of these methods to allow your game or app to support a wider or narrower set of devices and input methods, or to allow more advanced interactions with more advanced devices. + There are a couple ways to handle "controller" input: + - Using [XRController3D] nodes and their [signal XRController3D.button_pressed] and [signal XRController3D.button_released] signals. This is how controllers are typically handled in XR apps in Godot, however, this will only work with advanced VR controllers like the Oculus Touch or Index controllers, for example. + - Using the [signal select], [signal squeeze] and related signals. This method will work for both advanced VR controllers, and non-traditional input sources like a tap on the screen, a spoken voice command or a button press on the device itself. + You can use both methods to allow your game or app to support a wider or narrower set of devices and input methods, or to allow more advanced interactions with more advanced devices. </description> <tutorials> <link title="How to make a VR game for WebXR with Godot">https://www.snopekgames.com/blog/2020/how-make-vr-game-webxr-godot</link> </tutorials> <methods> - <method name="get_controller" qualifiers="const"> + <method name="get_input_source_target_ray_mode" qualifiers="const"> + <return type="int" enum="WebXRInterface.TargetRayMode" /> + <param index="0" name="input_source_id" type="int" /> + <description> + Returns the target ray mode for the given [code]input_source_id[/code]. + This can help interpret the input coming from that input source. See [url=https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource/targetRayMode]XRInputSource.targetRayMode[/url] for more information. + </description> + </method> + <method name="get_input_source_tracker" qualifiers="const"> <return type="XRPositionalTracker" /> - <param index="0" name="controller_id" type="int" /> + <param index="0" name="input_source_id" type="int" /> <description> - Gets an [XRPositionalTracker] for the given [code]controller_id[/code]. - In the context of WebXR, a "controller" can be an advanced VR controller like the Oculus Touch or Index controllers, or even a tap on the screen, a spoken voice command or a button press on the device itself. When a non-traditional controller is used, interpret the position and orientation of the [XRPositionalTracker] as a ray pointing at the object the user wishes to interact with. - Use this method to get information about the controller that triggered one of these signals: + Gets an [XRPositionalTracker] for the given [code]input_source_id[/code]. + In the context of WebXR, an input source can be an advanced VR controller like the Oculus Touch or Index controllers, or even a tap on the screen, a spoken voice command or a button press on the device itself. When a non-traditional input source is used, interpret the position and orientation of the [XRPositionalTracker] as a ray pointing at the object the user wishes to interact with. + Use this method to get information about the input source that triggered one of these signals: - [signal selectstart] - [signal select] - [signal selectend] @@ -109,6 +116,13 @@ - [signal squeezestart] </description> </method> + <method name="is_input_source_active" qualifiers="const"> + <return type="bool" /> + <param index="0" name="input_source_id" type="int" /> + <description> + Returns [code]true[/code] if there is an active input source with the given [code]input_source_id[/code]. + </description> + </method> <method name="is_session_supported"> <return type="void" /> <param index="0" name="session_mode" type="String" /> @@ -120,11 +134,6 @@ </method> </methods> <members> - <member name="bounds_geometry" type="PackedVector3Array" setter="" getter="get_bounds_geometry"> - The vertices of a polygon which defines the boundaries of the user's play area. - This will only be available if [member reference_space_type] is [code]"bounded-floor"[/code] and only on certain browsers and devices that support it. - The [signal reference_space_reset] signal may indicate when this changes. - </member> <member name="optional_features" type="String" setter="set_optional_features" getter="get_optional_features"> A comma-seperated list of optional features used by [method XRInterface.initialize] when setting up the WebXR session. If a user's browser or device doesn't support one of the given features, initialization will continue, but you won't be able to use the requested feature. @@ -137,7 +146,7 @@ </member> <member name="requested_reference_space_types" type="String" setter="set_requested_reference_space_types" getter="get_requested_reference_space_types"> A comma-seperated list of reference space types used by [method XRInterface.initialize] when setting up the WebXR session. - The reference space types are requested in order, and the first on supported by the users device or browser will be used. The [member reference_space_type] property contains the reference space type that was ultimately used. + The reference space types are requested in order, and the first one supported by the users device or browser will be used. The [member reference_space_type] property contains the reference space type that was ultimately selected. This doesn't have any effect on the interface when already initialized. Possible values come from [url=https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpaceType]WebXR's XRReferenceSpaceType[/url]. If you want to use a particular reference space type, it must be listed in either [member required_features] or [member optional_features]. </member> @@ -161,35 +170,35 @@ <signal name="reference_space_reset"> <description> Emitted to indicate that the reference space has been reset or reconfigured. - When (or whether) this is emitted depends on the user's browser or device, but may include when the user has changed the dimensions of their play space (which you may be able to access via [member bounds_geometry]) or pressed/held a button to recenter their position. + When (or whether) this is emitted depends on the user's browser or device, but may include when the user has changed the dimensions of their play space (which you may be able to access via [method XRInterface.get_play_area]) or pressed/held a button to recenter their position. See [url=https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpace/reset_event]WebXR's XRReferenceSpace reset event[/url] for more information. </description> </signal> <signal name="select"> - <param index="0" name="controller_id" type="int" /> + <param index="0" name="input_source_id" type="int" /> <description> - Emitted after one of the "controllers" has finished its "primary action". - Use [method get_controller] to get more information about the controller. + Emitted after one of the input sources has finished its "primary action". + Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source. </description> </signal> <signal name="selectend"> - <param index="0" name="controller_id" type="int" /> + <param index="0" name="input_source_id" type="int" /> <description> - Emitted when one of the "controllers" has finished its "primary action". - Use [method get_controller] to get more information about the controller. + Emitted when one of the input sources has finished its "primary action". + Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source. </description> </signal> <signal name="selectstart"> - <param index="0" name="controller_id" type="int" /> + <param index="0" name="input_source_id" type="int" /> <description> - Emitted when one of the "controllers" has started its "primary action". - Use [method get_controller] to get more information about the controller. + Emitted when one of the input source has started its "primary action". + Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source. </description> </signal> <signal name="session_ended"> <description> Emitted when the user ends the WebXR session (which can be done using UI from the browser or device). - At this point, you should do [code]get_viewport().xr = false[/code] to instruct Godot to resume rendering to the screen. + At this point, you should do [code]get_viewport().use_xr = false[/code] to instruct Godot to resume rendering to the screen. </description> </signal> <signal name="session_failed"> @@ -202,7 +211,7 @@ <signal name="session_started"> <description> Emitted by [method XRInterface.initialize] if the session is successfully started. - At this point, it's safe to do [code]get_viewport().xr = true[/code] to instruct Godot to start rendering to the AR/VR device. + At this point, it's safe to do [code]get_viewport().use_xr = true[/code] to instruct Godot to start rendering to the XR device. </description> </signal> <signal name="session_supported"> @@ -213,24 +222,24 @@ </description> </signal> <signal name="squeeze"> - <param index="0" name="controller_id" type="int" /> + <param index="0" name="input_source_id" type="int" /> <description> - Emitted after one of the "controllers" has finished its "primary squeeze action". - Use [method get_controller] to get more information about the controller. + Emitted after one of the input sources has finished its "primary squeeze action". + Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source. </description> </signal> <signal name="squeezeend"> - <param index="0" name="controller_id" type="int" /> + <param index="0" name="input_source_id" type="int" /> <description> - Emitted when one of the "controllers" has finished its "primary squeeze action". - Use [method get_controller] to get more information about the controller. + Emitted when one of the input sources has finished its "primary squeeze action". + Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source. </description> </signal> <signal name="squeezestart"> - <param index="0" name="controller_id" type="int" /> + <param index="0" name="input_source_id" type="int" /> <description> - Emitted when one of the "controllers" has started its "primary squeeze action". - Use [method get_controller] to get more information about the controller. + Emitted when one of the input sources has started its "primary squeeze action". + Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source. </description> </signal> <signal name="visibility_state_changed"> @@ -239,4 +248,18 @@ </description> </signal> </signals> + <constants> + <constant name="TARGET_RAY_MODE_UNKNOWN" value="0" enum="TargetRayMode"> + We don't know the the target ray mode. + </constant> + <constant name="TARGET_RAY_MODE_GAZE" value="1" enum="TargetRayMode"> + Target ray originates at the viewer's eyes and points in the direction they are looking. + </constant> + <constant name="TARGET_RAY_MODE_TRACKED_POINTER" value="2" enum="TargetRayMode"> + Target ray from a handheld pointer, most likely a VR touch controller. + </constant> + <constant name="TARGET_RAY_MODE_SCREEN" value="3" enum="TargetRayMode"> + Target ray from touch screen, mouse or other tactile input device. + </constant> + </constants> </class> diff --git a/modules/webxr/godot_webxr.h b/modules/webxr/godot_webxr.h index d8d5bd99cc..e31a1d307e 100644 --- a/modules/webxr/godot_webxr.h +++ b/modules/webxr/godot_webxr.h @@ -37,12 +37,18 @@ extern "C" { #include "stddef.h" +enum WebXRInputEvent { + WEBXR_INPUT_EVENT_SELECTSTART, + WEBXR_INPUT_EVENT_SELECTEND, + WEBXR_INPUT_EVENT_SQUEEZESTART, + WEBXR_INPUT_EVENT_SQUEEZEEND, +}; + typedef void (*GodotWebXRSupportedCallback)(char *p_session_mode, int p_supported); typedef void (*GodotWebXRStartedCallback)(char *p_reference_space_type); typedef void (*GodotWebXREndedCallback)(); typedef void (*GodotWebXRFailedCallback)(char *p_message); -typedef void (*GodotWebXRControllerCallback)(); -typedef void (*GodotWebXRInputEventCallback)(char *p_signal_name, int p_controller_id); +typedef void (*GodotWebXRInputEventCallback)(int p_event_type, int p_input_source_id); typedef void (*GodotWebXRSimpleEventCallback)(char *p_signal_name); extern int godot_webxr_is_supported(); @@ -56,26 +62,33 @@ extern void godot_webxr_initialize( GodotWebXRStartedCallback p_on_session_started, GodotWebXREndedCallback p_on_session_ended, GodotWebXRFailedCallback p_on_session_failed, - GodotWebXRControllerCallback p_on_controller_changed, GodotWebXRInputEventCallback p_on_input_event, GodotWebXRSimpleEventCallback p_on_simple_event); extern void godot_webxr_uninitialize(); extern int godot_webxr_get_view_count(); -extern int *godot_webxr_get_render_target_size(); -extern float *godot_webxr_get_transform_for_eye(int p_eye); -extern float *godot_webxr_get_projection_for_eye(int p_eye); -extern void godot_webxr_commit(unsigned int p_texture); +extern bool godot_webxr_get_render_target_size(int *r_size); +extern bool godot_webxr_get_transform_for_view(int p_view, float *r_transform); +extern bool godot_webxr_get_projection_for_view(int p_view, float *r_transform); +extern unsigned int godot_webxr_get_color_texture(); +extern unsigned int godot_webxr_get_depth_texture(); +extern unsigned int godot_webxr_get_velocity_texture(); -extern void godot_webxr_sample_controller_data(); -extern int godot_webxr_get_controller_count(); -extern int godot_webxr_is_controller_connected(int p_controller); -extern float *godot_webxr_get_controller_transform(int p_controller); -extern int *godot_webxr_get_controller_buttons(int p_controller); -extern int *godot_webxr_get_controller_axes(int p_controller); +extern bool godot_webxr_update_input_source( + int p_input_source_id, + float *r_target_pose, + int *r_target_ray_mode, + int *r_touch_index, + int *r_has_grip_pose, + float *r_grip_pose, + int *r_has_standard_mapping, + int *r_button_count, + float *r_buttons, + int *r_axes_count, + float *r_axes); extern char *godot_webxr_get_visibility_state(); -extern int *godot_webxr_get_bounds_geometry(); +extern int godot_webxr_get_bounds_geometry(float **r_points); #ifdef __cplusplus } diff --git a/modules/webxr/native/library_godot_webxr.js b/modules/webxr/native/library_godot_webxr.js index 714768347c..eaf251d48f 100644 --- a/modules/webxr/native/library_godot_webxr.js +++ b/modules/webxr/native/library_godot_webxr.js @@ -33,9 +33,14 @@ const GodotWebXR = { gl: null, session: null, + gl_binding: null, + layer: null, space: null, frame: null, pose: null, + view_count: 1, + input_sources: new Array(16), + touches: new Array(5), // Monkey-patch the requestAnimationFrame() used by Emscripten for the main // loop, so that we can swap it out for XRSession.requestAnimationFrame() @@ -76,34 +81,128 @@ const GodotWebXR = { }, 0); }, - // Holds the controllers list between function calls. - controllers: [], + getLayer: () => { + const new_view_count = (GodotWebXR.pose) ? GodotWebXR.pose.views.length : 1; + let layer = GodotWebXR.layer; - // Updates controllers array, where the left hand (or sole tracker) is - // the first element, and the right hand is the second element, and any - // others placed at the 3rd position and up. - sampleControllers: () => { - if (!GodotWebXR.session || !GodotWebXR.frame) { - return; + // If the view count hasn't changed since creating this layer, then + // we can simply return it. + if (layer && GodotWebXR.view_count === new_view_count) { + return layer; } - let other_index = 2; - const controllers = []; - GodotWebXR.session.inputSources.forEach((input_source) => { - if (input_source.targetRayMode === 'tracked-pointer') { - if (input_source.handedness === 'right') { - controllers[1] = input_source; - } else if (input_source.handedness === 'left' || !controllers[0]) { - controllers[0] = input_source; + if (!GodotWebXR.session || !GodotWebXR.gl_binding) { + return null; + } + + const gl = GodotWebXR.gl; + + layer = GodotWebXR.gl_binding.createProjectionLayer({ + textureType: new_view_count > 1 ? 'texture-array' : 'texture', + colorFormat: gl.RGBA8, + depthFormat: gl.DEPTH_COMPONENT24, + }); + GodotWebXR.session.updateRenderState({ layers: [layer] }); + + GodotWebXR.layer = layer; + GodotWebXR.view_count = new_view_count; + return layer; + }, + + getSubImage: () => { + if (!GodotWebXR.pose) { + return null; + } + const layer = GodotWebXR.getLayer(); + if (layer === null) { + return null; + } + + // Because we always use "texture-array" for multiview and "texture" + // when there is only 1 view, it should be safe to only grab the + // subimage for the first view. + return GodotWebXR.gl_binding.getViewSubImage(layer, GodotWebXR.pose.views[0]); + }, + + getTextureId: (texture) => { + if (texture.name !== undefined) { + return texture.name; + } + + const id = GL.getNewId(GL.textures); + texture.name = id; + GL.textures[id] = texture; + + return id; + }, + + addInputSource: (input_source) => { + let name = -1; + if (input_source.targetRayMode === 'tracked-pointer' && input_source.handedness === 'left') { + name = 0; + } else if (input_source.targetRayMode === 'tracked-pointer' && input_source.handedness === 'right') { + name = 1; + } else { + for (let i = 2; i < 16; i++) { + if (!GodotWebXR.input_sources[i]) { + name = i; + break; } - } else { - controllers[other_index++] = input_source; } - }); - GodotWebXR.controllers = controllers; + } + if (name >= 0) { + GodotWebXR.input_sources[name] = input_source; + input_source.name = name; + + // Find a free touch index for screen sources. + if (input_source.targetRayMode === 'screen') { + let touch_index = -1; + for (let i = 0; i < 5; i++) { + if (!GodotWebXR.touches[i]) { + touch_index = i; + break; + } + } + if (touch_index >= 0) { + GodotWebXR.touches[touch_index] = input_source; + input_source.touch_index = touch_index; + } + } + } + return name; }, - getControllerId: (input_source) => GodotWebXR.controllers.indexOf(input_source), + removeInputSource: (input_source) => { + if (input_source.name !== undefined) { + const name = input_source.name; + if (name >= 0 && name < 16) { + GodotWebXR.input_sources[name] = null; + } + + if (input_source.touch_index !== undefined) { + const touch_index = input_source.touch_index; + if (touch_index >= 0 && touch_index < 5) { + GodotWebXR.touches[touch_index] = null; + } + } + return name; + } + return -1; + }, + + getInputSourceId: (input_source) => { + if (input_source !== undefined) { + return input_source.name; + } + return -1; + }, + + getTouchIndex: (input_source) => { + if (input_source.touch_index !== undefined) { + return input_source.touch_index; + } + return -1; + }, }, godot_webxr_is_supported__proxy: 'sync', @@ -132,8 +231,8 @@ const GodotWebXR = { godot_webxr_initialize__deps: ['emscripten_webgl_get_current_context'], godot_webxr_initialize__proxy: 'sync', - godot_webxr_initialize__sig: 'viiiiiiiiii', - godot_webxr_initialize: function (p_session_mode, p_required_features, p_optional_features, p_requested_reference_spaces, p_on_session_started, p_on_session_ended, p_on_session_failed, p_on_controller_changed, p_on_input_event, p_on_simple_event) { + godot_webxr_initialize__sig: 'viiiiiiiii', + godot_webxr_initialize: function (p_session_mode, p_required_features, p_optional_features, p_requested_reference_spaces, p_on_session_started, p_on_session_ended, p_on_session_failed, p_on_input_event, p_on_simple_event) { GodotWebXR.monkeyPatchRequestAnimationFrame(true); const session_mode = GodotRuntime.parseString(p_session_mode); @@ -143,7 +242,6 @@ const GodotWebXR = { const onstarted = GodotRuntime.get_func(p_on_session_started); const onended = GodotRuntime.get_func(p_on_session_ended); const onfailed = GodotRuntime.get_func(p_on_session_failed); - const oncontroller = GodotRuntime.get_func(p_on_controller_changed); const oninputevent = GodotRuntime.get_func(p_on_input_event); const onsimpleevent = GodotRuntime.get_func(p_on_simple_event); @@ -163,24 +261,18 @@ const GodotWebXR = { }); session.addEventListener('inputsourceschange', function (evt) { - let controller_changed = false; - [evt.added, evt.removed].forEach((lst) => { - lst.forEach((input_source) => { - if (input_source.targetRayMode === 'tracked-pointer') { - controller_changed = true; - } - }); - }); - if (controller_changed) { - oncontroller(); - } + evt.added.forEach(GodotWebXR.addInputSource); + evt.removed.forEach(GodotWebXR.removeInputSource); }); - ['selectstart', 'select', 'selectend', 'squeezestart', 'squeeze', 'squeezeend'].forEach((input_event) => { + ['selectstart', 'selectend', 'squeezestart', 'squeezeend'].forEach((input_event, index) => { session.addEventListener(input_event, function (evt) { - const c_str = GodotRuntime.allocString(input_event); - oninputevent(c_str, GodotWebXR.getControllerId(evt.inputSource)); - GodotRuntime.free(c_str); + // Since this happens in-between normal frames, we need to + // grab the frame from the event in order to get poses for + // the input sources. + GodotWebXR.frame = evt.frame; + oninputevent(index, GodotWebXR.getInputSourceId(evt.inputSource)); + GodotWebXR.frame = null; }); }); @@ -195,9 +287,10 @@ const GodotWebXR = { GodotWebXR.gl = gl; gl.makeXRCompatible().then(function () { - session.updateRenderState({ - baseLayer: new XRWebGLLayer(session, gl), - }); + GodotWebXR.gl_binding = new XRWebGLBinding(session, gl); // eslint-disable-line no-undef + + // This will trigger the layer to get created. + GodotWebXR.getLayer(); function onReferenceSpaceSuccess(reference_space, reference_space_type) { GodotWebXR.space = reference_space; @@ -266,9 +359,14 @@ const GodotWebXR = { } GodotWebXR.session = null; + GodotWebXR.gl_binding = null; + GodotWebXR.layer = null; GodotWebXR.space = null; GodotWebXR.frame = null; GodotWebXR.pose = null; + GodotWebXR.view_count = 1; + GodotWebXR.input_sources = new Array(16); + GodotWebXR.touches = new Array(5); // Disable the monkey-patched window.requestAnimationFrame() and // pause/restart the main loop to activate it on all platforms. @@ -280,215 +378,186 @@ const GodotWebXR = { godot_webxr_get_view_count__sig: 'i', godot_webxr_get_view_count: function () { if (!GodotWebXR.session || !GodotWebXR.pose) { - return 0; + return 1; } - return GodotWebXR.pose.views.length; + const view_count = GodotWebXR.pose.views.length; + return view_count > 0 ? view_count : 1; }, godot_webxr_get_render_target_size__proxy: 'sync', - godot_webxr_get_render_target_size__sig: 'i', - godot_webxr_get_render_target_size: function () { - if (!GodotWebXR.session || !GodotWebXR.pose) { - return 0; + godot_webxr_get_render_target_size__sig: 'ii', + godot_webxr_get_render_target_size: function (r_size) { + const subimage = GodotWebXR.getSubImage(); + if (subimage === null) { + return false; } - const glLayer = GodotWebXR.session.renderState.baseLayer; - const view = GodotWebXR.pose.views[0]; - const viewport = glLayer.getViewport(view); + GodotRuntime.setHeapValue(r_size + 0, subimage.viewport.width, 'i32'); + GodotRuntime.setHeapValue(r_size + 4, subimage.viewport.height, 'i32'); - const buf = GodotRuntime.malloc(2 * 4); - GodotRuntime.setHeapValue(buf + 0, viewport.width, 'i32'); - GodotRuntime.setHeapValue(buf + 4, viewport.height, 'i32'); - return buf; + return true; }, - godot_webxr_get_transform_for_eye__proxy: 'sync', - godot_webxr_get_transform_for_eye__sig: 'ii', - godot_webxr_get_transform_for_eye: function (p_eye) { + godot_webxr_get_transform_for_view__proxy: 'sync', + godot_webxr_get_transform_for_view__sig: 'iii', + godot_webxr_get_transform_for_view: function (p_view, r_transform) { if (!GodotWebXR.session || !GodotWebXR.pose) { - return 0; + return false; } const views = GodotWebXR.pose.views; let matrix; - if (p_eye === 0) { - matrix = GodotWebXR.pose.transform.matrix; + if (p_view >= 0) { + matrix = views[p_view].transform.matrix; } else { - matrix = views[p_eye - 1].transform.matrix; - } - const buf = GodotRuntime.malloc(16 * 4); - for (let i = 0; i < 16; i++) { - GodotRuntime.setHeapValue(buf + (i * 4), matrix[i], 'float'); - } - return buf; - }, - - godot_webxr_get_projection_for_eye__proxy: 'sync', - godot_webxr_get_projection_for_eye__sig: 'ii', - godot_webxr_get_projection_for_eye: function (p_eye) { - if (!GodotWebXR.session || !GodotWebXR.pose) { - return 0; + // For -1 (or any other negative value) return the HMD transform. + matrix = GodotWebXR.pose.transform.matrix; } - const view_index = (p_eye === 2 /* ARVRInterface::EYE_RIGHT */) ? 1 : 0; - const matrix = GodotWebXR.pose.views[view_index].projectionMatrix; - const buf = GodotRuntime.malloc(16 * 4); for (let i = 0; i < 16; i++) { - GodotRuntime.setHeapValue(buf + (i * 4), matrix[i], 'float'); + GodotRuntime.setHeapValue(r_transform + (i * 4), matrix[i], 'float'); } - return buf; + + return true; }, - godot_webxr_commit__proxy: 'sync', - godot_webxr_commit__sig: 'vi', - godot_webxr_commit: function (p_texture) { + godot_webxr_get_projection_for_view__proxy: 'sync', + godot_webxr_get_projection_for_view__sig: 'iii', + godot_webxr_get_projection_for_view: function (p_view, r_transform) { if (!GodotWebXR.session || !GodotWebXR.pose) { - return; + return false; } - const glLayer = GodotWebXR.session.renderState.baseLayer; - const views = GodotWebXR.pose.views; - const gl = GodotWebXR.gl; - - const texture = GL.textures[p_texture]; - - const orig_framebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); - const orig_read_framebuffer = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING); - const orig_read_buffer = gl.getParameter(gl.READ_BUFFER); - const orig_draw_framebuffer = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING); - - // Copy from Godot render target into framebuffer from WebXR. - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - for (let i = 0; i < views.length; i++) { - const viewport = glLayer.getViewport(views[i]); - - const read_fbo = gl.createFramebuffer(); - gl.bindFramebuffer(gl.READ_FRAMEBUFFER, read_fbo); - if (views.length > 1) { - gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, texture, 0, i); - } else { - gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); - } - gl.readBuffer(gl.COLOR_ATTACHMENT0); - gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, glLayer.framebuffer); - - // Flip Y upside down on destination. - gl.blitFramebuffer(0, 0, viewport.width, viewport.height, - viewport.x, viewport.y + viewport.height, viewport.x + viewport.width, viewport.y, - gl.COLOR_BUFFER_BIT, gl.NEAREST); - - gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); - gl.deleteFramebuffer(read_fbo); + const matrix = GodotWebXR.pose.views[p_view].projectionMatrix; + for (let i = 0; i < 16; i++) { + GodotRuntime.setHeapValue(r_transform + (i * 4), matrix[i], 'float'); } - // Restore state. - gl.bindFramebuffer(gl.FRAMEBUFFER, orig_framebuffer); - gl.bindFramebuffer(gl.READ_FRAMEBUFFER, orig_read_framebuffer); - gl.readBuffer(orig_read_buffer); - gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, orig_draw_framebuffer); + return true; }, - godot_webxr_sample_controller_data__proxy: 'sync', - godot_webxr_sample_controller_data__sig: 'v', - godot_webxr_sample_controller_data: function () { - GodotWebXR.sampleControllers(); + godot_webxr_get_color_texture__proxy: 'sync', + godot_webxr_get_color_texture__sig: 'i', + godot_webxr_get_color_texture: function () { + const subimage = GodotWebXR.getSubImage(); + if (subimage === null) { + return 0; + } + return GodotWebXR.getTextureId(subimage.colorTexture); }, - godot_webxr_get_controller_count__proxy: 'sync', - godot_webxr_get_controller_count__sig: 'i', - godot_webxr_get_controller_count: function () { - if (!GodotWebXR.session || !GodotWebXR.frame) { + godot_webxr_get_depth_texture__proxy: 'sync', + godot_webxr_get_depth_texture__sig: 'i', + godot_webxr_get_depth_texture: function () { + const subimage = GodotWebXR.getSubImage(); + if (subimage === null) { return 0; } - return GodotWebXR.controllers.length; + if (!subimage.depthStencilTexture) { + return 0; + } + return GodotWebXR.getTextureId(subimage.depthStencilTexture); }, - godot_webxr_is_controller_connected__proxy: 'sync', - godot_webxr_is_controller_connected__sig: 'ii', - godot_webxr_is_controller_connected: function (p_controller) { - if (!GodotWebXR.session || !GodotWebXR.frame) { - return false; + godot_webxr_get_velocity_texture__proxy: 'sync', + godot_webxr_get_velocity_texture__sig: 'i', + godot_webxr_get_velocity_texture: function () { + const subimage = GodotWebXR.getSubImage(); + if (subimage === null) { + return 0; + } + if (!subimage.motionVectorTexture) { + return 0; } - return !!GodotWebXR.controllers[p_controller]; + return GodotWebXR.getTextureId(subimage.motionVectorTexture); }, - godot_webxr_get_controller_transform__proxy: 'sync', - godot_webxr_get_controller_transform__sig: 'ii', - godot_webxr_get_controller_transform: function (p_controller) { + godot_webxr_update_input_source__proxy: 'sync', + godot_webxr_update_input_source__sig: 'iiiiiiiiiiii', + godot_webxr_update_input_source: function (p_input_source_id, r_target_pose, r_target_ray_mode, r_touch_index, r_has_grip_pose, r_grip_pose, r_has_standard_mapping, r_button_count, r_buttons, r_axes_count, r_axes) { if (!GodotWebXR.session || !GodotWebXR.frame) { return 0; } - const controller = GodotWebXR.controllers[p_controller]; - if (!controller) { - return 0; + if (p_input_source_id < 0 || p_input_source_id >= GodotWebXR.input_sources.length || !GodotWebXR.input_sources[p_input_source_id]) { + return false; } + const input_source = GodotWebXR.input_sources[p_input_source_id]; const frame = GodotWebXR.frame; const space = GodotWebXR.space; - const pose = frame.getPose(controller.targetRaySpace, space); - if (!pose) { + // Target pose. + const target_pose = frame.getPose(input_source.targetRaySpace, space); + if (!target_pose) { // This can mean that the controller lost tracking. - return 0; + return false; } - const matrix = pose.transform.matrix; - - const buf = GodotRuntime.malloc(16 * 4); + const target_pose_matrix = target_pose.transform.matrix; for (let i = 0; i < 16; i++) { - GodotRuntime.setHeapValue(buf + (i * 4), matrix[i], 'float'); + GodotRuntime.setHeapValue(r_target_pose + (i * 4), target_pose_matrix[i], 'float'); } - return buf; - }, - godot_webxr_get_controller_buttons__proxy: 'sync', - godot_webxr_get_controller_buttons__sig: 'ii', - godot_webxr_get_controller_buttons: function (p_controller) { - if (GodotWebXR.controllers.length === 0) { - return 0; - } + // Target ray mode. + let target_ray_mode = 0; + switch (input_source.targetRayMode) { + case 'gaze': + target_ray_mode = 1; + break; - const controller = GodotWebXR.controllers[p_controller]; - if (!controller || !controller.gamepad) { - return 0; - } - - const button_count = controller.gamepad.buttons.length; + case 'tracked-pointer': + target_ray_mode = 2; + break; - const buf = GodotRuntime.malloc((button_count + 1) * 4); - GodotRuntime.setHeapValue(buf, button_count, 'i32'); - for (let i = 0; i < button_count; i++) { - GodotRuntime.setHeapValue(buf + 4 + (i * 4), controller.gamepad.buttons[i].value, 'float'); - } - return buf; - }, + case 'screen': + target_ray_mode = 3; + break; - godot_webxr_get_controller_axes__proxy: 'sync', - godot_webxr_get_controller_axes__sig: 'ii', - godot_webxr_get_controller_axes: function (p_controller) { - if (GodotWebXR.controllers.length === 0) { - return 0; + default: } - - const controller = GodotWebXR.controllers[p_controller]; - if (!controller || !controller.gamepad) { - return 0; + GodotRuntime.setHeapValue(r_target_ray_mode, target_ray_mode, 'i32'); + + // Touch index. + GodotRuntime.setHeapValue(r_touch_index, GodotWebXR.getTouchIndex(input_source), 'i32'); + + // Grip pose. + let has_grip_pose = false; + if (input_source.gripSpace) { + const grip_pose = frame.getPose(input_source.gripSpace, space); + if (grip_pose) { + const grip_pose_matrix = grip_pose.transform.matrix; + for (let i = 0; i < 16; i++) { + GodotRuntime.setHeapValue(r_grip_pose + (i * 4), grip_pose_matrix[i], 'float'); + } + has_grip_pose = true; + } } + GodotRuntime.setHeapValue(r_has_grip_pose, has_grip_pose ? 1 : 0, 'i32'); + + // Gamepad data (mapping, buttons and axes). + let has_standard_mapping = false; + let button_count = 0; + let axes_count = 0; + if (input_source.gamepad) { + if (input_source.gamepad.mapping === 'xr-standard') { + has_standard_mapping = true; + } - const axes_count = controller.gamepad.axes.length; + button_count = Math.min(input_source.gamepad.buttons.length, 10); + for (let i = 0; i < button_count; i++) { + GodotRuntime.setHeapValue(r_buttons + (i * 4), input_source.gamepad.buttons[i].value, 'float'); + } - const buf = GodotRuntime.malloc((axes_count + 1) * 4); - GodotRuntime.setHeapValue(buf, axes_count, 'i32'); - for (let i = 0; i < axes_count; i++) { - let value = controller.gamepad.axes[i]; - if (i === 1 || i === 3) { - // Invert the Y-axis on thumbsticks and trackpads, in order to - // match OpenXR and other XR platform SDKs. - value *= -1.0; + axes_count = Math.min(input_source.gamepad.axes.length, 10); + for (let i = 0; i < axes_count; i++) { + GodotRuntime.setHeapValue(r_axes + (i * 4), input_source.gamepad.axes[i], 'float'); } - GodotRuntime.setHeapValue(buf + 4 + (i * 4), value, 'float'); } - return buf; + GodotRuntime.setHeapValue(r_has_standard_mapping, has_standard_mapping ? 1 : 0, 'i32'); + GodotRuntime.setHeapValue(r_button_count, button_count, 'i32'); + GodotRuntime.setHeapValue(r_axes_count, axes_count, 'i32'); + + return true; }, godot_webxr_get_visibility_state__proxy: 'sync', @@ -502,8 +571,8 @@ const GodotWebXR = { }, godot_webxr_get_bounds_geometry__proxy: 'sync', - godot_webxr_get_bounds_geometry__sig: 'i', - godot_webxr_get_bounds_geometry: function () { + godot_webxr_get_bounds_geometry__sig: 'ii', + godot_webxr_get_bounds_geometry: function (r_points) { if (!GodotWebXR.space || !GodotWebXR.space.boundsGeometry) { return 0; } @@ -513,7 +582,7 @@ const GodotWebXR = { return 0; } - const buf = GodotRuntime.malloc(((point_count * 3) + 1) * 4); + const buf = GodotRuntime.malloc(point_count * 3 * 4); GodotRuntime.setHeapValue(buf, point_count, 'i32'); for (let i = 0; i < point_count; i++) { const point = GodotWebXR.space.boundsGeometry[i]; @@ -521,8 +590,9 @@ const GodotWebXR = { GodotRuntime.setHeapValue(buf + ((i * 3) + 2) * 4, point.y, 'float'); GodotRuntime.setHeapValue(buf + ((i * 3) + 3) * 4, point.z, 'float'); } + GodotRuntime.setHeapValue(r_points, buf, 'i32'); - return buf; + return point_count; }, }; diff --git a/modules/webxr/native/webxr.externs.js b/modules/webxr/native/webxr.externs.js index 9ea105aa93..4b88820b19 100644 --- a/modules/webxr/native/webxr.externs.js +++ b/modules/webxr/native/webxr.externs.js @@ -1,3 +1,7 @@ +/* + * WebXR Device API + */ + /** * @type {XR} */ @@ -497,3 +501,681 @@ XRPose.prototype.transform; * @type {boolean} */ XRPose.prototype.emulatedPosition; + +/* + * WebXR Layers API Level 1 + */ + +/** + * @constructor XRLayer + */ +function XRLayer() {} + +/** + * @constructor XRLayerEventInit + */ +function XRLayerEventInit() {} + +/** + * @type {XRLayer} + */ +XRLayerEventInit.prototype.layer; + +/** + * @constructor XRLayerEvent + * + * @param {string} type + * @param {XRLayerEventInit} init + */ +function XRLayerEvent(type, init) {}; + +/** + * @type {XRLayer} + */ +XRLayerEvent.prototype.layer; + +/** + * @constructor XRCompositionLayer + * @extends {XRLayer} + */ +function XRCompositionLayer() {}; + +/** + * @type {string} + */ +XRCompositionLayer.prototype.layout; + +/** + * @type {boolean} + */ +XRCompositionLayer.prototype.blendTextureAberrationCorrection; + +/** + * @type {?boolean} + */ +XRCompositionLayer.prototype.chromaticAberrationCorrection; + +/** + * @type {boolean} + */ +XRCompositionLayer.prototype.forceMonoPresentation; + +/** + * @type {number} + */ +XRCompositionLayer.prototype.opacity; + +/** + * @type {number} + */ +XRCompositionLayer.prototype.mipLevels; + +/** + * @type {boolean} + */ +XRCompositionLayer.prototype.needsRedraw; + +/** + * @return {void} + */ +XRCompositionLayer.prototype.destroy = function () {}; + +/** + * @constructor XRProjectionLayer + * @extends {XRCompositionLayer} + */ +function XRProjectionLayer() {} + +/** + * @type {number} + */ +XRProjectionLayer.prototype.textureWidth; + +/** + * @type {number} + */ +XRProjectionLayer.prototype.textureHeight; + +/** + * @type {number} + */ +XRProjectionLayer.prototype.textureArrayLength; + +/** + * @type {boolean} + */ +XRProjectionLayer.prototype.ignoreDepthValues; + +/** + * @type {?number} + */ +XRProjectionLayer.prototype.fixedFoveation; + +/** + * @type {XRRigidTransform} + */ +XRProjectionLayer.prototype.deltaPose; + +/** + * @constructor XRQuadLayer + * @extends {XRCompositionLayer} + */ +function XRQuadLayer() {} + +/** + * @type {XRSpace} + */ +XRQuadLayer.prototype.space; + +/** + * @type {XRRigidTransform} + */ +XRQuadLayer.prototype.transform; + +/** + * @type {number} + */ +XRQuadLayer.prototype.width; + +/** + * @type {number} + */ +XRQuadLayer.prototype.height; + +/** + * @type {?function (XRLayerEvent)} + */ +XRQuadLayer.prototype.onredraw; + +/** + * @constructor XRCylinderLayer + * @extends {XRCompositionLayer} + */ +function XRCylinderLayer() {} + +/** + * @type {XRSpace} + */ +XRCylinderLayer.prototype.space; + +/** + * @type {XRRigidTransform} + */ +XRCylinderLayer.prototype.transform; + +/** + * @type {number} + */ +XRCylinderLayer.prototype.radius; + +/** + * @type {number} + */ +XRCylinderLayer.prototype.centralAngle; + +/** + * @type {number} + */ +XRCylinderLayer.prototype.aspectRatio; + +/** + * @type {?function (XRLayerEvent)} + */ +XRCylinderLayer.prototype.onredraw; + +/** + * @constructor XREquirectLayer + * @extends {XRCompositionLayer} + */ +function XREquirectLayer() {} + +/** + * @type {XRSpace} + */ +XREquirectLayer.prototype.space; + +/** + * @type {XRRigidTransform} + */ +XREquirectLayer.prototype.transform; + +/** + * @type {number} + */ +XREquirectLayer.prototype.radius; + +/** + * @type {number} + */ +XREquirectLayer.prototype.centralHorizontalAngle; + +/** + * @type {number} + */ +XREquirectLayer.prototype.upperVerticalAngle; + +/** + * @type {number} + */ +XREquirectLayer.prototype.lowerVerticalAngle; + +/** + * @type {?function (XRLayerEvent)} + */ +XREquirectLayer.prototype.onredraw; + +/** + * @constructor XRCubeLayer + * @extends {XRCompositionLayer} + */ +function XRCubeLayer() {} + +/** + * @type {XRSpace} + */ +XRCubeLayer.prototype.space; + +/** + * @type {DOMPointReadOnly} + */ +XRCubeLayer.prototype.orientation; + +/** + * @type {?function (XRLayerEvent)} + */ +XRCubeLayer.prototype.onredraw; + +/** + * @constructor XRSubImage + */ +function XRSubImage() {} + +/** + * @type {XRViewport} + */ +XRSubImage.prototype.viewport; + +/** + * @constructor XRWebGLSubImage + * @extends {XRSubImage} + */ +function XRWebGLSubImage () {} + +/** + * @type {WebGLTexture} + */ +XRWebGLSubImage.prototype.colorTexture; + +/** + * @type {?WebGLTexture} + */ +XRWebGLSubImage.prototype.depthStencilTexture; + +/** + * @type {?WebGLTexture} + */ +XRWebGLSubImage.prototype.motionVectorTexture; + +/** + * @type {?number} + */ +XRWebGLSubImage.prototype.imageIndex; + +/** + * @type {number} + */ +XRWebGLSubImage.prototype.colorTextureWidth; + +/** + * @type {number} + */ +XRWebGLSubImage.prototype.colorTextureHeight; + +/** + * @type {?number} + */ +XRWebGLSubImage.prototype.depthStencilTextureWidth; + +/** + * @type {?number} + */ +XRWebGLSubImage.prototype.depthStencilTextureHeight; + +/** + * @type {?number} + */ + +XRWebGLSubImage.prototype.motionVectorTextureWidth; + +/** + * @type {?number} + */ +XRWebGLSubImage.prototype.motionVectorTextureHeight; + +/** + * @constructor XRProjectionLayerInit + */ +function XRProjectionLayerInit() {} + +/** + * @type {string} + */ +XRProjectionLayerInit.prototype.textureType; + +/** + * @type {number} + */ +XRProjectionLayerInit.prototype.colorFormat; + +/** + * @type {number} + */ +XRProjectionLayerInit.prototype.depthFormat; + +/** + * @type {number} + */ +XRProjectionLayerInit.prototype.scaleFactor; + +/** + * @constructor XRLayerInit + */ +function XRLayerInit() {} + +/** + * @type {XRSpace} + */ +XRLayerInit.prototype.space; + +/** + * @type {number} + */ +XRLayerInit.prototype.colorFormat; + +/** + * @type {number} + */ +XRLayerInit.prototype.depthFormat; + +/** + * @type {number} + */ +XRLayerInit.prototype.mipLevels; + +/** + * @type {number} + */ +XRLayerInit.prototype.viewPixelWidth; + +/** + * @type {number} + */ +XRLayerInit.prototype.viewPixelHeight; + +/** + * @type {string} + */ +XRLayerInit.prototype.layout; + +/** + * @type {boolean} + */ +XRLayerInit.prototype.isStatic; + +/** + * @constructor XRQuadLayerInit + * @extends {XRLayerInit} + */ +function XRQuadLayerInit() {} + +/** + * @type {string} + */ +XRQuadLayerInit.prototype.textureType; + +/** + * @type {?XRRigidTransform} + */ +XRQuadLayerInit.prototype.transform; + +/** + * @type {number} + */ +XRQuadLayerInit.prototype.width; + +/** + * @type {number} + */ +XRQuadLayerInit.prototype.height; + +/** + * @constructor XRCylinderLayerInit + * @extends {XRLayerInit} + */ +function XRCylinderLayerInit() {} + +/** + * @type {string} + */ +XRCylinderLayerInit.prototype.textureType; + +/** + * @type {?XRRigidTransform} + */ +XRCylinderLayerInit.prototype.transform; + +/** + * @type {number} + */ +XRCylinderLayerInit.prototype.radius; + +/** + * @type {number} + */ +XRCylinderLayerInit.prototype.centralAngle; + +/** + * @type {number} + */ +XRCylinderLayerInit.prototype.aspectRatio; + +/** + * @constructor XREquirectLayerInit + * @extends {XRLayerInit} + */ +function XREquirectLayerInit() {} + +/** + * @type {string} + */ +XREquirectLayerInit.prototype.textureType; + +/** + * @type {?XRRigidTransform} + */ +XREquirectLayerInit.prototype.transform; + +/** + * @type {number} + */ +XREquirectLayerInit.prototype.radius; + +/** + * @type {number} + */ +XREquirectLayerInit.prototype.centralHorizontalAngle; + +/** + * @type {number} + */ +XREquirectLayerInit.prototype.upperVerticalAngle; + +/** + * @type {number} + */ +XREquirectLayerInit.prototype.lowerVerticalAngle; + +/** + * @constructor XRCubeLayerInit + * @extends {XRLayerInit} + */ +function XRCubeLayerInit() {} + +/** + * @type {DOMPointReadOnly} + */ +XRCubeLayerInit.prototype.orientation; + +/** + * @constructor XRWebGLBinding + * + * @param {XRSession} session + * @param {WebGLRenderContext|WebGL2RenderingContext} context + */ +function XRWebGLBinding(session, context) {} + +/** + * @type {number} + */ +XRWebGLBinding.prototype.nativeProjectionScaleFactor; + +/** + * @type {number} + */ +XRWebGLBinding.prototype.usesDepthValues; + +/** + * @param {XRProjectionLayerInit} init + * @return {XRProjectionLayer} + */ +XRWebGLBinding.prototype.createProjectionLayer = function (init) {}; + +/** + * @param {XRQuadLayerInit} init + * @return {XRQuadLayer} + */ +XRWebGLBinding.prototype.createQuadLayer = function (init) {}; + +/** + * @param {XRCylinderLayerInit} init + * @return {XRCylinderLayer} + */ +XRWebGLBinding.prototype.createCylinderLayer = function (init) {}; + +/** + * @param {XREquirectLayerInit} init + * @return {XREquirectLayer} + */ +XRWebGLBinding.prototype.createEquirectLayer = function (init) {}; + +/** + * @param {XRCubeLayerInit} init + * @return {XRCubeLayer} + */ +XRWebGLBinding.prototype.createCubeLayer = function (init) {}; + +/** + * @param {XRCompositionLayer} layer + * @param {XRFrame} frame + * @param {string} eye + * @return {XRWebGLSubImage} + */ +XRWebGLBinding.prototype.getSubImage = function (layer, frame, eye) {}; + +/** + * @param {XRProjectionLayer} layer + * @param {XRView} view + * @return {XRWebGLSubImage} + */ +XRWebGLBinding.prototype.getViewSubImage = function (layer, view) {}; + +/** + * @constructor XRMediaLayerInit + */ +function XRMediaLayerInit() {} + +/** + * @type {XRSpace} + */ +XRMediaLayerInit.prototype.space; + +/** + * @type {string} + */ +XRMediaLayerInit.prototype.layout; + +/** + * @type {boolean} + */ +XRMediaLayerInit.prototype.invertStereo; + +/** + * @constructor XRMediaQuadLayerInit + * @extends {XRMediaLayerInit} + */ +function XRMediaQuadLayerInit() {} + +/** + * @type {XRRigidTransform} + */ +XRMediaQuadLayerInit.prototype.transform; + +/** + * @type {number} + */ +XRMediaQuadLayerInit.prototype.width; + +/** + * @type {number} + */ +XRMediaQuadLayerInit.prototype.height; + +/** + * @constructor XRMediaCylinderLayerInit + * @extends {XRMediaLayerInit} + */ +function XRMediaCylinderLayerInit() {} + +/** + * @type {XRRigidTransform} + */ +XRMediaCylinderLayerInit.prototype.transform; + +/** + * @type {number} + */ +XRMediaCylinderLayerInit.prototype.radius; + +/** + * @type {number} + */ +XRMediaCylinderLayerInit.prototype.centralAngle; + +/** + * @type {?number} + */ +XRMediaCylinderLayerInit.prototype.aspectRatio; + +/** + * @constructor XRMediaEquirectLayerInit + * @extends {XRMediaLayerInit} + */ +function XRMediaEquirectLayerInit() {} + +/** + * @type {XRRigidTransform} + */ +XRMediaEquirectLayerInit.prototype.transform; + +/** + * @type {number} + */ +XRMediaEquirectLayerInit.prototype.radius; + +/** + * @type {number} + */ +XRMediaEquirectLayerInit.prototype.centralHorizontalAngle; + +/** + * @type {number} + */ +XRMediaEquirectLayerInit.prototype.upperVerticalAngle; + +/** + * @type {number} + */ +XRMediaEquirectLayerInit.prototype.lowerVerticalAngle; + +/** + * @constructor XRMediaBinding + * + * @param {XRSession} session + */ +function XRMediaBinding(session) {} + +/** + * @param {HTMLVideoElement} video + * @param {XRMediaQuadLayerInit} init + * @return {XRQuadLayer} + */ +XRMediaBinding.prototype.createQuadLayer = function(video, init) {}; + +/** + * @param {HTMLVideoElement} video + * @param {XRMediaCylinderLayerInit} init + * @return {XRCylinderLayer} + */ +XRMediaBinding.prototype.createCylinderLayer = function(video, init) {}; + +/** + * @param {HTMLVideoElement} video + * @param {XRMediaEquirectLayerInit} init + * @return {XREquirectLayer} + */ +XRMediaBinding.prototype.createEquirectLayer = function(video, init) {}; + +/** + * @type {Array<XRLayer>} + */ +XRRenderState.prototype.layers; diff --git a/modules/webxr/webxr_interface.cpp b/modules/webxr/webxr_interface.cpp index b0ad53523a..c0580df172 100644 --- a/modules/webxr/webxr_interface.cpp +++ b/modules/webxr/webxr_interface.cpp @@ -42,9 +42,10 @@ void WebXRInterface::_bind_methods() { ClassDB::bind_method(D_METHOD("get_reference_space_type"), &WebXRInterface::get_reference_space_type); ClassDB::bind_method(D_METHOD("set_requested_reference_space_types", "requested_reference_space_types"), &WebXRInterface::set_requested_reference_space_types); ClassDB::bind_method(D_METHOD("get_requested_reference_space_types"), &WebXRInterface::get_requested_reference_space_types); - ClassDB::bind_method(D_METHOD("get_controller", "controller_id"), &WebXRInterface::get_controller); + ClassDB::bind_method(D_METHOD("is_input_source_active", "input_source_id"), &WebXRInterface::is_input_source_active); + ClassDB::bind_method(D_METHOD("get_input_source_tracker", "input_source_id"), &WebXRInterface::get_input_source_tracker); + ClassDB::bind_method(D_METHOD("get_input_source_target_ray_mode", "input_source_id"), &WebXRInterface::get_input_source_target_ray_mode); ClassDB::bind_method(D_METHOD("get_visibility_state"), &WebXRInterface::get_visibility_state); - ClassDB::bind_method(D_METHOD("get_bounds_geometry"), &WebXRInterface::get_bounds_geometry); ADD_PROPERTY(PropertyInfo(Variant::STRING, "session_mode", PROPERTY_HINT_NONE), "set_session_mode", "get_session_mode"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "required_features", PROPERTY_HINT_NONE), "set_required_features", "get_required_features"); @@ -52,20 +53,24 @@ void WebXRInterface::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "requested_reference_space_types", PROPERTY_HINT_NONE), "set_requested_reference_space_types", "get_requested_reference_space_types"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "reference_space_type", PROPERTY_HINT_NONE), "", "get_reference_space_type"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "visibility_state", PROPERTY_HINT_NONE), "", "get_visibility_state"); - ADD_PROPERTY(PropertyInfo(Variant::PACKED_VECTOR3_ARRAY, "bounds_geometry", PROPERTY_HINT_NONE), "", "get_bounds_geometry"); ADD_SIGNAL(MethodInfo("session_supported", PropertyInfo(Variant::STRING, "session_mode"), PropertyInfo(Variant::BOOL, "supported"))); ADD_SIGNAL(MethodInfo("session_started")); ADD_SIGNAL(MethodInfo("session_ended")); ADD_SIGNAL(MethodInfo("session_failed", PropertyInfo(Variant::STRING, "message"))); - ADD_SIGNAL(MethodInfo("selectstart", PropertyInfo(Variant::INT, "controller_id"))); - ADD_SIGNAL(MethodInfo("select", PropertyInfo(Variant::INT, "controller_id"))); - ADD_SIGNAL(MethodInfo("selectend", PropertyInfo(Variant::INT, "controller_id"))); - ADD_SIGNAL(MethodInfo("squeezestart", PropertyInfo(Variant::INT, "controller_id"))); - ADD_SIGNAL(MethodInfo("squeeze", PropertyInfo(Variant::INT, "controller_id"))); - ADD_SIGNAL(MethodInfo("squeezeend", PropertyInfo(Variant::INT, "controller_id"))); + ADD_SIGNAL(MethodInfo("selectstart", PropertyInfo(Variant::INT, "input_source_id"))); + ADD_SIGNAL(MethodInfo("select", PropertyInfo(Variant::INT, "input_source_id"))); + ADD_SIGNAL(MethodInfo("selectend", PropertyInfo(Variant::INT, "input_source_id"))); + ADD_SIGNAL(MethodInfo("squeezestart", PropertyInfo(Variant::INT, "input_source_id"))); + ADD_SIGNAL(MethodInfo("squeeze", PropertyInfo(Variant::INT, "input_source_id"))); + ADD_SIGNAL(MethodInfo("squeezeend", PropertyInfo(Variant::INT, "input_source_id"))); ADD_SIGNAL(MethodInfo("visibility_state_changed")); ADD_SIGNAL(MethodInfo("reference_space_reset")); + + BIND_ENUM_CONSTANT(TARGET_RAY_MODE_UNKNOWN); + BIND_ENUM_CONSTANT(TARGET_RAY_MODE_GAZE); + BIND_ENUM_CONSTANT(TARGET_RAY_MODE_TRACKED_POINTER); + BIND_ENUM_CONSTANT(TARGET_RAY_MODE_SCREEN); } diff --git a/modules/webxr/webxr_interface.h b/modules/webxr/webxr_interface.h index 801643bfa6..1afeb5bab0 100644 --- a/modules/webxr/webxr_interface.h +++ b/modules/webxr/webxr_interface.h @@ -45,6 +45,13 @@ protected: static void _bind_methods(); public: + enum TargetRayMode { + TARGET_RAY_MODE_UNKNOWN, + TARGET_RAY_MODE_GAZE, + TARGET_RAY_MODE_TRACKED_POINTER, + TARGET_RAY_MODE_SCREEN, + }; + virtual void is_session_supported(const String &p_session_mode) = 0; virtual void set_session_mode(String p_session_mode) = 0; virtual String get_session_mode() const = 0; @@ -55,9 +62,12 @@ public: virtual void set_requested_reference_space_types(String p_requested_reference_space_types) = 0; virtual String get_requested_reference_space_types() const = 0; virtual String get_reference_space_type() const = 0; - virtual Ref<XRPositionalTracker> get_controller(int p_controller_id) const = 0; + virtual bool is_input_source_active(int p_input_source_id) const = 0; + virtual Ref<XRPositionalTracker> get_input_source_tracker(int p_input_source_id) const = 0; + virtual TargetRayMode get_input_source_target_ray_mode(int p_input_source_id) const = 0; virtual String get_visibility_state() const = 0; - virtual PackedVector3Array get_bounds_geometry() const = 0; }; +VARIANT_ENUM_CAST(WebXRInterface::TargetRayMode); + #endif // WEBXR_INTERFACE_H diff --git a/modules/webxr/webxr_interface_js.cpp b/modules/webxr/webxr_interface_js.cpp index f6ed9f027e..265f6626a7 100644 --- a/modules/webxr/webxr_interface_js.cpp +++ b/modules/webxr/webxr_interface_js.cpp @@ -37,6 +37,8 @@ #include "drivers/gles3/storage/texture_storage.h" #include "emscripten.h" #include "godot_webxr.h" +#include "scene/main/scene_tree.h" +#include "scene/main/window.h" #include "servers/rendering/renderer_compositor.h" #include "servers/rendering/rendering_server_globals.h" @@ -89,25 +91,14 @@ void _emwebxr_on_session_failed(char *p_message) { interface->emit_signal(SNAME("session_failed"), message); } -void _emwebxr_on_controller_changed() { +extern "C" EMSCRIPTEN_KEEPALIVE void _emwebxr_on_input_event(int p_event_type, int p_input_source_id) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); Ref<XRInterface> interface = xr_server->find_interface("WebXR"); ERR_FAIL_COND(interface.is_null()); - static_cast<WebXRInterfaceJS *>(interface.ptr())->_on_controller_changed(); -} - -extern "C" EMSCRIPTEN_KEEPALIVE void _emwebxr_on_input_event(char *p_signal_name, int p_input_source) { - XRServer *xr_server = XRServer::get_singleton(); - ERR_FAIL_NULL(xr_server); - - Ref<XRInterface> interface = xr_server->find_interface("WebXR"); - ERR_FAIL_COND(interface.is_null()); - - StringName signal_name = StringName(p_signal_name); - interface->emit_signal(signal_name, p_input_source + 1); + ((WebXRInterfaceJS *)interface.ptr())->_on_input_event(p_event_type, p_input_source_id); } extern "C" EMSCRIPTEN_KEEPALIVE void _emwebxr_on_simple_event(char *p_signal_name) { @@ -165,16 +156,22 @@ String WebXRInterfaceJS::get_reference_space_type() const { return reference_space_type; } -Ref<XRPositionalTracker> WebXRInterfaceJS::get_controller(int p_controller_id) const { - XRServer *xr_server = XRServer::get_singleton(); - ERR_FAIL_NULL_V(xr_server, Ref<XRPositionalTracker>()); +bool WebXRInterfaceJS::is_input_source_active(int p_input_source_id) const { + ERR_FAIL_INDEX_V(p_input_source_id, input_source_count, false); + return input_sources[p_input_source_id].active; +} - // TODO support more then two controllers - if (p_controller_id >= 0 && p_controller_id < 2) { - return controllers[p_controller_id]; - }; +Ref<XRPositionalTracker> WebXRInterfaceJS::get_input_source_tracker(int p_input_source_id) const { + ERR_FAIL_INDEX_V(p_input_source_id, input_source_count, Ref<XRPositionalTracker>()); + return input_sources[p_input_source_id].tracker; +} - return Ref<XRPositionalTracker>(); +WebXRInterface::TargetRayMode WebXRInterfaceJS::get_input_source_target_ray_mode(int p_input_source_id) const { + ERR_FAIL_INDEX_V(p_input_source_id, input_source_count, WebXRInterface::TARGET_RAY_MODE_UNKNOWN); + if (!input_sources[p_input_source_id].active) { + return WebXRInterface::TARGET_RAY_MODE_UNKNOWN; + } + return input_sources[p_input_source_id].target_ray_mode; } String WebXRInterfaceJS::get_visibility_state() const { @@ -188,17 +185,18 @@ String WebXRInterfaceJS::get_visibility_state() const { return String(); } -PackedVector3Array WebXRInterfaceJS::get_bounds_geometry() const { +PackedVector3Array WebXRInterfaceJS::get_play_area() const { PackedVector3Array ret; - int *js_bounds = godot_webxr_get_bounds_geometry(); - if (js_bounds) { - ret.resize(js_bounds[0]); - for (int i = 0; i < js_bounds[0]; i++) { - float *js_vector3 = ((float *)js_bounds) + (i * 3) + 1; + float *points; + int point_count = godot_webxr_get_bounds_geometry(&points); + if (point_count > 0) { + ret.resize(point_count); + for (int i = 0; i < point_count; i++) { + float *js_vector3 = points + (i * 3); ret.set(i, Vector3(js_vector3[0], js_vector3[1], js_vector3[2])); } - free(js_bounds); + free(points); } return ret; @@ -209,7 +207,7 @@ StringName WebXRInterfaceJS::get_name() const { }; uint32_t WebXRInterfaceJS::get_capabilities() const { - return XRInterface::XR_STEREO | XRInterface::XR_MONO; + return XRInterface::XR_STEREO | XRInterface::XR_MONO | XRInterface::XR_VR | XRInterface::XR_AR; }; uint32_t WebXRInterfaceJS::get_view_count() { @@ -261,7 +259,6 @@ bool WebXRInterfaceJS::initialize() { &_emwebxr_on_session_started, &_emwebxr_on_session_ended, &_emwebxr_on_session_failed, - &_emwebxr_on_controller_changed, &_emwebxr_on_input_event, &_emwebxr_on_simple_event); }; @@ -287,6 +284,18 @@ void WebXRInterfaceJS::uninitialize() { godot_webxr_uninitialize(); + GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage); + if (texture_storage != nullptr) { + for (KeyValue<unsigned int, RID> &E : texture_cache) { + // Forcibly mark as not part of a render target so we can free it. + GLES3::Texture *texture = texture_storage->get_texture(E.value); + texture->is_render_target = false; + + texture_storage->texture_free(E.value); + } + } + + texture_cache.clear(); reference_space_type = ""; initialized = false; }; @@ -316,27 +325,26 @@ Size2 WebXRInterfaceJS::get_render_target_size() { return render_targetsize; } - int *js_size = godot_webxr_get_render_target_size(); - if (!initialized || js_size == nullptr) { - // As a temporary default (until WebXR is fully initialized), use half the window size. - Size2 temp = DisplayServer::get_singleton()->window_get_size(); - temp.width /= 2.0; - return temp; - } + int js_size[2]; + bool has_size = godot_webxr_get_render_target_size(js_size); - render_targetsize.width = js_size[0]; - render_targetsize.height = js_size[1]; + if (!initialized || !has_size) { + // As a temporary default (until WebXR is fully initialized), use the + // window size. + return DisplayServer::get_singleton()->window_get_size(); + } - free(js_size); + render_targetsize.width = (float)js_size[0]; + render_targetsize.height = (float)js_size[1]; return render_targetsize; }; Transform3D WebXRInterfaceJS::get_camera_transform() { - Transform3D transform_for_eye; + Transform3D camera_transform; XRServer *xr_server = XRServer::get_singleton(); - ERR_FAIL_NULL_V(xr_server, transform_for_eye); + ERR_FAIL_NULL_V(xr_server, camera_transform); if (initialized) { float world_scale = xr_server->get_world_scale(); @@ -345,181 +353,382 @@ Transform3D WebXRInterfaceJS::get_camera_transform() { Transform3D _head_transform = head_transform; _head_transform.origin *= world_scale; - transform_for_eye = (xr_server->get_reference_frame()) * _head_transform; + camera_transform = (xr_server->get_reference_frame()) * _head_transform; } - return transform_for_eye; + return camera_transform; }; Transform3D WebXRInterfaceJS::get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) { - Transform3D transform_for_eye; - XRServer *xr_server = XRServer::get_singleton(); - ERR_FAIL_NULL_V(xr_server, transform_for_eye); + ERR_FAIL_NULL_V(xr_server, p_cam_transform); + ERR_FAIL_COND_V(!initialized, p_cam_transform); - float *js_matrix = godot_webxr_get_transform_for_eye(p_view + 1); - if (!initialized || js_matrix == nullptr) { - transform_for_eye = p_cam_transform; - return transform_for_eye; + float js_matrix[16]; + bool has_transform = godot_webxr_get_transform_for_view(p_view, js_matrix); + if (!has_transform) { + return p_cam_transform; } - transform_for_eye = _js_matrix_to_transform(js_matrix); - free(js_matrix); + Transform3D transform_for_view = _js_matrix_to_transform(js_matrix); float world_scale = xr_server->get_world_scale(); // Scale only the center point of our eye transform, so we don't scale the // distance between the eyes. Transform3D _head_transform = head_transform; - transform_for_eye.origin -= _head_transform.origin; + transform_for_view.origin -= _head_transform.origin; _head_transform.origin *= world_scale; - transform_for_eye.origin += _head_transform.origin; + transform_for_view.origin += _head_transform.origin; - return p_cam_transform * xr_server->get_reference_frame() * transform_for_eye; + return p_cam_transform * xr_server->get_reference_frame() * transform_for_view; }; Projection WebXRInterfaceJS::get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) { - Projection eye; + Projection view; + + ERR_FAIL_COND_V(!initialized, view); - float *js_matrix = godot_webxr_get_projection_for_eye(p_view + 1); - if (!initialized || js_matrix == nullptr) { - return eye; + float js_matrix[16]; + bool has_projection = godot_webxr_get_projection_for_view(p_view, js_matrix); + if (!has_projection) { + return view; } int k = 0; for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { - eye.columns[i][j] = js_matrix[k++]; + view.columns[i][j] = js_matrix[k++]; } } - free(js_matrix); - // Copied from godot_oculus_mobile's ovr_mobile_session.cpp - eye.columns[2][2] = -(p_z_far + p_z_near) / (p_z_far - p_z_near); - eye.columns[3][2] = -(2.0f * p_z_far * p_z_near) / (p_z_far - p_z_near); + view.columns[2][2] = -(p_z_far + p_z_near) / (p_z_far - p_z_near); + view.columns[3][2] = -(2.0f * p_z_far * p_z_near) / (p_z_far - p_z_near); - return eye; + return view; +} + +bool WebXRInterfaceJS::pre_draw_viewport(RID p_render_target) { + GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage); + if (texture_storage == nullptr) { + return false; + } + + GLES3::RenderTarget *rt = texture_storage->get_render_target(p_render_target); + if (rt == nullptr) { + return false; + } + + // Cache the resources so we don't have to get them from JS twice. + color_texture = _get_color_texture(); + depth_texture = _get_depth_texture(); + + // Per the WebXR spec, it returns "opaque textures" to us, which may be the + // same WebGLTexture object (which would be the same GLuint in C++) but + // represent a different underlying resource (probably the next texture in + // the XR device's swap chain). In order to render to this texture, we need + // to re-attach it to the FBO, otherwise we get an "incomplete FBO" error. + // + // See: https://immersive-web.github.io/layers/#xropaquetextures + // + // This is why we're doing this sort of silly check: if the color and depth + // textures are the same this frame as last frame, we need to attach them + // again, despite the fact that the GLuint for them hasn't changed. + if (rt->overridden.is_overridden && rt->overridden.color == color_texture && rt->overridden.depth == depth_texture) { + GLES3::Config *config = GLES3::Config::get_singleton(); + bool use_multiview = rt->view_count > 1 && config->multiview_supported; + + glBindFramebuffer(GL_FRAMEBUFFER, rt->fbo); + if (use_multiview) { + glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, rt->color, 0, 0, rt->view_count); + glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, rt->depth, 0, 0, rt->view_count); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, rt->color, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, rt->depth, 0); + } + glBindFramebuffer(GL_FRAMEBUFFER, texture_storage->system_fbo); + } + + return true; } Vector<BlitToScreen> WebXRInterfaceJS::post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) { Vector<BlitToScreen> blit_to_screen; - if (!initialized) { - return blit_to_screen; + // We don't need to do anything here. + + return blit_to_screen; +}; + +RID WebXRInterfaceJS::_get_color_texture() { + unsigned int texture_id = godot_webxr_get_color_texture(); + if (texture_id == 0) { + return RID(); + } + + return _get_texture(texture_id); +} + +RID WebXRInterfaceJS::_get_depth_texture() { + unsigned int texture_id = godot_webxr_get_depth_texture(); + if (texture_id == 0) { + return RID(); + } + + return _get_texture(texture_id); +} + +RID WebXRInterfaceJS::_get_texture(unsigned int p_texture_id) { + RBMap<unsigned int, RID>::Element *cache = texture_cache.find(p_texture_id); + if (cache != nullptr) { + return cache->get(); } GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage); - if (!texture_storage) { - return blit_to_screen; + if (texture_storage == nullptr) { + return RID(); } - GLES3::RenderTarget *rt = texture_storage->get_render_target(p_render_target); + uint32_t view_count = godot_webxr_get_view_count(); + Size2 texture_size = get_render_target_size(); - godot_webxr_commit(rt->color); + RID texture = texture_storage->texture_create_external( + view_count == 1 ? GLES3::Texture::TYPE_2D : GLES3::Texture::TYPE_LAYERED, + Image::FORMAT_RGBA8, + p_texture_id, + (int)texture_size.width, + (int)texture_size.height, + 1, + view_count); - return blit_to_screen; -}; + texture_cache.insert(p_texture_id, texture); + + return texture; +} + +RID WebXRInterfaceJS::get_color_texture() { + return color_texture; +} + +RID WebXRInterfaceJS::get_depth_texture() { + return depth_texture; +} + +RID WebXRInterfaceJS::get_velocity_texture() { + unsigned int texture_id = godot_webxr_get_velocity_texture(); + if (texture_id == 0) { + return RID(); + } + + return _get_texture(texture_id); +} void WebXRInterfaceJS::process() { if (initialized) { // Get the "head" position. - float *js_matrix = godot_webxr_get_transform_for_eye(0); - if (js_matrix != nullptr) { + float js_matrix[16]; + if (godot_webxr_get_transform_for_view(-1, js_matrix)) { head_transform = _js_matrix_to_transform(js_matrix); - free(js_matrix); } if (head_tracker.is_valid()) { head_tracker->set_pose("default", head_transform, Vector3(), Vector3()); } - godot_webxr_sample_controller_data(); - int controller_count = godot_webxr_get_controller_count(); - for (int i = 0; i < controller_count; i++) { - _update_tracker(i); + // Update all input sources. + for (int i = 0; i < input_source_count; i++) { + _update_input_source(i); } }; }; -void WebXRInterfaceJS::_update_tracker(int p_controller_id) { +void WebXRInterfaceJS::_update_input_source(int p_input_source_id) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); - // need to support more then two controllers... - if (p_controller_id < 0 || p_controller_id > 1) { + InputSource &input_source = input_sources[p_input_source_id]; + + float target_pose[16]; + int tmp_target_ray_mode; + int touch_index; + int has_grip_pose; + float grip_pose[16]; + int has_standard_mapping; + int button_count; + float buttons[10]; + int axes_count; + float axes[10]; + + input_source.active = godot_webxr_update_input_source( + p_input_source_id, + target_pose, + &tmp_target_ray_mode, + &touch_index, + &has_grip_pose, + grip_pose, + &has_standard_mapping, + &button_count, + buttons, + &axes_count, + axes); + + if (!input_source.active) { + if (input_source.tracker.is_valid()) { + xr_server->remove_tracker(input_source.tracker); + input_source.tracker.unref(); + } return; } - Ref<XRPositionalTracker> tracker = controllers[p_controller_id]; - if (godot_webxr_is_controller_connected(p_controller_id)) { - if (tracker.is_null()) { - tracker.instantiate(); - tracker->set_tracker_type(XRServer::TRACKER_CONTROLLER); - // Controller id's 0 and 1 are always the left and right hands. - if (p_controller_id < 2) { - tracker->set_tracker_name(p_controller_id == 0 ? "left_hand" : "right_hand"); - tracker->set_tracker_desc(p_controller_id == 0 ? "Left hand controller" : "Right hand controller"); - tracker->set_tracker_hand(p_controller_id == 0 ? XRPositionalTracker::TRACKER_HAND_LEFT : XRPositionalTracker::TRACKER_HAND_RIGHT); - } else { - char name[1024]; - sprintf(name, "tracker_%i", p_controller_id); - tracker->set_tracker_name(name); - tracker->set_tracker_desc(name); - } - xr_server->add_tracker(tracker); + input_source.target_ray_mode = (WebXRInterface::TargetRayMode)tmp_target_ray_mode; + input_source.touch_index = touch_index; + + Ref<XRPositionalTracker> &tracker = input_source.tracker; + + if (tracker.is_null()) { + tracker.instantiate(); + + StringName tracker_name; + if (input_source.target_ray_mode == WebXRInterface::TargetRayMode::TARGET_RAY_MODE_SCREEN) { + tracker_name = touch_names[touch_index]; + } else { + tracker_name = tracker_names[p_input_source_id]; } - float *tracker_matrix = godot_webxr_get_controller_transform(p_controller_id); - if (tracker_matrix) { - // Note, poses should NOT have world scale and our reference frame applied! - Transform3D transform = _js_matrix_to_transform(tracker_matrix); - tracker->set_pose("default", transform, Vector3(), Vector3()); - free(tracker_matrix); + // Input source id's 0 and 1 are always the left and right hands. + if (p_input_source_id < 2) { + tracker->set_tracker_type(XRServer::TRACKER_CONTROLLER); + tracker->set_tracker_name(tracker_name); + tracker->set_tracker_desc(p_input_source_id == 0 ? "Left hand controller" : "Right hand controller"); + tracker->set_tracker_hand(p_input_source_id == 0 ? XRPositionalTracker::TRACKER_HAND_LEFT : XRPositionalTracker::TRACKER_HAND_RIGHT); + } else { + tracker->set_tracker_name(tracker_name); + tracker->set_tracker_desc(tracker_name); } + xr_server->add_tracker(tracker); + } - // TODO implement additional poses such as "aim" and "grip" + Transform3D aim_transform = _js_matrix_to_transform(target_pose); + tracker->set_pose(SNAME("default"), aim_transform, Vector3(), Vector3()); + tracker->set_pose(SNAME("aim"), aim_transform, Vector3(), Vector3()); + if (has_grip_pose) { + tracker->set_pose(SNAME("grip"), _js_matrix_to_transform(grip_pose), Vector3(), Vector3()); + } - int *buttons = godot_webxr_get_controller_buttons(p_controller_id); - if (buttons) { - // TODO buttons should be named properly, this is just a temporary fix - for (int i = 0; i < buttons[0]; i++) { - char name[1024]; - sprintf(name, "button_%i", i); + for (int i = 0; i < button_count; i++) { + StringName button_name = has_standard_mapping ? standard_button_names[i] : unknown_button_names[i]; + StringName button_pressure_name = has_standard_mapping ? standard_button_pressure_names[i] : unknown_button_pressure_names[i]; + float value = buttons[i]; + bool state = value > 0.0; + tracker->set_input(button_name, state); + tracker->set_input(button_pressure_name, value); + } - float value = *((float *)buttons + (i + 1)); - bool state = value > 0.0; - tracker->set_input(name, state); - } - free(buttons); + for (int i = 0; i < axes_count; i++) { + StringName axis_name = has_standard_mapping ? standard_axis_names[i] : unknown_axis_names[i]; + float value = axes[i]; + if (has_standard_mapping && (i == 1 || i == 3)) { + // Invert the Y-axis on thumbsticks and trackpads, in order to + // match OpenXR and other XR platform SDKs. + value = -value; } + tracker->set_input(axis_name, value); + } - int *axes = godot_webxr_get_controller_axes(p_controller_id); - if (axes) { - // TODO again just a temporary fix, split these between proper float and vector2 inputs - for (int i = 0; i < axes[0]; i++) { - char name[1024]; - sprintf(name, "axis_%i", i); + // Also create Vector2's for the thumbstick and trackpad when we have the + // standard mapping. + if (has_standard_mapping) { + if (axes_count >= 2) { + tracker->set_input(standard_vector_names[0], Vector2(axes[0], -axes[1])); + } + if (axes_count >= 4) { + tracker->set_input(standard_vector_names[1], Vector2(axes[2], -axes[3])); + } + } - float value = *((float *)axes + (i + 1)); - tracker->set_input(name, value); + if (input_source.target_ray_mode == WebXRInterface::TARGET_RAY_MODE_SCREEN) { + if (touch_index < 5 && axes_count >= 2) { + Vector2 joy_vector = Vector2(axes[0], axes[1]); + Vector2 position = _get_screen_position_from_joy_vector(joy_vector); + + if (touches[touch_index].is_touching) { + Vector2 delta = position - touches[touch_index].position; + + // If position has changed by at least 1 pixel, generate a drag event. + if (abs(delta.x) >= 1.0 || abs(delta.y) >= 1.0) { + Ref<InputEventScreenDrag> event; + event.instantiate(); + event->set_index(touch_index); + event->set_position(position); + event->set_relative(delta); + Input::get_singleton()->parse_input_event(event); + } } - free(axes); + + touches[touch_index].position = position; } - } else if (tracker.is_valid()) { - xr_server->remove_tracker(tracker); - controllers[p_controller_id].unref(); } } -void WebXRInterfaceJS::_on_controller_changed() { - // Register "virtual" gamepads with Godot for the ones we get from WebXR. - godot_webxr_sample_controller_data(); - for (int i = 0; i < 2; i++) { - bool controller_connected = godot_webxr_is_controller_connected(i); - if (controllers_state[i] != controller_connected) { - // Input::get_singleton()->joy_connection_changed(i + 100, controller_connected, i == 0 ? "Left" : "Right", ""); - controllers_state[i] = controller_connected; +void WebXRInterfaceJS::_on_input_event(int p_event_type, int p_input_source_id) { + // Get the latest data for this input source. For transient input sources, + // we may not have any data at all yet! + _update_input_source(p_input_source_id); + + if (p_event_type == WEBXR_INPUT_EVENT_SELECTSTART || p_event_type == WEBXR_INPUT_EVENT_SELECTEND) { + const InputSource &input_source = input_sources[p_input_source_id]; + if (input_source.target_ray_mode == WebXRInterface::TARGET_RAY_MODE_SCREEN) { + int touch_index = input_source.touch_index; + if (touch_index >= 0 && touch_index < 5) { + touches[touch_index].is_touching = (p_event_type == WEBXR_INPUT_EVENT_SELECTSTART); + + Ref<InputEventScreenTouch> event; + event.instantiate(); + event->set_index(touch_index); + event->set_position(touches[touch_index].position); + event->set_pressed(p_event_type == WEBXR_INPUT_EVENT_SELECTSTART); + + Input::get_singleton()->parse_input_event(event); + } } } + + switch (p_event_type) { + case WEBXR_INPUT_EVENT_SELECTSTART: + emit_signal("selectstart", p_input_source_id); + break; + + case WEBXR_INPUT_EVENT_SELECTEND: + emit_signal("selectend", p_input_source_id); + // Emit the 'select' event on our own (rather than intercepting the + // one from JavaScript) so that we don't have to needlessly call + // _update_input_source() a second time. + emit_signal("select", p_input_source_id); + break; + + case WEBXR_INPUT_EVENT_SQUEEZESTART: + emit_signal("squeezestart", p_input_source_id); + break; + + case WEBXR_INPUT_EVENT_SQUEEZEEND: + emit_signal("squeezeend", p_input_source_id); + // Again, we emit the 'squeeze' event on our own to avoid extra work. + emit_signal("squeeze", p_input_source_id); + break; + } +} + +Vector2 WebXRInterfaceJS::_get_screen_position_from_joy_vector(const Vector2 &p_joy_vector) { + SceneTree *scene_tree = Object::cast_to<SceneTree>(OS::get_singleton()->get_main_loop()); + if (!scene_tree) { + return Vector2(); + } + + Window *viewport = scene_tree->get_root(); + + Vector2 position_percentage((p_joy_vector.x + 1.0f) / 2.0f, ((p_joy_vector.y) + 1.0f) / 2.0f); + Vector2 position = (Size2)viewport->get_size() * position_percentage; + + return position; } WebXRInterfaceJS::WebXRInterfaceJS() { diff --git a/modules/webxr/webxr_interface_js.h b/modules/webxr/webxr_interface_js.h index 319adc2ac9..6b484a8872 100644 --- a/modules/webxr/webxr_interface_js.h +++ b/modules/webxr/webxr_interface_js.h @@ -39,6 +39,10 @@ The WebXR interface is a VR/AR interface that can be used on the web. */ +namespace GLES3 { +class TextureStorage; +} + class WebXRInterfaceJS : public WebXRInterface { GDCLASS(WebXRInterfaceJS, WebXRInterface); @@ -53,13 +57,32 @@ private: String requested_reference_space_types; String reference_space_type; - // TODO maybe turn into a vector to support more then 2 controllers... - bool controllers_state[2]; - Ref<XRPositionalTracker> controllers[2]; Size2 render_targetsize; - + RBMap<unsigned int, RID> texture_cache; + struct Touch { + bool is_touching = false; + Vector2 position; + } touches[5]; + + static constexpr uint8_t input_source_count = 16; + + struct InputSource { + Ref<XRPositionalTracker> tracker; + bool active = false; + TargetRayMode target_ray_mode; + int touch_index = -1; + } input_sources[input_source_count]; + + RID color_texture; + RID depth_texture; + + RID _get_color_texture(); + RID _get_depth_texture(); + RID _get_texture(unsigned int p_texture_id); Transform3D _js_matrix_to_transform(float *p_js_matrix); - void _update_tracker(int p_controller_id); + void _update_input_source(int p_input_source_id); + + Vector2 _get_screen_position_from_joy_vector(const Vector2 &p_joy_vector); public: virtual void is_session_supported(const String &p_session_mode) override; @@ -73,9 +96,11 @@ public: virtual String get_requested_reference_space_types() const override; void _set_reference_space_type(String p_reference_space_type); virtual String get_reference_space_type() const override; - virtual Ref<XRPositionalTracker> get_controller(int p_controller_id) const override; + virtual bool is_input_source_active(int p_input_source_id) const override; + virtual Ref<XRPositionalTracker> get_input_source_tracker(int p_input_source_id) const override; + virtual TargetRayMode get_input_source_target_ray_mode(int p_input_source_id) const override; virtual String get_visibility_state() const override; - virtual PackedVector3Array get_bounds_geometry() const override; + virtual PackedVector3Array get_play_area() const override; virtual StringName get_name() const override; virtual uint32_t get_capabilities() const override; @@ -89,14 +114,129 @@ public: virtual Transform3D get_camera_transform() override; virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) override; virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) override; + virtual bool pre_draw_viewport(RID p_render_target) override; virtual Vector<BlitToScreen> post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) override; + virtual RID get_color_texture() override; + virtual RID get_depth_texture() override; + virtual RID get_velocity_texture() override; virtual void process() override; - void _on_controller_changed(); + void _on_input_event(int p_event_type, int p_input_source_id); WebXRInterfaceJS(); ~WebXRInterfaceJS(); + +private: + StringName tracker_names[16] = { + StringName("left_hand"), + StringName("right_hand"), + StringName("tracker_2"), + StringName("tracker_3"), + StringName("tracker_4"), + StringName("tracker_5"), + StringName("tracker_6"), + StringName("tracker_7"), + StringName("tracker_8"), + StringName("tracker_9"), + StringName("tracker_10"), + StringName("tracker_11"), + StringName("tracker_12"), + StringName("tracker_13"), + StringName("tracker_14"), + StringName("tracker_15"), + }; + + StringName touch_names[5] = { + StringName("touch_0"), + StringName("touch_1"), + StringName("touch_2"), + StringName("touch_3"), + StringName("touch_4"), + }; + + StringName standard_axis_names[10] = { + StringName("touchpad_x"), + StringName("touchpad_y"), + StringName("thumbstick_x"), + StringName("thumbstick_y"), + StringName("axis_4"), + StringName("axis_5"), + StringName("axis_6"), + StringName("axis_7"), + StringName("axis_8"), + StringName("axis_9"), + }; + + StringName standard_vector_names[2] = { + StringName("touchpad"), + StringName("thumbstick"), + }; + + StringName standard_button_names[10] = { + StringName("trigger_click"), + StringName("grip_click"), + StringName("touchpad_click"), + StringName("thumbstick_click"), + StringName("ax_button"), + StringName("by_button"), + StringName("button_6"), + StringName("button_7"), + StringName("button_8"), + StringName("button_9"), + }; + + StringName standard_button_pressure_names[10] = { + StringName("trigger"), + StringName("grip"), + StringName("touchpad_click_pressure"), + StringName("thumbstick_click_pressure"), + StringName("ax_button_pressure"), + StringName("by_button_pressure"), + StringName("button_pressure_6"), + StringName("button_pressure_7"), + StringName("button_pressure_8"), + StringName("button_pressure_9"), + }; + + StringName unknown_button_names[10] = { + StringName("button_0"), + StringName("button_1"), + StringName("button_2"), + StringName("button_3"), + StringName("button_4"), + StringName("button_5"), + StringName("button_6"), + StringName("button_7"), + StringName("button_8"), + StringName("button_9"), + }; + + StringName unknown_axis_names[10] = { + StringName("axis_0"), + StringName("axis_1"), + StringName("axis_2"), + StringName("axis_3"), + StringName("axis_4"), + StringName("axis_5"), + StringName("axis_6"), + StringName("axis_7"), + StringName("axis_8"), + StringName("axis_9"), + }; + + StringName unknown_button_pressure_names[10] = { + StringName("button_pressure_0"), + StringName("button_pressure_1"), + StringName("button_pressure_2"), + StringName("button_pressure_3"), + StringName("button_pressure_4"), + StringName("button_pressure_5"), + StringName("button_pressure_6"), + StringName("button_pressure_7"), + StringName("button_pressure_8"), + StringName("button_pressure_9"), + }; }; #endif // WEB_ENABLED |