Get WebXR fully working in Godot 4!

This commit is contained in:
David Snopek 2022-11-18 20:43:11 -06:00
parent 84c404f6bc
commit 310bf39cd3
15 changed files with 1685 additions and 478 deletions

View File

@ -311,9 +311,14 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_
Size2i ssize = texture_storage->render_target_get_size(p_to_render_target); Size2i ssize = texture_storage->render_target_get_size(p_to_render_target);
// If we've overridden the render target's color texture, then we need
// to invert the Y axis, so 2D texture appear right side up.
// We're probably rendering directly to an XR device.
float y_scale = texture_storage->render_target_get_override_color(p_to_render_target).is_valid() ? -2.0f : 2.0f;
Transform3D screen_transform; Transform3D screen_transform;
screen_transform.translate_local(-(ssize.width / 2.0f), -(ssize.height / 2.0f), 0.0f); screen_transform.translate_local(-(ssize.width / 2.0f), -(ssize.height / 2.0f), 0.0f);
screen_transform.scale(Vector3(2.0f / ssize.width, 2.0f / ssize.height, 1.0f)); screen_transform.scale(Vector3(2.0f / ssize.width, y_scale / ssize.height, 1.0f));
_update_transform_to_mat4(screen_transform, state_buffer.screen_transform); _update_transform_to_mat4(screen_transform, state_buffer.screen_transform);
_update_transform_2d_to_mat4(p_canvas_transform, state_buffer.canvas_transform); _update_transform_2d_to_mat4(p_canvas_transform, state_buffer.canvas_transform);

View File

@ -1694,35 +1694,52 @@ void TextureStorage::_clear_render_target(RenderTarget *rt) {
return; return;
} }
// Dispose of the cached fbo's and the allocated textures
for (KeyValue<uint32_t, RenderTarget::RTOverridden::FBOCacheEntry> &E : rt->overridden.fbo_cache) {
glDeleteTextures(E.value.allocated_textures.size(), E.value.allocated_textures.ptr());
// Don't delete the current FBO, we'll do that a couple lines down.
if (E.value.fbo != rt->fbo) {
glDeleteFramebuffers(1, &E.value.fbo);
}
}
rt->overridden.fbo_cache.clear();
if (rt->fbo) { if (rt->fbo) {
glDeleteFramebuffers(1, &rt->fbo); glDeleteFramebuffers(1, &rt->fbo);
rt->fbo = 0; rt->fbo = 0;
} }
if (rt->overridden.color.is_null()) { if (rt->overridden.color.is_null()) {
glDeleteTextures(1, &rt->color); if (rt->texture.is_valid()) {
rt->color = 0; Texture *tex = get_texture(rt->texture);
} tex->alloc_height = 0;
tex->alloc_width = 0;
if (rt->overridden.depth.is_null()) { tex->width = 0;
glDeleteTextures(1, &rt->depth); tex->height = 0;
rt->depth = 0; tex->active = false;
} }
} else {
if (rt->texture.is_valid()) {
Texture *tex = get_texture(rt->texture);
tex->alloc_height = 0;
tex->alloc_width = 0;
tex->width = 0;
tex->height = 0;
tex->active = false;
}
if (rt->overridden.color.is_valid()) {
Texture *tex = get_texture(rt->overridden.color); Texture *tex = get_texture(rt->overridden.color);
tex->is_render_target = false; tex->is_render_target = false;
} }
if (rt->overridden.color.is_valid()) {
rt->overridden.color = RID();
} else if (rt->color) {
glDeleteTextures(1, &rt->color);
}
rt->color = 0;
if (rt->overridden.depth.is_valid()) {
rt->overridden.depth = RID();
} else if (rt->depth) {
glDeleteTextures(1, &rt->depth);
}
rt->depth = 0;
rt->overridden.velocity = RID();
rt->overridden.is_overridden = false;
if (rt->backbuffer_fbo != 0) { if (rt->backbuffer_fbo != 0) {
glDeleteFramebuffers(1, &rt->backbuffer_fbo); glDeleteFramebuffers(1, &rt->backbuffer_fbo);
glDeleteTextures(1, &rt->backbuffer); glDeleteTextures(1, &rt->backbuffer);
@ -1732,15 +1749,6 @@ void TextureStorage::_clear_render_target(RenderTarget *rt) {
_render_target_clear_sdf(rt); _render_target_clear_sdf(rt);
} }
void TextureStorage::_clear_render_target_overridden_fbo_cache(RenderTarget *rt) {
// Dispose of the cached fbo's and the allocated textures
for (KeyValue<uint32_t, RenderTarget::RTOverridden::FBOCacheEntry> &E : rt->overridden.fbo_cache) {
glDeleteTextures(E.value.allocated_textures.size(), E.value.allocated_textures.ptr());
glDeleteFramebuffers(1, &E.value.fbo);
}
rt->overridden.fbo_cache.clear();
}
RID TextureStorage::render_target_create() { RID TextureStorage::render_target_create() {
RenderTarget render_target; RenderTarget render_target;
//render_target.was_used = false; //render_target.was_used = false;
@ -1759,7 +1767,6 @@ RID TextureStorage::render_target_create() {
void TextureStorage::render_target_free(RID p_rid) { void TextureStorage::render_target_free(RID p_rid) {
RenderTarget *rt = render_target_owner.get_or_null(p_rid); RenderTarget *rt = render_target_owner.get_or_null(p_rid);
_clear_render_target(rt); _clear_render_target(rt);
_clear_render_target_overridden_fbo_cache(rt);
Texture *t = get_texture(rt->texture); Texture *t = get_texture(rt->texture);
if (t) { if (t) {
@ -1826,11 +1833,7 @@ void TextureStorage::render_target_set_override(RID p_render_target, RID p_color
if (p_color_texture.is_null() && p_depth_texture.is_null()) { if (p_color_texture.is_null() && p_depth_texture.is_null()) {
_clear_render_target(rt); _clear_render_target(rt);
rt->overridden.is_overridden = false; _update_render_target(rt);
rt->overridden.color = RID();
rt->overridden.depth = RID();
rt->size = Size2i();
_clear_render_target_overridden_fbo_cache(rt);
return; return;
} }
@ -1849,6 +1852,8 @@ void TextureStorage::render_target_set_override(RID p_render_target, RID p_color
RBMap<uint32_t, RenderTarget::RTOverridden::FBOCacheEntry>::Element *cache; RBMap<uint32_t, RenderTarget::RTOverridden::FBOCacheEntry>::Element *cache;
if ((cache = rt->overridden.fbo_cache.find(hash_key)) != nullptr) { if ((cache = rt->overridden.fbo_cache.find(hash_key)) != nullptr) {
rt->fbo = cache->get().fbo; rt->fbo = cache->get().fbo;
rt->color = cache->get().color;
rt->depth = cache->get().depth;
rt->size = cache->get().size; rt->size = cache->get().size;
rt->texture = p_color_texture; rt->texture = p_color_texture;
return; return;
@ -1858,6 +1863,8 @@ void TextureStorage::render_target_set_override(RID p_render_target, RID p_color
RenderTarget::RTOverridden::FBOCacheEntry new_entry; RenderTarget::RTOverridden::FBOCacheEntry new_entry;
new_entry.fbo = rt->fbo; new_entry.fbo = rt->fbo;
new_entry.color = rt->color;
new_entry.depth = rt->depth;
new_entry.size = rt->size; new_entry.size = rt->size;
// Keep track of any textures we had to allocate because they weren't overridden. // Keep track of any textures we had to allocate because they weren't overridden.
if (p_color_texture.is_null()) { if (p_color_texture.is_null()) {

View File

@ -344,6 +344,8 @@ struct RenderTarget {
struct FBOCacheEntry { struct FBOCacheEntry {
GLuint fbo; GLuint fbo;
GLuint color;
GLuint depth;
Size2i size; Size2i size;
Vector<GLuint> allocated_textures; Vector<GLuint> allocated_textures;
}; };
@ -412,7 +414,6 @@ private:
mutable RID_Owner<RenderTarget> render_target_owner; mutable RID_Owner<RenderTarget> render_target_owner;
void _clear_render_target(RenderTarget *rt); void _clear_render_target(RenderTarget *rt);
void _clear_render_target_overridden_fbo_cache(RenderTarget *rt);
void _update_render_target(RenderTarget *rt); void _update_render_target(RenderTarget *rt);
void _create_render_target_backbuffer(RenderTarget *rt); void _create_render_target_backbuffer(RenderTarget *rt);
void _render_target_allocate_sdf(RenderTarget *rt); void _render_target_allocate_sdf(RenderTarget *rt);

View File

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?> <?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"> <class name="WebXRInterface" inherits="XRInterface" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description> <brief_description>
AR/VR interface using WebXR. XR interface using WebXR.
</brief_description> </brief_description>
<description> <description>
WebXR is an open standard that allows creating VR and AR applications that run in the web browser. 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. 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). 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: Here's the minimum code required to start an immersive VR session:
[codeblock] [codeblock]
extends Node3D extends Node3D
@ -69,7 +69,7 @@
func _webxr_session_started(): func _webxr_session_started():
$Button.visible = false $Button.visible = false
# This tells Godot to start rendering to the headset. # 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 # 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 # types that you requested above. This is useful if you want the game to
# work a little differently in 'bounded-floor' versus 'local-floor'. # work a little differently in 'bounded-floor' versus 'local-floor'.
@ -79,28 +79,35 @@
$Button.visible = true $Button.visible = true
# If the user exits immersive mode, then we tell Godot to render to the web # If the user exits immersive mode, then we tell Godot to render to the web
# page again. # page again.
get_viewport().xr = false get_viewport().use_xr = false
func _webxr_session_failed(message): func _webxr_session_failed(message):
OS.alert("Failed to initialize: " + message) OS.alert("Failed to initialize: " + message)
[/codeblock] [/codeblock]
There are several ways to handle "controller" input: 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 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 [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 [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 input sources like a tap on the screen, a spoken voice command or a button press on the device itself.
- 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 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.
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.
</description> </description>
<tutorials> <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> <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> </tutorials>
<methods> <methods>
<method name="get_controller" qualifiers="const"> <method name="get_input_source_target_ray_mode" qualifiers="const">
<return type="XRPositionalTracker" /> <return type="int" enum="WebXRInterface.TargetRayMode" />
<param index="0" name="controller_id" type="int" /> <param index="0" name="input_source_id" type="int" />
<description> <description>
Gets an [XRPositionalTracker] for the given [code]controller_id[/code]. Returns the target ray mode for the given [code]input_source_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. 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.
Use this method to get information about the controller that triggered one of these signals: </description>
</method>
<method name="get_input_source_tracker" qualifiers="const">
<return type="XRPositionalTracker" />
<param index="0" name="input_source_id" type="int" />
<description>
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 selectstart]
- [signal select] - [signal select]
- [signal selectend] - [signal selectend]
@ -109,6 +116,13 @@
- [signal squeezestart] - [signal squeezestart]
</description> </description>
</method> </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"> <method name="is_session_supported">
<return type="void" /> <return type="void" />
<param index="0" name="session_mode" type="String" /> <param index="0" name="session_mode" type="String" />
@ -120,11 +134,6 @@
</method> </method>
</methods> </methods>
<members> <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"> <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. 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. 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>
<member name="requested_reference_space_types" type="String" setter="set_requested_reference_space_types" getter="get_requested_reference_space_types"> <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. 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. 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]. 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> </member>
@ -161,35 +170,35 @@
<signal name="reference_space_reset"> <signal name="reference_space_reset">
<description> <description>
Emitted to indicate that the reference space has been reset or reconfigured. 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. See [url=https://developer.mozilla.org/en-US/docs/Web/API/XRReferenceSpace/reset_event]WebXR's XRReferenceSpace reset event[/url] for more information.
</description> </description>
</signal> </signal>
<signal name="select"> <signal name="select">
<param index="0" name="controller_id" type="int" /> <param index="0" name="input_source_id" type="int" />
<description> <description>
Emitted after one of the "controllers" has finished its "primary action". Emitted after one of the input sources has finished its "primary action".
Use [method get_controller] to get more information about the controller. Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source.
</description> </description>
</signal> </signal>
<signal name="selectend"> <signal name="selectend">
<param index="0" name="controller_id" type="int" /> <param index="0" name="input_source_id" type="int" />
<description> <description>
Emitted when one of the "controllers" has finished its "primary action". Emitted when one of the input sources has finished its "primary action".
Use [method get_controller] to get more information about the controller. Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source.
</description> </description>
</signal> </signal>
<signal name="selectstart"> <signal name="selectstart">
<param index="0" name="controller_id" type="int" /> <param index="0" name="input_source_id" type="int" />
<description> <description>
Emitted when one of the "controllers" has started its "primary action". Emitted when one of the input source has started its "primary action".
Use [method get_controller] to get more information about the controller. Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source.
</description> </description>
</signal> </signal>
<signal name="session_ended"> <signal name="session_ended">
<description> <description>
Emitted when the user ends the WebXR session (which can be done using UI from the browser or device). 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> </description>
</signal> </signal>
<signal name="session_failed"> <signal name="session_failed">
@ -202,7 +211,7 @@
<signal name="session_started"> <signal name="session_started">
<description> <description>
Emitted by [method XRInterface.initialize] if the session is successfully started. 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> </description>
</signal> </signal>
<signal name="session_supported"> <signal name="session_supported">
@ -213,24 +222,24 @@
</description> </description>
</signal> </signal>
<signal name="squeeze"> <signal name="squeeze">
<param index="0" name="controller_id" type="int" /> <param index="0" name="input_source_id" type="int" />
<description> <description>
Emitted after one of the "controllers" has finished its "primary squeeze action". Emitted after one of the input sources has finished its "primary squeeze action".
Use [method get_controller] to get more information about the controller. Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source.
</description> </description>
</signal> </signal>
<signal name="squeezeend"> <signal name="squeezeend">
<param index="0" name="controller_id" type="int" /> <param index="0" name="input_source_id" type="int" />
<description> <description>
Emitted when one of the "controllers" has finished its "primary squeeze action". Emitted when one of the input sources has finished its "primary squeeze action".
Use [method get_controller] to get more information about the controller. Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source.
</description> </description>
</signal> </signal>
<signal name="squeezestart"> <signal name="squeezestart">
<param index="0" name="controller_id" type="int" /> <param index="0" name="input_source_id" type="int" />
<description> <description>
Emitted when one of the "controllers" has started its "primary squeeze action". Emitted when one of the input sources has started its "primary squeeze action".
Use [method get_controller] to get more information about the controller. Use [method get_input_source_tracker] and [method get_input_source_target_ray_mode] to get more information about the input source.
</description> </description>
</signal> </signal>
<signal name="visibility_state_changed"> <signal name="visibility_state_changed">
@ -239,4 +248,18 @@
</description> </description>
</signal> </signal>
</signals> </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> </class>

View File

@ -37,12 +37,18 @@ extern "C" {
#include "stddef.h" #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 (*GodotWebXRSupportedCallback)(char *p_session_mode, int p_supported);
typedef void (*GodotWebXRStartedCallback)(char *p_reference_space_type); typedef void (*GodotWebXRStartedCallback)(char *p_reference_space_type);
typedef void (*GodotWebXREndedCallback)(); typedef void (*GodotWebXREndedCallback)();
typedef void (*GodotWebXRFailedCallback)(char *p_message); typedef void (*GodotWebXRFailedCallback)(char *p_message);
typedef void (*GodotWebXRControllerCallback)(); typedef void (*GodotWebXRInputEventCallback)(int p_event_type, int p_input_source_id);
typedef void (*GodotWebXRInputEventCallback)(char *p_signal_name, int p_controller_id);
typedef void (*GodotWebXRSimpleEventCallback)(char *p_signal_name); typedef void (*GodotWebXRSimpleEventCallback)(char *p_signal_name);
extern int godot_webxr_is_supported(); extern int godot_webxr_is_supported();
@ -56,26 +62,33 @@ extern void godot_webxr_initialize(
GodotWebXRStartedCallback p_on_session_started, GodotWebXRStartedCallback p_on_session_started,
GodotWebXREndedCallback p_on_session_ended, GodotWebXREndedCallback p_on_session_ended,
GodotWebXRFailedCallback p_on_session_failed, GodotWebXRFailedCallback p_on_session_failed,
GodotWebXRControllerCallback p_on_controller_changed,
GodotWebXRInputEventCallback p_on_input_event, GodotWebXRInputEventCallback p_on_input_event,
GodotWebXRSimpleEventCallback p_on_simple_event); GodotWebXRSimpleEventCallback p_on_simple_event);
extern void godot_webxr_uninitialize(); extern void godot_webxr_uninitialize();
extern int godot_webxr_get_view_count(); extern int godot_webxr_get_view_count();
extern int *godot_webxr_get_render_target_size(); extern bool godot_webxr_get_render_target_size(int *r_size);
extern float *godot_webxr_get_transform_for_eye(int p_eye); extern bool godot_webxr_get_transform_for_view(int p_view, float *r_transform);
extern float *godot_webxr_get_projection_for_eye(int p_eye); extern bool godot_webxr_get_projection_for_view(int p_view, float *r_transform);
extern void godot_webxr_commit(unsigned int p_texture); 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 bool godot_webxr_update_input_source(
extern int godot_webxr_get_controller_count(); int p_input_source_id,
extern int godot_webxr_is_controller_connected(int p_controller); float *r_target_pose,
extern float *godot_webxr_get_controller_transform(int p_controller); int *r_target_ray_mode,
extern int *godot_webxr_get_controller_buttons(int p_controller); int *r_touch_index,
extern int *godot_webxr_get_controller_axes(int p_controller); 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 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 #ifdef __cplusplus
} }

View File

@ -33,9 +33,14 @@ const GodotWebXR = {
gl: null, gl: null,
session: null, session: null,
gl_binding: null,
layer: null,
space: null, space: null,
frame: null, frame: null,
pose: 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 // Monkey-patch the requestAnimationFrame() used by Emscripten for the main
// loop, so that we can swap it out for XRSession.requestAnimationFrame() // loop, so that we can swap it out for XRSession.requestAnimationFrame()
@ -76,34 +81,128 @@ const GodotWebXR = {
}, 0); }, 0);
}, },
// Holds the controllers list between function calls. getLayer: () => {
controllers: [], 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 // If the view count hasn't changed since creating this layer, then
// the first element, and the right hand is the second element, and any // we can simply return it.
// others placed at the 3rd position and up. if (layer && GodotWebXR.view_count === new_view_count) {
sampleControllers: () => { return layer;
if (!GodotWebXR.session || !GodotWebXR.frame) {
return;
} }
let other_index = 2; if (!GodotWebXR.session || !GodotWebXR.gl_binding) {
const controllers = []; return null;
GodotWebXR.session.inputSources.forEach((input_source) => { }
if (input_source.targetRayMode === 'tracked-pointer') {
if (input_source.handedness === 'right') { const gl = GodotWebXR.gl;
controllers[1] = input_source;
} else if (input_source.handedness === 'left' || !controllers[0]) { layer = GodotWebXR.gl_binding.createProjectionLayer({
controllers[0] = input_source; textureType: new_view_count > 1 ? 'texture-array' : 'texture',
} colorFormat: gl.RGBA8,
} else { depthFormat: gl.DEPTH_COMPONENT24,
controllers[other_index++] = input_source;
}
}); });
GodotWebXR.controllers = controllers; GodotWebXR.session.updateRenderState({ layers: [layer] });
GodotWebXR.layer = layer;
GodotWebXR.view_count = new_view_count;
return layer;
}, },
getControllerId: (input_source) => GodotWebXR.controllers.indexOf(input_source), 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;
}
}
}
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;
},
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', godot_webxr_is_supported__proxy: 'sync',
@ -132,8 +231,8 @@ const GodotWebXR = {
godot_webxr_initialize__deps: ['emscripten_webgl_get_current_context'], godot_webxr_initialize__deps: ['emscripten_webgl_get_current_context'],
godot_webxr_initialize__proxy: 'sync', godot_webxr_initialize__proxy: 'sync',
godot_webxr_initialize__sig: 'viiiiiiiiii', 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_controller_changed, p_on_input_event, p_on_simple_event) { 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); GodotWebXR.monkeyPatchRequestAnimationFrame(true);
const session_mode = GodotRuntime.parseString(p_session_mode); const session_mode = GodotRuntime.parseString(p_session_mode);
@ -143,7 +242,6 @@ const GodotWebXR = {
const onstarted = GodotRuntime.get_func(p_on_session_started); const onstarted = GodotRuntime.get_func(p_on_session_started);
const onended = GodotRuntime.get_func(p_on_session_ended); const onended = GodotRuntime.get_func(p_on_session_ended);
const onfailed = GodotRuntime.get_func(p_on_session_failed); 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 oninputevent = GodotRuntime.get_func(p_on_input_event);
const onsimpleevent = GodotRuntime.get_func(p_on_simple_event); const onsimpleevent = GodotRuntime.get_func(p_on_simple_event);
@ -163,24 +261,18 @@ const GodotWebXR = {
}); });
session.addEventListener('inputsourceschange', function (evt) { session.addEventListener('inputsourceschange', function (evt) {
let controller_changed = false; evt.added.forEach(GodotWebXR.addInputSource);
[evt.added, evt.removed].forEach((lst) => { evt.removed.forEach(GodotWebXR.removeInputSource);
lst.forEach((input_source) => {
if (input_source.targetRayMode === 'tracked-pointer') {
controller_changed = true;
}
});
});
if (controller_changed) {
oncontroller();
}
}); });
['selectstart', 'select', 'selectend', 'squeezestart', 'squeeze', 'squeezeend'].forEach((input_event) => { ['selectstart', 'selectend', 'squeezestart', 'squeezeend'].forEach((input_event, index) => {
session.addEventListener(input_event, function (evt) { session.addEventListener(input_event, function (evt) {
const c_str = GodotRuntime.allocString(input_event); // Since this happens in-between normal frames, we need to
oninputevent(c_str, GodotWebXR.getControllerId(evt.inputSource)); // grab the frame from the event in order to get poses for
GodotRuntime.free(c_str); // 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; GodotWebXR.gl = gl;
gl.makeXRCompatible().then(function () { gl.makeXRCompatible().then(function () {
session.updateRenderState({ GodotWebXR.gl_binding = new XRWebGLBinding(session, gl); // eslint-disable-line no-undef
baseLayer: new XRWebGLLayer(session, gl),
}); // This will trigger the layer to get created.
GodotWebXR.getLayer();
function onReferenceSpaceSuccess(reference_space, reference_space_type) { function onReferenceSpaceSuccess(reference_space, reference_space_type) {
GodotWebXR.space = reference_space; GodotWebXR.space = reference_space;
@ -266,9 +359,14 @@ const GodotWebXR = {
} }
GodotWebXR.session = null; GodotWebXR.session = null;
GodotWebXR.gl_binding = null;
GodotWebXR.layer = null;
GodotWebXR.space = null; GodotWebXR.space = null;
GodotWebXR.frame = null; GodotWebXR.frame = null;
GodotWebXR.pose = 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 // Disable the monkey-patched window.requestAnimationFrame() and
// pause/restart the main loop to activate it on all platforms. // 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__sig: 'i',
godot_webxr_get_view_count: function () { godot_webxr_get_view_count: function () {
if (!GodotWebXR.session || !GodotWebXR.pose) { 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__proxy: 'sync',
godot_webxr_get_render_target_size__sig: 'i', godot_webxr_get_render_target_size__sig: 'ii',
godot_webxr_get_render_target_size: function () { godot_webxr_get_render_target_size: function (r_size) {
if (!GodotWebXR.session || !GodotWebXR.pose) { const subimage = GodotWebXR.getSubImage();
return 0; if (subimage === null) {
return false;
} }
const glLayer = GodotWebXR.session.renderState.baseLayer; GodotRuntime.setHeapValue(r_size + 0, subimage.viewport.width, 'i32');
const view = GodotWebXR.pose.views[0]; GodotRuntime.setHeapValue(r_size + 4, subimage.viewport.height, 'i32');
const viewport = glLayer.getViewport(view);
const buf = GodotRuntime.malloc(2 * 4); return true;
GodotRuntime.setHeapValue(buf + 0, viewport.width, 'i32');
GodotRuntime.setHeapValue(buf + 4, viewport.height, 'i32');
return buf;
}, },
godot_webxr_get_transform_for_eye__proxy: 'sync', godot_webxr_get_transform_for_view__proxy: 'sync',
godot_webxr_get_transform_for_eye__sig: 'ii', godot_webxr_get_transform_for_view__sig: 'iii',
godot_webxr_get_transform_for_eye: function (p_eye) { godot_webxr_get_transform_for_view: function (p_view, r_transform) {
if (!GodotWebXR.session || !GodotWebXR.pose) { if (!GodotWebXR.session || !GodotWebXR.pose) {
return 0; return false;
} }
const views = GodotWebXR.pose.views; const views = GodotWebXR.pose.views;
let matrix; let matrix;
if (p_eye === 0) { if (p_view >= 0) {
matrix = GodotWebXR.pose.transform.matrix; matrix = views[p_view].transform.matrix;
} else { } else {
matrix = views[p_eye - 1].transform.matrix; // For -1 (or any other negative value) return the HMD transform.
matrix = GodotWebXR.pose.transform.matrix;
} }
const buf = GodotRuntime.malloc(16 * 4);
for (let i = 0; i < 16; i++) { 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_get_projection_for_eye__proxy: 'sync', godot_webxr_get_projection_for_view__proxy: 'sync',
godot_webxr_get_projection_for_eye__sig: 'ii', godot_webxr_get_projection_for_view__sig: 'iii',
godot_webxr_get_projection_for_eye: function (p_eye) { godot_webxr_get_projection_for_view: function (p_view, r_transform) {
if (!GodotWebXR.session || !GodotWebXR.pose) { if (!GodotWebXR.session || !GodotWebXR.pose) {
return 0;
}
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');
}
return buf;
},
godot_webxr_commit__proxy: 'sync',
godot_webxr_commit__sig: 'vi',
godot_webxr_commit: function (p_texture) {
if (!GodotWebXR.session || !GodotWebXR.pose) {
return;
}
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);
}
// 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);
},
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_controller_count__proxy: 'sync',
godot_webxr_get_controller_count__sig: 'i',
godot_webxr_get_controller_count: function () {
if (!GodotWebXR.session || !GodotWebXR.frame) {
return 0;
}
return GodotWebXR.controllers.length;
},
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; return false;
} }
return !!GodotWebXR.controllers[p_controller];
const matrix = GodotWebXR.pose.views[p_view].projectionMatrix;
for (let i = 0; i < 16; i++) {
GodotRuntime.setHeapValue(r_transform + (i * 4), matrix[i], 'float');
}
return true;
}, },
godot_webxr_get_controller_transform__proxy: 'sync', godot_webxr_get_color_texture__proxy: 'sync',
godot_webxr_get_controller_transform__sig: 'ii', godot_webxr_get_color_texture__sig: 'i',
godot_webxr_get_controller_transform: function (p_controller) { godot_webxr_get_color_texture: function () {
const subimage = GodotWebXR.getSubImage();
if (subimage === null) {
return 0;
}
return GodotWebXR.getTextureId(subimage.colorTexture);
},
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;
}
if (!subimage.depthStencilTexture) {
return 0;
}
return GodotWebXR.getTextureId(subimage.depthStencilTexture);
},
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.getTextureId(subimage.motionVectorTexture);
},
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) { if (!GodotWebXR.session || !GodotWebXR.frame) {
return 0; return 0;
} }
const controller = GodotWebXR.controllers[p_controller]; if (p_input_source_id < 0 || p_input_source_id >= GodotWebXR.input_sources.length || !GodotWebXR.input_sources[p_input_source_id]) {
if (!controller) { return false;
return 0;
} }
const input_source = GodotWebXR.input_sources[p_input_source_id];
const frame = GodotWebXR.frame; const frame = GodotWebXR.frame;
const space = GodotWebXR.space; const space = GodotWebXR.space;
const pose = frame.getPose(controller.targetRaySpace, space); // Target pose.
if (!pose) { const target_pose = frame.getPose(input_source.targetRaySpace, space);
if (!target_pose) {
// This can mean that the controller lost tracking. // This can mean that the controller lost tracking.
return 0; return false;
} }
const matrix = pose.transform.matrix; const target_pose_matrix = target_pose.transform.matrix;
const buf = GodotRuntime.malloc(16 * 4);
for (let i = 0; i < 16; i++) { 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;
} }
const controller = GodotWebXR.controllers[p_controller]; // Target ray mode.
if (!controller || !controller.gamepad) { let target_ray_mode = 0;
return 0; switch (input_source.targetRayMode) {
case 'gaze':
target_ray_mode = 1;
break;
case 'tracked-pointer':
target_ray_mode = 2;
break;
case 'screen':
target_ray_mode = 3;
break;
default:
} }
GodotRuntime.setHeapValue(r_target_ray_mode, target_ray_mode, 'i32');
const button_count = controller.gamepad.buttons.length; // Touch index.
GodotRuntime.setHeapValue(r_touch_index, GodotWebXR.getTouchIndex(input_source), 'i32');
const buf = GodotRuntime.malloc((button_count + 1) * 4); // Grip pose.
GodotRuntime.setHeapValue(buf, button_count, 'i32'); let has_grip_pose = false;
for (let i = 0; i < button_count; i++) { if (input_source.gripSpace) {
GodotRuntime.setHeapValue(buf + 4 + (i * 4), controller.gamepad.buttons[i].value, 'float'); const grip_pose = frame.getPose(input_source.gripSpace, space);
} if (grip_pose) {
return buf; 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');
godot_webxr_get_controller_axes__proxy: 'sync', }
godot_webxr_get_controller_axes__sig: 'ii', has_grip_pose = true;
godot_webxr_get_controller_axes: function (p_controller) {
if (GodotWebXR.controllers.length === 0) {
return 0;
}
const controller = GodotWebXR.controllers[p_controller];
if (!controller || !controller.gamepad) {
return 0;
}
const axes_count = controller.gamepad.axes.length;
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;
} }
GodotRuntime.setHeapValue(buf + 4 + (i * 4), value, 'float');
} }
return buf; 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;
}
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');
}
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(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', 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__proxy: 'sync',
godot_webxr_get_bounds_geometry__sig: 'i', godot_webxr_get_bounds_geometry__sig: 'ii',
godot_webxr_get_bounds_geometry: function () { godot_webxr_get_bounds_geometry: function (r_points) {
if (!GodotWebXR.space || !GodotWebXR.space.boundsGeometry) { if (!GodotWebXR.space || !GodotWebXR.space.boundsGeometry) {
return 0; return 0;
} }
@ -513,7 +582,7 @@ const GodotWebXR = {
return 0; 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'); GodotRuntime.setHeapValue(buf, point_count, 'i32');
for (let i = 0; i < point_count; i++) { for (let i = 0; i < point_count; i++) {
const point = GodotWebXR.space.boundsGeometry[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) + 2) * 4, point.y, 'float');
GodotRuntime.setHeapValue(buf + ((i * 3) + 3) * 4, point.z, 'float'); GodotRuntime.setHeapValue(buf + ((i * 3) + 3) * 4, point.z, 'float');
} }
GodotRuntime.setHeapValue(r_points, buf, 'i32');
return buf; return point_count;
}, },
}; };

View File

@ -1,3 +1,7 @@
/*
* WebXR Device API
*/
/** /**
* @type {XR} * @type {XR}
*/ */
@ -497,3 +501,681 @@ XRPose.prototype.transform;
* @type {boolean} * @type {boolean}
*/ */
XRPose.prototype.emulatedPosition; 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;

View File

@ -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("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("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_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_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, "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"); 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, "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, "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::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_supported", PropertyInfo(Variant::STRING, "session_mode"), PropertyInfo(Variant::BOOL, "supported")));
ADD_SIGNAL(MethodInfo("session_started")); ADD_SIGNAL(MethodInfo("session_started"));
ADD_SIGNAL(MethodInfo("session_ended")); ADD_SIGNAL(MethodInfo("session_ended"));
ADD_SIGNAL(MethodInfo("session_failed", PropertyInfo(Variant::STRING, "message"))); ADD_SIGNAL(MethodInfo("session_failed", PropertyInfo(Variant::STRING, "message")));
ADD_SIGNAL(MethodInfo("selectstart", PropertyInfo(Variant::INT, "controller_id"))); ADD_SIGNAL(MethodInfo("selectstart", PropertyInfo(Variant::INT, "input_source_id")));
ADD_SIGNAL(MethodInfo("select", PropertyInfo(Variant::INT, "controller_id"))); ADD_SIGNAL(MethodInfo("select", PropertyInfo(Variant::INT, "input_source_id")));
ADD_SIGNAL(MethodInfo("selectend", PropertyInfo(Variant::INT, "controller_id"))); ADD_SIGNAL(MethodInfo("selectend", PropertyInfo(Variant::INT, "input_source_id")));
ADD_SIGNAL(MethodInfo("squeezestart", PropertyInfo(Variant::INT, "controller_id"))); ADD_SIGNAL(MethodInfo("squeezestart", PropertyInfo(Variant::INT, "input_source_id")));
ADD_SIGNAL(MethodInfo("squeeze", PropertyInfo(Variant::INT, "controller_id"))); ADD_SIGNAL(MethodInfo("squeeze", PropertyInfo(Variant::INT, "input_source_id")));
ADD_SIGNAL(MethodInfo("squeezeend", PropertyInfo(Variant::INT, "controller_id"))); ADD_SIGNAL(MethodInfo("squeezeend", PropertyInfo(Variant::INT, "input_source_id")));
ADD_SIGNAL(MethodInfo("visibility_state_changed")); ADD_SIGNAL(MethodInfo("visibility_state_changed"));
ADD_SIGNAL(MethodInfo("reference_space_reset")); 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);
} }

View File

@ -45,6 +45,13 @@ protected:
static void _bind_methods(); static void _bind_methods();
public: 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 is_session_supported(const String &p_session_mode) = 0;
virtual void set_session_mode(String p_session_mode) = 0; virtual void set_session_mode(String p_session_mode) = 0;
virtual String get_session_mode() const = 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 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_requested_reference_space_types() const = 0;
virtual String get_reference_space_type() 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 String get_visibility_state() const = 0;
virtual PackedVector3Array get_bounds_geometry() const = 0;
}; };
VARIANT_ENUM_CAST(WebXRInterface::TargetRayMode);
#endif // WEBXR_INTERFACE_H #endif // WEBXR_INTERFACE_H

View File

@ -37,6 +37,8 @@
#include "drivers/gles3/storage/texture_storage.h" #include "drivers/gles3/storage/texture_storage.h"
#include "emscripten.h" #include "emscripten.h"
#include "godot_webxr.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/renderer_compositor.h"
#include "servers/rendering/rendering_server_globals.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); 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(); XRServer *xr_server = XRServer::get_singleton();
ERR_FAIL_NULL(xr_server); ERR_FAIL_NULL(xr_server);
Ref<XRInterface> interface = xr_server->find_interface("WebXR"); Ref<XRInterface> interface = xr_server->find_interface("WebXR");
ERR_FAIL_COND(interface.is_null()); ERR_FAIL_COND(interface.is_null());
static_cast<WebXRInterfaceJS *>(interface.ptr())->_on_controller_changed(); ((WebXRInterfaceJS *)interface.ptr())->_on_input_event(p_event_type, p_input_source_id);
}
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);
} }
extern "C" EMSCRIPTEN_KEEPALIVE void _emwebxr_on_simple_event(char *p_signal_name) { 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; return reference_space_type;
} }
Ref<XRPositionalTracker> WebXRInterfaceJS::get_controller(int p_controller_id) const { bool WebXRInterfaceJS::is_input_source_active(int p_input_source_id) const {
XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_INDEX_V(p_input_source_id, input_source_count, false);
ERR_FAIL_NULL_V(xr_server, Ref<XRPositionalTracker>()); return input_sources[p_input_source_id].active;
}
// TODO support more then two controllers Ref<XRPositionalTracker> WebXRInterfaceJS::get_input_source_tracker(int p_input_source_id) const {
if (p_controller_id >= 0 && p_controller_id < 2) { ERR_FAIL_INDEX_V(p_input_source_id, input_source_count, Ref<XRPositionalTracker>());
return controllers[p_controller_id]; 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 { String WebXRInterfaceJS::get_visibility_state() const {
@ -188,17 +185,18 @@ String WebXRInterfaceJS::get_visibility_state() const {
return String(); return String();
} }
PackedVector3Array WebXRInterfaceJS::get_bounds_geometry() const { PackedVector3Array WebXRInterfaceJS::get_play_area() const {
PackedVector3Array ret; PackedVector3Array ret;
int *js_bounds = godot_webxr_get_bounds_geometry(); float *points;
if (js_bounds) { int point_count = godot_webxr_get_bounds_geometry(&points);
ret.resize(js_bounds[0]); if (point_count > 0) {
for (int i = 0; i < js_bounds[0]; i++) { ret.resize(point_count);
float *js_vector3 = ((float *)js_bounds) + (i * 3) + 1; 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])); ret.set(i, Vector3(js_vector3[0], js_vector3[1], js_vector3[2]));
} }
free(js_bounds); free(points);
} }
return ret; return ret;
@ -209,7 +207,7 @@ StringName WebXRInterfaceJS::get_name() const {
}; };
uint32_t WebXRInterfaceJS::get_capabilities() 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() { uint32_t WebXRInterfaceJS::get_view_count() {
@ -261,7 +259,6 @@ bool WebXRInterfaceJS::initialize() {
&_emwebxr_on_session_started, &_emwebxr_on_session_started,
&_emwebxr_on_session_ended, &_emwebxr_on_session_ended,
&_emwebxr_on_session_failed, &_emwebxr_on_session_failed,
&_emwebxr_on_controller_changed,
&_emwebxr_on_input_event, &_emwebxr_on_input_event,
&_emwebxr_on_simple_event); &_emwebxr_on_simple_event);
}; };
@ -287,6 +284,18 @@ void WebXRInterfaceJS::uninitialize() {
godot_webxr_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 = ""; reference_space_type = "";
initialized = false; initialized = false;
}; };
@ -316,27 +325,26 @@ Size2 WebXRInterfaceJS::get_render_target_size() {
return render_targetsize; return render_targetsize;
} }
int *js_size = godot_webxr_get_render_target_size(); int js_size[2];
if (!initialized || js_size == nullptr) { bool has_size = godot_webxr_get_render_target_size(js_size);
// As a temporary default (until WebXR is fully initialized), use half the window size.
Size2 temp = DisplayServer::get_singleton()->window_get_size(); if (!initialized || !has_size) {
temp.width /= 2.0; // As a temporary default (until WebXR is fully initialized), use the
return temp; // window size.
return DisplayServer::get_singleton()->window_get_size();
} }
render_targetsize.width = js_size[0]; render_targetsize.width = (float)js_size[0];
render_targetsize.height = js_size[1]; render_targetsize.height = (float)js_size[1];
free(js_size);
return render_targetsize; return render_targetsize;
}; };
Transform3D WebXRInterfaceJS::get_camera_transform() { Transform3D WebXRInterfaceJS::get_camera_transform() {
Transform3D transform_for_eye; Transform3D camera_transform;
XRServer *xr_server = XRServer::get_singleton(); 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) { if (initialized) {
float world_scale = xr_server->get_world_scale(); float world_scale = xr_server->get_world_scale();
@ -345,181 +353,382 @@ Transform3D WebXRInterfaceJS::get_camera_transform() {
Transform3D _head_transform = head_transform; Transform3D _head_transform = head_transform;
_head_transform.origin *= world_scale; _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 WebXRInterfaceJS::get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) {
Transform3D transform_for_eye;
XRServer *xr_server = XRServer::get_singleton(); 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); float js_matrix[16];
if (!initialized || js_matrix == nullptr) { bool has_transform = godot_webxr_get_transform_for_view(p_view, js_matrix);
transform_for_eye = p_cam_transform; if (!has_transform) {
return transform_for_eye; return p_cam_transform;
} }
transform_for_eye = _js_matrix_to_transform(js_matrix); Transform3D transform_for_view = _js_matrix_to_transform(js_matrix);
free(js_matrix);
float world_scale = xr_server->get_world_scale(); float world_scale = xr_server->get_world_scale();
// Scale only the center point of our eye transform, so we don't scale the // Scale only the center point of our eye transform, so we don't scale the
// distance between the eyes. // distance between the eyes.
Transform3D _head_transform = head_transform; Transform3D _head_transform = head_transform;
transform_for_eye.origin -= _head_transform.origin; transform_for_view.origin -= _head_transform.origin;
_head_transform.origin *= world_scale; _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 WebXRInterfaceJS::get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) {
Projection eye; Projection view;
float *js_matrix = godot_webxr_get_projection_for_eye(p_view + 1); ERR_FAIL_COND_V(!initialized, view);
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; int k = 0;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) { 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 // 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); view.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[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> WebXRInterfaceJS::post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) {
Vector<BlitToScreen> blit_to_screen; Vector<BlitToScreen> blit_to_screen;
if (!initialized) { // We don't need to do anything here.
return blit_to_screen;
}
GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage);
if (!texture_storage) {
return blit_to_screen;
}
GLES3::RenderTarget *rt = texture_storage->get_render_target(p_render_target);
godot_webxr_commit(rt->color);
return blit_to_screen; 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 == nullptr) {
return RID();
}
uint32_t view_count = godot_webxr_get_view_count();
Size2 texture_size = get_render_target_size();
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);
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() { void WebXRInterfaceJS::process() {
if (initialized) { if (initialized) {
// Get the "head" position. // Get the "head" position.
float *js_matrix = godot_webxr_get_transform_for_eye(0); float js_matrix[16];
if (js_matrix != nullptr) { if (godot_webxr_get_transform_for_view(-1, js_matrix)) {
head_transform = _js_matrix_to_transform(js_matrix); head_transform = _js_matrix_to_transform(js_matrix);
free(js_matrix);
} }
if (head_tracker.is_valid()) { if (head_tracker.is_valid()) {
head_tracker->set_pose("default", head_transform, Vector3(), Vector3()); head_tracker->set_pose("default", head_transform, Vector3(), Vector3());
} }
godot_webxr_sample_controller_data(); // Update all input sources.
int controller_count = godot_webxr_get_controller_count(); for (int i = 0; i < input_source_count; i++) {
for (int i = 0; i < controller_count; i++) { _update_input_source(i);
_update_tracker(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(); XRServer *xr_server = XRServer::get_singleton();
ERR_FAIL_NULL(xr_server); ERR_FAIL_NULL(xr_server);
// need to support more then two controllers... InputSource &input_source = input_sources[p_input_source_id];
if (p_controller_id < 0 || p_controller_id > 1) {
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; return;
} }
Ref<XRPositionalTracker> tracker = controllers[p_controller_id]; input_source.target_ray_mode = (WebXRInterface::TargetRayMode)tmp_target_ray_mode;
if (godot_webxr_is_controller_connected(p_controller_id)) { input_source.touch_index = touch_index;
if (tracker.is_null()) {
tracker.instantiate(); 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];
}
// 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_type(XRServer::TRACKER_CONTROLLER);
// Controller id's 0 and 1 are always the left and right hands. tracker->set_tracker_name(tracker_name);
if (p_controller_id < 2) { tracker->set_tracker_desc(p_input_source_id == 0 ? "Left hand controller" : "Right hand controller");
tracker->set_tracker_name(p_controller_id == 0 ? "left_hand" : "right_hand"); tracker->set_tracker_hand(p_input_source_id == 0 ? XRPositionalTracker::TRACKER_HAND_LEFT : XRPositionalTracker::TRACKER_HAND_RIGHT);
tracker->set_tracker_desc(p_controller_id == 0 ? "Left hand controller" : "Right hand controller"); } else {
tracker->set_tracker_hand(p_controller_id == 0 ? XRPositionalTracker::TRACKER_HAND_LEFT : XRPositionalTracker::TRACKER_HAND_RIGHT); tracker->set_tracker_name(tracker_name);
} else { tracker->set_tracker_desc(tracker_name);
char name[1024]; }
sprintf(name, "tracker_%i", p_controller_id); xr_server->add_tracker(tracker);
tracker->set_tracker_name(name); }
tracker->set_tracker_desc(name);
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());
}
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);
}
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);
}
// 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]));
}
}
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);
}
} }
xr_server->add_tracker(tracker);
touches[touch_index].position = position;
} }
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);
}
// TODO implement additional poses such as "aim" and "grip"
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);
float value = *((float *)buttons + (i + 1));
bool state = value > 0.0;
tracker->set_input(name, state);
}
free(buttons);
}
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);
float value = *((float *)axes + (i + 1));
tracker->set_input(name, value);
}
free(axes);
}
} else if (tracker.is_valid()) {
xr_server->remove_tracker(tracker);
controllers[p_controller_id].unref();
} }
} }
void WebXRInterfaceJS::_on_controller_changed() { void WebXRInterfaceJS::_on_input_event(int p_event_type, int p_input_source_id) {
// Register "virtual" gamepads with Godot for the ones we get from WebXR. // Get the latest data for this input source. For transient input sources,
godot_webxr_sample_controller_data(); // we may not have any data at all yet!
for (int i = 0; i < 2; i++) { _update_input_source(p_input_source_id);
bool controller_connected = godot_webxr_is_controller_connected(i);
if (controllers_state[i] != controller_connected) { if (p_event_type == WEBXR_INPUT_EVENT_SELECTSTART || p_event_type == WEBXR_INPUT_EVENT_SELECTEND) {
// Input::get_singleton()->joy_connection_changed(i + 100, controller_connected, i == 0 ? "Left" : "Right", ""); const InputSource &input_source = input_sources[p_input_source_id];
controllers_state[i] = controller_connected; 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() { WebXRInterfaceJS::WebXRInterfaceJS() {

View File

@ -39,6 +39,10 @@
The WebXR interface is a VR/AR interface that can be used on the web. The WebXR interface is a VR/AR interface that can be used on the web.
*/ */
namespace GLES3 {
class TextureStorage;
}
class WebXRInterfaceJS : public WebXRInterface { class WebXRInterfaceJS : public WebXRInterface {
GDCLASS(WebXRInterfaceJS, WebXRInterface); GDCLASS(WebXRInterfaceJS, WebXRInterface);
@ -53,13 +57,32 @@ private:
String requested_reference_space_types; String requested_reference_space_types;
String reference_space_type; 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; 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); 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: public:
virtual void is_session_supported(const String &p_session_mode) override; 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; virtual String get_requested_reference_space_types() const override;
void _set_reference_space_type(String p_reference_space_type); void _set_reference_space_type(String p_reference_space_type);
virtual String get_reference_space_type() const override; 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 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 StringName get_name() const override;
virtual uint32_t get_capabilities() const override; virtual uint32_t get_capabilities() const override;
@ -89,14 +114,129 @@ public:
virtual Transform3D get_camera_transform() override; virtual Transform3D get_camera_transform() override;
virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_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 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 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; virtual void process() override;
void _on_controller_changed(); void _on_input_event(int p_event_type, int p_input_source_id);
WebXRInterfaceJS(); WebXRInterfaceJS();
~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 #endif // WEB_ENABLED

View File

@ -38,15 +38,20 @@ sys_env.AddJSLibraries(
"js/libs/library_godot_webgl2.js", "js/libs/library_godot_webgl2.js",
] ]
) )
sys_env.AddJSExterns(
[
"js/libs/library_godot_webgl2.externs.js",
]
)
if env["javascript_eval"]: if env["javascript_eval"]:
sys_env.AddJSLibraries(["js/libs/library_godot_javascript_singleton.js"]) sys_env.AddJSLibraries(["js/libs/library_godot_javascript_singleton.js"])
for lib in sys_env["JS_LIBS"]: for lib in sys_env["JS_LIBS"]:
sys_env.Append(LINKFLAGS=["--js-library", lib.abspath]) sys_env.Append(LINKFLAGS=["--js-library", lib.abspath])
for js in env["JS_PRE"]: for js in sys_env["JS_PRE"]:
sys_env.Append(LINKFLAGS=["--pre-js", js.abspath]) sys_env.Append(LINKFLAGS=["--pre-js", js.abspath])
for ext in env["JS_EXTERNS"]: for ext in sys_env["JS_EXTERNS"]:
sys_env["ENV"]["EMCC_CLOSURE_ARGS"] += " --externs " + ext.abspath sys_env["ENV"]["EMCC_CLOSURE_ARGS"] += " --externs " + ext.abspath
build = [] build = []

View File

@ -0,0 +1,36 @@
/**
* @constructor OVR_multiview2
*/
function OVR_multiview2() {}
/**
* @type {number}
*/
OVR_multiview2.prototype.FRAMEBUFFER_ATTACHMENT_TEXTURE_NUM_VIEWS_OVR;
/**
* @type {number}
*/
OVR_multiview2.prototype.FRAMEBUFFER_ATTACHMENT_TEXTURE_BASE_VIEW_INDEX_OVR;
/**
* @type {number}
*/
OVR_multiview2.prototype.MAX_VIEWS_OVR;
/**
* @type {number}
*/
OVR_multiview2.prototype.FRAMEBUFFER_INCOMPLETE_VIEW_TARGETS_OVR;
/**
* @param {number} target
* @param {number} attachment
* @param {WebGLTexture} texture
* @param {number} level
* @param {number} baseViewIndex
* @param {number} numViews
* @return {void}
*/
OVR_multiview2.prototype.framebufferTextureMultiviewOVR = function(target, attachment, texture, level, baseViewIndex, numViews) {};

View File

@ -37,14 +37,15 @@ const GodotWebGL2 = {
godot_webgl2_glFramebufferTextureMultiviewOVR: function (target, attachment, texture, level, base_view_index, num_views) { godot_webgl2_glFramebufferTextureMultiviewOVR: function (target, attachment, texture, level, base_view_index, num_views) {
const context = GL.currentContext; const context = GL.currentContext;
if (typeof context.multiviewExt === 'undefined') { if (typeof context.multiviewExt === 'undefined') {
const ext = context.GLctx.getExtension('OVR_multiview2'); const /** OVR_multiview2 */ ext = context.GLctx.getExtension('OVR_multiview2');
if (!ext) { if (!ext) {
console.error('Trying to call glFramebufferTextureMultiviewOVR() without the OVR_multiview2 extension'); console.error('Trying to call glFramebufferTextureMultiviewOVR() without the OVR_multiview2 extension');
return; return;
} }
context.multiviewExt = ext; context.multiviewExt = ext;
} }
context.multiviewExt.framebufferTextureMultiviewOVR(target, attachment, GL.textures[texture], level, base_view_index, num_views); const /** OVR_multiview2 */ ext = context.multiviewExt;
ext.framebufferTextureMultiviewOVR(target, attachment, GL.textures[texture], level, base_view_index, num_views);
}, },
}; };

View File

@ -2520,7 +2520,7 @@ void RendererSceneCull::render_camera(const Ref<RenderSceneBuffers> &p_render_bu
Projection projections[RendererSceneRender::MAX_RENDER_VIEWS]; Projection projections[RendererSceneRender::MAX_RENDER_VIEWS];
uint32_t view_count = p_xr_interface->get_view_count(); uint32_t view_count = p_xr_interface->get_view_count();
ERR_FAIL_COND_MSG(view_count > RendererSceneRender::MAX_RENDER_VIEWS, "Requested view count is not supported"); ERR_FAIL_COND_MSG(view_count == 0 || view_count > RendererSceneRender::MAX_RENDER_VIEWS, "Requested view count is not supported");
float aspect = p_viewport_size.width / (float)p_viewport_size.height; float aspect = p_viewport_size.width / (float)p_viewport_size.height;