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 000000000..9457576ef Binary files /dev/null and b/doc/webxr.png differ diff --git a/package/ci/travis-emscripten.sh b/package/ci/travis-emscripten.sh index 4bf845731..2fde3c937 100755 --- a/package/ci/travis-emscripten.sh +++ b/package/ci/travis-emscripten.sh @@ -136,6 +136,7 @@ cmake .. \ -DWITH_TRIANGLE_SOKOL_EXAMPLE=OFF \ -DWITH_VIEWER_EXAMPLE=OFF \ -DWITH_WEBVR_EXAMPLE=ON \ + -DWITH_WEBXR_EXAMPLE=ON \ -G Ninja # Otherwise the job gets killed (probably because using too much memory) ninja -j4 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7a98cc148..3e3d198cb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ # # 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. # @@ -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...
+
+ + +
+
+ +