diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b904aac..c6fee837 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,6 @@ option(LOVR_ENABLE_JSON "Bundle with lua-cjson" ON) option(LOVR_USE_LUAJIT "Use LuaJIT instead of Lua" ON) option(LOVR_USE_OPENVR "Enable the OpenVR backend for the headset module" ON) option(LOVR_USE_OPENXR "Enable the OpenXR backend for the headset module" OFF) -option(LOVR_USE_WEBVR "Enable the WebVR backend for the headset module" OFF) option(LOVR_USE_WEBXR "Enable the WebXR backend for the headset module" OFF) option(LOVR_USE_OCULUS "Enable the LibOVR backend for the headset module (be sure to also set LOVR_OCULUS_PATH to point to the Oculus SDK)" OFF) option(LOVR_USE_VRAPI "Enable the VrApi backend for the headset module" OFF) @@ -50,16 +49,8 @@ if(EMSCRIPTEN) "-s FULL_ES2=1 " "-s FULL_ES3=1 " "-s FORCE_FILESYSTEM=1 " - "-s \"EXPORTED_FUNCTIONS=[ " - "'_main','_lovrDestroy'," - "'_lovrCanvasCreateFromHandle'," - "'_lovrGraphicsSetCamera'," - "'_webvr_onAnimationFrame'," - "'_mat4_set','_mat4_identity','_mat4_invert','_mat4_multiply','_mat4_rotateQuat','_mat4_transform','_mat4_transformDirection'," - "'_quat_fromMat4','_quat_getAngleAxis'" - "]\" " + "-s \"EXPORTED_FUNCTIONS=['_main','_lovrDestroy','_lovrCanvasCreateFromHandle','_lovrCanvasDestroy','_lovrGraphicsSetCamera']\" " "-s \"EXTRA_EXPORTED_RUNTIME_METHODS=['getValue','setValue']\" " - "--js-library \"${CMAKE_CURRENT_SOURCE_DIR}/src/resources/webvr.js\" " "--js-library \"${CMAKE_CURRENT_SOURCE_DIR}/src/resources/webxr.js\" " "--shell-file \"${CMAKE_CURRENT_SOURCE_DIR}/src/resources/lovr.html\"" ) @@ -69,7 +60,6 @@ if(EMSCRIPTEN) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${LOVR_EMSCRIPTEN_FLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${LOVR_EMSCRIPTEN_FLAGS}") set(CMAKE_EXECUTABLE_SUFFIX ".html") - set(LOVR_USE_WEBVR ON) set(LOVR_USE_WEBXR ON) set(LOVR_USE_OPENVR OFF) elseif(ANDROID) @@ -342,7 +332,6 @@ set(LOVR_SRC src/main.c src/core/arr.c src/core/fs.c - src/core/maf.c src/core/map.c src/core/png.c src/core/ref.c @@ -490,10 +479,6 @@ if(LOVR_ENABLE_HEADSET) add_definitions(-DLOVR_USE_PICO) target_sources(lovr PRIVATE src/modules/headset/headset_pico.c) endif() - if(LOVR_USE_WEBVR) - add_definitions(-DLOVR_USE_WEBVR) - target_sources(lovr PRIVATE src/modules/headset/headset_webvr.c) - endif() if(LOVR_USE_WEBXR) add_definitions(-DLOVR_USE_WEBXR) target_sources(lovr PRIVATE src/modules/headset/headset_webxr.c) diff --git a/README.md b/README.md index 4b103bff..5fb31f92 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ You can use LÖVR to easily create VR experiences without much setup or programm Features --- -- **Cross-Platform** - Runs on Windows, Mac, Linux, Android, and on the web using WebAssembly and WebVR. +- **Cross-Platform** - Runs on Windows, Mac, Linux, Android, and on the web using WebAssembly and WebXR. - **Cross-Device** - Supports Vive/Index, Oculus Rift/Go/Quest, Pico, Windows MR, and has a VR simulator. - **Beginner-friendly** - Simple VR scenes can be created in just a few lines of Lua. - **Fast** - Writen in C99 and scripted with LuaJIT, includes optimized single-pass stereo rendering. diff --git a/Tupfile b/Tupfile index fa936b25..58ca3be8 100644 --- a/Tupfile +++ b/Tupfile @@ -6,7 +6,6 @@ SRC += src/main.c endif SRC += src/core/arr.c SRC += src/core/fs.c -SRC += src/core/maf.c SRC += src/core/map.c ifneq (@(PICO),y) SRC += src/core/os_$(PLATFORM).c @@ -30,7 +29,6 @@ SRC_@(HEADSET)@(OPENXR) += src/modules/headset/headset_openxr.c SRC_@(HEADSET)@(OCULUS) += src/modules/headset/headset_oculus.c SRC_@(HEADSET)@(VRAPI) += src/modules/headset/headset_vrapi.c SRC_@(HEADSET)@(PICO) += src/modules/headset/headset_pico.c -SRC_@(HEADSET)@(WEBVR) += src/modules/headset/headset_webvr.c SRC_@(HEADSET)@(WEBXR) += src/modules/headset/headset_webxr.c SRC_@(HEADSET)@(LEAP) += src/modules/headset/headset_leap.c SRC_@(MATH) += src/modules/math/*.c diff --git a/Tuprules.tup b/Tuprules.tup index 998c100d..c5546434 100644 --- a/Tuprules.tup +++ b/Tuprules.tup @@ -265,12 +265,7 @@ ifeq ($(PLATFORM),web) LDFLAGS += -s FULL_ES3 LDFLAGS += -s GL_PREINITIALIZED_CONTEXT LDFLAGS += -s FORCE_FILESYSTEM - LDFLAGS += -s EXPORTED_FUNCTIONS="[ - LDFLAGS += '_main', - LDFLAGS += '_lovrCanvasCreateFromHandle','_lovrGraphicsSetCamera', - LDFLAGS += '_mat4_set','_mat4_identity','_mat4_invert','_mat4_multiply','_mat4_rotateQuat', - LDFLAGS += '_mat4_transform','_mat4_transformDirection' - LDFLAGS += ]" + LDFLAGS += -s EXPORTED_FUNCTIONS="['_main','_lovrCanvasCreateFromHandle','_lovrCanvasDestroy','_lovrGraphicsSetCamera']" LDFLAGS_@(WEBXR) += --js-library $(ROOT)/src/resources/webxr.js LDFLAGS += --shell-file $(ROOT)/src/resources/lovr.html CFLAGS_@(THREAD) += -s USE_PTHREADS=1 diff --git a/src/api/l_headset.c b/src/api/l_headset.c index 76381e1d..419f79a8 100644 --- a/src/api/l_headset.c +++ b/src/api/l_headset.c @@ -16,7 +16,6 @@ StringEntry HeadsetDrivers[] = { [DRIVER_OPENXR] = ENTRY("openxr"), [DRIVER_VRAPI] = ENTRY("vrapi"), [DRIVER_PICO] = ENTRY("pico"), - [DRIVER_WEBVR] = ENTRY("webvr"), [DRIVER_WEBXR] = ENTRY("webxr"), { 0 } }; diff --git a/src/core/maf.c b/src/core/maf.c deleted file mode 100644 index 6ab3c791..00000000 --- a/src/core/maf.c +++ /dev/null @@ -1,2 +0,0 @@ -#define MAF_EXPORT -#include "maf.h" diff --git a/src/modules/event/event.c b/src/modules/event/event.c index 6f251d8a..e1aafe90 100644 --- a/src/modules/event/event.c +++ b/src/modules/event/event.c @@ -53,7 +53,9 @@ void lovrEventDestroy() { for (size_t i = 0; i < state.events.length; i++) { Event* event = &state.events.data[i]; switch (event->type) { +#if LOVR_ENABLE_THREAD case EVENT_THREAD_ERROR: lovrRelease(Thread, event->data.thread.thread); break; +#endif case EVENT_CUSTOM: for (uint32_t j = 0; j < event->data.custom.count; j++) { lovrVariantDestroy(&event->data.custom.data[j]); diff --git a/src/modules/headset/headset.c b/src/modules/headset/headset.c index 2c36c8cf..c7eaaeb8 100644 --- a/src/modules/headset/headset.c +++ b/src/modules/headset/headset.c @@ -36,9 +36,6 @@ bool lovrHeadsetInit(HeadsetDriver* drivers, size_t count, float offset, uint32_ #ifdef LOVR_USE_PICO case DRIVER_PICO: interface = &lovrHeadsetPicoDriver; break; #endif -#ifdef LOVR_USE_WEBVR - case DRIVER_WEBVR: interface = &lovrHeadsetWebVRDriver; break; -#endif #ifdef LOVR_USE_WEBXR case DRIVER_WEBXR: interface = &lovrHeadsetWebXRDriver; break; #endif diff --git a/src/modules/headset/headset.h b/src/modules/headset/headset.h index 2e66ad86..dd8047a4 100644 --- a/src/modules/headset/headset.h +++ b/src/modules/headset/headset.h @@ -18,7 +18,6 @@ typedef enum { DRIVER_OPENXR, DRIVER_VRAPI, DRIVER_PICO, - DRIVER_WEBVR, DRIVER_WEBXR } HeadsetDriver; @@ -150,7 +149,6 @@ extern HeadsetInterface lovrHeadsetOpenVRDriver; extern HeadsetInterface lovrHeadsetOpenXRDriver; extern HeadsetInterface lovrHeadsetVrApiDriver; extern HeadsetInterface lovrHeadsetPicoDriver; -extern HeadsetInterface lovrHeadsetWebVRDriver; extern HeadsetInterface lovrHeadsetWebXRDriver; extern HeadsetInterface lovrHeadsetDesktopDriver; extern HeadsetInterface lovrHeadsetLeapMotionDriver; diff --git a/src/modules/headset/headset_webvr.c b/src/modules/headset/headset_webvr.c deleted file mode 100644 index b611ed06..00000000 --- a/src/modules/headset/headset_webvr.c +++ /dev/null @@ -1,78 +0,0 @@ -#include "headset/headset.h" -#include "graphics/graphics.h" -#include -#include - -// Provided by resources/webvr.js -extern bool webvr_init(float offset, uint32_t msaa); -extern void webvr_destroy(void); -extern bool webvr_getName(char* name, size_t length); -extern HeadsetOrigin webvr_getOriginType(void); -extern double webvr_getDisplayTime(void); -extern void webvr_getDisplayDimensions(uint32_t* width, uint32_t* height); -extern const float* webvr_getDisplayMask(uint32_t* count); -extern uint32_t webvr_getViewCount(void); -extern bool webvr_getViewPose(uint32_t view, float* position, float* orientation); -extern bool webvr_getViewAngles(uint32_t view, float* left, float* right, float* up, float* down); -extern void webvr_getClipDistance(float* near, float* far); -extern void webvr_setClipDistance(float near, float far); -extern void webvr_getBoundsDimensions(float* width, float* depth); -extern const float* webvr_getBoundsGeometry(uint32_t* count); -extern bool webvr_getPose(Device device, float* position, float* orientation); -extern bool webvr_getVelocity(Device device, float* velocity, float* angularVelocity); -extern bool webvr_isDown(Device device, DeviceButton button, bool* down, bool* changed); -extern bool webvr_isTouched(Device device, DeviceButton button, bool* touched); -extern bool webvr_getAxis(Device device, DeviceAxis axis, float* value); -extern bool webvr_vibrate(Device device, float strength, float duration, float frequency); -extern struct ModelData* webvr_newModelData(Device device, bool animated); -extern bool webvr_animate(Device device, struct Model* model); -extern void webvr_update(float dt); - -static struct { - void (*renderCallback)(void*); - void* renderData; -} state; - -void webvr_onAnimationFrame(float* leftView, float* rightView, float* leftProjection, float* rightProjection) { - Camera camera = { .canvas = NULL, .stereo = true }; - memcpy(camera.projection[0], leftProjection, 16 * sizeof(float)); - memcpy(camera.projection[1], rightProjection, 16 * sizeof(float)); - memcpy(camera.viewMatrix[0], leftView, 16 * sizeof(float)); - memcpy(camera.viewMatrix[1], rightView, 16 * sizeof(float)); - lovrGraphicsSetCamera(&camera, true); - state.renderCallback(state.renderData); - lovrGraphicsSetCamera(NULL, false); -} - -void webvr_renderTo(void (*callback)(void*), void* userdata) { - state.renderCallback = callback; - state.renderData = userdata; -} - -HeadsetInterface lovrHeadsetWebVRDriver = { - .driverType = DRIVER_WEBVR, - .init = webvr_init, - .destroy = webvr_destroy, - .getName = webvr_getName, - .getOriginType = webvr_getOriginType, - .getDisplayTime = webvr_getDisplayTime, - .getDisplayDimensions = webvr_getDisplayDimensions, - .getDisplayMask = webvr_getDisplayMask, - .getViewCount = webvr_getViewCount, - .getViewPose = webvr_getViewPose, - .getViewAngles = webvr_getViewAngles, - .getClipDistance = webvr_getClipDistance, - .setClipDistance = webvr_setClipDistance, - .getBoundsDimensions = webvr_getBoundsDimensions, - .getBoundsGeometry = webvr_getBoundsGeometry, - .getPose = webvr_getPose, - .getVelocity = webvr_getVelocity, - .isDown = webvr_isDown, - .isTouched = webvr_isTouched, - .getAxis = webvr_getAxis, - .vibrate = webvr_vibrate, - .newModelData = webvr_newModelData, - .animate = webvr_animate, - .renderTo = webvr_renderTo, - .update = webvr_update -}; diff --git a/src/modules/headset/headset_webxr.c b/src/modules/headset/headset_webxr.c index 80b2b7f3..5a823429 100644 --- a/src/modules/headset/headset_webxr.c +++ b/src/modules/headset/headset_webxr.c @@ -27,7 +27,7 @@ extern void webxr_renderTo(void (*callback)(void*), void* userdata); extern void webxr_update(float dt); HeadsetInterface lovrHeadsetWebXRDriver = { - .driverType = DRIVER_WEBVR, + .driverType = DRIVER_WEBXR, .init = webxr_init, .destroy = webxr_destroy, .getName = webxr_getName, diff --git a/src/resources/boot.lua b/src/resources/boot.lua index d57a57e0..6bd45c7b 100644 --- a/src/resources/boot.lua +++ b/src/resources/boot.lua @@ -99,7 +99,7 @@ function lovr.boot() debug = false }, headset = { - drivers = { 'leap', 'openxr', 'oculus', 'vrapi', 'pico', 'openvr', 'webxr', 'webvr', 'desktop' }, + drivers = { 'leap', 'openxr', 'oculus', 'vrapi', 'pico', 'openvr', 'webxr', 'desktop' }, offset = 1.7, msaa = 4 }, diff --git a/src/resources/lovr.html b/src/resources/lovr.html index 18727949..e0cd40e4 100644 --- a/src/resources/lovr.html +++ b/src/resources/lovr.html @@ -56,8 +56,8 @@ }); var Module = window.Module = { - arguments: ['./'], - preRun: [findDisplay], + arguments: [], + preRun: [], postRun: [], print: console.log.bind(console), printErr: console.error.bind(console), @@ -66,29 +66,51 @@ preinitializedWebGLContext: context }; - function findDisplay() { - if (navigator.getVRDisplays) { - Module.addRunDependency('lovrDisplay'); - navigator.getVRDisplays(). - then(function(displays) { - Module.lovrDisplay = displays[0]; - container.appendChild(button); - }).finally(function() { - Module.removeRunDependency('lovrDisplay'); - }); - } + // To run a LÖVR project on this page, create a .lovr (zip) file of its folder and serve it + // alongside the HTML file. Then set the 'project' variable below to the project's filename. + // This downloads the .lovr file into the virtual filesystem and adds it as a virtual command + // line argument before booting up LÖVR. + // Example: var project = 'app.lovr'; + var project = null; + if (project) { + Module.arguments.push(project); + Module.preRun.push(function() { + Module.FS_createPreloadedFile('/', project, project, true, false); + }); } - button.addEventListener('click', function() { - if (Module.lovrDisplay && Module.lovrDisplay.capabilities.canPresent) { - var eventName = Module.lovrDisplay.isPresenting ? 'lovr.exitvr' : 'lovr.entervr'; - window.dispatchEvent(new CustomEvent(eventName)); - } - }); + // If WebXR is supported and immersive sessions are supported, add a button to the DOM that + // controls starting/stopping the immersive session. + if (navigator.xr) { + navigator.xr.isSessionSupported('immersive-vr').then(function(supported) { + if (!supported) return; - window.addEventListener('vrdisplaypresentchange', function() { - button.textContent = Module.lovrDisplay.isPresenting ? 'Exit VR' : 'Enter VR'; - }); + container.appendChild(button); + + var active = false; + + function onEnter() { + active = true; + button.textContent = 'Exit VR'; + } + + function onExit() { + active = false; + button.textContent = 'Enter VR'; + } + + button.addEventListener('click', function() { + if (!active) { + Module.lovr.enterVR().then(function(session) { + session.addEventListener('end', onExit); + onEnter(); + }); + } else { + Module.lovr.exitVR().then(onExit); + } + } + }); + } {{{ SCRIPT }}} diff --git a/src/resources/webvr.js b/src/resources/webvr.js deleted file mode 100644 index 48344229..00000000 --- a/src/resources/webvr.js +++ /dev/null @@ -1,336 +0,0 @@ -var LibraryLOVR = { - $webvr: { - buttonMap: { - 'OpenVR Gamepad': [1, 1, null, 0, 2], - 'Oculus Touch (Left)': [1, 1, 0, null, 2, null, null, null, 3, 4], - 'Oculus Touch (Right)': [1, 1, 0, null, 2, null, 3, 4, null, null], - 'Spatial Controller (Spatial Interaction Source) 045E-065D': [0, 0, 1, 3, 2, 4] - }, - - refreshGamepads: function(event) { - if (event.gamepad.hand) { - var device = ({ - 'left': C.DEVICE_HAND_LEFT, - 'right': C.DEVICE_HAND_RIGHT - })[event.gamepad.hand]; - - if (device) { - webvr.gamepads[device] = event.gamepad; - webvr.poses[device] = event.gamepad.pose; - } - } - } - }, - - webvr_init: function(offset, msaa) { - if (webvr.initialized || !Module.lovrDisplay) { - return false; - } - - var a, b, c, d, e, canvas, display; - webvr.initialized = true; - webvr.display = display = Module.lovrDisplay; - webvr.display.depthNear = .1; - webvr.display.depthFar = 100; - webvr.canvas = canvas = Module.canvas; - webvr.frameData = new VRFrameData(); - webvr.gamepads = []; - webvr.poses = []; - webvr.offset = offset; - webvr.poseTransform = Module._malloc(64); - webvr.matA = a = Module._malloc(64); - webvr.matB = b = Module._malloc(64); - webvr.matC = c = Module._malloc(64); - webvr.matD = d = Module._malloc(64); - webvr.matE = e = Module._malloc(64); - webvr.width = display.getEyeParameters('left').renderWidth * 2; - webvr.height = display.getEyeParameters('left').renderHeight; - Browser.setCanvasSize(webvr.width, webvr.height); - - webvr.onentervr = function() { - if (!display.isPresenting) { - display.requestPresent([{ source: canvas }]); - } - }; - - webvr.onexitvr = function() { - if (display.isPresenting) { - display.exitPresent(); - } - }; - - webvr.frameId = display.requestAnimationFrame(function onAnimationFrame() { - webvr.frameId = display.requestAnimationFrame(onAnimationFrame); - display.getFrameData(webvr.frameData); - webvr.poses[0] = webvr.frameData.pose; - - if (webvr.display.stageParameters && webvr.display.stageParameters.sittingToStandingTransform) { - HEAPF32.set(webvr.display.stageParameters.sittingToStandingTransform, webvr.poseTransform >> 2); - } else { - Module._mat4_identity(webvr.poseTransform); - HEAPF32[webvr.poseTransform >> 2 + 13] = webvr.offset; - } - - Module._mat4_set(e, webvr.poseTransform); - Module._mat4_invert(e); - HEAPF32.set(webvr.frameData.leftViewMatrix, a >> 2); - HEAPF32.set(webvr.frameData.rightViewMatrix, b >> 2); - HEAPF32.set(webvr.frameData.leftProjectionMatrix, c >> 2); - HEAPF32.set(webvr.frameData.rightProjectionMatrix, d >> 2); - Module._mat4_multiply(a, e); - Module._mat4_multiply(b, e); - Module._webvr_onAnimationFrame(a, b, c, d); - - if (display.isPresenting) { - display.submitFrame(); - } - }); - - window.addEventListener('lovr.entervr', webvr.onentervr); - window.addEventListener('lovr.exitvr', webvr.onexitvr); - window.addEventListener('vrdisplaypresentchange', webvr.onvrdisplaypresentchange); - window.addEventListener('gamepadconnected', webvr.refreshGamepads); - window.addEventListener('gamepaddisconnected', webvr.refreshGamepads); - return true; - }, - - webvr_destroy: function() { - if (!webvr.initialized) { - return; - } - - webvr.initialized = false; - Module._free(webvr.poseTransform); - Module._free(webvr.matA); - Module._free(webvr.matB); - Module._free(webvr.matC); - Module._free(webvr.matD); - Module._free(webvr.matE); - - window.removeEventListener('lovr.entervr', webvr.onentervr); - window.removeEventListener('lovr.exitvr', webvr.onexitvr); - window.removeEventListener('vrdisplaypresentchange', webvr.onvrdisplaypresentchange); - window.removeEventListener('gamepadconnected', webvr.refreshGamepads); - window.removeEventListener('gamepaddisconnected', webvr.refreshGamepads); - - if (webvr.frameId) { - webvr.display.cancelAnimationFrame(webvr.frameId); - } - }, - - webvr_getName: function() { - return false; - }, - - webvr_getOriginType: function() { - return webvr.display.stageParameters ? C.ORIGIN_FLOOR : C.ORIGIN_HEAD; - }, - - webvr_getDisplayTime: function() { - return webvr.frameData.timestamp / 1000; - }, - - webvr_getDisplayDimensions: function(width, height) { - HEAPU32[width >> 2] = webvr.width; - HEAPU32[height >> 2] = webvr.height; - }, - - webvr_getDisplayMask: function(count) { - HEAPU32[count >> 2] = 0; - return 0; - }, - - webvr_getViewCount: function() { - return 2; - }, - - webvr_getViewPose: function(view, position, orientation) { - return false; // TODO - }, - - webvr_getViewAngles: function(view, left, right, up, down) { - return false; // TODO - }, - - webvr_getClipDistance: function(clipNear, clipFar) { - HEAPF32[clipNear >> 2] = webvr.display.depthNear; - HEAPF32[clipFar >> 2] = webvr.display.depthFar; - }, - - webvr_setClipDistance: function(clipNear, clipFar) { - webvr.display.depthNear = clipNear; - webvr.display.depthFar = clipFar; - }, - - webvr_getBoundsDimensions: function(width, depth) { - var stage = webvr.display.stageParameters; - if (stage) { - HEAPF32[width >> 2] = stage.sizeX; - HEAPF32[depth >> 2] = stage.sizeZ; - } else { - HEAPF32[width >> 2] = HEAPF32[depth >> 2] = 0; - } - }, - - webvr_getBoundsGeometry: function(count) { - HEAP32[count >> 2] = 0; - return 0; - }, - - webvr_getPose: function(device, position, orientation) { - var pose = webvr.poses[device]; - if (!pose) { return false; } - - if (pose.position) { - HEAPF32.set(pose.position, position >> 2); - Module._mat4_transform(webvr.poseTransform, position); - } else { - HEAPF32.fill(0, position >> 2, position >> 2 + 3); - } - - if (pose.orientation) { - HEAPF32.set(pose.orientation, orientation >> 2); - Module._mat4_set(webvr.matA, webvr.poseTransform); - Module._mat4_rotateQuat(webvr.matA, orientation); - Module._quat_fromMat4(orientation, webvr.matA); - } else { - HEAPF32.fill(0, orientation >> 2, orientation >> 2 + 4); - } - - return true; - }, - - webvr_getVelocity: function(device, velocity, angularVelocity) { - var pose = webvr.poses[device]; - if (!pose) { return false; } - - if (pose.linearVelocity) { - HEAPF32.set(pose.linearVelocity, velocity >> 2); - Module._mat4_transformDirection(webvr.poseTransform, velocity); - } else { - HEAPF32.fill(0, velocity >> 2, velocity >> 2 + 3); - } - - if (pose.angularVelocity) { - HEAPF32.set(pose.angularVelocity, angularVelocity >> 2); - Module._mat4_transformDirection(webvr.poseTransform, angularVelocity); - } else { - HEAPF32.fill(0, angularVelocity >> 2, angularVelocity >> 2 + 3); - } - - return true; - }, - - webvr_isDown: function(device, button, down, changed) { - var gamepad = webvr.gamepads[device]; - - if (!gamepad || !gamepad.id || !webvr.buttonMap[gamepad.id] || !webvr.buttonMap[gamepad.id][button]) { - return false; - } - - HEAPF32[down >> 2] = gamepad.buttons[webvr.buttonMap[gamepad.id][button]].pressed; - HEAPF32[changed >> 2] = false; // TODO - return true; - }, - - webvr_isTouched: function(device, button, touched) { - var gamepad = webvr.gamepads[device]; - - if (!gamepad || !gamepad.id || !webvr.buttonMap[gamepad.id] || !webvr.buttonMap[gamepad.id][button]) { - return false; - } - - HEAPF32[touched >> 2] = gamepad.buttons[webvr.buttonMap[gamepad.id][button]].touched; - return true; - }, - - webvr_getAxis: function(device, axis, value) { - var gamepad = webvr.gamepads[device]; - - if (!gamepad) { - return false; - } - - if (gamepad.id.startsWith('OpenVR')) { - switch (axis) { - case C.AXIS_TRIGGER: HEAPF32[value >> 2] = gamepad.buttons[1].value; return true; - case C.AXIS_TOUCHPAD: - HEAPF32[value >> 2 + 0] = gamepad.axes[0]; - HEAPF32[value >> 2 + 1] = gamepad.axes[1]; - return true; - default: return false; - } - } else if (gamepad.id.startsWith('Oculus')) { - switch (axis) { - case C.AXIS_TRIGGER: HEAPF32[value >> 2] = gamepad.buttons[1].value; return true; - case C.AXIS_GRIP: HEAPF32[value >> 2] = gamepad.buttons[2].value; return true; - case C.AXIS_THUMBSTICK: - HEAPF32[value >> 2 + 0] = gamepad.axes[0]; - HEAPF32[value >> 2 + 1] = gamepad.axes[1]; - return true; - default: return false; - } - } else if (gamepad.id.startsWith('Spatial Controller')) { - switch (axis) { - case C.AXIS_TRIGGER: HEAPF32[value >> 2] = gamepad.buttons[0].value; return true; - case C.AXIS_THUMBSTICK: - HEAPF32[value >> 2 + 0] = gamepad.axes[0]; - HEAPF32[value >> 2 + 1] = gamepad.axes[1]; - return true; - case C.AXIS_TOUCHPAD: - HEAPF32[value >> 2 + 0] = gamepad.axes[2]; - HEAPF32[value >> 2 + 1] = gamepad.axes[3]; - return true; - default: return false; - } - } - - return false; - }, - - webvr_vibrate: function(device, strength, duration, frequency) { - var gamepad = webvr.gamepads[device]; - - if (gamepad && gamepad.hapticActuators && gamepad.hapticActuators[0]) { - gamepad.hapticActuators[0].pulse(strength, duration * 1000); - return true; - } - - return false; - }, - - webvr_newModelData: function(device, animated) { - return C.NULL; - }, - - webvr_animate: function(device, model) { - return false; - }, - - webvr_update: function(dt) { - // - }, - - $C: { - NULL: 0, - - // HeadsetOrigin - ORIGIN_HEAD: 0, - ORIGIN_FLOOR: 1, - - // Device - DEVICE_HAND_LEFT: 0, - DEVICE_HAND_RIGHT: 1, - - // DeviceAxis - AXIS_TRIGGER: 0, - AXIS_THUMBSTICK: 1, - AXIS_TOUCHPAD: 2, - AXIS_PINCH: 3, - AXIS_GRIP: 4 - } -}; - -autoAddDeps(LibraryLOVR, '$webvr'); -autoAddDeps(LibraryLOVR, '$C'); -mergeInto(LibraryManager.library, LibraryLOVR); diff --git a/src/resources/webxr.js b/src/resources/webxr.js index b4b19ffa..71c80e89 100644 --- a/src/resources/webxr.js +++ b/src/resources/webxr.js @@ -6,92 +6,187 @@ var webxr = { return false; } - state.layer = null; + state.sessions = {}; + state.session = null; state.frame = null; - state.canvas = null; - state.camera = null; state.clipNear = .1; state.clipFar = 1000.0; - state.referenceSpaceType = null; state.renderCallback = null; state.renderUserdata = null; - state.animationFrame = null; - state.displayTime = null; + state.camera = Module._malloc(264 /* sizeof(Camera) */); + state.boundsGeometry = 0; /* NULL */ + state.boundsGeometryCount = 0; - navigator.xr.requestSession('inline').then(function(session) { - state.referenceSpaceType = 'viewer'; - session.requestReferenceSpace(state.referenceSpaceType).then(function(referenceSpace) { - state.session = session; - state.layer = new XRWebGLLayer(session, Module.preinitializedWebGLContext); + var mappings = { + 'oculus-touch-left': [0, 3, null, 1, null, null, null, 4, 5], + 'oculus-touch-right': [0, 3, null, 1, null, 4, 5, null, null], + 'valve-index': [0, 3, 2, 1, null, 4, null, 4, null], + 'microsoft-mixed-reality': [0, 3, 2, 1], + 'htc-vive': [0, null, 2, 1], + 'generic-trigger': [0], + 'generic-trigger-touchpad': [0, null, 2], + 'generic-trigger-thumbstick': [0, 3], + 'generic-trigger-touchpad-thumbstick': [0, 3, 2], + 'generic-trigger-squeeze': [0, null, null, 1], + 'generic-trigger-squeeze-touchpad': [0, null, 2, 1], + 'generic-trigger-squeeze-touchpad-thumbstick': [0, 3, 2, 1], + 'generic-trigger-squeeze-thumbstick': [0, 3, null, 1], + 'generic-hand-select': [0], + }; - var framebuffer = 0; + function startSession(mode, options) { + return navigator.xr.requestSession(mode, options).then(function(session) { + var spaces = { + 'inline': ['viewer'], + 'immersive-vr': ['bounded-floor', 'local-floor'] + }; - if (state.layer.framebuffer) { - framebuffer = GL.getNewId(GL.framebuffers); - GL.framebuffers[framebuffer] = state.layer.framebuffer; - } + // This is confusing but it basically keeps trying to request successive reference spaces + // until one succeeds. $space is a promise that resolves to a reference space + var $space = spaces[mode].reduce(function(chain, spaceType) { + return chain.catch(function() { + session.spaceType = spaceType; + return session.requestReferenceSpace(spaceType); + }); + }, Promise.reject()); - var sizeof_CanvasFlags = 16; - var flags = Module.stackAlloc(sizeof_CanvasFlags); - HEAPU8.fill(0, flags, flags + sizeof_CanvasFlags); // memset(&flags, 0, sizeof(CanvasFlags)); - var width = state.layer.framebufferWidth; - var height = state.layer.framebufferHeight; - state.canvas = Module['_lovrCanvasCreateFromHandle'](width, height, flags, framebuffer, 0, 0, 1, true); - Module.stackRestore(flags); + session.inputSources = []; - var sizeof_Camera = 264; - state.camera = Module._malloc(sizeof_Camera); - HEAPU32[(state.camera + 4) >> 2] = state.canvas; // state.camera.canvas = state.canvas + return $space.then(function(space) { + state.session = session; + state.sessions[mode] = session; + session.layer = new XRWebGLLayer(session, Module.preinitializedWebGLContext); + session.updateRenderState({ + baseLayer: session.layer, + inlineVerticalFieldOfView: mode === 'inline' ? (67.0 * Math.PI / 180.0) : undefined + }); - session.updateRenderState({ - baseLayer: state.layer - }); - - state.animationFrame = session.requestAnimationFrame(function onFrame(t, frame) { - state.animationFrame = session.requestAnimationFrame(onFrame); - state.displayTime = t; - state.frame = frame; - - var views = frame.getViewerPose(referenceSpace).views; - - var stereo = views.length > 1; - HEAPU8[state.camera + 0] = stereo; // camera.stereo = stereo - - var matrices = (state.camera + 8) >> 2; - HEAPF32.set(views[0].transform.inverse.matrix, matrices + 0); - HEAPF32.set(views[0].projectionMatrix, matrices + 32); - - if (stereo) { - HEAPF32.set(views[1].transform.inverse.matrix, matrices + 16); - HEAPF32.set(views[1].projectionMatrix, matrices + 48); + if (session.spaceType.includes('floor')) { + session.space = space; + } else { + session.space = space.getOffsetReferenceSpace(new XRRigidTransform({ y: -offset })); } - Module['_lovrGraphicsSetCamera'](state.camera, true); + session.framebufferId = 0; - if (state.renderCallback) { + if (session.layer.framebuffer) { + session.framebufferId = GL.getNewId(GL.framebuffers); + GL.framebuffers[session.framebufferId] = session.layer.framebuffer; + } + + var sizeof_CanvasFlags = 16; + var flags = Module.stackAlloc(sizeof_CanvasFlags); + HEAPU8.fill(0, flags, flags + sizeof_CanvasFlags); // memset(&flags, 0, sizeof(CanvasFlags)); + HEAPU8[flags + 12] = mode === 'inline' ? 0 : 1; // flags.stereo + var width = session.layer.framebufferWidth; + var height = session.layer.framebufferHeight; + session.canvas = Module['_lovrCanvasCreateFromHandle'](width, height, flags, session.framebufferId, 0, 0, 1, true); + Module.stackRestore(flags); + + session.animationFrame = session.requestAnimationFrame(function onFrame(t, frame) { + session.animationFrame = session.requestAnimationFrame(onFrame); + session.displayTime = t; + session.frame = frame; + session.viewer = frame.getViewerPose(session.space); + + if (!state.renderCallback) return; + + var views = session.viewer.views; + var stereo = views.length > 1; + var matrices = (state.camera + 8) >> 2; + HEAPU8[state.camera + 0] = stereo; // camera.stereo = stereo + HEAPU32[(state.camera + 4) >> 2] = session.canvas; // camera.canvas = session.canvas + HEAPF32.set(views[0].transform.inverse.matrix, matrices + 0); + HEAPF32.set(views[0].projectionMatrix, matrices + 32); + if (stereo) { + HEAPF32.set(views[1].transform.inverse.matrix, matrices + 16); + HEAPF32.set(views[1].projectionMatrix, matrices + 48); + } + + Module['_lovrGraphicsSetCamera'](state.camera, true); Module['dynCall_vi'](state.renderCallback, state.renderUserdata); - } + Module['_lovrGraphicsSetCamera'](0, false); + }); - Module['_lovrGraphicsSetCamera'](0, false); + session.addEventListener('inputsourceschange', function(event) { + session.inputSources.forEach(function(inputSource, i) { + if (event.removed.includes(inputSource)) { + session.inputSources[i] = null; + } + }); + + event.added.forEach(function(inputSource) { + if (inputSource.handedness === 'left') { + session.inputSources[1 /* DEVICE_HAND_LEFT */] = inputSource; + } else if (inputSource.handedness === 'right') { + session.inputSources[2 /* DEVICE_HAND_RIGHT */] = inputSource; + } + + for (var i = 0; i < inputSource.profiles.length; i++) { + var profile = inputSource.profiles[i]; + + // So far Oculus touch controllers are the only "meaningfully handed" controllers + // If more appear then a more general approach should be used + if (profile === 'oculus-touch') { + profile = profile + '-' + inputSource.handedness; + } + + if (mappings[profile]) { + inputSource.mapping = mappings[profile]; + break; + } + } + }); + }); + + session.addEventListener('end', function() { + delete state.sessions[session.mode]; + + if (session.canvas) { + Module['_lovrCanvasDestroy'](session.canvas); + Module._free(session.canvas - 4); + } + + if (session.framebufferId) { + GL.framebuffers[session.framebufferId].name = 0; + GL.framebuffers[session.framebufferId] = null; + } + + // If the immersive session ends (for any reason), switch back to the inline session + if (session.mode === 'immersive-vr') { + state.session = state.sessions.inline; + } + }); + + return session; }); }); - }); + } + + Module.lovr = Module.lovr || {}; + Module.lovr.enterVR = function() { + return startSession('immersive-vr', { + requiredFeatures: ['local-floor'], + optionalFeatures: ['bounded-floor'] + }); + }; + + Module.lovr.exitVR = function() { + return (state.session && state.session.mode === 'immersive-vr') ? state.session.end() : Promise.resolve(); + }; + + startSession('inline'); return true; }, webxr_destroy: function() { - function cleanup() { - // TODO release canvas - Module._free(state.camera|0); + for (mode in state.sessions) { + state.sessions[mode].end(); } - if (state.session) { - state.session.cancelAnimationFrame(state.animationFrame); - state.session.end().then(cleanup); - } else { - cleanup(); - } + Module._free(state.camera|0); + Module._free(state.boundsGeometry|0); }, webxr_getName: function(name, size) { @@ -99,20 +194,22 @@ var webxr = { }, webxr_getOriginType: function() { - if (state.referenceSpaceType === 'local-floor' || state.referenceSpaceType === 'bounded-floor') { - return 1; /* ORIGIN_FLOOR */ - } - - return 0; /* ORIGIN_HEAD */ + if (!state.session) return 0; + return state.session.spaceType.includes('floor') ? 1 /* ORIGIN_FLOOR */ : 0 /* ORIGIN_HEAD */; }, webxr_getDisplayTime: function() { - return state.displayTime; + return state.session ? (state.session.displayTime / 1000.0) : 0; }, webxr_getDisplayDimensions: function(width, height) { - HEAPU32[width >> 2] = state.layer.framebufferWidth; - HEAPU32[height >> 2] = state.layer.framebufferHeight; + if (state.session) { + HEAPU32[width >> 2] = state.session.layer.framebufferWidth; + HEAPU32[height >> 2] = state.session.layer.framebufferHeight; + } else { + HEAPU32[width >> 2] = 0; + HEAPU32[height >> 2] = 0; + } }, webxr_getDisplayFrequency: function() { @@ -124,34 +221,37 @@ var webxr = { }, webxr_getViewCount: function() { - if (!state.frame) { + if (!state.session) { return 0; } - return getViewerPose(state.frame).views.length; + return state.session.viewer.views.length; }, webxr_getViewPose: function(index, position, orientation) { - if (!state.frame) { + if (!state.session || !state.session.viewer) { return false; } - var view = getViewerPose(state.frame).views[index]; - if (view) { - HEAPF32[position >> 2 + 0] = view.transform.position.x; - HEAPF32[position >> 2 + 1] = view.transform.position.y; - HEAPF32[position >> 2 + 2] = view.transform.position.z; - HEAPF32[position >> 2 + 3] = view.transform.position.w; - HEAPF32[orientation >> 2 + 0] = view.transform.orientation.x; - HEAPF32[orientation >> 2 + 1] = view.transform.orientation.y; - HEAPF32[orientation >> 2 + 2] = view.transform.orientation.z; - HEAPF32[orientation >> 2 + 3] = view.transform.orientation.w; + + var view = state.session.viewer.views[index]; + if (state.session.viewer.views[index]) { + var transform = view.transform; + HEAPF32[position >> 2 + 0] = transform.position.x; + HEAPF32[position >> 2 + 1] = transform.position.y; + HEAPF32[position >> 2 + 2] = transform.position.z; + HEAPF32[position >> 2 + 3] = transform.position.w; + HEAPF32[orientation >> 2 + 0] = transform.orientation.x; + HEAPF32[orientation >> 2 + 1] = transform.orientation.y; + HEAPF32[orientation >> 2 + 2] = transform.orientation.z; + HEAPF32[orientation >> 2 + 3] = transform.orientation.w; return true; } + return false; }, webxr_getViewAngles: function(index, left, right, up, down) { - return false; + return false; // TODO }, webxr_getClipDistance: function(clipNear, clipFar) { @@ -160,6 +260,7 @@ var webxr = { }, webxr_setClipDistance: function(clipNear, clipFar) { + if (!state.session) return; state.clipNear = clipNear; state.clipFar = clipFar; state.session.updateRenderState({ @@ -169,40 +270,144 @@ var webxr = { }, webxr_getBoundsDimensions: function(width, depth) { - HEAPF32[width >> 2] = 0.0; + HEAPF32[width >> 2] = 0.0; // Unsupported, see #557 HEAPF32[depth >> 2] = 0.0; }, webxr_getBoundsGeometry: function(count) { - return 0; /* NULL */ // TODO + if (!state.session || !(state.session.space instanceof XRBoundedReferenceSpace)) { + return 0; /* NULL */ + } + + var points = state.session.space.boundsGeometry; + + if (state.boundsGeometryCount < points.length) { + Module._free(state.boundsGeometry|0); + state.boundsGeometry = Module._malloc(4 * 4 * points.length); + if (state.boundsGeometry === 0) { + return state.boundsGeometry; + } + } + + for (var i = 0; i < points.length; i++) { + HEAPF32.set(points[i], state.boundsGeometry + 4 * i); + } + + return state.boundsGeometry; }, webxr_getPose: function(device, position, orientation) { - return false; // TODO + if (!state.session || !state.session.viewer) return false; + + if (device === 0 /* DEVICE_HEAD */) { + var transform = state.session.viewer.transform; + HEAPF32[position >> 2 + 0] = transform.position.x; + HEAPF32[position >> 2 + 1] = transform.position.y; + HEAPF32[position >> 2 + 2] = transform.position.z; + HEAPF32[position >> 2 + 3] = transform.position.w; + HEAPF32[orientation >> 2 + 0] = transform.orientation.x; + HEAPF32[orientation >> 2 + 1] = transform.orientation.y; + HEAPF32[orientation >> 2 + 2] = transform.orientation.z; + HEAPF32[orientation >> 2 + 3] = transform.orientation.w; + return true; + } + + if (state.session.inputSources[device]) { + var inputSource = state.session.inputSources[device]; + var space = inputSource.gripSpace || inputSource.targetRaySpace; + var transform = state.session.frame.getPose(space, state.session.space).transform; + HEAPF32[position >> 2 + 0] = transform.position.x; + HEAPF32[position >> 2 + 1] = transform.position.y; + HEAPF32[position >> 2 + 2] = transform.position.z; + HEAPF32[position >> 2 + 3] = transform.position.w; + HEAPF32[orientation >> 2 + 0] = transform.orientation.x; + HEAPF32[orientation >> 2 + 1] = transform.orientation.y; + HEAPF32[orientation >> 2 + 2] = transform.orientation.z; + HEAPF32[orientation >> 2 + 3] = transform.orientation.w; + return true; + } + + return false; }, webxr_getVelocity: function(device, velocity, angularVelocity) { - return false; // TODO + return false; // Unsupported, see #619 }, webxr_isDown: function(device, button, down, changed) { - return false; // TODO + if (!state.session) return false; + + var inputSource = state.session.inputSources[device]; + if (!inputSource || !inputSource.gamepad || !inputSource.mapping || !inputSource.mapping[button]) { + return false; + } + + HEAPU32[down >> 2] = inputSource.gamepad.buttons[inputSource.mapping[button]].pressed ? 1 : 0; + HEAPU32[changed >> 2] = 0; // TODO + return true; }, webxr_isTouched: function(device, button, touched) { - return false; // TODO + if (!state.session) return false; + + var inputSource = state.session.inputSources[device]; + if (!inputSource || !inputSource.gamepad || !inputSource.mapping || !inputSource.mapping[button]) { + return false; + } + + HEAPU32[touched >> 2] = inputSource.gamepad.buttons[inputSource.mapping[button]].touched ? 1 : 0; + return true; }, webxr_getAxis: function(device, axis, value) { - return false; // TODO + if (!state.session) return false; + + var inputSource = state.session.inputSources[device]; + if (!inputSource || !inputSource.gamepad || !inputSource.mapping) { + return false; + } + + switch (axis) { + // These 1D axes are queried as buttons in the Gamepad API + // The DeviceAxis enumerants match the DeviceButton ones, so they're interchangeable + case 0: /* AXIS_TRIGGER */ + case 3: /* AXIS_GRIP */ + if (inputSource.mapping[axis]) { + HEAPF32[value >> 2] = inputSource.gamepad.buttons[inputSource.mapping[axis]].value; + return true; + } + return false; + + case 1: /* AXIS_THUMBSTICK */ + HEAPF32[value >> 2 + 0] = inputSource.gamepad.axes[2]; + HEAPF32[value >> 2 + 1] = inputSource.gamepad.axes[3]; + return true; + + case 2: /* AXIS_TOUCHPAD */ + HEAPF32[value >> 2 + 0] = inputSource.gamepad.axes[0]; + HEAPF32[value >> 2 + 1] = inputSource.gamepad.axes[1]; + return true; + + default: + return false; + } }, webxr_vibrate: function(device, strength, duration, frequency) { - return false; // TODO + if (!state.session) return false; + + var inputSource = state.session.inputSources[device]; + if (!inputSource || !inputSource.gamepad || !inputSource.gamepad.hapticActuators || !inputSource.gamepad.hapticActuators[0]) { + return false; + } + + // Not technically an official WebXR feature, but widely supported + inputSource.gamepad.hapticActuators[0].pulse(strength, duration * 1000); + return true; }, webxr_newModelData: function(device, animated) { - return 0; /* NULL */ // TODO + return 0; /* NULL */ }, webxr_animate: function(device, model) {