From 9014047d934506422fb29826667ec3e47545eb83 Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Fri, 22 Sep 2023 14:43:05 -0400 Subject: [PATCH 1/6] refactor(webxrrenderwindowhelper): refactor WebXR rendering support Refactors WebXR rendering support. Under previous behavior WebXR rendering was embedded in vtkOpenGLRenderWindow. This change removes the dependency of vtkOpenGLRenderWindow on WebXR and moves XR components into vtkWebXRRenderWindowHelper. The helper class holds a reference to an underlying render window and is responsible to start/run/stop XR rendering. BREAKING CHANGE: Removes WebXR API from `vtkOpenGLRenderWindow`. Applications requiring XR support must instantiate `vtkWebXRRenderWindowHelper` instead. --- .../Rendering/OpenGL/RenderWindow/index.d.ts | 34 -- .../Rendering/OpenGL/RenderWindow/index.js | 234 +------------- .../RenderWindowHelper}/Constants.d.ts | 3 +- .../RenderWindowHelper}/Constants.js | 0 .../WebXR/RenderWindowHelper/index.d.ts | 79 +++++ .../WebXR/RenderWindowHelper/index.js | 300 ++++++++++++++++++ 6 files changed, 382 insertions(+), 268 deletions(-) rename Sources/Rendering/{OpenGL/RenderWindow => WebXR/RenderWindowHelper}/Constants.d.ts (82%) rename Sources/Rendering/{OpenGL/RenderWindow => WebXR/RenderWindowHelper}/Constants.js (100%) create mode 100644 Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts create mode 100644 Sources/Rendering/WebXR/RenderWindowHelper/index.js diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.d.ts b/Sources/Rendering/OpenGL/RenderWindow/index.d.ts index 9eb17b20ee5..8605a8a78dc 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.d.ts +++ b/Sources/Rendering/OpenGL/RenderWindow/index.d.ts @@ -24,10 +24,6 @@ export interface IOpenGLRenderWindowInitialValues { webgl2?: boolean; defaultToWebgl2?: boolean; activeFramebuffer?: any; - xrSession?: any; - xrSessionIsAR?: boolean; - xrReferenceSpace?: any; - xrSupported?: boolean; imageFormat?: 'image/png'; useOffScreen?: boolean; useBackgroundImage?: boolean; @@ -231,36 +227,6 @@ 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. - */ - 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 - */ - stopXR(): void; - - /** - * - */ - xrRender(): void; - /** * */ diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index b98003896bc..63a6bd0aa3f 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -8,14 +8,9 @@ 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'; +import { createContextProxyHandler } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow/ContextProxy'; const { vtkDebugMacro, vtkErrorMacro } = macro; -const { XrSessionTypes } = Constants; const SCREENSHOT_PLACEHOLDER = { position: 'absolute', @@ -25,11 +20,6 @@ const SCREENSHOT_PLACEHOLDER = { height: '100%', }; -const DEFAULT_RESET_FACTORS = { - rescaleFactor: 0.25, // isotropic scale factor reduces apparent size of objects - translateZ: -1.5, // default translation initializes object in front of camera -}; - function checkRenderTargetSupport(gl, format, type) { // create temporary frame buffer and texture const framebuffer = gl.createFramebuffer(); @@ -277,223 +267,6 @@ function vtkOpenGLRenderWindow(publicAPI, model) { return new Proxy(result, cachingContextHandler); }; - // Request an XR session on the user device with WebXR, - // typically in response to a user request such as a button press - publicAPI.startXR = (xrSessionType) => { - if (navigator.xr === undefined) { - throw new Error('WebXR is not available'); - } - - model.xrSessionType = - xrSessionType !== undefined ? xrSessionType : XrSessionTypes.HmdVR; - const isXrSessionAR = [ - XrSessionTypes.HmdAR, - XrSessionTypes.MobileAR, - ].includes(model.xrSessionType); - const sessionType = isXrSessionAR ? 'immersive-ar' : 'immersive-vr'; - if (!navigator.xr.isSessionSupported(sessionType)) { - if (isXrSessionAR) { - throw new Error('Device does not support AR session'); - } else { - throw new Error('VR display is not available'); - } - } - if (model.xrSession === null) { - navigator.xr.requestSession(sessionType).then(publicAPI.enterXR, () => { - throw new Error('Failed to create XR session!'); - }); - } else { - throw new Error('XR 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.xrSession !== null) { - const gl = publicAPI.get3DContext(); - await gl.makeXRCompatible(); - - const glLayer = new global.XRWebGLLayer( - model.xrSession, - // constructor needs unproxied context - gl[GET_UNDERLYING_CONTEXT]() - ); - publicAPI.setSize(glLayer.framebufferWidth, glLayer.framebufferHeight); - - model.xrSession.updateRenderState({ - baseLayer: glLayer, - }); - - model.xrSession.requestReferenceSpace('local').then((refSpace) => { - model.xrReferenceSpace = refSpace; - }); - - // Initialize transparent background for augmented reality session - const isXrSessionAR = [ - XrSessionTypes.HmdAR, - XrSessionTypes.MobileAR, - ].includes(model.xrSessionType); - if (isXrSessionAR) { - const ren = model.renderable.getRenderers()[0]; - model.preXrSessionBackground = ren.getBackground(); - ren.setBackground([0, 0, 0, 0]); - } - - publicAPI.resetXRScene(); - - model.renderable.getInteractor().switchToXRAnimation(); - model.xrSceneFrame = model.xrSession.requestAnimationFrame( - publicAPI.xrRender - ); - } else { - throw new Error('Failed to enter XR with a null xrSession.'); - } - }; - - publicAPI.resetXRScene = ( - rescaleFactor = DEFAULT_RESET_FACTORS.rescaleFactor, - translateZ = DEFAULT_RESET_FACTORS.translateZ - ) => { - // Adjust world-to-physical parameters for different modalities - - const ren = model.renderable.getRenderers()[0]; - ren.resetCamera(); - - const camera = ren.getActiveCamera(); - let physicalScale = camera.getPhysicalScale(); - const physicalTranslation = camera.getPhysicalTranslation(); - - const rescaledTranslateZ = translateZ * physicalScale; - physicalScale /= rescaleFactor; - physicalTranslation[2] += rescaledTranslateZ; - - camera.setPhysicalScale(physicalScale); - camera.setPhysicalTranslation(physicalTranslation); - // Clip at 0.1m, 100.0m in physical space by default - camera.setClippingRange(0.1 * physicalScale, 100.0 * physicalScale); - }; - - publicAPI.stopXR = async () => { - if (navigator.xr === undefined) { - // WebXR polyfill not available so nothing to do - return; - } - - 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; - } - - if (model.oldCanvasSize !== undefined) { - publicAPI.setSize(...model.oldCanvasSize); - } - - // Reset to default canvas - const ren = model.renderable.getRenderers()[0]; - - if (model.preXrSessionBackground != null) { - ren.setBackground(model.preXrSessionBackground); - model.preXrSessionBackground = null; - } - - ren.getActiveCamera().setProjectionMatrix(null); - ren.resetCamera(); - - ren.setViewport(0.0, 0, 1.0, 1.0); - publicAPI.traverseAllPasses(); - }; - - publicAPI.xrRender = async (t, frame) => { - const xrSession = frame.session; - const isXrSessionHMD = [ - XrSessionTypes.HmdVR, - XrSessionTypes.HmdAR, - ].includes(model.xrSessionType); - - model.renderable - .getInteractor() - .updateXRGamepads(xrSession, frame, model.xrReferenceSpace); - - model.xrSceneFrame = model.xrSession.requestAnimationFrame( - publicAPI.xrRender - ); - - const xrPose = frame.getViewerPose(model.xrReferenceSpace); - - if (xrPose) { - const gl = publicAPI.get3DContext(); - - if ( - model.xrSessionType === XrSessionTypes.MobileAR && - model.oldCanvasSize !== undefined - ) { - gl.canvas.width = model.oldCanvasSize[0]; - gl.canvas.height = model.oldCanvasSize[1]; - } - - const glLayer = xrSession.renderState.baseLayer; - 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, index) => { - const viewport = glLayer.getViewport(view); - - if (isXrSessionHMD) { - 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; - } - } 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 - .getActiveCamera() - .computeViewParametersFromPhysicalMatrix( - view.transform.inverse.matrix - ); - ren.getActiveCamera().setProjectionMatrix(view.projectionMatrix); - - 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); - } - }; - publicAPI.restoreContext = () => { const rp = vtkRenderPass.newInstance(); rp.setCurrentOperation('Release'); @@ -1308,9 +1081,6 @@ const DEFAULT_VALUES = { webgl2: false, defaultToWebgl2: true, // attempt webgl2 on by default activeFramebuffer: null, - xrSession: null, - xrReferenceSpace: null, - xrSupported: true, imageFormat: 'image/png', useOffScreen: false, useBackgroundImage: false, @@ -1363,9 +1133,7 @@ export function extend(publicAPI, model, initialValues = {}) { 'shaderCache', 'textureUnitManager', 'webgl2', - 'vrDisplay', 'useBackgroundImage', - 'xrSupported', 'activeFramebuffer', ]); diff --git a/Sources/Rendering/OpenGL/RenderWindow/Constants.d.ts b/Sources/Rendering/WebXR/RenderWindowHelper/Constants.d.ts similarity index 82% rename from Sources/Rendering/OpenGL/RenderWindow/Constants.d.ts rename to Sources/Rendering/WebXR/RenderWindowHelper/Constants.d.ts index 8268da0b469..0fb9e372110 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/Constants.d.ts +++ b/Sources/Rendering/WebXR/RenderWindowHelper/Constants.d.ts @@ -1,7 +1,8 @@ export declare enum XrSessionTypes { HmdVR = 0, MobileAR = 1, - LookingGlassVR = 2 + LookingGlassVR = 2, + HmdAR = 3 } declare const _default: { diff --git a/Sources/Rendering/OpenGL/RenderWindow/Constants.js b/Sources/Rendering/WebXR/RenderWindowHelper/Constants.js similarity index 100% rename from Sources/Rendering/OpenGL/RenderWindow/Constants.js rename to Sources/Rendering/WebXR/RenderWindowHelper/Constants.js diff --git a/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts b/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts new file mode 100644 index 00000000000..083c8b60736 --- /dev/null +++ b/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts @@ -0,0 +1,79 @@ +import { vtkObject } from '../../../interfaces'; +import vtkOpenGLRenderWindow from '../../OpenGL/RenderWindow'; + +/** + * + */ +export interface IWebXRRenderWindowHelperInitialValues { + initialized: boolean, + initCanvasSize?: [number, number], + initBackground?: [number, number, number, number], + renderWindow?: vtkOpenGLRenderWindow, + xrSession?: any, + xrSessionType: number, + xrReferenceSpace?: any, +} + +export interface vtkWebXRRenderWindowHelper extends vtkObject { + + /** + * Initialize the instance. + */ + initialize(): void; + + /** + * Request an XR session on the user device with WebXR, + * typically in response to a user request such as a button press. + */ + startXR(xrSessionType: Number): 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 + */ + stopXR(): void; + + /** + * + */ + xrRender(): void; +} + +/** + * Method used to decorate a given object (publicAPI+model) with vtkWebXRRenderWindowHelper characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {IWebXRRenderWindowHelperInitialValues} [initialValues] (default: {}) + */ +export function extend(publicAPI: object, model: object, initialValues?: IWebXRRenderWindowHelperInitialValues): void; + +/** + * Method used to create a new instance of vtkWebXRRenderWindowHelper. + * @param {IWebXRRenderWindowHelperInitialValues} [initialValues] for pre-setting some of its content + */ +export function newInstance(initialValues?: IWebXRRenderWindowHelperInitialValues): vtkWebXRRenderWindowHelper; + +/** + * WebXR rendering helper + * + * vtkWebXRRenderWindowHelper is designed to wrap a vtkRenderWindow for XR rendering. + */ +export declare const vtkWebXRRenderWindowHelper: { + newInstance: typeof newInstance, + extend: typeof extend, +}; +export default vtkWebXRRenderWindowHelper; diff --git a/Sources/Rendering/WebXR/RenderWindowHelper/index.js b/Sources/Rendering/WebXR/RenderWindowHelper/index.js new file mode 100644 index 00000000000..e29d05b78bd --- /dev/null +++ b/Sources/Rendering/WebXR/RenderWindowHelper/index.js @@ -0,0 +1,300 @@ +import macro from 'vtk.js/Sources/macros'; +import Constants from 'vtk.js/Sources/Rendering/WebXR/RenderWindowHelper/Constants'; +import { GET_UNDERLYING_CONTEXT } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow/ContextProxy'; + +const { XrSessionTypes } = Constants; + +const DEFAULT_RESET_FACTORS = { + rescaleFactor: 0.25, // isotropic scale factor reduces apparent size of objects + translateZ: -1.5, // default translation initializes object in front of camera +}; + +// ---------------------------------------------------------------------------- +// vtkWebXRRenderWindowHelper methods +// ---------------------------------------------------------------------------- + +function vtkWebXRRenderWindowHelper(publicAPI, model) { + // Set our className + model.classHierarchy.push('vtkWebXRRenderWindowHelper'); + + publicAPI.initialize = (renderWindow) => { + if (!model.initialized) { + model.renderWindow = renderWindow; + model.initialized = true; + } + }; + + publicAPI.getXrSupported = () => navigator.xr !== undefined; + + // Request an XR session on the user device with WebXR, + // typically in response to a user request such as a button press + publicAPI.startXR = (xrSessionType) => { + if (navigator.xr === undefined) { + throw new Error('WebXR is not available'); + } + + model.xrSessionType = + xrSessionType !== undefined ? xrSessionType : XrSessionTypes.HmdVR; + const isXrSessionAR = [ + XrSessionTypes.HmdAR, + XrSessionTypes.MobileAR, + ].includes(model.xrSessionType); + const sessionType = isXrSessionAR ? 'immersive-ar' : 'immersive-vr'; + if (!navigator.xr.isSessionSupported(sessionType)) { + if (isXrSessionAR) { + throw new Error('Device does not support AR session'); + } else { + throw new Error('VR display is not available'); + } + } + if (model.xrSession === null) { + navigator.xr.requestSession(sessionType).then(publicAPI.enterXR, () => { + throw new Error('Failed to create XR session!'); + }); + } else { + throw new Error('XR 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.initCanvasSize = model.renderWindow.getSize(); + + if (model.xrSession !== null) { + const gl = model.renderWindow.get3DContext(); + await gl.makeXRCompatible(); + + const glLayer = new global.XRWebGLLayer( + model.xrSession, + // constructor needs unproxied context + gl[GET_UNDERLYING_CONTEXT]() + ); + model.renderWindow.setSize( + glLayer.framebufferWidth, + glLayer.framebufferHeight + ); + + model.xrSession.updateRenderState({ + baseLayer: glLayer, + }); + + model.xrSession.requestReferenceSpace('local').then((refSpace) => { + model.xrReferenceSpace = refSpace; + }); + + // Initialize transparent background for augmented reality session + const isXrSessionAR = [ + XrSessionTypes.HmdAR, + XrSessionTypes.MobileAR, + ].includes(model.xrSessionType); + if (isXrSessionAR) { + const ren = model.renderWindow.getRenderable().getRenderers()[0]; + model.initBackground = ren.getBackground(); + ren.setBackground([0, 0, 0, 0]); + } + + publicAPI.resetXRScene(); + + model.renderWindow.getRenderable().getInteractor().switchToXRAnimation(); + model.xrSceneFrame = model.xrSession.requestAnimationFrame( + publicAPI.xrRender + ); + } else { + throw new Error('Failed to enter XR with a null xrSession.'); + } + }; + + publicAPI.resetXRScene = ( + rescaleFactor = DEFAULT_RESET_FACTORS.rescaleFactor, + translateZ = DEFAULT_RESET_FACTORS.translateZ + ) => { + // Adjust world-to-physical parameters for different modalities + + const ren = model.renderWindow.getRenderable().getRenderers()[0]; // TESTME + ren.resetCamera(); + + const camera = ren.getActiveCamera(); + let physicalScale = camera.getPhysicalScale(); + const physicalTranslation = camera.getPhysicalTranslation(); + + const rescaledTranslateZ = translateZ * physicalScale; + physicalScale /= rescaleFactor; + physicalTranslation[2] += rescaledTranslateZ; + + camera.setPhysicalScale(physicalScale); + camera.setPhysicalTranslation(physicalTranslation); + // Clip at 0.1m, 100.0m in physical space by default + camera.setClippingRange(0.1 * physicalScale, 100.0 * physicalScale); + }; + + publicAPI.stopXR = async () => { + if (navigator.xr === undefined) { + // WebXR polyfill not available so nothing to do + return; + } + + if (model.xrSession !== null) { + model.xrSession.cancelAnimationFrame(model.xrSceneFrame); + model.renderWindow + .getRenderable() + .getInteractor() + .returnFromXRAnimation(); + const gl = model.renderWindow.get3DContext(); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + await model.xrSession.end().catch((error) => { + if (!(error instanceof DOMException)) { + throw error; + } + }); + model.xrSession = null; + } + + if (model.initCanvasSize !== null) { + model.renderWindow.setSize(...model.initCanvasSize); + } + + // Reset to default canvas + const ren = model.renderWindow.getRenderable().getRenderers()[0]; + + if (model.initBackground != null) { + ren.setBackground(model.initBackground); + model.initBackground = null; + } + + ren.getActiveCamera().setProjectionMatrix(null); + ren.resetCamera(); + + ren.setViewport(0.0, 0, 1.0, 1.0); + model.renderWindow.traverseAllPasses(); + }; + + publicAPI.xrRender = async (t, frame) => { + const xrSession = frame.session; + const isXrSessionHMD = [ + XrSessionTypes.HmdVR, + XrSessionTypes.HmdAR, + ].includes(model.xrSessionType); + + model.renderWindow + .getRenderable() + .getInteractor() + .updateXRGamepads(xrSession, frame, model.xrReferenceSpace); + + model.xrSceneFrame = model.xrSession.requestAnimationFrame( + publicAPI.xrRender + ); + + const xrPose = frame.getViewerPose(model.xrReferenceSpace); + + if (xrPose) { + const gl = model.renderWindow.get3DContext(); + + if ( + model.xrSessionType === XrSessionTypes.MobileAR && + model.initCanvasSize !== null + ) { + gl.canvas.width = model.initCanvasSize[0]; + gl.canvas.height = model.initCanvasSize[1]; + } + + const glLayer = xrSession.renderState.baseLayer; + gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.clear(gl.DEPTH_BUFFER_BIT); + model.renderWindow.setSize( + glLayer.framebufferWidth, + glLayer.framebufferHeight + ); + + // get the first renderer + const ren = model.renderWindow.getRenderable().getRenderers()[0]; + + // Do a render pass for each eye + xrPose.views.forEach((view, index) => { + const viewport = glLayer.getViewport(view); + + if (isXrSessionHMD) { + 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; + } + } 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 + .getActiveCamera() + .computeViewParametersFromPhysicalMatrix( + view.transform.inverse.matrix + ); + ren.getActiveCamera().setProjectionMatrix(view.projectionMatrix); + + model.renderWindow.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); + } + }; + + publicAPI.delete = macro.chain(publicAPI.delete); +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + initialized: false, + initCanvasSize: null, + initBackground: null, + renderWindow: null, + xrSession: null, + xrSessionType: 0, + xrReferenceSpace: null, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Build VTK API + macro.obj(publicAPI, model); + macro.event(publicAPI, model, 'event'); + + macro.setGet(publicAPI, model, ['renderWindow']); + + // Object methods + vtkWebXRRenderWindowHelper(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance( + extend, + 'vtkWebXRRenderWindowHelper' +); + +// ---------------------------------------------------------------------------- + +export default { + newInstance, + extend, +}; From 5e84620a869e1f58bc81752e93099e7faa3fb995 Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Fri, 22 Sep 2023 14:44:01 -0400 Subject: [PATCH 2/6] docs(webxr): update WebXR examples for refactored XR rendering approach Updates WebXR examples for refactoring to `vtkWebXRRenderWindowHelper`. --- Examples/Applications/GeometryViewer/index.js | 15 +++++++++------ Examples/Applications/SkyboxViewer/index.js | 13 ++++++++----- Examples/Geometry/AR/index.js | 16 ++++++++-------- Examples/Geometry/LookingGlass/index.js | 12 +++++++----- Examples/Geometry/VR/index.js | 12 +++++++----- Examples/Volume/WebXRChestCTBlendedCVR/index.js | 15 ++++++++------- Examples/Volume/WebXRHeadFullVolumeCVR/index.js | 17 ++++++++--------- Examples/Volume/WebXRHeadGradientCVR/index.js | 17 ++++++++--------- Examples/Volume/WebXRVolume/index.js | 17 ++++++++--------- 9 files changed, 71 insertions(+), 63 deletions(-) diff --git a/Examples/Applications/GeometryViewer/index.js b/Examples/Applications/GeometryViewer/index.js index 56a6140fc7b..d9846eadf0c 100644 --- a/Examples/Applications/GeometryViewer/index.js +++ b/Examples/Applications/GeometryViewer/index.js @@ -14,11 +14,12 @@ import vtkScalarBarActor from '@kitware/vtk.js/Rendering/Core/ScalarBarActor'; import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; 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'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants'; // Force DataAccessHelper to have access to various data source import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; @@ -36,6 +37,7 @@ let autoInit = true; let background = [0, 0, 0]; let fullScreenRenderWindow; let renderWindow; +let xrRenderWindowHelper; let renderer; let scalarBarActor; @@ -162,6 +164,9 @@ function createViewer(container) { }); renderer = fullScreenRenderWindow.getRenderer(); renderWindow = fullScreenRenderWindow.getRenderWindow(); + xrRenderWindowHelper = vtkWebXRRenderWindowHelper.newInstance({ + renderWindow: fullScreenRenderWindow.getApiSpecificRenderWindow(), + }); renderWindow.getInteractor().setDesiredUpdateRate(15); container.appendChild(rootControllerContainer); @@ -252,7 +257,7 @@ function createPipeline(fileName, fileContents) { if ( navigator.xr !== undefined && - fullScreenRenderWindow.getApiSpecificRenderWindow().getXrSupported() && + xrRenderWindowHelper.getXrSupported() && requestedXrSessionType !== null ) { controlContainer.appendChild(immersionSelector); @@ -431,9 +436,7 @@ function createPipeline(fileName, fileContents) { function toggleXR() { if (immersionSelector.textContent.startsWith('Start')) { - fullScreenRenderWindow - .getApiSpecificRenderWindow() - .startXR(requestedXrSessionType); + xrRenderWindowHelper.startXR(requestedXrSessionType); immersionSelector.textContent = [ XrSessionTypes.HmdAR, XrSessionTypes.MobileAR, @@ -441,7 +444,7 @@ function createPipeline(fileName, fileContents) { ? 'Exit AR' : 'Exit VR'; } else { - fullScreenRenderWindow.getApiSpecificRenderWindow().stopXR(); + xrRenderWindowHelper.stopXR(); immersionSelector.textContent = [ XrSessionTypes.HmdAR, XrSessionTypes.MobileAR, diff --git a/Examples/Applications/SkyboxViewer/index.js b/Examples/Applications/SkyboxViewer/index.js index 92705f89af2..bfe1c1dd0bc 100644 --- a/Examples/Applications/SkyboxViewer/index.js +++ b/Examples/Applications/SkyboxViewer/index.js @@ -10,10 +10,11 @@ import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpD import macro from '@kitware/vtk.js/macros'; import vtkDeviceOrientationToCamera from '@kitware/vtk.js/Interaction/Misc/DeviceOrientationToCamera'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; 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 { XrSessionTypes } from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow/Constants'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants'; // Force DataAccessHelper to have access to various data source import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; @@ -157,6 +158,9 @@ function createVisualization(container, mapReader) { containerStyle: { height: '100%', width: '100%', position: 'absolute' }, }); const renderWindow = fullScreenRenderer.getRenderWindow(); + const xrRenderWindowHelper = vtkWebXRRenderWindowHelper.newInstance({ + renderWindow: fullScreenRenderer.getApiSpecificRenderWindow(), + }); const mainRenderer = fullScreenRenderer.getRenderer(); const interactor = fullScreenRenderer.getInteractor(); const actor = vtkSkybox.newInstance(); @@ -202,6 +206,7 @@ function createVisualization(container, mapReader) { // add vr option button if supported if ( navigator.xr !== undefined && + xrRenderWindowHelper.getXrSupported() && navigator.xr.isSessionSupported('immersive-vr') ) { const button = document.createElement('button'); @@ -213,12 +218,10 @@ function createVisualization(container, mapReader) { document.querySelector('body').appendChild(button); button.addEventListener('click', () => { if (button.textContent === 'Send To VR') { - fullScreenRenderer - .getApiSpecificRenderWindow() - .startXR(XrSessionTypes.HmdVR); + xrRenderWindowHelper.startXR(XrSessionTypes.HmdVR); button.textContent = 'Return From VR'; } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); + xrRenderWindowHelper.stopXR(); button.textContent = 'Send To VR'; } }); diff --git a/Examples/Geometry/AR/index.js b/Examples/Geometry/AR/index.js index 9a60d36304f..647446b70e7 100644 --- a/Examples/Geometry/AR/index.js +++ b/Examples/Geometry/AR/index.js @@ -7,11 +7,12 @@ 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 vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract'; 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'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants'; // Force DataAccessHelper to have access to various data source import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; @@ -36,6 +37,9 @@ const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ }); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow(); +const xrRenderWindowHelper = vtkWebXRRenderWindowHelper.newInstance({ + renderWindow: fullScreenRenderer.getApiSpecificRenderWindow(), +}); // ---------------------------------------------------------------------------- // Example code @@ -87,18 +91,14 @@ renderWindow.render(); fullScreenRenderer.addController(controlPanel); const arbutton = document.querySelector('.arbutton'); -arbutton.disabled = !fullScreenRenderer - .getApiSpecificRenderWindow() - .getXrSupported(); +arbutton.disabled = !xrRenderWindowHelper.getXrSupported(); arbutton.addEventListener('click', (e) => { if (arbutton.textContent === 'Start AR') { - fullScreenRenderer - .getApiSpecificRenderWindow() - .startXR(requestedXrSessionType); + xrRenderWindowHelper.startXR(requestedXrSessionType); arbutton.textContent = 'Exit AR'; } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); + xrRenderWindowHelper.stopXR(); arbutton.textContent = 'Start AR'; } }); diff --git a/Examples/Geometry/LookingGlass/index.js b/Examples/Geometry/LookingGlass/index.js index b6e6abd7d5e..76f3f2dba8a 100644 --- a/Examples/Geometry/LookingGlass/index.js +++ b/Examples/Geometry/LookingGlass/index.js @@ -10,10 +10,11 @@ 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 vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; 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'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants'; // Force DataAccessHelper to have access to various data source import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; @@ -42,6 +43,9 @@ const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ }); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow(); +const xrRenderWindowHelper = vtkWebXRRenderWindowHelper.newInstance({ + renderWindow: fullScreenRenderer.getApiSpecificRenderWindow(), +}); // ---------------------------------------------------------------------------- // Example code @@ -110,12 +114,10 @@ resolutionChange.addEventListener('input', (e) => { vrbutton.addEventListener('click', (e) => { if (vrbutton.textContent === 'Send To Looking Glass') { - fullScreenRenderer - .getApiSpecificRenderWindow() - .startXR(XrSessionTypes.LookingGlassVR); + xrRenderWindowHelper.startXR(XrSessionTypes.LookingGlassVR); vrbutton.textContent = 'Return From Looking Glass'; } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); + xrRenderWindowHelper.stopXR(); vrbutton.textContent = 'Send To Looking Glass'; } }); diff --git a/Examples/Geometry/VR/index.js b/Examples/Geometry/VR/index.js index ce2d9051f4c..889803b77ea 100644 --- a/Examples/Geometry/VR/index.js +++ b/Examples/Geometry/VR/index.js @@ -10,10 +10,11 @@ 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 vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; 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'; +import { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants'; // Force DataAccessHelper to have access to various data source import '@kitware/vtk.js/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; @@ -46,6 +47,9 @@ const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ }); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow(); +const XRHelper = vtkWebXRRenderWindowHelper.newInstance({ + renderWindow: fullScreenRenderer.getApiSpecificRenderWindow(), +}); // ---------------------------------------------------------------------------- // Example code @@ -114,12 +118,10 @@ resolutionChange.addEventListener('input', (e) => { vrbutton.addEventListener('click', (e) => { if (vrbutton.textContent === 'Send To VR') { - fullScreenRenderer - .getApiSpecificRenderWindow() - .startXR(XrSessionTypes.HmdVR); + XRHelper.startXR(XrSessionTypes.HmdVR); vrbutton.textContent = 'Return From VR'; } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); + XRHelper.stopXR(); vrbutton.textContent = 'Send To VR'; } }); diff --git a/Examples/Volume/WebXRChestCTBlendedCVR/index.js b/Examples/Volume/WebXRChestCTBlendedCVR/index.js index 7d246cef19e..38c144b2eba 100644 --- a/Examples/Volume/WebXRChestCTBlendedCVR/index.js +++ b/Examples/Volume/WebXRChestCTBlendedCVR/index.js @@ -10,6 +10,7 @@ import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract'; @@ -18,7 +19,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 { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants'; import './WebXRVolume.module.css'; @@ -32,6 +33,9 @@ const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ }); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow(); +const xrRenderWindowHelper = vtkWebXRRenderWindowHelper.newInstance({ + renderWindow: fullScreenRenderer.getApiSpecificRenderWindow(), +}); // ---------------------------------------------------------------------------- // Set up pipeline objects @@ -118,10 +122,7 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { let enterText = 'XR not available!'; const exitText = 'Exit XR'; xrButton.textContent = enterText; - if ( - navigator.xr !== undefined && - fullScreenRenderer.getApiSpecificRenderWindow().getXrSupported() - ) { + if (navigator.xr !== undefined && xrRenderWindowHelper.getXrSupported()) { navigator.xr.isSessionSupported('immersive-ar').then((arSupported) => { if (arSupported) { xrSessionType = XrSessionTypes.MobileAR; @@ -140,10 +141,10 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { } xrButton.addEventListener('click', () => { if (xrButton.textContent === enterText) { - fullScreenRenderer.getApiSpecificRenderWindow().startXR(xrSessionType); + xrRenderWindowHelper.startXR(xrSessionType); xrButton.textContent = exitText; } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); + xrRenderWindowHelper.stopXR(); xrButton.textContent = enterText; } }); diff --git a/Examples/Volume/WebXRHeadFullVolumeCVR/index.js b/Examples/Volume/WebXRHeadFullVolumeCVR/index.js index 7f15a12f92d..b85b9bb5f54 100644 --- a/Examples/Volume/WebXRHeadFullVolumeCVR/index.js +++ b/Examples/Volume/WebXRHeadFullVolumeCVR/index.js @@ -10,6 +10,7 @@ import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract'; @@ -18,7 +19,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 { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants'; import './WebXRVolume.module.css'; @@ -32,6 +33,9 @@ const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ }); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow(); +const xrRenderWindowHelper = vtkWebXRRenderWindowHelper.newInstance({ + renderWindow: fullScreenRenderer.getApiSpecificRenderWindow(), +}); // ---------------------------------------------------------------------------- // Set up pipeline objects @@ -149,10 +153,7 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { let enterText = 'XR not available!'; const exitText = 'Exit XR'; xrButton.textContent = enterText; - if ( - navigator.xr !== undefined && - fullScreenRenderer.getApiSpecificRenderWindow().getXrSupported() - ) { + if (navigator.xr !== undefined && xrRenderWindowHelper.getXrSupported()) { enterText = requestedXrSessionType === XrSessionTypes.MobileAR ? 'Start AR' @@ -161,12 +162,10 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { } xrButton.addEventListener('click', () => { if (xrButton.textContent === enterText) { - fullScreenRenderer - .getApiSpecificRenderWindow() - .startXR(requestedXrSessionType); + xrRenderWindowHelper.startXR(requestedXrSessionType); xrButton.textContent = exitText; } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); + xrRenderWindowHelper.stopXR(); xrButton.textContent = enterText; } }); diff --git a/Examples/Volume/WebXRHeadGradientCVR/index.js b/Examples/Volume/WebXRHeadGradientCVR/index.js index 669c3defd38..5698a566abf 100644 --- a/Examples/Volume/WebXRHeadGradientCVR/index.js +++ b/Examples/Volume/WebXRHeadGradientCVR/index.js @@ -10,6 +10,7 @@ import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract'; @@ -18,7 +19,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 { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants'; import './WebXRVolume.module.css'; @@ -32,6 +33,9 @@ const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ }); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow(); +const xrRenderWindowHelper = vtkWebXRRenderWindowHelper.newInstance({ + renderWindow: fullScreenRenderer.getApiSpecificRenderWindow(), +}); // ---------------------------------------------------------------------------- // Set up pipeline objects @@ -146,10 +150,7 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { let enterText = 'XR not available!'; const exitText = 'Exit XR'; xrButton.textContent = enterText; - if ( - navigator.xr !== undefined && - fullScreenRenderer.getApiSpecificRenderWindow().getXrSupported() - ) { + if (navigator.xr !== undefined && xrRenderWindowHelper.getXrSupported()) { enterText = requestedXrSessionType === XrSessionTypes.MobileAR ? 'Start AR' @@ -158,12 +159,10 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { } xrButton.addEventListener('click', () => { if (xrButton.textContent === enterText) { - fullScreenRenderer - .getApiSpecificRenderWindow() - .startXR(requestedXrSessionType); + xrRenderWindowHelper.startXR(requestedXrSessionType); xrButton.textContent = exitText; } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); + xrRenderWindowHelper.stopXR(); xrButton.textContent = enterText; } }); diff --git a/Examples/Volume/WebXRVolume/index.js b/Examples/Volume/WebXRVolume/index.js index c191d620d58..56405f4ce11 100644 --- a/Examples/Volume/WebXRVolume/index.js +++ b/Examples/Volume/WebXRVolume/index.js @@ -10,6 +10,7 @@ import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkWebXRRenderWindowHelper from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper'; import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import vtkImageReslice from '@kitware/vtk.js/Imaging/Core/ImageReslice'; import vtkMath from '@kitware/vtk.js/Common/Core/Math'; @@ -18,7 +19,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 { XrSessionTypes } from '@kitware/vtk.js/Rendering/WebXR/RenderWindowHelper/Constants'; import './WebXRVolume.module.css'; @@ -32,6 +33,9 @@ const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ }); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow(); +const xrRenderWindowHelper = vtkWebXRRenderWindowHelper.newInstance({ + renderWindow: fullScreenRenderer.getApiSpecificRenderWindow(), +}); // ---------------------------------------------------------------------------- // Set up pipeline objects @@ -220,10 +224,7 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { let enterText = 'XR not available!'; const exitText = 'Exit XR'; xrButton.textContent = enterText; - if ( - navigator.xr !== undefined && - fullScreenRenderer.getApiSpecificRenderWindow().getXrSupported() - ) { + if (navigator.xr !== undefined && xrRenderWindowHelper.getXrSupported()) { enterText = requestedXrSessionType === XrSessionTypes.MobileAR ? 'Start AR' @@ -232,12 +233,10 @@ HttpDataAccessHelper.fetchBinary(fileURL).then((fileContents) => { } xrButton.addEventListener('click', () => { if (xrButton.textContent === enterText) { - fullScreenRenderer - .getApiSpecificRenderWindow() - .startXR(requestedXrSessionType); + xrRenderWindowHelper.startXR(requestedXrSessionType); xrButton.textContent = exitText; } else { - fullScreenRenderer.getApiSpecificRenderWindow().stopXR(); + xrRenderWindowHelper.stopXR(); xrButton.textContent = enterText; } }); From 9ebb55646774c50d698268a316f60dbaee7dd078 Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Mon, 25 Sep 2023 09:05:47 -0400 Subject: [PATCH 3/6] fix(vtkwebxrrenderwindowhelper): Replace global XRGLLayer definition Replaces `global` definition of XRGLLayer with imported definition from `window`. Addresses observed issue in VolView development where the `global` definition is not yet available at call time. --- Sources/Rendering/WebXR/RenderWindowHelper/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/Rendering/WebXR/RenderWindowHelper/index.js b/Sources/Rendering/WebXR/RenderWindowHelper/index.js index e29d05b78bd..c6a05159901 100644 --- a/Sources/Rendering/WebXR/RenderWindowHelper/index.js +++ b/Sources/Rendering/WebXR/RenderWindowHelper/index.js @@ -66,7 +66,10 @@ function vtkWebXRRenderWindowHelper(publicAPI, model) { const gl = model.renderWindow.get3DContext(); await gl.makeXRCompatible(); - const glLayer = new global.XRWebGLLayer( + // XRWebGLLayer definition is deferred to here to give any WebXR polyfill + // an opportunity to override this definition. + const { XRWebGLLayer } = window; + const glLayer = new XRWebGLLayer( model.xrSession, // constructor needs unproxied context gl[GET_UNDERLYING_CONTEXT]() From 5fd41aec55b668a8b864ae5c60634e1f2463d7eb Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Tue, 26 Sep 2023 13:45:45 -0400 Subject: [PATCH 4/6] feat(vtkwebxrrenderwindowhelper): update helper class interface for application logic Updates `vtkWebXRRenderWindowHelper` interface to expose the underlying XR session and to emit the VTK "modified" event when state changes. Motivated by subsequent VolView application development. --- Sources/Rendering/WebXR/RenderWindowHelper/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Rendering/WebXR/RenderWindowHelper/index.js b/Sources/Rendering/WebXR/RenderWindowHelper/index.js index c6a05159901..bef190d1fab 100644 --- a/Sources/Rendering/WebXR/RenderWindowHelper/index.js +++ b/Sources/Rendering/WebXR/RenderWindowHelper/index.js @@ -104,6 +104,8 @@ function vtkWebXRRenderWindowHelper(publicAPI, model) { model.xrSceneFrame = model.xrSession.requestAnimationFrame( publicAPI.xrRender ); + + publicAPI.modified(); } else { throw new Error('Failed to enter XR with a null xrSession.'); } @@ -115,7 +117,7 @@ function vtkWebXRRenderWindowHelper(publicAPI, model) { ) => { // Adjust world-to-physical parameters for different modalities - const ren = model.renderWindow.getRenderable().getRenderers()[0]; // TESTME + const ren = model.renderWindow.getRenderable().getRenderers()[0]; ren.resetCamera(); const camera = ren.getActiveCamera(); @@ -172,6 +174,8 @@ function vtkWebXRRenderWindowHelper(publicAPI, model) { ren.setViewport(0.0, 0, 1.0, 1.0); model.renderWindow.traverseAllPasses(); + + publicAPI.modified(); }; publicAPI.xrRender = async (t, frame) => { @@ -282,7 +286,7 @@ export function extend(publicAPI, model, initialValues = {}) { macro.obj(publicAPI, model); macro.event(publicAPI, model, 'event'); - macro.setGet(publicAPI, model, ['renderWindow']); + macro.setGet(publicAPI, model, ['renderWindow', 'xrSession']); // Object methods vtkWebXRRenderWindowHelper(publicAPI, model); From 49f338d0098d8af843272611c0e1fa337021064b Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Tue, 26 Sep 2023 15:20:25 -0400 Subject: [PATCH 5/6] refactor(vtkwebxrrenderwindowhelper): refine TypeScript definitions and public interface Improves TypeScript definitions and removes previous publicAPI methods for setting the XR session and rendering an XR frame. Motivated by subsequent application development in VolView. --- .../WebXR/RenderWindowHelper/index.d.ts | 28 +++++++++++++------ .../WebXR/RenderWindowHelper/index.js | 11 ++++---- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts b/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts index 083c8b60736..f5684e9f014 100644 --- a/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts +++ b/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts @@ -1,5 +1,7 @@ import { vtkObject } from '../../../interfaces'; +import { Nullable } from '../../../types'; import vtkOpenGLRenderWindow from '../../OpenGL/RenderWindow'; +import { XRSession } from window; /** * @@ -8,8 +10,8 @@ export interface IWebXRRenderWindowHelperInitialValues { initialized: boolean, initCanvasSize?: [number, number], initBackground?: [number, number, number, number], - renderWindow?: vtkOpenGLRenderWindow, - xrSession?: any, + renderWindow?: Nullable, + xrSession?: Nullable, xrSessionType: number, xrReferenceSpace?: any, } @@ -31,7 +33,7 @@ export interface vtkWebXRRenderWindowHelper extends vtkObject { * When an XR session is available, set up the XRWebGLLayer * and request the first animation frame for the device */ - enterXR(): void, + enterXR(): void; /** * Adjust world-to-physical parameters for different viewing modalities @@ -39,17 +41,27 @@ export interface vtkWebXRRenderWindowHelper extends vtkObject { * @param {Number} inputRescaleFactor * @param {Number} inputTranslateZ */ - resetXRScene(inputRescaleFactor: number, inputTranslateZ: number): void, + resetXRScene(inputRescaleFactor: number, inputTranslateZ: number): void; /** * Request to stop the current XR session */ stopXR(): void; - /** - * - */ - xrRender(): void; + /** + * Get the underlying render window to drive XR rendering. + */ + getRenderWindow(): Nullable; + + /** + * Set the underlying render window to drive XR rendering. + */ + setRenderWindow(renderWindow:Nullable); + + /** + * Get the active WebXR session. + */ + getXrSession(): Nullable; } /** diff --git a/Sources/Rendering/WebXR/RenderWindowHelper/index.js b/Sources/Rendering/WebXR/RenderWindowHelper/index.js index bef190d1fab..4bb1da3a3a9 100644 --- a/Sources/Rendering/WebXR/RenderWindowHelper/index.js +++ b/Sources/Rendering/WebXR/RenderWindowHelper/index.js @@ -102,7 +102,7 @@ function vtkWebXRRenderWindowHelper(publicAPI, model) { model.renderWindow.getRenderable().getInteractor().switchToXRAnimation(); model.xrSceneFrame = model.xrSession.requestAnimationFrame( - publicAPI.xrRender + model.xrRender ); publicAPI.modified(); @@ -178,7 +178,7 @@ function vtkWebXRRenderWindowHelper(publicAPI, model) { publicAPI.modified(); }; - publicAPI.xrRender = async (t, frame) => { + model.xrRender = async (t, frame) => { const xrSession = frame.session; const isXrSessionHMD = [ XrSessionTypes.HmdVR, @@ -190,9 +190,7 @@ function vtkWebXRRenderWindowHelper(publicAPI, model) { .getInteractor() .updateXRGamepads(xrSession, frame, model.xrReferenceSpace); - model.xrSceneFrame = model.xrSession.requestAnimationFrame( - publicAPI.xrRender - ); + model.xrSceneFrame = model.xrSession.requestAnimationFrame(model.xrRender); const xrPose = frame.getViewerPose(model.xrReferenceSpace); @@ -286,7 +284,8 @@ export function extend(publicAPI, model, initialValues = {}) { macro.obj(publicAPI, model); macro.event(publicAPI, model, 'event'); - macro.setGet(publicAPI, model, ['renderWindow', 'xrSession']); + macro.get(publicAPI, model, ['xrSession']); + macro.setGet(publicAPI, model, ['renderWindow']); // Object methods vtkWebXRRenderWindowHelper(publicAPI, model); From f400bf900d1c512670fe4a74af1ab55c2d333fde Mon Sep 17 00:00:00 2001 From: Tom Birdsong Date: Tue, 26 Sep 2023 15:57:56 -0400 Subject: [PATCH 6/6] refactor(vtkwebxrrenderwindowhelper): add WebXR TypeScript definitions Add WebXR TypeScript definitions to vtkWebXRRenderWindowHelper to resolve CI failures. --- Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts | 1 - package-lock.json | 11 +++++++++++ package.json | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts b/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts index f5684e9f014..8486a5a3d7f 100644 --- a/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts +++ b/Sources/Rendering/WebXR/RenderWindowHelper/index.d.ts @@ -1,7 +1,6 @@ import { vtkObject } from '../../../interfaces'; import { Nullable } from '../../../types'; import vtkOpenGLRenderWindow from '../../OpenGL/RenderWindow'; -import { XRSession } from window; /** * diff --git a/package-lock.json b/package-lock.json index 81dd9bf4a9e..efcc27fe5b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "7.22.11", + "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", @@ -3809,6 +3810,11 @@ "@types/node": "*" } }, + "node_modules/@types/webxr": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.5.tgz", + "integrity": "sha512-HVOsSRTQYx3zpVl0c0FBmmmcY/60BkQLzVnpE9M1aG4f2Z0aKlBWfj4XZ2zr++XNBfkQWYcwhGlmuu44RJPDqg==" + }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -23141,6 +23147,11 @@ "@types/node": "*" } }, + "@types/webxr": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.5.tgz", + "integrity": "sha512-HVOsSRTQYx3zpVl0c0FBmmmcY/60BkQLzVnpE9M1aG4f2Z0aKlBWfj4XZ2zr++XNBfkQWYcwhGlmuu44RJPDqg==" + }, "@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", diff --git a/package.json b/package.json index bc42ea2c2c8..46f59cd545a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "module": "./dist/esm/index.js", "dependencies": { "@babel/runtime": "7.22.11", + "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3",