godot/modules/webxr/native/library_godot_webxr.js

531 lines
19 KiB
JavaScript

/*************************************************************************/
/* 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', '$runtimeKeepalivePush', '$runtimeKeepalivePop'],
$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();
runtimeKeepalivePush(); // eslint-disable-line no-undef
window.setTimeout(function () {
runtimeKeepalivePop(); // eslint-disable-line no-undef
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);