diff options
Diffstat (limited to 'modules/webxr/native/library_godot_webxr.js')
-rw-r--r-- | modules/webxr/native/library_godot_webxr.js | 528 |
1 files changed, 528 insertions, 0 deletions
diff --git a/modules/webxr/native/library_godot_webxr.js b/modules/webxr/native/library_godot_webxr.js new file mode 100644 index 0000000000..c476a54c59 --- /dev/null +++ b/modules/webxr/native/library_godot_webxr.js @@ -0,0 +1,528 @@ +/*************************************************************************/ +/* library_godot_webxr.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ +const GodotWebXR = { + $GodotWebXR__deps: ['$Browser', '$GL', '$GodotRuntime'], + $GodotWebXR: { + gl: null, + + session: null, + space: null, + frame: null, + pose: null, + + // Monkey-patch the requestAnimationFrame() used by Emscripten for the main + // loop, so that we can swap it out for XRSession.requestAnimationFrame() + // when an XR session is started. + orig_requestAnimationFrame: null, + requestAnimationFrame: (callback) => { + if (GodotWebXR.session && GodotWebXR.space) { + const onFrame = function (time, frame) { + GodotWebXR.frame = frame; + GodotWebXR.pose = frame.getViewerPose(GodotWebXR.space); + callback(time); + GodotWebXR.frame = null; + GodotWebXR.pose = null; + }; + GodotWebXR.session.requestAnimationFrame(onFrame); + } else { + GodotWebXR.orig_requestAnimationFrame(callback); + } + }, + monkeyPatchRequestAnimationFrame: (enable) => { + if (GodotWebXR.orig_requestAnimationFrame === null) { + GodotWebXR.orig_requestAnimationFrame = Browser.requestAnimationFrame; + } + Browser.requestAnimationFrame = enable + ? GodotWebXR.requestAnimationFrame : GodotWebXR.orig_requestAnimationFrame; + }, + pauseResumeMainLoop: () => { + // Once both GodotWebXR.session and GodotWebXR.space are set or + // unset, our monkey-patched requestAnimationFrame() should be + // enabled or disabled. When using the WebXR API Emulator, this + // gets picked up automatically, however, in the Oculus Browser + // on the Quest, we need to pause and resume the main loop. + Browser.mainLoop.pause(); + window.setTimeout(function () { + Browser.mainLoop.resume(); + }, 0); + }, + + // Holds the controllers list between function calls. + controllers: [], + + // Updates controllers array, where the left hand (or sole tracker) is + // the first element, and the right hand is the second element, and any + // others placed at the 3rd position and up. + sampleControllers: () => { + if (!GodotWebXR.session || !GodotWebXR.frame) { + return; + } + + let other_index = 2; + const controllers = []; + GodotWebXR.session.inputSources.forEach((input_source) => { + if (input_source.targetRayMode === 'tracked-pointer') { + if (input_source.handedness === 'right') { + controllers[1] = input_source; + } else if (input_source.handedness === 'left' || !controllers[0]) { + controllers[0] = input_source; + } + } else { + controllers[other_index++] = input_source; + } + }); + GodotWebXR.controllers = controllers; + }, + + getControllerId: (input_source) => GodotWebXR.controllers.indexOf(input_source), + }, + + godot_webxr_is_supported__proxy: 'sync', + godot_webxr_is_supported__sig: 'i', + godot_webxr_is_supported: function () { + return !!navigator.xr; + }, + + godot_webxr_is_session_supported__proxy: 'sync', + godot_webxr_is_session_supported__sig: 'vii', + godot_webxr_is_session_supported: function (p_session_mode, p_callback) { + const session_mode = GodotRuntime.parseString(p_session_mode); + const cb = GodotRuntime.get_func(p_callback); + if (navigator.xr) { + navigator.xr.isSessionSupported(session_mode).then(function (supported) { + const c_str = GodotRuntime.allocString(session_mode); + cb(c_str, supported ? 1 : 0); + GodotRuntime.free(c_str); + }); + } else { + const c_str = GodotRuntime.allocString(session_mode); + cb(c_str, 0); + GodotRuntime.free(c_str); + } + }, + + godot_webxr_initialize__deps: ['emscripten_webgl_get_current_context'], + godot_webxr_initialize__proxy: 'sync', + godot_webxr_initialize__sig: 'viiiiiiiiii', + godot_webxr_initialize: function (p_session_mode, p_required_features, p_optional_features, p_requested_reference_spaces, p_on_session_started, p_on_session_ended, p_on_session_failed, p_on_controller_changed, p_on_input_event, p_on_simple_event) { + GodotWebXR.monkeyPatchRequestAnimationFrame(true); + + const session_mode = GodotRuntime.parseString(p_session_mode); + const required_features = GodotRuntime.parseString(p_required_features).split(',').map((s) => s.trim()).filter((s) => s !== ''); + const optional_features = GodotRuntime.parseString(p_optional_features).split(',').map((s) => s.trim()).filter((s) => s !== ''); + const requested_reference_space_types = GodotRuntime.parseString(p_requested_reference_spaces).split(',').map((s) => s.trim()); + const onstarted = GodotRuntime.get_func(p_on_session_started); + const onended = GodotRuntime.get_func(p_on_session_ended); + const onfailed = GodotRuntime.get_func(p_on_session_failed); + const oncontroller = GodotRuntime.get_func(p_on_controller_changed); + const oninputevent = GodotRuntime.get_func(p_on_input_event); + const onsimpleevent = GodotRuntime.get_func(p_on_simple_event); + + const session_init = {}; + if (required_features.length > 0) { + session_init['requiredFeatures'] = required_features; + } + if (optional_features.length > 0) { + session_init['optionalFeatures'] = optional_features; + } + + navigator.xr.requestSession(session_mode, session_init).then(function (session) { + GodotWebXR.session = session; + + session.addEventListener('end', function (evt) { + onended(); + }); + + session.addEventListener('inputsourceschange', function (evt) { + let controller_changed = false; + [evt.added, evt.removed].forEach((lst) => { + lst.forEach((input_source) => { + if (input_source.targetRayMode === 'tracked-pointer') { + controller_changed = true; + } + }); + }); + if (controller_changed) { + oncontroller(); + } + }); + + ['selectstart', 'select', 'selectend', 'squeezestart', 'squeeze', 'squeezeend'].forEach((input_event) => { + session.addEventListener(input_event, function (evt) { + const c_str = GodotRuntime.allocString(input_event); + oninputevent(c_str, GodotWebXR.getControllerId(evt.inputSource)); + GodotRuntime.free(c_str); + }); + }); + + session.addEventListener('visibilitychange', function (evt) { + const c_str = GodotRuntime.allocString('visibility_state_changed'); + onsimpleevent(c_str); + GodotRuntime.free(c_str); + }); + + const gl_context_handle = _emscripten_webgl_get_current_context(); // eslint-disable-line no-undef + const gl = GL.getContext(gl_context_handle).GLctx; + GodotWebXR.gl = gl; + + gl.makeXRCompatible().then(function () { + session.updateRenderState({ + baseLayer: new XRWebGLLayer(session, gl), + }); + + function onReferenceSpaceSuccess(reference_space, reference_space_type) { + GodotWebXR.space = reference_space; + + // Using reference_space.addEventListener() crashes when + // using the polyfill with the WebXR Emulator extension, + // so we set the event property instead. + reference_space.onreset = function (evt) { + const c_str = GodotRuntime.allocString('reference_space_reset'); + onsimpleevent(c_str); + GodotRuntime.free(c_str); + }; + + // Now that both GodotWebXR.session and GodotWebXR.space are + // set, we need to pause and resume the main loop for the XR + // main loop to kick in. + GodotWebXR.pauseResumeMainLoop(); + + // Call in setTimeout() so that errors in the onstarted() + // callback don't bubble up here and cause Godot to try the + // next reference space. + window.setTimeout(function () { + const c_str = GodotRuntime.allocString(reference_space_type); + onstarted(c_str); + GodotRuntime.free(c_str); + }, 0); + } + + function requestReferenceSpace() { + const reference_space_type = requested_reference_space_types.shift(); + session.requestReferenceSpace(reference_space_type) + .then((refSpace) => { + onReferenceSpaceSuccess(refSpace, reference_space_type); + }) + .catch(() => { + if (requested_reference_space_types.length === 0) { + const c_str = GodotRuntime.allocString('Unable to get any of the requested reference space types'); + onfailed(c_str); + GodotRuntime.free(c_str); + } else { + requestReferenceSpace(); + } + }); + } + + requestReferenceSpace(); + }).catch(function (error) { + const c_str = GodotRuntime.allocString(`Unable to make WebGL context compatible with WebXR: ${error}`); + onfailed(c_str); + GodotRuntime.free(c_str); + }); + }).catch(function (error) { + const c_str = GodotRuntime.allocString(`Unable to start session: ${error}`); + onfailed(c_str); + GodotRuntime.free(c_str); + }); + }, + + godot_webxr_uninitialize__proxy: 'sync', + godot_webxr_uninitialize__sig: 'v', + godot_webxr_uninitialize: function () { + if (GodotWebXR.session) { + GodotWebXR.session.end() + // Prevent exception when session has already ended. + .catch((e) => { }); + } + + GodotWebXR.session = null; + GodotWebXR.space = null; + GodotWebXR.frame = null; + GodotWebXR.pose = null; + + // Disable the monkey-patched window.requestAnimationFrame() and + // pause/restart the main loop to activate it on all platforms. + GodotWebXR.monkeyPatchRequestAnimationFrame(false); + GodotWebXR.pauseResumeMainLoop(); + }, + + godot_webxr_get_view_count__proxy: 'sync', + godot_webxr_get_view_count__sig: 'i', + godot_webxr_get_view_count: function () { + if (!GodotWebXR.session || !GodotWebXR.pose) { + return 0; + } + return GodotWebXR.pose.views.length; + }, + + godot_webxr_get_render_target_size__proxy: 'sync', + godot_webxr_get_render_target_size__sig: 'i', + godot_webxr_get_render_target_size: function () { + if (!GodotWebXR.session || !GodotWebXR.pose) { + return 0; + } + + const glLayer = GodotWebXR.session.renderState.baseLayer; + const view = GodotWebXR.pose.views[0]; + const viewport = glLayer.getViewport(view); + + const buf = GodotRuntime.malloc(2 * 4); + 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_eye__sig: 'ii', + godot_webxr_get_transform_for_eye: function (p_eye) { + if (!GodotWebXR.session || !GodotWebXR.pose) { + return 0; + } + + const views = GodotWebXR.pose.views; + let matrix; + if (p_eye === 0) { + matrix = GodotWebXR.pose.transform.matrix; + } else { + matrix = views[p_eye - 1].transform.matrix; + } + const buf = GodotRuntime.malloc(16 * 4); + for (let i = 0; i < 16; i++) { + GodotRuntime.setHeapValue(buf + (i * 4), matrix[i], 'float'); + } + return buf; + }, + + godot_webxr_get_projection_for_eye__proxy: 'sync', + godot_webxr_get_projection_for_eye__sig: 'ii', + godot_webxr_get_projection_for_eye: function (p_eye) { + if (!GodotWebXR.session || !GodotWebXR.pose) { + return 0; + } + + 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 !!GodotWebXR.controllers[p_controller]; + }, + + godot_webxr_get_controller_transform__proxy: 'sync', + godot_webxr_get_controller_transform__sig: 'ii', + godot_webxr_get_controller_transform: function (p_controller) { + if (!GodotWebXR.session || !GodotWebXR.frame) { + return 0; + } + + const controller = GodotWebXR.controllers[p_controller]; + if (!controller) { + return 0; + } + + const frame = GodotWebXR.frame; + const space = GodotWebXR.space; + + const pose = frame.getPose(controller.targetRaySpace, space); + if (!pose) { + // This can mean that the controller lost tracking. + return 0; + } + const matrix = pose.transform.matrix; + + const buf = GodotRuntime.malloc(16 * 4); + for (let i = 0; i < 16; i++) { + GodotRuntime.setHeapValue(buf + (i * 4), matrix[i], 'float'); + } + return buf; + }, + + godot_webxr_get_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]; + if (!controller || !controller.gamepad) { + return 0; + } + + const button_count = controller.gamepad.buttons.length; + + const buf = GodotRuntime.malloc((button_count + 1) * 4); + GodotRuntime.setHeapValue(buf, button_count, 'i32'); + for (let i = 0; i < button_count; i++) { + GodotRuntime.setHeapValue(buf + 4 + (i * 4), controller.gamepad.buttons[i].value, 'float'); + } + return buf; + }, + + godot_webxr_get_controller_axes__proxy: 'sync', + godot_webxr_get_controller_axes__sig: 'ii', + godot_webxr_get_controller_axes: function (p_controller) { + if (GodotWebXR.controllers.length === 0) { + return 0; + } + + 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; + }, + + godot_webxr_get_visibility_state__proxy: 'sync', + godot_webxr_get_visibility_state__sig: 'i', + godot_webxr_get_visibility_state: function () { + if (!GodotWebXR.session || !GodotWebXR.session.visibilityState) { + return 0; + } + + return GodotRuntime.allocString(GodotWebXR.session.visibilityState); + }, + + godot_webxr_get_bounds_geometry__proxy: 'sync', + godot_webxr_get_bounds_geometry__sig: 'i', + godot_webxr_get_bounds_geometry: function () { + if (!GodotWebXR.space || !GodotWebXR.space.boundsGeometry) { + return 0; + } + + const point_count = GodotWebXR.space.boundsGeometry.length; + if (point_count === 0) { + return 0; + } + + const buf = GodotRuntime.malloc(((point_count * 3) + 1) * 4); + GodotRuntime.setHeapValue(buf, point_count, 'i32'); + for (let i = 0; i < point_count; i++) { + const point = GodotWebXR.space.boundsGeometry[i]; + GodotRuntime.setHeapValue(buf + ((i * 3) + 1) * 4, point.x, 'float'); + GodotRuntime.setHeapValue(buf + ((i * 3) + 2) * 4, point.y, 'float'); + GodotRuntime.setHeapValue(buf + ((i * 3) + 3) * 4, point.z, 'float'); + } + + return buf; + }, +}; + +autoAddDeps(GodotWebXR, '$GodotWebXR'); +mergeInto(LibraryManager.library, GodotWebXR); |