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
+
+
+
+[![Looking Glass Cone Example][LookingGlassCone]](../examples/LookingGlass.html)
+[![GeometryViewer Example][GeometryViewer]](../examples/GeometryViewer.html?xrSessionType=2&fileURL=https://data.kitware.com/api/v1/item/59de9de58d777f31ac641dc5/download)
+[![GeometryViewer Brain Blood Vessels][GeometryViewerBrainBloodVessels]](../examples/GeometryViewer/GeometryViewer.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])
+[![GeometryViewer Chest CT][GeometryViewerchestCT]](../examples/GeometryViewer/GeometryViewer.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])
+[![XR Volume Example][WebXRVolume]](../examples/WebXRVolume.html?xrSessionType=2)
+[![XR Gradient Example][HeadGradient]](../examples/WebXRHeadGradientCVR.html?xrSessionType=2)
+
+
+
### 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',