diff --git a/Documentation/content/docs/develop_webxr.md b/Documentation/content/docs/develop_webxr.md index 86687b9f6b9..18bfa1e2789 100644 --- a/Documentation/content/docs/develop_webxr.md +++ b/Documentation/content/docs/develop_webxr.md @@ -59,6 +59,19 @@ vtk.js supports virtual and augmented reality rendering via the [WebXR device AP +### Holographic Examples + + + ### For Developers Developers without access to XR hardware may find it convenient to install and use the [Mozilla WebXR emulator](https://github.com/MozillaReality/WebXR-emulator-extension) in their browser. @@ -81,3 +94,4 @@ While WebXR has broad industry support, it is not yet implemented in all browser [HeadFullVolume]: ../docs/gallery/HeadFullVolume.png [ChestCTHybrid]: ../docs/gallery/ChestCTHybrid.png [HeadGradient]: ../docs/gallery/HeadGradient.png +[LookingGlassCone]: ../docs/gallery/LookingGlassCone.png diff --git a/Documentation/content/docs/gallery/LookingGlassCone.png b/Documentation/content/docs/gallery/LookingGlassCone.png new file mode 100644 index 00000000000..83310aaf124 Binary files /dev/null and b/Documentation/content/docs/gallery/LookingGlassCone.png differ diff --git a/Examples/Applications/GeometryViewer/index.js b/Examples/Applications/GeometryViewer/index.js index 206c6965a4e..12c082f3d6b 100644 --- a/Examples/Applications/GeometryViewer/index.js +++ b/Examples/Applications/GeometryViewer/index.js @@ -18,6 +18,7 @@ import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract'; import vtkXMLPolyDataReader from '@kitware/vtk.js/IO/XML/XMLPolyDataReader'; import vtkFPSMonitor from '@kitware/vtk.js/Interaction/UI/FPSMonitor'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow/Constants'; // Force DataAccessHelper to have access to various data source import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; @@ -75,6 +76,44 @@ function preventDefaults(e) { e.stopPropagation(); } +// WebXR +let requestedXrSessionType = + userParams.xrSessionType !== undefined ? userParams.xrSessionType : null; +if ( + requestedXrSessionType !== null && + !Object.values(XrSessionTypes).includes(requestedXrSessionType) +) { + console.warn( + 'Could not parse requested XR session type: ', + requestedXrSessionType + ); + requestedXrSessionType = null; +} + +if (requestedXrSessionType === XrSessionTypes.LookingGlassVR) { + // Import the Looking Glass WebXR Polyfill override + // Assumes that the Looking Glass Bridge native application is already running. + // See https://docs.lookingglassfactory.com/developer-tools/webxr + import( + // eslint-disable-next-line import/no-unresolved, import/extensions + /* webpackIgnore: true */ 'https://unpkg.com/@lookingglass/webxr@0.3.0/dist/@lookingglass/bundle/webxr.js' + ).then((obj) => { + // eslint-disable-next-line no-new + new obj.LookingGlassWebXRPolyfill(); + }); +} else if (requestedXrSessionType === null && navigator.xr !== undefined) { + // Determine supported session type + navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { + if (arSupported) { + requestedXrSessionType = XrSessionTypes.MobileAR; + } else { + navigator.xr.isSessionSupported('immersive-vr').then((vrSupported) => { + requestedXrSessionType = vrSupported ? XrSessionTypes.HmdVR : null; + }); + } + }); +} + // ---------------------------------------------------------------------------- // DOM containers for UI control // ---------------------------------------------------------------------------- @@ -197,7 +236,10 @@ function createPipeline(fileName, fileContents) { const immersionSelector = document.createElement('button'); immersionSelector.setAttribute('class', selectorClass); - immersionSelector.innerHTML = 'Start AR'; + immersionSelector.innerHTML = + requestedXrSessionType === XrSessionTypes.MobileAR + ? 'Start AR' + : 'Start VR'; const controlContainer = document.createElement('div'); controlContainer.setAttribute('class', style.control); @@ -210,8 +252,8 @@ function createPipeline(fileName, fileContents) { if ( navigator.xr !== undefined && - navigator.xr.isSessionSupported('immersive-ar') && - fullScreenRenderWindow.getApiSpecificRenderWindow().getXrSupported() + fullScreenRenderWindow.getApiSpecificRenderWindow().getXrSupported() && + requestedXrSessionType !== null ) { controlContainer.appendChild(immersionSelector); } @@ -387,21 +429,29 @@ function createPipeline(fileName, fileContents) { // Immersion handling // -------------------------------------------------------------------- - function toggleAR() { - const SESSION_IS_AR = true; - if (immersionSelector.textContent === 'Start AR') { + function toggleXR() { + if (requestedXrSessionType === XrSessionTypes.MobileAR) { fullScreenRenderWindow.setBackground([...background, 0]); + } + + if (immersionSelector.textContent.startsWith('Start')) { fullScreenRenderWindow .getApiSpecificRenderWindow() - .startXR(SESSION_IS_AR); - immersionSelector.textContent = 'Exit AR'; + .startXR(requestedXrSessionType); + immersionSelector.textContent = + requestedXrSessionType === XrSessionTypes.MobileAR + ? 'Exit AR' + : 'Exit VR'; } else { fullScreenRenderWindow.setBackground([...background, 255]); - fullScreenRenderWindow.getApiSpecificRenderWindow().stopXR(SESSION_IS_AR); - immersionSelector.textContent = 'Start AR'; + fullScreenRenderWindow.getApiSpecificRenderWindow().stopXR(); + immersionSelector.textContent = + requestedXrSessionType === XrSessionTypes.MobileAR + ? 'Start AR' + : 'Start VR'; } } - immersionSelector.addEventListener('click', toggleAR); + immersionSelector.addEventListener('click', toggleXR); // -------------------------------------------------------------------- // Pipeline handling diff --git a/Examples/Applications/GeometryViewer/index.md b/Examples/Applications/GeometryViewer/index.md index 15079720bc5..6b6d3673e5a 100644 --- a/Examples/Applications/GeometryViewer/index.md +++ b/Examples/Applications/GeometryViewer/index.md @@ -14,4 +14,9 @@ Also using extra argument to the URL allow to view remote VTP like the links bel - [Brain Blood Vessels](https://kitware.github.io/vtk-js/examples/GeometryViewer/index.html?fileURL=[https://data.kitware.com/api/v1/file/61f041f14acac99f42c2ff9a/download,https://data.kitware.com/api/v1/file/61f042024acac99f42c2ffa6/download,https://data.kitware.com/api/v1/file/61f042b74acac99f42c30079/download]) 57.71 MB - [Chest CT](https://kitware.github.io/vtk-js/examples/GeometryViewer/index.html?fileURL=[https://data.kitware.com/api/v1/file/61f044354acac99f42c30276/download,https://data.kitware.com/api/v1/file/61f0440f4acac99f42c30191/download,https://data.kitware.com/api/v1/file/61f044204acac99f42c30267/download]) 63.75 MB +Virtual reality, augmented reality, and holographic viewing is supported for WebXR devices using the `xrSessionType` URL parameter. The following links provide holographic examples for a Looking Glass display: +- [diskout.vtp (holographic)](https://kitware.github.io/vtk-js/examples/GeometryViewer/GeometryViewer.html?xrSessionType=2&fileURL=https://data.kitware.com/api/v1/item/59de9de58d777f31ac641dc5/download) 471.9 kB +- [Brain Blood Vessels (holographic)](https://kitware.github.io/vtk-js/examples/GeometryViewer/index.html?xrSessionType=2&fileURL=[https://data.kitware.com/api/v1/file/61f041f14acac99f42c2ff9a/download,https://data.kitware.com/api/v1/file/61f042024acac99f42c2ffa6/download,https://data.kitware.com/api/v1/file/61f042b74acac99f42c30079/download]) 57.71 MB +- [Chest CT (holographic)](https://kitware.github.io/vtk-js/examples/GeometryViewer/index.html?xrSessionType=2&fileURL=[https://data.kitware.com/api/v1/file/61f044354acac99f42c30276/download,https://data.kitware.com/api/v1/file/61f0440f4acac99f42c30191/download,https://data.kitware.com/api/v1/file/61f044204acac99f42c30267/download]) 63.75 MB + [HTML]: https://kitware.github.io/vtk-js/examples/GeometryViewer/GeometryViewer.html diff --git a/Examples/Applications/SkyboxViewer/index.js b/Examples/Applications/SkyboxViewer/index.js index d46da350b40..92705f89af2 100644 --- a/Examples/Applications/SkyboxViewer/index.js +++ b/Examples/Applications/SkyboxViewer/index.js @@ -13,7 +13,7 @@ import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreen import vtkSkybox from '@kitware/vtk.js/Rendering/Core/Skybox'; import vtkSkyboxReader from '@kitware/vtk.js/IO/Misc/SkyboxReader'; import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract'; -// import vtkMobileVR from '@kitware/vtk.js/Common/System/MobileVR'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow/Constants'; // Force DataAccessHelper to have access to various data source import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; @@ -213,7 +213,9 @@ function createVisualization(container, mapReader) { document.querySelector('body').appendChild(button); button.addEventListener('click', () => { if (button.textContent === 'Send To VR') { - fullScreenRenderer.getApiSpecificRenderWindow().startXR(); + fullScreenRenderer + .getApiSpecificRenderWindow() + .startXR(XrSessionTypes.HmdVR); button.textContent = 'Return From VR'; } else { fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); diff --git a/Examples/Geometry/AR/index.js b/Examples/Geometry/AR/index.js index ae914e3d368..3dc822bace2 100644 --- a/Examples/Geometry/AR/index.js +++ b/Examples/Geometry/AR/index.js @@ -10,6 +10,7 @@ import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreen import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; import { AttributeTypes } from '@kitware/vtk.js/Common/DataModel/DataSetAttributes/Constants'; import { FieldDataTypes } from '@kitware/vtk.js/Common/DataModel/DataSet/Constants'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow/Constants'; // Force DataAccessHelper to have access to various data source import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; @@ -82,15 +83,16 @@ arbutton.disabled = !fullScreenRenderer .getApiSpecificRenderWindow() .getXrSupported(); -const SESSION_IS_AR = true; arbutton.addEventListener('click', (e) => { if (arbutton.textContent === 'Start AR') { fullScreenRenderer.setBackground([0, 0, 0, 0]); - fullScreenRenderer.getApiSpecificRenderWindow().startXR(SESSION_IS_AR); + fullScreenRenderer + .getApiSpecificRenderWindow() + .startXR(XrSessionTypes.MobileAR); arbutton.textContent = 'Exit AR'; } else { fullScreenRenderer.setBackground([0, 0, 0, 255]); - fullScreenRenderer.getApiSpecificRenderWindow().stopXR(SESSION_IS_AR); + fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); arbutton.textContent = 'Start AR'; } }); diff --git a/Examples/Geometry/LookingGlass/controller.html b/Examples/Geometry/LookingGlass/controller.html new file mode 100644 index 00000000000..2b0fb13dd88 --- /dev/null +++ b/Examples/Geometry/LookingGlass/controller.html @@ -0,0 +1,21 @@ + + + + + + + + + + +
+ +
+ +
+ +
diff --git a/Examples/Geometry/LookingGlass/index.js b/Examples/Geometry/LookingGlass/index.js new file mode 100644 index 00000000000..b6e6abd7d5e --- /dev/null +++ b/Examples/Geometry/LookingGlass/index.js @@ -0,0 +1,132 @@ +// For streamlined VR development install the WebXR emulator extension +// https://github.com/MozillaReality/WebXR-emulator-extension + +import '@kitware/vtk.js/favicon'; + +// Load the rendering pieces we want to use (for both WebGL and WebGPU) +import '@kitware/vtk.js/Rendering/Profiles/Geometry'; + +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkCalculator from '@kitware/vtk.js/Filters/General/Calculator'; +import vtkConeSource from '@kitware/vtk.js/Filters/Sources/ConeSource'; +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import { AttributeTypes } from '@kitware/vtk.js/Common/DataModel/DataSetAttributes/Constants'; +import { FieldDataTypes } from '@kitware/vtk.js/Common/DataModel/DataSet/Constants'; +import { XrSessionTypes } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow/Constants'; + +// Force DataAccessHelper to have access to various data source +import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; +import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; +import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; + +import controlPanel from './controller.html'; + +// Import the Looking Glass WebXR Polyfill override +// Assumes that the Looking Glass Bridge native application is already running. +// See https://docs.lookingglassfactory.com/developer-tools/webxr +import( + // eslint-disable-next-line import/no-unresolved, import/extensions + /* webpackIgnore: true */ 'https://unpkg.com/@lookingglass/webxr@0.3.0/dist/@lookingglass/bundle/webxr.js' +).then((obj) => { + // eslint-disable-next-line no-new + new obj.LookingGlassWebXRPolyfill(); +}); + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ + background: [0, 0, 0], +}); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- +// create a filter on the fly, sort of cool, this is a random scalars +// filter we create inline, for a simple cone you would not need +// this +// ---------------------------------------------------------------------------- + +const coneSource = vtkConeSource.newInstance({ height: 1.0, radius: 0.5 }); +const filter = vtkCalculator.newInstance(); + +filter.setInputConnection(coneSource.getOutputPort()); +// filter.setFormulaSimple(FieldDataTypes.CELL, [], 'random', () => Math.random()); +filter.setFormula({ + getArrays: (inputDataSets) => ({ + input: [], + output: [ + { + location: FieldDataTypes.CELL, + name: 'Random', + dataType: 'Float32Array', + attribute: AttributeTypes.SCALARS, + }, + ], + }), + evaluate: (arraysIn, arraysOut) => { + const [scalars] = arraysOut.map((d) => d.getData()); + for (let i = 0; i < scalars.length; i++) { + scalars[i] = Math.random(); + } + }, +}); + +const mapper = vtkMapper.newInstance(); +mapper.setInputConnection(filter.getOutputPort()); + +const actor = vtkActor.newInstance(); +actor.setMapper(mapper); +actor.setPosition(0.0, 0.0, -20.0); + +renderer.addActor(actor); +renderer.resetCamera(); +renderWindow.render(); + +// ----------------------------------------------------------- +// UI control handling +// ----------------------------------------------------------- + +fullScreenRenderer.addController(controlPanel); +const representationSelector = document.querySelector('.representations'); +const resolutionChange = document.querySelector('.resolution'); +const vrbutton = document.querySelector('.vrbutton'); + +representationSelector.addEventListener('change', (e) => { + const newRepValue = Number(e.target.value); + actor.getProperty().setRepresentation(newRepValue); + renderWindow.render(); +}); + +resolutionChange.addEventListener('input', (e) => { + const resolution = Number(e.target.value); + coneSource.setResolution(resolution); + renderWindow.render(); +}); + +vrbutton.addEventListener('click', (e) => { + if (vrbutton.textContent === 'Send To Looking Glass') { + fullScreenRenderer + .getApiSpecificRenderWindow() + .startXR(XrSessionTypes.LookingGlassVR); + vrbutton.textContent = 'Return From Looking Glass'; + } else { + fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); + vrbutton.textContent = 'Send To Looking Glass'; + } +}); + +// ----------------------------------------------------------- +// Make some variables global so that you can inspect and +// modify objects in your browser's developer console: +// ----------------------------------------------------------- + +global.source = coneSource; +global.mapper = mapper; +global.actor = actor; +global.renderer = renderer; +global.renderWindow = renderWindow; diff --git a/Examples/Geometry/LookingGlass/index.md b/Examples/Geometry/LookingGlass/index.md new file mode 100644 index 00000000000..9186c5843cb --- /dev/null +++ b/Examples/Geometry/LookingGlass/index.md @@ -0,0 +1,14 @@ +## Holographic Scenes with Looking Glass + +vtk.js supports rendering 3D holograms to [Looking Glass](https://lookingglassfactory.com/) holographic displays. The following are required for getting started: +- A physical Looking Glass display with an HDMI and USB connection to the computer running the vtk.js scene; +- The [Looking Glass Bridge](https://lookingglassfactory.com/software/looking-glass-bridge) native application; and +- The [Looking Glass WebXR Polyfill](https://github.com/Looking-Glass/looking-glass-webxr) (already fetched in this example). + +Clicking "Send to Looking Glass" in the example above will open a new popup window on the connected Looking Glass display. Double-clicking the window will maximize the hologram view to display properly. + +If the Looking Glass display is disconnected or the Looking Glass Bridge application is not running then a non-composited, "swizzled" view will be shown in a popup window. + +The Looking Glass display composites a "quilt" of multiple scene renderings into a hologram with "depth" when viewed from multiple angles. vtk.js generates multiple scene renderings for each frame in order to generate new "quilt". + +More information on holograms and Looking Glass displays is available in the [Looking Glass documentation](https://docs.lookingglassfactory.com/). diff --git a/Examples/Geometry/VR/index.js b/Examples/Geometry/VR/index.js index 00934a2437b..ce2d9051f4c 100644 --- a/Examples/Geometry/VR/index.js +++ b/Examples/Geometry/VR/index.js @@ -13,6 +13,7 @@ import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreen import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; import { AttributeTypes } from '@kitware/vtk.js/Common/DataModel/DataSetAttributes/Constants'; import { FieldDataTypes } from '@kitware/vtk.js/Common/DataModel/DataSet/Constants'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow/Constants'; // Force DataAccessHelper to have access to various data source import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; @@ -113,7 +114,9 @@ resolutionChange.addEventListener('input', (e) => { vrbutton.addEventListener('click', (e) => { if (vrbutton.textContent === 'Send To VR') { - fullScreenRenderer.getApiSpecificRenderWindow().startXR(); + fullScreenRenderer + .getApiSpecificRenderWindow() + .startXR(XrSessionTypes.HmdVR); vrbutton.textContent = 'Return From VR'; } else { fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); diff --git a/Examples/Volume/WebXRChestCTBlendedCVR/index.js b/Examples/Volume/WebXRChestCTBlendedCVR/index.js index 4a4d68fa9c0..4044729a130 100644 --- a/Examples/Volume/WebXRChestCTBlendedCVR/index.js +++ b/Examples/Volume/WebXRChestCTBlendedCVR/index.js @@ -18,6 +18,7 @@ import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; import vtkXMLImageDataReader from '@kitware/vtk.js/IO/XML/XMLImageDataReader'; import vtkImageReslice from '@kitware/vtk.js/Imaging/Core/ImageReslice'; import vtkMath from '@kitware/vtk.js/Common/Core/Math'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow/Constants'; import './WebXRVolume.module.css'; @@ -112,8 +113,6 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { renderWindow.render(); // Add button to launch AR (default) or VR scene - const VR = 1; - const AR = 2; let xrSessionType = 0; const xrButton = document.createElement('button'); let enterText = 'XR not available!'; @@ -125,13 +124,13 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { ) { navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { if (arSupported) { - xrSessionType = AR; + xrSessionType = XrSessionTypes.MobileAR; enterText = 'Start AR'; xrButton.textContent = enterText; } else { navigator.xr.isSessionSupported('immersive-vr').then((vrSupported) => { if (vrSupported) { - xrSessionType = VR; + xrSessionType = XrSessionTypes.HmdVR; enterText = 'Start VR'; xrButton.textContent = enterText; } @@ -141,18 +140,14 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { } xrButton.addEventListener('click', () => { if (xrButton.textContent === enterText) { - if (xrSessionType === AR) { + if (xrSessionType === XrSessionTypes.MobileAR) { fullScreenRenderer.setBackground([0, 0, 0, 0]); } - fullScreenRenderer - .getApiSpecificRenderWindow() - .startXR(xrSessionType === AR); + fullScreenRenderer.getApiSpecificRenderWindow().startXR(xrSessionType); xrButton.textContent = exitText; } else { fullScreenRenderer.setBackground([...background, 255]); - fullScreenRenderer - .getApiSpecificRenderWindow() - .stopXR(xrSessionType === AR); + fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); xrButton.textContent = enterText; } }); diff --git a/Examples/Volume/WebXRHeadFullVolumeCVR/index.js b/Examples/Volume/WebXRHeadFullVolumeCVR/index.js index 8ea7282177d..8c2e1358949 100644 --- a/Examples/Volume/WebXRHeadFullVolumeCVR/index.js +++ b/Examples/Volume/WebXRHeadFullVolumeCVR/index.js @@ -18,6 +18,7 @@ import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; import vtkXMLImageDataReader from '@kitware/vtk.js/IO/XML/XMLImageDataReader'; import vtkImageReslice from '@kitware/vtk.js/Imaging/Core/ImageReslice'; import vtkMath from '@kitware/vtk.js/Common/Core/Math'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow/Constants'; import './WebXRVolume.module.css'; @@ -55,8 +56,43 @@ const ofun = vtkPiecewiseFunction.newInstance(); const { fileURL = 'https://data.kitware.com/api/v1/file/59de9dca8d777f31ac641dc2/download', + xrSessionType = null, } = vtkURLExtract.extractURLParameters(); +// Validate input parameters +let requestedXrSessionType = xrSessionType; +if (!Object.values(XrSessionTypes).includes(requestedXrSessionType)) { + console.warn( + 'Could not parse requested XR session type: ', + requestedXrSessionType + ); + requestedXrSessionType = null; +} + +if (requestedXrSessionType === XrSessionTypes.LookingGlassVR) { + // Import the Looking Glass WebXR Polyfill override + // Assumes that the Looking Glass Bridge native application is already running. + // See https://docs.lookingglassfactory.com/developer-tools/webxr + import( + // eslint-disable-next-line import/no-unresolved, import/extensions + /* webpackIgnore: true */ 'https://unpkg.com/@lookingglass/webxr@0.3.0/dist/@lookingglass/bundle/webxr.js' + ).then((obj) => { + // eslint-disable-next-line no-new + new obj.LookingGlassWebXRPolyfill({ numViews: 12 }); + }); +} else if (requestedXrSessionType === null) { + // Determine supported session type + navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { + if (arSupported) { + requestedXrSessionType = XrSessionTypes.MobileAR; + } else { + navigator.xr.isSessionSupported('immersive-vr').then((vrSupported) => { + requestedXrSessionType = vrSupported ? XrSessionTypes.HmdVR : null; + }); + } + }); +} + HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { // Read data vtiReader.parseAsArrayBuffer(fileContents); @@ -109,9 +145,6 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { renderWindow.render(); // Add button to launch AR (default) or VR scene - const VR = 1; - const AR = 2; - let xrSessionType = 0; const xrButton = document.createElement('button'); let enterText = 'XR not available!'; const exitText = 'Exit XR'; @@ -120,36 +153,24 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { navigator.xr !== undefined && fullScreenRenderer.getApiSpecificRenderWindow().getXrSupported() ) { - navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { - if (arSupported) { - xrSessionType = AR; - enterText = 'Start AR'; - xrButton.textContent = enterText; - } else { - navigator.xr.isSessionSupported('immersive-vr').then((vrSupported) => { - if (vrSupported) { - xrSessionType = VR; - enterText = 'Start VR'; - xrButton.textContent = enterText; - } - }); - } - }); + enterText = + requestedXrSessionType === XrSessionTypes.MobileAR + ? 'Start AR' + : 'Start VR'; + xrButton.textContent = enterText; } xrButton.addEventListener('click', () => { if (xrButton.textContent === enterText) { - if (xrSessionType === AR) { + if (requestedXrSessionType === XrSessionTypes.MobileAR) { fullScreenRenderer.setBackground([0, 0, 0, 0]); } fullScreenRenderer .getApiSpecificRenderWindow() - .startXR(xrSessionType === AR); + .startXR(requestedXrSessionType); xrButton.textContent = exitText; } else { fullScreenRenderer.setBackground([...background, 255]); - fullScreenRenderer - .getApiSpecificRenderWindow() - .stopXR(xrSessionType === AR); + fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); xrButton.textContent = enterText; } }); diff --git a/Examples/Volume/WebXRHeadGradientCVR/index.js b/Examples/Volume/WebXRHeadGradientCVR/index.js index 48f5e093b70..aab77c54ccf 100644 --- a/Examples/Volume/WebXRHeadGradientCVR/index.js +++ b/Examples/Volume/WebXRHeadGradientCVR/index.js @@ -18,6 +18,7 @@ import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; import vtkXMLImageDataReader from '@kitware/vtk.js/IO/XML/XMLImageDataReader'; import vtkImageReslice from '@kitware/vtk.js/Imaging/Core/ImageReslice'; import vtkMath from '@kitware/vtk.js/Common/Core/Math'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow/Constants'; import './WebXRVolume.module.css'; @@ -55,8 +56,43 @@ const ofun = vtkPiecewiseFunction.newInstance(); const { fileURL = 'https://data.kitware.com/api/v1/file/59de9dca8d777f31ac641dc2/download', + xrSessionType = null, } = vtkURLExtract.extractURLParameters(); +// Validate input parameters +let requestedXrSessionType = xrSessionType; +if (!Object.values(XrSessionTypes).includes(requestedXrSessionType)) { + console.warn( + 'Could not parse requested XR session type: ', + requestedXrSessionType + ); + requestedXrSessionType = null; +} + +if (requestedXrSessionType === XrSessionTypes.LookingGlassVR) { + // Import the Looking Glass WebXR Polyfill override + // Assumes that the Looking Glass Bridge native application is already running. + // See https://docs.lookingglassfactory.com/developer-tools/webxr + import( + // eslint-disable-next-line import/no-unresolved, import/extensions + /* webpackIgnore: true */ 'https://unpkg.com/@lookingglass/webxr@0.3.0/dist/@lookingglass/bundle/webxr.js' + ).then((obj) => { + // eslint-disable-next-line no-new + new obj.LookingGlassWebXRPolyfill(); + }); +} else if (requestedXrSessionType === null) { + // Determine supported session type + navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { + if (arSupported) { + requestedXrSessionType = XrSessionTypes.MobileAR; + } else { + navigator.xr.isSessionSupported('immersive-vr').then((vrSupported) => { + requestedXrSessionType = vrSupported ? XrSessionTypes.HmdVR : null; + }); + } + }); +} + HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { // Read data vtiReader.parseAsArrayBuffer(fileContents); @@ -106,9 +142,6 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { renderWindow.render(); // Add button to launch AR (default) or VR scene - const VR = 1; - const AR = 2; - let xrSessionType = 0; const xrButton = document.createElement('button'); let enterText = 'XR not available!'; const exitText = 'Exit XR'; @@ -117,36 +150,24 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { navigator.xr !== undefined && fullScreenRenderer.getApiSpecificRenderWindow().getXrSupported() ) { - navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { - if (arSupported) { - xrSessionType = AR; - enterText = 'Start AR'; - xrButton.textContent = enterText; - } else { - navigator.xr.isSessionSupported('immersive-vr').then((vrSupported) => { - if (vrSupported) { - xrSessionType = VR; - enterText = 'Start VR'; - xrButton.textContent = enterText; - } - }); - } - }); + enterText = + requestedXrSessionType === XrSessionTypes.MobileAR + ? 'Start AR' + : 'Start VR'; + xrButton.textContent = enterText; } xrButton.addEventListener('click', () => { if (xrButton.textContent === enterText) { - if (xrSessionType === AR) { + if (requestedXrSessionType === XrSessionTypes.MobileAR) { fullScreenRenderer.setBackground([0, 0, 0, 0]); } fullScreenRenderer .getApiSpecificRenderWindow() - .startXR(xrSessionType === AR); + .startXR(requestedXrSessionType); xrButton.textContent = exitText; } else { fullScreenRenderer.setBackground([...background, 255]); - fullScreenRenderer - .getApiSpecificRenderWindow() - .stopXR(xrSessionType === AR); + fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); xrButton.textContent = enterText; } }); diff --git a/Examples/Volume/WebXRVolume/index.js b/Examples/Volume/WebXRVolume/index.js index 3dd8bbf0d96..af01c0834d0 100644 --- a/Examples/Volume/WebXRVolume/index.js +++ b/Examples/Volume/WebXRVolume/index.js @@ -18,6 +18,7 @@ import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract'; import vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; import vtkXMLImageDataReader from '@kitware/vtk.js/IO/XML/XMLImageDataReader'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow/Constants'; import './WebXRVolume.module.css'; @@ -55,16 +56,57 @@ const ofun = vtkPiecewiseFunction.newInstance(); const { fileURL = 'https://data.kitware.com/api/v1/file/59de9dca8d777f31ac641dc2/download', + xrSessionType = null, + colorPreset = null, + resliceVolume = true, } = vtkURLExtract.extractURLParameters(); +// Validate input parameters +let requestedXrSessionType = xrSessionType; +if (!Object.values(XrSessionTypes).includes(requestedXrSessionType)) { + console.warn( + 'Could not parse requested XR session type: ', + requestedXrSessionType + ); + requestedXrSessionType = null; +} + +if (requestedXrSessionType === XrSessionTypes.LookingGlassVR) { + // Import the Looking Glass WebXR Polyfill override + // Assumes that the Looking Glass Bridge native application is already running. + // See https://docs.lookingglassfactory.com/developer-tools/webxr + import( + // eslint-disable-next-line import/no-unresolved, import/extensions + /* webpackIgnore: true */ 'https://unpkg.com/@lookingglass/webxr@0.3.0/dist/@lookingglass/bundle/webxr.js' + ).then((obj) => { + // eslint-disable-next-line no-new + new obj.LookingGlassWebXRPolyfill(); + }); +} else if (requestedXrSessionType === null) { + // Determine supported session type + navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { + if (arSupported) { + requestedXrSessionType = XrSessionTypes.MobileAR; + } else { + navigator.xr.isSessionSupported('immersive-vr').then((vrSupported) => { + requestedXrSessionType = vrSupported ? XrSessionTypes.HmdVR : null; + }); + } + }); +} + HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { // Read data vtiReader.parseAsArrayBuffer(fileContents); - // Rotate 90 degrees forward so that default head volume faces camera - const rotateX = mat4.create(); - mat4.fromRotation(rotateX, vtkMath.radiansFromDegrees(90), [-1, 0, 0]); - reslicer.setResliceAxes(rotateX); + if (resliceVolume) { + // Rotate 90 degrees forward so that default head volume faces camera + const rotateX = mat4.create(); + mat4.fromRotation(rotateX, vtkMath.radiansFromDegrees(90), [-1, 0, 0]); + reslicer.setResliceAxes(rotateX); + } else { + reslicer.setResliceAxes(mat4.create()); + } const data = reslicer.getOutputData(0); const dataArray = @@ -82,11 +124,43 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { ); mapper.setSampleDistance(sampleDistance); - ctfun.addRGBPoint(dataRange[0], 0.0, 0.3, 0.3); - ctfun.addRGBPoint(dataRange[1], 1.0, 1.0, 1.0); - ofun.addPoint(dataRange[0], 0.0); - ofun.addPoint((dataRange[1] - dataRange[0]) / 4, 0.0); - ofun.addPoint(dataRange[1], 0.5); + // https://github.com/Kitware/VolView/blob/f6b1aaa587d1a80ccd99dd9fbab309c58cde08f7/src/vtk/MedicalColorPresets.json + if (colorPreset === 'CT-AAA') { + ctfun.addRGBPoint(-3024, 0.0, 0, 0); + ctfun.addRGBPoint(143, 0.62, 0.36, 0.18); + ctfun.addRGBPoint(166, 0.88, 0.6, 0.29); + ctfun.addRGBPoint(214, 1, 1, 1); + ctfun.addRGBPoint(419, 1, 0.94, 0.95); + ctfun.addRGBPoint(3071, 0.83, 0.66, 1); + + ofun.addPoint(-3024, 0); + ofun.addPoint(144, 0); + ofun.addPoint(166, 0.69); + ofun.addPoint(214, 0.7); + ofun.addPoint(420, 0.83); + ofun.addPoint(3071, 0.8); + } else if (colorPreset === 'CT-Cardiac2') { + ctfun.addRGBPoint(-3024, 0.0, 0, 0); + ctfun.addRGBPoint(42, 0.55, 0.25, 0.15); + ctfun.addRGBPoint(163, 0.92, 0.64, 0.06); + ctfun.addRGBPoint(278, 1, 0.88, 0.62); + ctfun.addRGBPoint(1587, 1, 1, 1); + ctfun.addRGBPoint(3071, 0.83, 0.66, 1); + + ofun.addPoint(-3024, 0); + ofun.addPoint(43, 0); + ofun.addPoint(163, 0.42); + ofun.addPoint(277, 0.78); + ofun.addPoint(1587, 0.75); + ofun.addPoint(3071, 0.8); + } else { + // Scale color and opacity transfer functions to data intensity range + ctfun.addRGBPoint(dataRange[0], 0.0, 0.3, 0.3); + ctfun.addRGBPoint(dataRange[1], 1.0, 1.0, 1.0); + ofun.addPoint(dataRange[0], 0.0); + ofun.addPoint((dataRange[1] - dataRange[0]) / 4, 0.0); + ofun.addPoint(dataRange[1], 0.5); + } actor.getProperty().setRGBTransferFunction(0, ctfun); actor.getProperty().setScalarOpacity(0, ofun); actor.getProperty().setInterpolationTypeToLinear(); @@ -96,9 +170,6 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { renderWindow.render(); // Add button to launch AR (default) or VR scene - const VR = 1; - const AR = 2; - let xrSessionType = 0; const xrButton = document.createElement('button'); let enterText = 'XR not available!'; const exitText = 'Exit XR'; @@ -107,36 +178,24 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { navigator.xr !== undefined && fullScreenRenderer.getApiSpecificRenderWindow().getXrSupported() ) { - navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { - if (arSupported) { - xrSessionType = AR; - enterText = 'Start AR'; - xrButton.textContent = enterText; - } else { - navigator.xr.isSessionSupported('immersive-vr').then((vrSupported) => { - if (vrSupported) { - xrSessionType = VR; - enterText = 'Start VR'; - xrButton.textContent = enterText; - } - }); - } - }); + enterText = + requestedXrSessionType === XrSessionTypes.MobileAR + ? 'Start AR' + : 'Start VR'; + xrButton.textContent = enterText; } xrButton.addEventListener('click', () => { if (xrButton.textContent === enterText) { - if (xrSessionType === AR) { + if (requestedXrSessionType === XrSessionTypes.MobileAR) { fullScreenRenderer.setBackground([0, 0, 0, 0]); } fullScreenRenderer .getApiSpecificRenderWindow() - .startXR(xrSessionType === AR); + .startXR(requestedXrSessionType); xrButton.textContent = exitText; } else { fullScreenRenderer.setBackground([...background, 255]); - fullScreenRenderer - .getApiSpecificRenderWindow() - .stopXR(xrSessionType === AR); + fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); xrButton.textContent = enterText; } }); diff --git a/Examples/Volume/WebXRVolume/index.md b/Examples/Volume/WebXRVolume/index.md index 4420fd6f0e2..b48a8c51e75 100644 --- a/Examples/Volume/WebXRVolume/index.md +++ b/Examples/Volume/WebXRVolume/index.md @@ -9,8 +9,18 @@ Volume rendering can be processor-intensive. Smaller volumes may give better per - [binary-head.vti](https://kitware.github.io/vtk-js/examples/WebXRVolume/WebXRVolume.html?fileURL=https://data.kitware.com/api/v1/file/59de9dca8d777f31ac641dc2/download) 15.6 MB - [binary-head-2.vti](https://kitware.github.io/vtk-js/examples/WebXRVolume/WebXRVolume.html?fileURL=https://data.kitware.com/api/v1/file/629921a64acac99f429a45a7/download) 361 kB +- [Kitware_CTA_Head_and_Neck.vti](https://kitware.github.io/vtk-js/examples/WebXRVolume/WebXRVolume.html?fileURL=https://data.kitware.com/api/v1/file/63fe3f237b0dfcc98f66a857/download&colorPreset=CT-Cardiac2&resliceVolume=true) 5.025 MB - [tiny-image.vti](https://kitware.github.io/vtk-js/examples/WebXRVolume/WebXRVolume.html?fileURL=https://data.kitware.com/api/v1/file/624320e74acac99f42254a25/download) 1.6 kB +### Looking Glass Holographic Support + +Holographic scenes can be rendered to a Looking Glass display by specifying the `xrSessionType` in the example URL. + +- [binary-head.vti (holographic)](https://kitware.github.io/vtk-js/examples/WebXRVolume/WebXRVolume.html?xrSessionType=2&fileURL=https://data.kitware.com/api/v1/file/59de9dca8d777f31ac641dc2/download) 15.6 MB +- [3DUS-fetus.vti (holographic)](https://kitware.github.io/vtk-js/examples/WebXRVolume/WebXRVolume.html?xrSessionType=2&fileURL=https://data.kitware.com/api/v1/file/63fe43217b0dfcc98f66a85a/download) 30.22 MB +- [Kitware_CTA_Head_and_Neck.vti (holographic)](https://kitware.github.io/vtk-js/examples/WebXRVolume/WebXRVolume.html?xrSessionType=2&fileURL=https://data.kitware.com/api/v1/file/63fe3f237b0dfcc98f66a857/download&colorPreset=CT-Cardiac2&resliceVolume=false) 5.025 MB + + ### See Also [Full list of WebXR Examples](https://kitware.github.io/vtk-js/docs/develop_webxr.html) diff --git a/Sources/Rendering/OpenGL/RenderWindow/Constants.d.ts b/Sources/Rendering/OpenGL/RenderWindow/Constants.d.ts new file mode 100644 index 00000000000..8268da0b469 --- /dev/null +++ b/Sources/Rendering/OpenGL/RenderWindow/Constants.d.ts @@ -0,0 +1,10 @@ +export declare enum XrSessionTypes { + HmdVR = 0, + MobileAR = 1, + LookingGlassVR = 2 +} + +declare const _default: { + XrSessionTypes: typeof XrSessionTypes; +}; +export default _default; diff --git a/Sources/Rendering/OpenGL/RenderWindow/Constants.js b/Sources/Rendering/OpenGL/RenderWindow/Constants.js new file mode 100644 index 00000000000..2e53ada10fb --- /dev/null +++ b/Sources/Rendering/OpenGL/RenderWindow/Constants.js @@ -0,0 +1,9 @@ +export const XrSessionTypes = { + HmdVR: 0, // Head-mounted display (HMD), two-camera virtual reality session + MobileAR: 1, // Mobile device, single-camera augmented reality session + LookingGlassVR: 2, // Looking Glass hologram display, N-camera virtual reality session +}; + +export default { + XrSessionTypes, +}; diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.d.ts b/Sources/Rendering/OpenGL/RenderWindow/index.d.ts index b93b084d30b..0dbee520d8b 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.d.ts +++ b/Sources/Rendering/OpenGL/RenderWindow/index.d.ts @@ -232,19 +232,34 @@ export interface vtkOpenGLRenderWindow extends vtkOpenGLRenderWindowBase { get3DContext(options: I3DContextOptions): Nullable; /** - * + * Request an XR session on the user device with WebXR, + * typically in response to a user request such as a button press. */ - startVR(): void; + startXR(): void; + + /** + * When an XR session is available, set up the XRWebGLLayer + * and request the first animation frame for the device + */ + enterXR(): void, + + /** + * Adjust world-to-physical parameters for different viewing modalities + * + * @param {Number} inputRescaleFactor + * @param {Number} inputTranslateZ + */ + resetXRScene(inputRescaleFactor: number, inputTranslateZ: number): void, /** - * + * Request to stop the current XR session */ - stopVR(): void; + stopXR(): void; /** * */ - vrRender(): void; + xrRender(): void; /** * diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index 21e202180f6..db4d7d71e48 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -8,12 +8,14 @@ import vtkOpenGLTextureUnitManager from 'vtk.js/Sources/Rendering/OpenGL/Texture import vtkOpenGLViewNodeFactory from 'vtk.js/Sources/Rendering/OpenGL/ViewNodeFactory'; import vtkRenderPass from 'vtk.js/Sources/Rendering/SceneGraph/RenderPass'; import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderWindowViewNode'; +import Constants from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow/Constants'; import { createContextProxyHandler, GET_UNDERLYING_CONTEXT, } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow/ContextProxy'; const { vtkDebugMacro, vtkErrorMacro } = macro; +const { XrSessionTypes } = Constants; const SCREENSHOT_PLACEHOLDER = { position: 'absolute', @@ -293,12 +295,14 @@ function vtkOpenGLRenderWindow(publicAPI, model) { // Request an XR session on the user device with WebXR, // typically in response to a user request such as a button press - publicAPI.startXR = (isAR) => { + publicAPI.startXR = (xrSessionType) => { if (navigator.xr === undefined) { throw new Error('WebXR is not available'); } - model.xrSessionIsAR = isAR; + model.xrSessionType = + xrSessionType !== undefined ? xrSessionType : XrSessionTypes.HmdVR; + const isAR = xrSessionType === XrSessionTypes.MobileAR; const sessionType = isAR ? 'immersive-ar' : 'immersive-vr'; if (!navigator.xr.isSessionSupported(sessionType)) { if (isAR) { @@ -357,22 +361,20 @@ function vtkOpenGLRenderWindow(publicAPI, model) { inputTranslateZ = DEFAULT_RESET_FACTORS.vr.translateZ ) => { // Adjust world-to-physical parameters for different modalities - // Default parameter values are for VR (model.xrSessionIsAR == false) + // Default parameter values are for HMD VR let rescaleFactor = inputRescaleFactor; let translateZ = inputTranslateZ; + const isXrSessionAR = model.xrSessionType === XrSessionTypes.MobileAR; if ( - model.xrSessionIsAR && + isXrSessionAR && rescaleFactor === DEFAULT_RESET_FACTORS.vr.rescaleFactor ) { // Scale down by default in AR rescaleFactor = DEFAULT_RESET_FACTORS.ar.rescaleFactor; } - if ( - model.xrSessionIsAR && - translateZ === DEFAULT_RESET_FACTORS.vr.translateZ - ) { + if (isXrSessionAR && translateZ === DEFAULT_RESET_FACTORS.vr.translateZ) { // Default closer to the camera in AR translateZ = DEFAULT_RESET_FACTORS.ar.translateZ; } @@ -443,7 +445,10 @@ function vtkOpenGLRenderWindow(publicAPI, model) { if (xrPose) { const gl = publicAPI.get3DContext(); - if (model.xrSessionIsAR && model.oldCanvasSize !== undefined) { + if ( + model.xrSessionType === XrSessionTypes.MobileAR && + model.oldCanvasSize !== undefined + ) { gl.canvas.width = model.oldCanvasSize[0]; gl.canvas.height = model.oldCanvasSize[1]; } @@ -452,19 +457,18 @@ function vtkOpenGLRenderWindow(publicAPI, model) { gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer); gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.DEPTH_BUFFER_BIT); + publicAPI.setSize(glLayer.framebufferWidth, glLayer.framebufferHeight); // get the first renderer const ren = model.renderable.getRenderers()[0]; // Do a render pass for each eye - xrPose.views.forEach((view) => { + xrPose.views.forEach((view, index) => { const viewport = glLayer.getViewport(view); - gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height); - // TODO: Appropriate handling for AR passthrough on HMDs // with two eyes will require further investigation. - if (!model.xrSessionIsAR) { + if (model.xrSessionType === XrSessionTypes.HmdVR) { if (view.eye === 'left') { ren.setViewport(0, 0, 0.5, 1.0); } else if (view.eye === 'right') { @@ -473,6 +477,15 @@ function vtkOpenGLRenderWindow(publicAPI, model) { // No handling for non-eye viewport return; } + } else if (model.xrSessionType === XrSessionTypes.LookingGlassVR) { + const startX = viewport.x / glLayer.framebufferWidth; + const startY = viewport.y / glLayer.framebufferHeight; + const endX = (viewport.x + viewport.width) / glLayer.framebufferWidth; + const endY = + (viewport.y + viewport.height) / glLayer.framebufferHeight; + ren.setViewport(startX, startY, endX, endY); + } else { + ren.setViewport(0, 0, 1, 1); } ren @@ -484,6 +497,11 @@ function vtkOpenGLRenderWindow(publicAPI, model) { publicAPI.traverseAllPasses(); }); + + // Reset scissorbox before any subsequent rendering to external displays + // on frame end, such as rendering to a Looking Glass display. + gl.scissor(0, 0, glLayer.framebufferWidth, glLayer.framebufferHeight); + gl.disable(gl.SCISSOR_TEST); } }; @@ -1288,7 +1306,6 @@ const DEFAULT_VALUES = { defaultToWebgl2: true, // attempt webgl2 on by default activeFramebuffer: null, xrSession: null, - xrSessionIsAR: false, xrReferenceSpace: null, xrSupported: true, imageFormat: 'image/png',