import macro from 'vtk.js/Sources/macros'; import Constants from 'vtk.js/Sources/Rendering/WebXR/RenderWindowHelper/Constants'; import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; import vtkLineSource from 'vtk.js/Sources/Filters/Sources/LineSource'; import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; import { GET_UNDERLYING_CONTEXT } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow/ContextProxy'; import { vec3 } from 'gl-matrix';
const { XrSessionTypes } = Constants;
const DEFAULT_RESET_FACTORS = { rescaleFactor: 0.25, translateZ: -1.5, };
function createRay() { const targetRayLineSource = vtkLineSource.newInstance(); const targetRayMapper = vtkMapper.newInstance(); targetRayMapper.setInputConnection(targetRayLineSource.getOutputPort());
const targetRayActor = vtkActor.newInstance(); targetRayActor.getProperty().setColor(1, 0, 0); targetRayActor.getProperty().setLineWidth(5);
targetRayActor.setMapper(targetRayMapper); targetRayActor.setPickable(false);
return { lineSource: targetRayLineSource, mapper: targetRayMapper, actor: targetRayActor, visible: false, }; }
function vtkWebXRRenderWindowHelper(publicAPI, model) { model.classHierarchy.push('vtkWebXRRenderWindowHelper');
publicAPI.initialize = (renderWindow) => { if (!model.initialized) { model.renderWindow = renderWindow; model.initialized = true; } };
publicAPI.getXrSupported = () => navigator.xr !== undefined;
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!'); } };
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 { XRWebGLLayer } = window; const glLayer = new XRWebGLLayer( model.xrSession, 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; });
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( model.xrRender );
publicAPI.modified(); } else { throw new Error('Failed to enter XR with a null xrSession.'); } };
publicAPI.resetXRScene = ( rescaleFactor = DEFAULT_RESET_FACTORS.rescaleFactor, translateZ = DEFAULT_RESET_FACTORS.translateZ ) => {
const ren = model.renderWindow.getRenderable().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); camera.setClippingRange(0.1 * physicalScale, 100.0 * physicalScale); };
publicAPI.stopXR = async () => { if (navigator.xr === undefined) { 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); }
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.modified(); };
model.xrRender = async (t, frame) => { const xrSession = frame.session; const isXrSessionHMD = [ XrSessionTypes.HmdVR, XrSessionTypes.HmdAR, ].includes(model.xrSessionType); if (isXrSessionHMD && model.drawControllersRay && model.xrReferenceSpace) { const renderer = model.renderWindow.getRenderable().getRenderers()[0]; const camera = renderer.getActiveCamera(); const physicalToWorldMatrix = []; camera.getPhysicalToWorldMatrix(physicalToWorldMatrix);
xrSession.inputSources.forEach((inputSource) => { if ( inputSource.targetRaySpace == null || inputSource.gripSpace == null || inputSource.targetRayMode !== 'tracked-pointer' ) { return; }
if (model.inputSourceToRay[inputSource.handedness] == null) { model.inputSourceToRay[inputSource.handedness] = createRay(); }
const ray = model.inputSourceToRay[inputSource.handedness];
const targetRayPose = frame.getPose( inputSource.targetRaySpace, model.xrReferenceSpace );
if (targetRayPose == null) { return; }
const targetRayPosition = vec3.fromValues( targetRayPose.transform.position.x, targetRayPose.transform.position.y, targetRayPose.transform.position.z );
const dir = camera.physicalOrientationToWorldDirection([ targetRayPose.transform.orientation.x, targetRayPose.transform.orientation.y, targetRayPose.transform.orientation.z, targetRayPose.transform.orientation.w, ]);
const targetRayWorldPosition = vec3.transformMat4( [], targetRayPosition, physicalToWorldMatrix );
const dist = renderer.getActiveCamera().getClippingRange()[1];
if (!ray.visible) { renderer.addActor(ray.actor); ray.visible = true; }
ray.lineSource.setPoint1( targetRayWorldPosition[0] - dir[0] * dist, targetRayWorldPosition[1] - dir[1] * dist, targetRayWorldPosition[2] - dir[2] * dist ); ray.lineSource.setPoint2(...targetRayWorldPosition); });
model.renderWindow.render(); }
model.renderWindow .getRenderable() .getInteractor() .updateXRGamepads(xrSession, frame, model.xrReferenceSpace);
model.xrSceneFrame = model.xrSession.requestAnimationFrame(model.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 );
const ren = model.renderWindow.getRenderable().getRenderers()[0];
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 { 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(); });
gl.scissor(0, 0, glLayer.framebufferWidth, glLayer.framebufferHeight); gl.disable(gl.SCISSOR_TEST); } };
publicAPI.delete = macro.chain(publicAPI.delete); }
function defaultValues() { return { initialized: false, drawControllersRay: false, inputSourceToRay: {}, initCanvasSize: null, initBackground: null, renderWindow: null, xrSession: null, xrSessionType: 0, xrReferenceSpace: null, }; }
export function extend(publicAPI, model, initialValues = {}) { Object.assign(model, defaultValues(), initialValues);
macro.obj(publicAPI, model); macro.event(publicAPI, model, 'event');
macro.get(publicAPI, model, ['xrSession']); macro.setGet(publicAPI, model, ['renderWindow', 'drawControllersRay']);
vtkWebXRRenderWindowHelper(publicAPI, model); }
export const newInstance = macro.newInstance( extend, 'vtkWebXRRenderWindowHelper' );
export default { newInstance, extend, ...Constants, };
|