virtualx-engine/modules/webxr/native/library_godot_webxr.js
Rémi Verschelde 1426cd3b3a
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".

Backported from #70885.
2023-01-10 15:26:54 +01:00

702 lines
23 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'],
$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);
},
// Some custom WebGL code for blitting our eye textures to the
// framebuffer we get from WebXR.
shaderProgram: null,
programInfo: null,
buffer: null,
// Vertex shader source.
vsSource: `
const vec2 scale = vec2(0.5, 0.5);
attribute vec4 aVertexPosition;
varying highp vec2 vTextureCoord;
void main () {
gl_Position = aVertexPosition;
vTextureCoord = aVertexPosition.xy * scale + scale;
}
`,
// Fragment shader source.
fsSource: `
varying highp vec2 vTextureCoord;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, vTextureCoord);
}
`,
initShaderProgram: (gl, vsSource, fsSource) => {
const vertexShader = GodotWebXR.loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = GodotWebXR.loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
GodotRuntime.error(`Unable to initialize the shader program: ${gl.getProgramInfoLog(shaderProgram)}`);
return null;
}
return shaderProgram;
},
loadShader: (gl, type, source) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
GodotRuntime.error(`An error occurred compiling the shader: ${gl.getShaderInfoLog(shader)}`);
gl.deleteShader(shader);
return null;
}
return shader;
},
initBuffer: (gl) => {
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
1.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
return positionBuffer;
},
blitTexture: (gl, texture) => {
if (GodotWebXR.shaderProgram === null) {
GodotWebXR.shaderProgram = GodotWebXR.initShaderProgram(gl, GodotWebXR.vsSource, GodotWebXR.fsSource);
GodotWebXR.programInfo = {
program: GodotWebXR.shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(GodotWebXR.shaderProgram, 'aVertexPosition'),
},
uniformLocations: {
uSampler: gl.getUniformLocation(GodotWebXR.shaderProgram, 'uSampler'),
},
};
GodotWebXR.buffer = GodotWebXR.initBuffer(gl);
}
const orig_program = gl.getParameter(gl.CURRENT_PROGRAM);
gl.useProgram(GodotWebXR.shaderProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, GodotWebXR.buffer);
gl.vertexAttribPointer(GodotWebXR.programInfo.attribLocations.vertexPosition, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(GodotWebXR.programInfo.attribLocations.vertexPosition);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(GodotWebXR.programInfo.uniformLocations.uSampler, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Restore state.
gl.bindTexture(gl.TEXTURE_2D, null);
gl.disableVertexAttribArray(GodotWebXR.programInfo.attribLocations.vertexPosition);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.useProgram(orig_program);
},
// 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) {
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', 'selectend', 'select', 'squeezestart', 'squeezeend', 'squeeze'].forEach((input_event, index) => {
session.addEventListener(input_event, function (evt) {
// Some controllers won't exist until an event occurs,
// for example, with "screen" input sources (touch).
GodotWebXR.sampleControllers();
oninputevent(index, GodotWebXR.getControllerId(evt.inputSource));
});
});
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_targetsize__proxy: 'sync',
godot_webxr_get_render_targetsize__sig: 'i',
godot_webxr_get_render_targetsize: 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_for_eye__proxy: 'sync',
godot_webxr_commit_for_eye__sig: 'vii',
godot_webxr_commit_for_eye: function (p_eye, p_texture_id) {
if (!GodotWebXR.session || !GodotWebXR.pose) {
return;
}
const view_index = (p_eye === 2 /* ARVRInterface::EYE_RIGHT */) ? 1 : 0;
const glLayer = GodotWebXR.session.renderState.baseLayer;
const view = GodotWebXR.pose.views[view_index];
const viewport = glLayer.getViewport(view);
const gl = GodotWebXR.gl;
const orig_framebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
const orig_viewport = gl.getParameter(gl.VIEWPORT);
// Bind to WebXR's framebuffer.
gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
GodotWebXR.blitTexture(gl, GL.textures[p_texture_id]);
// Restore state.
gl.bindFramebuffer(gl.FRAMEBUFFER, orig_framebuffer);
gl.viewport(orig_viewport[0], orig_viewport[1], orig_viewport[2], orig_viewport[3]);
},
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: 'iii',
godot_webxr_get_controller_buttons: function (p_controller, p_xr_standard_mapping) {
if (GodotWebXR.controllers.length === 0) {
return 0;
}
const controller = GodotWebXR.controllers[p_controller];
if (!controller || !controller.gamepad) {
return 0;
}
let buttons = controller.gamepad.buttons;
if (controller.gamepad.mapping === 'xr-standard' && p_xr_standard_mapping) {
buttons = [
// 0 = unused,
0,
// 1 = B/Y
buttons[5],
// 2 = Grip
buttons[1],
// 3
buttons[3],
// 4
buttons[6],
// 5
buttons[7],
// 6
buttons[8],
// 7 = A/X
buttons[4],
// 8
buttons[9],
// 9
buttons[10],
// 10
buttons[11],
// 11
buttons[12],
// 12
buttons[13],
// 13
buttons[14],
// 14 = Pad
buttons[2],
// 15 = Trigger
buttons[0],
];
}
const buf = GodotRuntime.malloc((buttons.length + 1) * 4);
GodotRuntime.setHeapValue(buf, buttons.length, 'i32');
for (let i = 0; i < buttons.length; i++) {
GodotRuntime.setHeapValue(buf + 4 + (i * 4), (buttons[i] ? buttons[i].value : 0.0), 'float');
}
return buf;
},
godot_webxr_get_controller_axes__proxy: 'sync',
godot_webxr_get_controller_axes__sig: 'iii',
godot_webxr_get_controller_axes: function (p_controller, p_xr_standard_mapping) {
if (GodotWebXR.controllers.length === 0) {
return 0;
}
const controller = GodotWebXR.controllers[p_controller];
if (!controller || !controller.gamepad) {
return 0;
}
let axes = controller.gamepad.axes;
if (controller.gamepad.mapping === 'xr-standard') {
if (p_xr_standard_mapping) {
const trigger_axis = controller.gamepad.buttons[0].value;
const grip_axis = controller.gamepad.buttons[1].value;
axes = [
// 0 = Thumbstick X
axes[2],
// 1 = Thumbstick Y
axes[3] * -1.0,
// 2 = Trigger
trigger_axis,
// 3 = Grip (to match mistake in Oculus mobile plugin).
grip_axis,
// 4 = Grip
grip_axis,
// 5 = unused
0,
// 6 = Trackpad X
axes[0],
// 7 = Trackpad Y
axes[1] * -1.0,
];
} else {
// Invert the Y-axis on thumbsticks and trackpads, in order to
// match OpenXR and other XR platform SDKs.
axes[1] *= -1.0;
axes[3] *= -1.0;
}
}
const buf = GodotRuntime.malloc((axes.length + 1) * 4);
GodotRuntime.setHeapValue(buf, axes.length, 'i32');
for (let i = 0; i < axes.length; i++) {
GodotRuntime.setHeapValue(buf + 4 + (i * 4), axes[i], 'float');
}
return buf;
},
godot_webxr_get_controller_target_ray_mode__proxy: 'sync',
godot_webxr_get_controller_target_ray_mode__sig: 'ii',
godot_webxr_get_controller_target_ray_mode: function (p_controller) {
if (p_controller < 0 || p_controller >= GodotWebXR.controllers.length) {
return 0;
}
const controller = GodotWebXR.controllers[p_controller];
if (!controller) {
return 0;
}
switch (controller.targetRayMode) {
case 'gaze':
return 1;
case 'tracked-pointer':
return 2;
case 'screen':
return 3;
default:
break;
}
return 0;
},
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);