virtualx-engine/modules/webxr/native/library_godot_webxr.js
Rémi Verschelde d95794ec8a
One Copyright Update to rule them all
As many open source projects have started doing it, we're removing the
current year from the copyright notice, so that we don't need to bump
it every year.

It seems like only the first year of publication is technically
relevant for copyright notices, and even that seems to be something
that many companies stopped listing altogether (in a version controlled
codebase, the commits are a much better source of date of publication
than a hardcoded copyright statement).

We also now list Godot Engine contributors first as we're collectively
the current maintainers of the project, and we clarify that the
"exclusive" copyright of the co-founders covers the timespan before
opensourcing (their further contributions are included as part of Godot
Engine contributors).

Also fixed "cf." Frenchism - it's meant as "refer to / see".
2023-01-05 13:25:55 +01:00

601 lines
20 KiB
JavaScript

/**************************************************************************/
/* library_godot_webxr.js */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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,
gl_binding: null,
layer: null,
space: null,
frame: null,
pose: null,
view_count: 1,
input_sources: new Array(16),
touches: new Array(5),
// Monkey-patch the requestAnimationFrame() used by Emscripten for the main
// loop, so that we can swap it out for XRSession.requestAnimationFrame()
// 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);
},
getLayer: () => {
const new_view_count = (GodotWebXR.pose) ? GodotWebXR.pose.views.length : 1;
let layer = GodotWebXR.layer;
// If the view count hasn't changed since creating this layer, then
// we can simply return it.
if (layer && GodotWebXR.view_count === new_view_count) {
return layer;
}
if (!GodotWebXR.session || !GodotWebXR.gl_binding) {
return null;
}
const gl = GodotWebXR.gl;
layer = GodotWebXR.gl_binding.createProjectionLayer({
textureType: new_view_count > 1 ? 'texture-array' : 'texture',
colorFormat: gl.RGBA8,
depthFormat: gl.DEPTH_COMPONENT24,
});
GodotWebXR.session.updateRenderState({ layers: [layer] });
GodotWebXR.layer = layer;
GodotWebXR.view_count = new_view_count;
return layer;
},
getSubImage: () => {
if (!GodotWebXR.pose) {
return null;
}
const layer = GodotWebXR.getLayer();
if (layer === null) {
return null;
}
// Because we always use "texture-array" for multiview and "texture"
// when there is only 1 view, it should be safe to only grab the
// subimage for the first view.
return GodotWebXR.gl_binding.getViewSubImage(layer, GodotWebXR.pose.views[0]);
},
getTextureId: (texture) => {
if (texture.name !== undefined) {
return texture.name;
}
const id = GL.getNewId(GL.textures);
texture.name = id;
GL.textures[id] = texture;
return id;
},
addInputSource: (input_source) => {
let name = -1;
if (input_source.targetRayMode === 'tracked-pointer' && input_source.handedness === 'left') {
name = 0;
} else if (input_source.targetRayMode === 'tracked-pointer' && input_source.handedness === 'right') {
name = 1;
} else {
for (let i = 2; i < 16; i++) {
if (!GodotWebXR.input_sources[i]) {
name = i;
break;
}
}
}
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__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: 'viiiiiiiii',
godot_webxr_initialize: function (p_session_mode, p_required_features, p_optional_features, p_requested_reference_spaces, p_on_session_started, p_on_session_ended, p_on_session_failed, p_on_input_event, p_on_simple_event) {
GodotWebXR.monkeyPatchRequestAnimationFrame(true);
const session_mode = GodotRuntime.parseString(p_session_mode);
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 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) {
evt.added.forEach(GodotWebXR.addInputSource);
evt.removed.forEach(GodotWebXR.removeInputSource);
});
['selectstart', 'selectend', 'squeezestart', 'squeezeend'].forEach((input_event, index) => {
session.addEventListener(input_event, function (evt) {
// Since this happens in-between normal frames, we need to
// grab the frame from the event in order to get poses for
// the input sources.
GodotWebXR.frame = evt.frame;
oninputevent(index, GodotWebXR.getInputSourceId(evt.inputSource));
GodotWebXR.frame = null;
});
});
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 () {
GodotWebXR.gl_binding = new XRWebGLBinding(session, gl); // eslint-disable-line no-undef
// This will trigger the layer to get created.
GodotWebXR.getLayer();
function onReferenceSpaceSuccess(reference_space, reference_space_type) {
GodotWebXR.space = reference_space;
// 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.gl_binding = null;
GodotWebXR.layer = null;
GodotWebXR.space = null;
GodotWebXR.frame = null;
GodotWebXR.pose = null;
GodotWebXR.view_count = 1;
GodotWebXR.input_sources = new Array(16);
GodotWebXR.touches = new Array(5);
// Disable the monkey-patched window.requestAnimationFrame() and
// pause/restart the main loop to activate it on all platforms.
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 1;
}
const view_count = GodotWebXR.pose.views.length;
return view_count > 0 ? view_count : 1;
},
godot_webxr_get_render_target_size__proxy: 'sync',
godot_webxr_get_render_target_size__sig: 'ii',
godot_webxr_get_render_target_size: function (r_size) {
const subimage = GodotWebXR.getSubImage();
if (subimage === null) {
return false;
}
GodotRuntime.setHeapValue(r_size + 0, subimage.viewport.width, 'i32');
GodotRuntime.setHeapValue(r_size + 4, subimage.viewport.height, 'i32');
return true;
},
godot_webxr_get_transform_for_view__proxy: 'sync',
godot_webxr_get_transform_for_view__sig: 'iii',
godot_webxr_get_transform_for_view: function (p_view, r_transform) {
if (!GodotWebXR.session || !GodotWebXR.pose) {
return false;
}
const views = GodotWebXR.pose.views;
let matrix;
if (p_view >= 0) {
matrix = views[p_view].transform.matrix;
} else {
// For -1 (or any other negative value) return the HMD transform.
matrix = GodotWebXR.pose.transform.matrix;
}
for (let i = 0; i < 16; i++) {
GodotRuntime.setHeapValue(r_transform + (i * 4), matrix[i], 'float');
}
return true;
},
godot_webxr_get_projection_for_view__proxy: 'sync',
godot_webxr_get_projection_for_view__sig: 'iii',
godot_webxr_get_projection_for_view: function (p_view, r_transform) {
if (!GodotWebXR.session || !GodotWebXR.pose) {
return false;
}
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_color_texture__proxy: 'sync',
godot_webxr_get_color_texture__sig: 'i',
godot_webxr_get_color_texture: function () {
const subimage = GodotWebXR.getSubImage();
if (subimage === null) {
return 0;
}
return GodotWebXR.getTextureId(subimage.colorTexture);
},
godot_webxr_get_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) {
return 0;
}
if (p_input_source_id < 0 || p_input_source_id >= GodotWebXR.input_sources.length || !GodotWebXR.input_sources[p_input_source_id]) {
return false;
}
const input_source = GodotWebXR.input_sources[p_input_source_id];
const frame = GodotWebXR.frame;
const space = GodotWebXR.space;
// Target pose.
const target_pose = frame.getPose(input_source.targetRaySpace, space);
if (!target_pose) {
// This can mean that the controller lost tracking.
return false;
}
const target_pose_matrix = target_pose.transform.matrix;
for (let i = 0; i < 16; i++) {
GodotRuntime.setHeapValue(r_target_pose + (i * 4), target_pose_matrix[i], 'float');
}
// Target ray mode.
let target_ray_mode = 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');
// Touch index.
GodotRuntime.setHeapValue(r_touch_index, GodotWebXR.getTouchIndex(input_source), 'i32');
// Grip pose.
let has_grip_pose = false;
if (input_source.gripSpace) {
const grip_pose = frame.getPose(input_source.gripSpace, space);
if (grip_pose) {
const grip_pose_matrix = grip_pose.transform.matrix;
for (let i = 0; i < 16; i++) {
GodotRuntime.setHeapValue(r_grip_pose + (i * 4), grip_pose_matrix[i], 'float');
}
has_grip_pose = true;
}
}
GodotRuntime.setHeapValue(r_has_grip_pose, has_grip_pose ? 1 : 0, 'i32');
// Gamepad data (mapping, buttons and axes).
let has_standard_mapping = false;
let button_count = 0;
let axes_count = 0;
if (input_source.gamepad) {
if (input_source.gamepad.mapping === 'xr-standard') {
has_standard_mapping = true;
}
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__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: 'ii',
godot_webxr_get_bounds_geometry: function (r_points) {
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 * 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');
}
GodotRuntime.setHeapValue(r_points, buf, 'i32');
return point_count;
},
};
autoAddDeps(GodotWebXR, '$GodotWebXR');
mergeInto(LibraryManager.library, GodotWebXR);