From e16a908975507737d14da6d5064e9b34a171b479 Mon Sep 17 00:00:00 2001 From: Squareys Date: Mon, 9 Dec 2019 13:43:56 +0100 Subject: [PATCH] webxr: Inital example code emscripten-webxr is at e101ce7cfc3c92962a65771f43c2d61775edd348 Signed-off-by: Squareys --- CMakeLists.txt | 1 + doc/changelog-examples.dox | 2 + doc/credits.dox | 3 +- doc/webxr.dox | 80 ++++++++++ doc/webxr.png | Bin 0 -> 49549 bytes package/ci/travis-emscripten.sh | 1 + src/CMakeLists.txt | 5 + src/webvr/CMakeLists.txt | 3 +- src/webxr/CMakeLists.txt | 80 ++++++++++ src/webxr/WebXrExample.cpp | 257 ++++++++++++++++++++++++++++++++ src/webxr/library_webxr.js | 253 +++++++++++++++++++++++++++++++ src/webxr/webxr.h | 169 +++++++++++++++++++++ src/webxr/webxr.html | 21 +++ 13 files changed, 873 insertions(+), 2 deletions(-) create mode 100644 doc/webxr.dox create mode 100644 doc/webxr.png create mode 100644 src/webxr/CMakeLists.txt create mode 100644 src/webxr/WebXrExample.cpp create mode 100644 src/webxr/library_webxr.js create mode 100644 src/webxr/webxr.h create mode 100644 src/webxr/webxr.html diff --git a/CMakeLists.txt b/CMakeLists.txt index cf54de96e..ec7607ee9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,7 @@ option(WITH_TRIANGLE_VULKAN_EXAMPLE "Build Vulkan Triangle example" OFF) option(WITH_VIEWER_EXAMPLE "Build Viewer example (requires the AnySceneImporter plugin)" OFF) if(CORRADE_TARGET_EMSCRIPTEN) option(WITH_WEBVR_EXAMPLE "Build WebVR example" OFF) + option(WITH_WEBXR_EXAMPLE "Build WebXR example" OFF) endif() # A single output location. After a decade of saying NO THIS IS A NON-SOLUTION diff --git a/doc/changelog-examples.dox b/doc/changelog-examples.dox index 1bd1bae7d..cf4a5bde9 100644 --- a/doc/changelog-examples.dox +++ b/doc/changelog-examples.dox @@ -37,6 +37,8 @@ namespace Magnum { - New @ref examples-fluidsimulation3d example (see [mosra/magnum-examples#69](https://github.com/mosra/magnum-examples/pull/69), [mosra/magnum-examples#70](https://github.com/mosra/magnum-examples/pull/70)) +- New @ref examples-webxr example (see + [mosra/magnum-examples#73](https://github.com/mosra/magnum-examples/pull/73)) @subsection changelog-examples-latest-changes Changes and improvements diff --git a/doc/credits.dox b/doc/credits.dox index 93d48c1e6..2888e6570 100644 --- a/doc/credits.dox +++ b/doc/credits.dox @@ -47,7 +47,8 @@ namespace Magnum { fixes - **Jonathan Hale** ([\@Squareys](https://github.com/Squareys)) --- the @ref examples-ovr, @ref examples-audio, @ref examples-arealights, - @ref examples-webvr, @ref examples-leapmotion and @ref examples-imgui + @ref examples-webvr, @ref examples-leapmotion, @ref examples-imgui and + @ref examples-webxr examples, porting to @ref Platform::EmscriptenApplication - **Konstantinos Chatzilygeroudis** ([\@costashatz](https://github.com/costashatz)) --- the @ref examples-dart example diff --git a/doc/webxr.dox b/doc/webxr.dox new file mode 100644 index 000000000..3ce052419 --- /dev/null +++ b/doc/webxr.dox @@ -0,0 +1,80 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + Copyright © 2019 Jonathan Hale + + 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. +*/ + +namespace Magnum { +/** @page examples-webxr WebXR +@brief Basic WebXR application. + +@m_footernavigation + +@image html webxr.png width=480px + +This example is a basic demonstration of how to use the +[Emscripten WebXR library](https://github.com/VhiteRabbit/emscripten-webxr) +with Magnum. + +For the full experience, this example requires a WebXR 1.0 capable browser and +a VR headset. Before Chrome 79 or Oculus Browser 7.0 you may need to enable WebXR +support explicitly on your browser --- on Chrome go into `chrome://flags` and +search for WebXR. Works on Android as well. + +@m_div{m-button m-primary} @m_div{m-big} Live web demo @m_enddiv @m_div{m-small} uses WebAssembly & WebGL @m_enddiv @m_enddiv + +Note that there is also a @ref examples-webvr "WebVR example", a +@ref examples-ovr "Oculus VR example" and a +@ref examples-leapmotion "Leap Motion example". + +@section examples-webxr-controls Controls + +- @m_class{m-label m-default} **Mouse click** --- enter VR +- @m_class{m-label m-default} **Esc** --- exit VR + +@section examples-webxr-credits Credits + +This example was originally contributed by [Jonathan Hale](https://github.com/Squareys) from +[Vhite Rabbit](https://github.com/VhiteRabbit). + +@m_class{m-block m-success} + +@thirdparty This example makes use of the [emscripten-webxr](https://github.com/VhiteRabbit/emscripten-webxr) + library, released under the @m_class{m-label m-success} **MIT** + ([license text](https://github.com/VhiteRabbit/emscripten-webxr/blob/master/LICENSE), + [choosealicense.com](https://choosealicense.com/licenses/MIT/)). + Attribution is appreciated but not required. + +@section examples-webxr-source Source + +Full source code is linked below and together with assets also available in the +[magnum-examples GitHub repository](https://github.com/mosra/magnum-examples/tree/master/src/webxr). + +- @ref webxr/CMakeLists.txt "CMakeLists.txt" +- @ref webxr/WebVrExample.cpp "WebXrExample.cpp" + +@example webxr/CMakeLists.txt @m_examplenavigation{examples-webxr,webxr/} @m_footernavigation +@example webxr/WebXrExample.cpp @m_examplenavigation{examples-webxr,webxr/} @m_footernavigation + +*/ +} diff --git a/doc/webxr.png b/doc/webxr.png new file mode 100644 index 0000000000000000000000000000000000000000..9457576efe4e0f04bf4e6c6e0898b742c5f20c15 GIT binary patch literal 49549 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 +# 2019 — Jonathan Hale # # This is free and unencumbered software released into the public domain. # @@ -129,3 +130,7 @@ endif() if(WITH_WEBVR_EXAMPLE) add_subdirectory(webvr) endif() + +if(WITH_WEBXR_EXAMPLE) + add_subdirectory(webxr) +endif() diff --git a/src/webvr/CMakeLists.txt b/src/webvr/CMakeLists.txt index f2f3eb981..1de9bc157 100644 --- a/src/webvr/CMakeLists.txt +++ b/src/webvr/CMakeLists.txt @@ -53,7 +53,8 @@ find_package(Magnum REQUIRED Primitives Shaders EmscriptenApplication) - +e101ce7cfc3c92962a65771f43c2d61775edd348) +endif() set_directory_properties(PROPERTIES CORRADE_USE_PEDANTIC_FLAGS ON) add_executable(magnum-webvr diff --git a/src/webxr/CMakeLists.txt b/src/webxr/CMakeLists.txt new file mode 100644 index 000000000..cc9e62067 --- /dev/null +++ b/src/webxr/CMakeLists.txt @@ -0,0 +1,80 @@ +# +# This file is part of Magnum. +# +# Original authors — credit is appreciated but not required: +# +# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 — +# Vladimír Vondruš +# 2019 — Jonathan Hale +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or distribute +# this software, either in source code form or as a compiled binary, for any +# purpose, commercial or non-commercial, and by any means. +# +# In jurisdictions that recognize copyright laws, the author or authors of +# this software dedicate any and all copyright interest in the software to +# the public domain. We make this dedication for the benefit of the public +# at large and to the detriment of our heirs and successors. We intend this +# dedication to be an overt act of relinquishment in perpetuity of all +# present and future rights to this software under copyright law. +# +# 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 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. +# + +cmake_minimum_required(VERSION 3.4) + +project(MagnumWebXrExample CXX) + +# Add module path in case this is project root +if(PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/../../modules/" ${CMAKE_MODULE_PATH}) +endif() + +find_package(Magnum REQUIRED + GL + MeshTools + Primitives + Shaders + EmscriptenApplication) + +set_directory_properties(PROPERTIES CORRADE_USE_PEDANTIC_FLAGS ON) + +add_executable(magnum-webxr WebXrExample.cpp) +target_link_libraries(magnum-webxr PRIVATE + Magnum::Application + Magnum::GL + Magnum::Magnum + Magnum::MeshTools + Magnum::Primitives + Magnum::Shaders) +target_link_options(magnum-webxr PRIVATE + "SHELL:--js-library ${CMAKE_CURRENT_BINARY_DIR}/library_webxr.js") + +# Copy the boilerplate next to the executable so it can be run directly from +# the build dir; provide an install target as well +add_custom_command(TARGET magnum-webxr POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${MAGNUM_EMSCRIPTENAPPLICATION_JS} + ${MAGNUM_WEBAPPLICATION_CSS} + $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/webxr.html + $/magnum-webxr.html) + +install(FILES webxr.html DESTINATION ${MAGNUM_DEPLOY_PREFIX}/webxr RENAME index.html) +install(TARGETS magnum-webxr DESTINATION ${MAGNUM_DEPLOY_PREFIX}/webxr) +install(FILES + ${MAGNUM_EMSCRIPTENAPPLICATION_JS} + ${MAGNUM_WEBAPPLICATION_CSS} + DESTINATION ${MAGNUM_DEPLOY_PREFIX}/webxr) +install(FILES + ${CMAKE_BINARY_DIR}/bin/magnum-webxr.js.mem + ${CMAKE_BINARY_DIR}/bin/magnum-webxr.wasm + DESTINATION ${MAGNUM_DEPLOY_PREFIX}/webxr OPTIONAL) diff --git a/src/webxr/WebXrExample.cpp b/src/webxr/WebXrExample.cpp new file mode 100644 index 000000000..6ade168b1 --- /dev/null +++ b/src/webxr/WebXrExample.cpp @@ -0,0 +1,257 @@ +/* + This file is part of Magnum. + + Original authors — credit is appreciated but not required: + + 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 — + Vladimír Vondruš + 2019 — Jonathan Hale + + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or distribute + this software, either in source code form or as a compiled binary, for any + purpose, commercial or non-commercial, and by any means. + + In jurisdictions that recognize copyright laws, the author or authors of + this software dedicate any and all copyright interest in the software to + the public domain. We make this dedication for the benefit of the public + at large and to the detriment of our heirs and successors. We intend this + dedication to be an overt act of relinquishment in perpetuity of all + present and future rights to this software under copyright law. + + 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 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. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "webxr.h" + +namespace Magnum { namespace Examples { + +using namespace Math::Literals; + +class WebXrExample: public Platform::Application { + public: + explicit WebXrExample(const Arguments& arguments); + ~WebXrExample(); + + /* Callbacks for WebXR */ + void onError(int error); + void drawWebXRFrame(WebXRView* views); + void sessionStart(); + void sessionEnd(); + + private: + void drawEvent() override; + void keyPressEvent(KeyEvent& e) override; + void mousePressEvent(MouseEvent& event) override; + + GL::Mesh _cubeMesh; + Matrix4 _cubeTransformations[4]{ + Matrix4::translation({ 0.0f, 0.0f, -3.0f})*Matrix4::rotationY(45.0_degf), + Matrix4::translation({ 5.0f, 0.0f, 0.0f})*Matrix4::rotationY(45.0_degf), + Matrix4::translation({-10.0f, 0.0f, 0.0f})*Matrix4::rotationY(45.0_degf), + Matrix4::translation({ 0.0f, 0.0f, 7.0f})*Matrix4::rotationY(45.0_degf)}; + Color3 _cubeColors[4]{ + 0xffff00_rgbf, 0xff0000_rgbf, 0x0000ff_rgbf, 0x00ffff_rgbf}; + + GL::Mesh _controllerMesh; + Matrix4 _controllerTransformations[2]; + Color3 _controllerColors[2]{ + {0.0f, 0.0f, 1.0f}, + {1.0f, 0.0f, 0.0f}}; + + Shaders::Phong _shader; + Matrix4 _projectionMatrices[2]; + Matrix4 _viewMatrices[2]; + Range2Di _viewports[2]; + + bool _inXR = false; +}; + +WebXrExample::WebXrExample(const Arguments& arguments): + Platform::Application(arguments, + Configuration{}.setSize({640, 320}), + GLConfiguration{}.setSampleCount(4)) +{ + GL::Renderer::enable(GL::Renderer::Feature::DepthTest); + + /* Setup cube mesh */ + _cubeMesh = MeshTools::compile(Primitives::cubeSolid()); + _controllerMesh = MeshTools::compile(Primitives::uvSphereSolid(8, 8)); + + /* Left and right controller placeholders */ + _shader.setSpecularColor(Color3(1.0f)) + .setShininess(20); + + webxr_init( + WEBXR_SESSION_MODE_IMMERSIVE_VR, + /* Frame callback */ + [](void* userData, int, float[16], WebXRView* views) { + static_cast(userData)->drawWebXRFrame(views); + }, + /* Session end callback */ + [](void* userData) { + static_cast(userData)->sessionStart(); + }, + /* Session end callback */ + [](void* userData) { + static_cast(userData)->sessionEnd(); + }, + /* Error callback */ + [](void* userData, int error) { + static_cast(userData)->onError(error); + }, + /* userData */ + this); + + redraw(); +} + +WebXrExample::~WebXrExample() { + if(_inXR) webxr_request_exit(); +} + +void WebXrExample::drawWebXRFrame(WebXRView* views) { + int viewIndex = 0; + for(WebXRView view : {views[0], views[1]}) { + _viewports[viewIndex] = Range2Di::fromSize( + {view.viewport[0], view.viewport[1]}, + {view.viewport[2], view.viewport[3]}); + _viewMatrices[viewIndex] = Matrix4::from(view.viewMatrix); + _projectionMatrices[viewIndex] = Matrix4::from(view.projectionMatrix); + + ++viewIndex; + } + + WebXRInputSource sources[2]; + int sourcesCount = 0; + webxr_get_input_sources(sources, 5, &sourcesCount); + + for(int i = 0; i < sourcesCount; ++i) { + webxr_get_input_pose(&sources[i], _controllerTransformations[i].data()); + } + + drawEvent(); +} + +void WebXrExample::drawEvent() { + if(!_inXR) { + /* Single view */ + _projectionMatrices[0] = Matrix4::perspectiveProjection(90.0_degf, + Vector2(windowSize()).aspectRatio(), 0.01f, 100.0f); + _viewMatrices[0] = Matrix4{}; + _viewports[0] = Range2Di{{}, windowSize()}; + + /* Set some default transformation for the controllers so that they don't + block view */ + _controllerTransformations[0] = Matrix4::translation({-0.5f, -0.4f, -1.0f}); + _controllerTransformations[1] = Matrix4::translation({0.5f, -0.4f, -1.0f}); + } + + GL::defaultFramebuffer.clear( + GL::FramebufferClear::Color|GL::FramebufferClear::Depth); + + const Vector3 lightPos{3.0f, 3.0f, 3.0f}; + + const int viewCount = _inXR ? 2 : 1; + for(int eye = 0; eye < viewCount; ++eye) { + GL::defaultFramebuffer.setViewport(_viewports[eye]); + + _shader.setLightPosition(_viewMatrices[eye].transformPoint(lightPos)) + .setProjectionMatrix(_projectionMatrices[eye]); + + /* Draw cubes */ + for(int i = 0; i < 4; ++i) { + const Matrix4& transformationMatrix = + _viewMatrices[eye]*_cubeTransformations[i]; + _shader.setTransformationMatrix(transformationMatrix) + .setNormalMatrix(transformationMatrix.rotationScaling()) + .setDiffuseColor(_cubeColors[i]); + _cubeMesh.draw(_shader); + } + + /* Draw controller models */ + for(int i = 0; i < 2; ++i) { + const Matrix4& transformationMatrix = + _viewMatrices[eye]*_controllerTransformations[i]*Matrix4::scaling(Vector3{0.05f}); + _shader.setTransformationMatrix(transformationMatrix) + .setNormalMatrix(transformationMatrix.rotationScaling()) + .setDiffuseColor(_controllerColors[i]); + _controllerMesh.draw(_shader); + } + } + + /* No need to call redraw() on WebXR, the webxr callback already does this */ +} + +void WebXrExample::sessionStart() { + if(_inXR) return; + _inXR = true; + + Debug{} << "Entered VR"; +} + +void WebXrExample::sessionEnd() { + _inXR = false; + Debug{} << "Exited VR"; + redraw(); +} + +void WebXrExample::onError(int error) { + switch(error) { + case WEBXR_ERR_API_UNSUPPORTED: + Error{} << "WebXR unsupported in this browser."; + break; + case WEBXR_ERR_GL_INCAPABLE: + Error{} << "GL context cannot be used to render to WebXR"; + break; + case WEBXR_ERR_SESSION_UNSUPPORTED: + Error{} << "VR not supported on this device"; + break; + default: + Error{} << "Unknown WebXR error with code" << error; + } +} + +void WebXrExample::keyPressEvent(KeyEvent& e) { + if(e.key() == KeyEvent::Key::Esc && _inXR) { + webxr_request_exit(); + } +} + +void WebXrExample::mousePressEvent(MouseEvent& event) { + if(event.button() != MouseEvent::Button::Left) return; + /* Request rendering to the XR device */ + webxr_request_session(); + event.setAccepted(); +} + +}} + +MAGNUM_APPLICATION_MAIN(Magnum::Examples::WebXrExample) diff --git a/src/webxr/library_webxr.js b/src/webxr/library_webxr.js new file mode 100644 index 000000000..25bb780d8 --- /dev/null +++ b/src/webxr/library_webxr.js @@ -0,0 +1,253 @@ +var LibraryWebXR = { + +$WebXR: { + _coordinateSystem: null, + _curRAF: null, + + _nativize_vec3: function(offset, vec) { + setValue(offset + 0, vec[0], 'float'); + setValue(offset + 4, vec[1], 'float'); + setValue(offset + 8, vec[2], 'float'); + + return offset + 12; + }, + + _nativize_matrix: function(offset, mat) { + for (var i = 0; i < 16; ++i) { + setValue(offset + i*4, mat[i], 'float'); + } + + return offset + 16*4; + }, + /* Sets input source values to offset and returns pointer after struct */ + _nativize_input_source: function(offset, inputSource, id) { + var handedness = -1; + if(inputSource.handedness == "left") handedness = 0; + else if(inputSource.handedness == "right") handedness = 1; + + var targetRayMode = 0; + if(inputSource.targetRayMode == "tracked-pointer") targetRayMode = 1; + else if(inputSource.targetRayMode == "screen") targetRayMode = 2; + + setValue(offset, id, 'i32'); + offset +=4; + setValue(offset, handedness, 'i32'); + offset +=4; + setValue(offset, targetRayMode, 'i32'); + offset +=4; + + return offset; + }, + + _set_input_callback: function(event, callback, userData) { + var s = Module['webxr_session']; + if(!s) return; + if(!callback) return; + + s.addEventListener(event, function(e) { + /* Nativize input source */ + var inputSource = Module._malloc(8); // 2*sizeof(int32) + WebXR._nativize_input_source(inputSource, e.inputSource, i); + + /* Call native callback */ + dynCall('vii', callback, [inputSource, userData]); + + _free(inputSource); + }); + }, + + _set_session_callback: function(event, callback, userData) { + var s = Module['webxr_session']; + if(!s) return; + if(!callback) return; + + s.addEventListener(event, function() { + dynCall('vi', callback, [userData]); + }); + } +}, + +webxr_init: function(mode, frameCallback, startSessionCallback, endSessionCallback, errorCallback, userData) { + function onError(errorCode) { + if(!errorCallback) return; + dynCall('vii', errorCallback, [userData, errorCode]); + }; + + function onSessionEnd(session) { + if(!endSessionCallback) return; + dynCall('vi', endSessionCallback, [userData]); + }; + + function onSessionStart() { + if(!startSessionCallback) return; + dynCall('vi', startSessionCallback, [userData]); + }; + + function onFrame(time, frame) { + if(!frameCallback) return; + /* Request next frame */ + const session = frame.session; + /* RAF is set to null on session end to avoid rendering */ + if(Module['webxr_session'] != null) session.requestAnimationFrame(onFrame); + + const pose = frame.getViewerPose(WebXR._coordinateSystem); + if(!pose) return; + + const SIZE_OF_WEBXR_VIEW = (16 + 16 + 4)*4; + const views = Module._malloc(SIZE_OF_WEBXR_VIEW*2 + 16*4); + + const glLayer = session.renderState.baseLayer; + pose.views.forEach(function(view) { + const viewport = glLayer.getViewport(view); + const viewMatrix = view.transform.inverse.matrix; + let offset = views + SIZE_OF_WEBXR_VIEW*(view.eye == 'left' ? 0 : 1); + + offset = WebXR._nativize_matrix(offset, viewMatrix); + offset = WebXR._nativize_matrix(offset, view.projectionMatrix); + + setValue(offset + 0, viewport.x, 'i32'); + setValue(offset + 4, viewport.y, 'i32'); + setValue(offset + 8, viewport.width, 'i32'); + setValue(offset + 12, viewport.height, 'i32'); + }); + + /* Model matrix */ + const modelMatrix = views + SIZE_OF_WEBXR_VIEW*2; + WebXR._nativize_matrix(modelMatrix, pose.transform.matrix); + + Module.ctx.bindFramebuffer(Module.ctx.FRAMEBUFFER, + glLayer.framebuffer); + /* HACK: This is not generally necessary, but chrome seems to detect whether the + * page is sending frames by waiting for depth buffer clear or something */ + // TODO still necessary? + Module.ctx.clear(Module.ctx.DEPTH_BUFFER_BIT); + + /* Set and reset environment for webxr_get_input_pose calls */ + Module['webxr_frame'] = frame; + dynCall('viiii', frameCallback, [userData, time, modelMatrix, views]); + Module['webxr_frame'] = null; + + _free(views); + }; + + function onSessionStarted(session) { + Module['webxr_session'] = session; + + // React to session ending + session.addEventListener('end', function() { + Module['webxr_session'].cancelAnimationFrame(WebXR._curRAF); + WebXR._curRAF = null; + Module['webxr_session'] = null; + onSessionEnd(); + }); + + // Give application a chance to react to session starting + // e.g. finish current desktop frame. + onSessionStart(); + + // Ensure our context can handle WebXR rendering + Module.ctx.makeXRCompatible().then(function() { + // Create the base layer + session.updateRenderState({ + baseLayer: new XRWebGLLayer(session, Module.ctx) + }); + + session.requestReferenceSpace('local').then(refSpace => { + WebXR._coordinateSystem = refSpace; + // Start rendering + session.requestAnimationFrame(onFrame); + }); + }, function(err) { + onError(-3); + }); + }; + + if(navigator.xr) { + // Check if XR session is supported + navigator.xr.isSessionSupported((['inline', 'immersive-vr', 'immersive-ar'])[mode]).then(function() { + Module['webxr_request_session_func'] = function() { + navigator.xr.requestSession('immersive-vr').then(onSessionStarted); + }; + }, function() { + onError(-4); + }); + } else { + /* Call error callback with "WebXR not supported" */ + onError(-2); + } +}, + +webxr_request_session: function() { + var s = Module['webxr_request_session_func']; + if(s) Module['webxr_request_session_func'](); +}, + +webxr_request_exit: function() { + var s = Module['webxr_session']; + if(s) Module['webxr_session'].end(); +}, + +webxr_set_projection_params: function(near, far) { + var s = Module['webxr_session']; + if(!s) return; + + s.depthNear = near; + s.depthFar = far; +}, + +webxr_set_session_blur_callback: function(callback, userData) { + WebXR._set_session_callback("blur", callback, userData); +}, + +webxr_set_session_focus_callback: function(callback, userData) { + WebXR._set_session_callback("focus", callback, userData); +}, + +webxr_set_select_callback: function(callback, userData) { + WebXR._set_input_callback("select", callback, userData); +}, +webxr_set_select_start_callback: function(callback, userData) { + WebXR._set_input_callback("selectstart", callback, userData); +}, +webxr_set_select_end_callback: function(callback, userData) { + WebXR._set_input_callback("selectend", callback, userData); +}, + +webxr_get_input_sources: function(outArrayPtr, max, outCountPtr) { + var s = Module['webxr_session']; + if(!s) return; // TODO(squareys) warning or return error + + var i = 0; + for (let inputSource of s.inputSources) { + if(i >= max) break; + outArrayPtr = WebXR._nativize_input_source(outArrayPtr, inputSource, i); + ++i; + } + setValue(outCountPtr, i, 'i32'); +}, + +webxr_get_input_pose: function(source, outPosePtr) { + var f = Module['webxr_frame']; + if(!f) { + console.warn("Cannot call webxr_get_input_pose outside of frame callback"); + return; + } + + const id = getValue(source, 'i32'); + const input = Module['webxr_session'].inputSources[id]; + + pose = f.getPose(input.gripSpace, WebXR._coordinateSystem); + + offset = outPosePtr; + /* WebXRRay */ + offset = WebXR._nativize_matrix(offset, pose.transform.matrix); + + /* WebXRInputPose */ + //offset = WebXR._nativize_matrix(offset, pose.gripMatrix); + //setValue(offset, pose.emulatedPosition, 'i32'); +}, + +}; + +autoAddDeps(LibraryWebXR, '$WebXR'); +mergeInto(LibraryManager.library, LibraryWebXR); diff --git a/src/webxr/webxr.h b/src/webxr/webxr.h new file mode 100644 index 000000000..bb7e02ea2 --- /dev/null +++ b/src/webxr/webxr.h @@ -0,0 +1,169 @@ +#ifndef WEBXR_H_ +#define WEBXR_H_ + +/** @file + * @brief Minimal WebXR Device API wrapper + */ + +#ifdef __cplusplus +extern "C" +#endif +{ + +/** Errors enum */ +enum WebXRError { + WEBXR_ERR_API_UNSUPPORTED = -2, /**< WebXR Device API not supported in this browser */ + WEBXR_ERR_GL_INCAPABLE = -3, /**< GL context cannot render WebXR */ + WEBXR_ERR_SESSION_UNSUPPORTED = -4, /**< given session mode not supported */ +}; + +/** WebXR handedness */ +enum WebXRHandedness { + WEBXR_HANDEDNESS_NONE = -1, + WEBXR_HANDEDNESS_LEFT = 0, + WEBXR_HANDEDNESS_RIGHT = 1, +}; + +/** WebXR target ray mode */ +enum WebXRTargetRayMode { + WEBXR_TARGET_RAY_MODE_GAZE = 0, + WEBXR_TARGET_RAY_MODE_TRACKED_POINTER = 1, + WEBXR_TARGET_RAY_MODE_SCREEN = 2, +}; + +/** WebXR 'XRSessionMode' enum*/ +enum WebXRSessionMode { + WEBXR_SESSION_MODE_INLINE = 0, /** "inline" */ + WEBXR_SESSION_MODE_IMMERSIVE_VR = 1, /** "immersive-vr" */ + WEBXR_SESSION_MODE_IMMERSIVE_AR = 2, /** "immersive-ar" */ +}; + +/** WebXR view */ +typedef struct WebXRView { + /* view matrix */ + float viewMatrix[16]; + /* projection matrix */ + float projectionMatrix[16]; + /* x, y, width, height of the eye viewport on target texture */ + int viewport[4]; +} WebXRView; + +typedef struct WebXRInputSource { + int id; + WebXRHandedness handedness; + WebXRTargetRayMode targetRayMode; +} WebXRInputSource; + +/** +Callback for errors + +@param userData User pointer passed to init_webxr() +@param error Error code +*/ +typedef void (*webxr_error_callback_func)(void* userData, int error); + +/** +Callback for frame rendering + +@param userData User pointer passed to init_webxr() +@param time Current frame time +@param modelMatrix Transformation of the XR Device to tracking origin +@param views Array of two @ref WebXRView +*/ +typedef void (*webxr_frame_callback_func)(void* userData, int time, float modelMatrix[16], WebXRView views[2]); + +/** +Callback for VR session start + +@param userData User pointer passed to set_session_start_callback +*/ +typedef void (*webxr_session_callback_func)(void* userData); + +/** +Init WebXR rendering + +@param mode Session mode from @ref WebXRSessionMode. +@param frameCallback Callback called every frame +@param sessionStartCallback Callback called when session is started +@param sessionEndCallback Callback called when session ended +@param errorCallback Callback called every frame +@param userData User data passed to the callbacks +*/ +extern void webxr_init( + WebXRSessionMode mode, + webxr_frame_callback_func frameCallback, + webxr_session_callback_func sessionStartCallback, + webxr_session_callback_func sessionEndCallback, + webxr_error_callback_func errorCallback, + void* userData); + +extern void webxr_set_session_blur_callback( + webxr_session_callback_func sessionBlurCallback, void* userData); +extern void webxr_set_session_focus_callback( + webxr_session_callback_func sessionFocusCallback, void* userData); + +/* +Request session presentation start + +Needs to be called from a [user activation event](https://html.spec.whatwg.org/multipage/interaction.html#triggered-by-user-activation). +*/ +extern void webxr_request_session(); + +/* +Request that the webxr presentation exits VR mode +*/ +extern void webxr_request_exit(); + +/** +Set projection matrix parameters for the webxr session + +@param near Distance of near clipping plane +@param far Distance of far clipping plane +*/ +extern void webxr_set_projection_params(float near, float far); + +/** + +WebXR Input + +*/ + +/** +Callback for primary input action. + +@param userData User pointer passed to @ref webxr_set_select_callback, @ref webxr_set_select_end_callback or @ref webxr_set_select_start_callback. +*/ +typedef void (*webxr_input_callback_func)(WebXRInputSource* inputSource, void* userData); + + +/** +Set callbacks for primary input action. +*/ +extern void webxr_set_select_callback( + webxr_input_callback_func callback, void* userData); +extern void webxr_set_select_start_callback( + webxr_input_callback_func callback, void* userData); +extern void webxr_set_select_end_callback( + webxr_input_callback_func callback, void* userData); + +/** +Get input sources. + +@param outArray @ref WebXRInputSource array to fill. +@param max Size of outArray (in elements). +@param outCount Will receive the number of input sources valid in outArray. +*/ +extern void webxr_get_input_sources( + WebXRInputSource* outArray, int max, int* outCount); + +/** +Get input pose. Can only be called during the frame callback. + +@param source The source to get the pose for. +@param outPose Where to store the pose. +*/ +extern void webxr_get_input_pose(WebXRInputSource* source, float* outMatrix); + +} + +#endif diff --git a/src/webxr/webxr.html b/src/webxr/webxr.html new file mode 100644 index 000000000..8814767a6 --- /dev/null +++ b/src/webxr/webxr.html @@ -0,0 +1,21 @@ + + + + + Magnum WebXR Example + + + + +

Magnum WebXR Example

+
+
+ +
Initialization...
+
+ + +
+
+ +