diff --git a/Examples/Applications/SkyboxViewer/index.js b/Examples/Applications/SkyboxViewer/index.js index e420d165e98..ab579f56cf4 100644 --- a/Examples/Applications/SkyboxViewer/index.js +++ b/Examples/Applications/SkyboxViewer/index.js @@ -6,10 +6,7 @@ import 'vtk.js/Sources/Rendering/Profiles/Geometry'; import HttpDataAccessHelper from 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import macro from 'vtk.js/Sources/macros'; import vtkDeviceOrientationToCamera from 'vtk.js/Sources/Interaction/Misc/DeviceOrientationToCamera'; -import vtkForwardPass from 'vtk.js/Sources/Rendering/OpenGL/ForwardPass'; import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; -import vtkRadialDistortionPass from 'vtk.js/Sources/Rendering/OpenGL/RadialDistortionPass'; -import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; import vtkSkybox from 'vtk.js/Sources/Rendering/Core/Skybox'; import vtkSkyboxReader from 'vtk.js/Sources/IO/Misc/SkyboxReader'; import vtkURLExtract from 'vtk.js/Sources/Common/Core/URLExtract'; @@ -41,14 +38,9 @@ let autoInit = true; const cameraFocalPoint = userParams.direction || [0, 0, -1]; const cameraViewUp = userParams.up || [0, 1, 0]; const cameraViewAngle = userParams.viewAngle || 100; -const enableVR = !!userParams.vr; const eyeSpacing = userParams.eye || 0.0; const grid = userParams.debug || false; const autoIncrementTimer = userParams.timer || 0; -const disableTouchNext = userParams.disableTouch || false; -const distk1 = userParams.k1 || 0.2; -const distk2 = userParams.k2 || 0.0; -const cameraCenterY = userParams.centerY || 0.0; const body = document.querySelector('body'); let fullScreenMetod = null; @@ -151,10 +143,8 @@ function createVisualization(container, mapReader) { const mainRenderer = fullScreenRenderer.getRenderer(); const interactor = fullScreenRenderer.getInteractor(); const actor = vtkSkybox.newInstance(); - let camera = mainRenderer.getActiveCamera(); - let leftRenderer = null; - let rightRenderer = null; - let updateCameraCallBack = mainRenderer.resetCameraClippingRange; + const camera = mainRenderer.getActiveCamera(); + const updateCameraCallBack = mainRenderer.resetCameraClippingRange; // Connect viz pipeline actor.addTexture(mapReader.getOutputData()); @@ -189,116 +179,28 @@ function createVisualization(container, mapReader) { updateSkybox(allPositions[nextIdx]); } - if (enableVR && vtkDeviceOrientationToCamera.isDeviceOrientationSupported()) { - // vtkMobileVR.getVRHeadset().then((headset) => { - // console.log('got headset'); - // console.log(headset); - // console.log(vtkMobileVR.hardware); - // }); - - leftRenderer = vtkRenderer.newInstance(); - rightRenderer = vtkRenderer.newInstance(); - - // Configure left/right renderers - leftRenderer.setViewport(0, 0, 0.5, 1); - leftRenderer.addActor(actor); - const leftCamera = leftRenderer.getActiveCamera(); - leftCamera.set(cameraConfiguration); - leftCamera.setWindowCenter(-eyeSpacing, -cameraCenterY); - - rightRenderer.setViewport(0.5, 0, 1, 1); - rightRenderer.addActor(actor); - const rightCamera = rightRenderer.getActiveCamera(); - rightCamera.set(cameraConfiguration); - rightCamera.setWindowCenter(eyeSpacing, -cameraCenterY); - - // Provide custom update callback + fake camera - updateCameraCallBack = () => { - leftRenderer.resetCameraClippingRange(); - rightRenderer.resetCameraClippingRange(); - }; - camera = { - setDeviceAngles(alpha, beta, gamma, screen) { - leftCamera.setDeviceAngles(alpha, beta, gamma, screen); - rightCamera.setDeviceAngles(alpha, beta, gamma, screen); - }, - }; - - // Reconfigure render window - renderWindow.addRenderer(leftRenderer); - renderWindow.addRenderer(rightRenderer); - renderWindow.removeRenderer(mainRenderer); - - const distPass = vtkRadialDistortionPass.newInstance(); - distPass.setK1(distk1); - distPass.setK2(distk2); - distPass.setCameraCenterY(cameraCenterY); - distPass.setCameraCenterX1(-eyeSpacing); - distPass.setCameraCenterX2(eyeSpacing); - distPass.setDelegates([vtkForwardPass.newInstance()]); - fullScreenRenderer.getAPISpecificRenderWindow().setRenderPasses([distPass]); - - // Hide any controller - fullScreenRenderer.setControllerVisibility(false); - - // Remove window interactions - interactor.unbindEvents(); - - // Attach touch control - if (!disableTouchNext) { - fullScreenRenderer - .getRootContainer() - .addEventListener('touchstart', nextPosition, true); - if (fullScreenMetod) { - fullScreenRenderer.getRootContainer().addEventListener( - 'touchend', - (e) => { - body[fullScreenMetod](); - }, - true - ); - } - } - - // Warning if browser does not support fullscreen - /* eslint-disable */ - if (navigator.userAgent.match('CriOS')) { - alert( - 'Chrome on iOS does not support fullscreen. Please use Safari instead.' - ); - } - if (navigator.userAgent.match('FxiOS')) { - alert( - 'Firefox on iOS does not support fullscreen. Please use Safari instead.' - ); - } - /* eslint-enable */ - } else { - camera.set(cameraConfiguration); - mainRenderer.addActor(actor); - - // add vr option button if supported - fullScreenRenderer.getApiSpecificRenderWindow().onHaveVRDisplay(() => { - if ( - fullScreenRenderer.getApiSpecificRenderWindow().getVrDisplay() - .capabilities.canPresent - ) { - const button = document.createElement('button'); - button.style.position = 'absolute'; - button.style.left = '10px'; - button.style.bottom = '10px'; - button.style.zIndex = 10000; + camera.set(cameraConfiguration); + mainRenderer.addActor(actor); + + // add vr option button if supported + if ( + navigator.xr !== undefined && + navigator.xr.isSessionSupported('immersive-vr') + ) { + const button = document.createElement('button'); + button.style.position = 'absolute'; + button.style.left = '10px'; + button.style.bottom = '10px'; + button.style.zIndex = 10000; + button.textContent = 'Send To VR'; + document.querySelector('body').appendChild(button); + button.addEventListener('click', () => { + if (button.textContent === 'Send To VR') { + fullScreenRenderer.getApiSpecificRenderWindow().startXR(); + button.textContent = 'Return From VR'; + } else { + fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); button.textContent = 'Send To VR'; - document.querySelector('body').appendChild(button); - button.addEventListener('click', () => { - if (button.textContent === 'Send To VR') { - fullScreenRenderer.getApiSpecificRenderWindow().startVR(); - button.textContent = 'Return From VR'; - } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopVR(); - button.textContent = 'Send To VR'; - } - }); } }); } diff --git a/Examples/Geometry/VR/index.js b/Examples/Geometry/VR/index.js index 3894605319c..332bd11401a 100644 --- a/Examples/Geometry/VR/index.js +++ b/Examples/Geometry/VR/index.js @@ -36,8 +36,7 @@ const renderWindow = fullScreenRenderer.getRenderWindow(); // this // ---------------------------------------------------------------------------- -const coneSource = vtkConeSource.newInstance({ height: 100.0, radius: 50.0 }); -// const coneSource = vtkConeSource.newInstance({ height: 1.0, radius: 0.5 }); +const coneSource = vtkConeSource.newInstance({ height: 100.0, radius: 50 }); const filter = vtkCalculator.newInstance(); filter.setInputConnection(coneSource.getOutputPort()); @@ -67,7 +66,7 @@ mapper.setInputConnection(filter.getOutputPort()); const actor = vtkActor.newInstance(); actor.setMapper(mapper); -actor.setPosition(20.0, 0.0, 0.0); +actor.setPosition(0.0, 0.0, -20.0); renderer.addActor(actor); renderer.resetCamera(); @@ -96,10 +95,10 @@ resolutionChange.addEventListener('input', (e) => { vrbutton.addEventListener('click', (e) => { if (vrbutton.textContent === 'Send To VR') { - fullScreenRenderer.getApiSpecificRenderWindow().startVR(); + fullScreenRenderer.getApiSpecificRenderWindow().startXR(); vrbutton.textContent = 'Return From VR'; } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopVR(); + fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); vrbutton.textContent = 'Send To VR'; } }); diff --git a/Sources/Rendering/Core/RenderWindowInteractor/index.js b/Sources/Rendering/Core/RenderWindowInteractor/index.js index 47dbda37ad3..b5ec257e2f8 100644 --- a/Sources/Rendering/Core/RenderWindowInteractor/index.js +++ b/Sources/Rendering/Core/RenderWindowInteractor/index.js @@ -366,7 +366,7 @@ function vtkRenderWindowInteractor(publicAPI, model) { return; } animationRequesters.add(requestor); - if (animationRequesters.size === 1) { + if (animationRequesters.size === 1 && !model.xrAnimation) { model.lastFrameTime = 0.1; model.lastFrameStart = Date.now(); model.animationRequest = requestAnimationFrame(publicAPI.handleAnimation); @@ -375,7 +375,7 @@ function vtkRenderWindowInteractor(publicAPI, model) { }; publicAPI.isAnimating = () => - model.vrAnimation || model.animationRequest !== null; + model.xrAnimation || model.animationRequest !== null; publicAPI.cancelAnimation = (requestor, skipWarning = false) => { if (!animationRequesters.has(requestor)) { @@ -398,17 +398,17 @@ function vtkRenderWindowInteractor(publicAPI, model) { } }; - publicAPI.switchToVRAnimation = () => { + publicAPI.switchToXRAnimation = () => { // cancel existing animation if any if (model.animationRequest) { cancelAnimationFrame(model.animationRequest); model.animationRequest = null; } - model.vrAnimation = true; + model.xrAnimation = true; }; - publicAPI.returnFromVRAnimation = () => { - model.vrAnimation = false; + publicAPI.returnFromXRAnimation = () => { + model.xrAnimation = false; if (animationRequesters.size !== 0) { model.FrameTime = -1; model.animationRequest = requestAnimationFrame(publicAPI.handleAnimation); @@ -756,7 +756,7 @@ function vtkRenderWindowInteractor(publicAPI, model) { // do not want extra renders as the make the apparent interaction // rate slower. publicAPI.render = () => { - if (model.animationRequest === null && !model.inRender) { + if (!publicAPI.isAnimating() && !model.inRender) { forceRender(); } }; diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index d76beddbd54..ed6d87ed230 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -1,16 +1,16 @@ +import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants'; import macro from 'vtk.js/Sources/macros'; import { registerViewConstructor } from 'vtk.js/Sources/Rendering/Core/RenderWindow'; import vtkForwardPass from 'vtk.js/Sources/Rendering/OpenGL/ForwardPass'; +import vtkOpenGLHardwareSelector from 'vtk.js/Sources/Rendering/OpenGL/HardwareSelector'; +import vtkShaderCache from 'vtk.js/Sources/Rendering/OpenGL/ShaderCache'; +import vtkOpenGLTextureUnitManager from 'vtk.js/Sources/Rendering/OpenGL/TextureUnitManager'; import vtkOpenGLViewNodeFactory from 'vtk.js/Sources/Rendering/OpenGL/ViewNodeFactory'; import vtkRenderPass from 'vtk.js/Sources/Rendering/SceneGraph/RenderPass'; -import vtkShaderCache from 'vtk.js/Sources/Rendering/OpenGL/ShaderCache'; import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderWindowViewNode'; -import vtkOpenGLTextureUnitManager from 'vtk.js/Sources/Rendering/OpenGL/TextureUnitManager'; -import vtkOpenGLHardwareSelector from 'vtk.js/Sources/Rendering/OpenGL/HardwareSelector'; -import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants'; const { vtkDebugMacro, vtkErrorMacro } = macro; -const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1; + const SCREENSHOT_PLACEHOLDER = { position: 'absolute', top: 0, @@ -227,6 +227,14 @@ function vtkOpenGLRenderWindow(publicAPI, model) { ) => { let result = null; + // Do we have webxr support + if ( + navigator.xr !== undefined && + navigator.xr.isSessionSupported('immersive-vr') + ) { + publicAPI.invokeHaveVRDisplay(); + } + const webgl2Supported = typeof WebGL2RenderingContext !== 'undefined'; model.webgl2 = false; if (model.defaultToWebgl2 && webgl2Supported) { @@ -243,20 +251,6 @@ function vtkOpenGLRenderWindow(publicAPI, model) { model.canvas.getContext('experimental-webgl', options); } - // Do we have webvr support - if (navigator.getVRDisplays) { - navigator.getVRDisplays().then((displays) => { - if (displays.length > 0) { - // take the first display for now - model.vrDisplay = displays[0]; - // set the clipping ranges - model.vrDisplay.depthNear = 0.01; // meters - model.vrDisplay.depthFar = 100.0; // meters - publicAPI.invokeHaveVRDisplay(); - } - }); - } - // prevent default context lost handler model.canvas.addEventListener( 'webglcontextlost', @@ -275,111 +269,131 @@ function vtkOpenGLRenderWindow(publicAPI, model) { return result; }; - publicAPI.startVR = () => { + // Request an XR session on the user device with WebXR, + // typically in response to a user request such as a button press + publicAPI.startXR = () => { + if (navigator.xr === undefined) { + throw new Error('WebXR is not available'); + } + + if (!navigator.xr.isSessionSupported('immersive-vr')) { + throw new Error('VR display is not available'); + } + if (model.xrSession === null) { + navigator.xr + .requestSession('immersive-vr') + .then(publicAPI.enterXR, () => { + throw new Error('Failed to create VR session!'); + }); + } else { + throw new Error('VR Session already exists!'); + } + }; + + // When an XR session is available, set up the XRWebGLLayer + // and request the first animation frame for the device + publicAPI.enterXR = async (xrSession) => { + model.xrSession = xrSession; model.oldCanvasSize = model.size.slice(); - if (model.vrDisplay.capabilities.canPresent) { - model.vrDisplay - .requestPresent([{ source: model.canvas }]) - .then(() => { - if ( - model.el && - model.vrDisplay.capabilities.hasExternalDisplay && - model.hideCanvasInVR - ) { - model.el.style.display = 'none'; - } - if (model.queryVRSize) { - const leftEye = model.vrDisplay.getEyeParameters('left'); - const rightEye = model.vrDisplay.getEyeParameters('right'); - const width = Math.floor( - leftEye.renderWidth + rightEye.renderWidth - ); - const height = Math.floor( - Math.max(leftEye.renderHeight, rightEye.renderHeight) - ); - publicAPI.setSize(width, height); - } else { - publicAPI.setSize(model.vrResolution); - } - const ren = model.renderable.getRenderers()[0]; - ren.resetCamera(); - model.vrFrameData = new VRFrameData(); - model.renderable.getInteractor().switchToVRAnimation(); + if (model.xrSession !== null) { + const gl = publicAPI.get3DContext(); + await gl.makeXRCompatible(); - model.vrSceneFrame = model.vrDisplay.requestAnimationFrame( - publicAPI.vrRender - ); - // If Broswer is chrome we need to request animation again to canvas update - if (IS_CHROME) { - model.vrSceneFrame = model.vrDisplay.requestAnimationFrame( - publicAPI.vrRender - ); - } - }) - .catch(() => { - console.error('failed to requestPresent'); - }); + const glLayer = new global.XRWebGLLayer(model.xrSession, gl); + publicAPI.setSize(glLayer.framebufferWidth, glLayer.framebufferHeight); + + model.xrSession.updateRenderState({ + baseLayer: glLayer, + }); + + model.xrSession.requestReferenceSpace('local').then((refSpace) => { + model.xrReferenceSpace = refSpace; + }); + + model.renderable.getInteractor().switchToXRAnimation(); + model.xrSceneFrame = model.xrSession.requestAnimationFrame( + publicAPI.xrRender + ); } else { - vtkErrorMacro('vrDisplay is not connected'); + throw new Error('Failed to enter VR with a null xrSession.'); } }; - publicAPI.stopVR = () => { - model.renderable.getInteractor().returnFromVRAnimation(); - model.vrDisplay.exitPresent(); - model.vrDisplay.cancelAnimationFrame(model.vrSceneFrame); + publicAPI.stopXR = async () => { + if (navigator.xr === undefined) { + // WebXR polyfill not available so nothing to do + return; + } - publicAPI.setSize(...model.oldCanvasSize); - if (model.el && model.vrDisplay.capabilities.hasExternalDisplay) { - model.el.style.display = 'block'; + if (model.xrSession !== null) { + model.xrSession.cancelAnimationFrame(model.xrSceneFrame); + model.renderable.getInteractor().returnFromXRAnimation(); + const gl = publicAPI.get3DContext(); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + await model.xrSession.end().catch((error) => { + if (!(error instanceof DOMException)) { + throw error; + } + }); + model.xrSession = null; } + publicAPI.setSize(...model.oldCanvasSize); + + // Reset to default canvas const ren = model.renderable.getRenderers()[0]; ren.getActiveCamera().setProjectionMatrix(null); + ren.resetCamera(); ren.setViewport(0.0, 0, 1.0, 1.0); publicAPI.traverseAllPasses(); }; - publicAPI.vrRender = () => { - // If not presenting for any reason, we do not submit frame - if (!model.vrDisplay.isPresenting) { - return; - } - model.renderable.getInteractor().updateGamepads(model.vrDisplay.displayId); - model.vrSceneFrame = model.vrDisplay.requestAnimationFrame( - publicAPI.vrRender + publicAPI.xrRender = async (t, frame) => { + const xrSession = frame.session; + + model.xrSceneFrame = model.xrSession.requestAnimationFrame( + publicAPI.xrRender ); - model.vrDisplay.getFrameData(model.vrFrameData); - // get the first renderer - const ren = model.renderable.getRenderers()[0]; + const xrPose = frame.getViewerPose(model.xrReferenceSpace); - // do the left eye - ren.setViewport(0, 0, 0.5, 1.0); - ren - .getActiveCamera() - .computeViewParametersFromPhysicalMatrix( - model.vrFrameData.leftViewMatrix - ); - ren - .getActiveCamera() - .setProjectionMatrix(model.vrFrameData.leftProjectionMatrix); - publicAPI.traverseAllPasses(); + if (xrPose) { + const gl = publicAPI.get3DContext(); + const glLayer = xrSession.renderState.baseLayer; + gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.clear(gl.DEPTH_BUFFER_BIT); - ren.setViewport(0.5, 0, 1.0, 1.0); - ren - .getActiveCamera() - .computeViewParametersFromPhysicalMatrix( - model.vrFrameData.rightViewMatrix - ); - ren - .getActiveCamera() - .setProjectionMatrix(model.vrFrameData.rightProjectionMatrix); - publicAPI.traverseAllPasses(); + // get the first renderer + const ren = model.renderable.getRenderers()[0]; + + // Do a render pass for each eye + xrPose.views.forEach((view) => { + const viewport = glLayer.getViewport(view); - model.vrDisplay.submitFrame(); + gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height); + if (view.eye === 'left') { + ren.setViewport(0, 0, 0.5, 1.0); + } else if (view.eye === 'right') { + ren.setViewport(0.5, 0, 1.0, 1.0); + } else { + // No handling for non-eye viewport + return; + } + + ren + .getActiveCamera() + .computeViewParametersFromPhysicalMatrix( + view.transform.inverse.matrix + ); + ren.getActiveCamera().setProjectionMatrix(view.projectionMatrix); + + publicAPI.traverseAllPasses(); + }); + } }; publicAPI.restoreContext = () => { @@ -1115,11 +1129,9 @@ const DEFAULT_VALUES = { notifyStartCaptureImage: false, webgl2: false, defaultToWebgl2: true, // attempt webgl2 on by default - vrResolution: [2160, 1200], - queryVRSize: false, - hideCanvasInVR: true, activeFramebuffer: null, - vrDisplay: null, + xrSession: null, + xrReferenceSpace: null, imageFormat: 'image/png', useOffScreen: false, useBackgroundImage: false, @@ -1185,8 +1197,6 @@ export function extend(publicAPI, model, initialValues = {}) { 'notifyStartCaptureImage', 'defaultToWebgl2', 'cursor', - 'queryVRSize', - 'hideCanvasInVR', 'useOffScreen', // might want to make this not call modified as // we change the active framebuffer a lot. Or maybe @@ -1195,7 +1205,7 @@ export function extend(publicAPI, model, initialValues = {}) { 'activeFramebuffer', ]); - macro.setGetArray(publicAPI, model, ['size', 'vrResolution'], 2); + macro.setGetArray(publicAPI, model, ['size'], 2); // Object methods vtkOpenGLRenderWindow(publicAPI, model);