@kitware/vtk.js
Version:
Visualization Toolkit for the Web
308 lines (282 loc) • 12.8 kB
JavaScript
import { m as macro } from '../../macros2.js';
import Constants from './RenderWindowHelper/Constants.js';
import vtkActor from '../Core/Actor.js';
import vtkLineSource from '../../Filters/Sources/LineSource.js';
import vtkMapper from '../Core/Mapper.js';
import { GET_UNDERLYING_CONTEXT } from '../OpenGL/RenderWindow/ContextProxy.js';
import { vec3 } from 'gl-matrix';
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
};
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
};
}
// ----------------------------------------------------------------------------
// 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();
// 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]());
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(model.xrRender);
publicAPI.modified();
} else {
throw new Error('Failed to enter XR with a null xrSession.');
}
};
publicAPI.resetXRScene = function () {
let rescaleFactor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_RESET_FACTORS.rescaleFactor;
let translateZ = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_RESET_FACTORS.translateZ;
// Adjust world-to-physical parameters for different modalities
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);
// 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);
// Remove controllers ray
const ren = model.renderWindow.getRenderable().getRenderers()[0];
model.xrSession.inputSources.forEach(inputSource => {
if (model.inputSourceToRay[inputSource.handedness]) {
ren.removeActor(model.inputSourceToRay[inputSource.handedness].actor);
model.inputSourceToRay[inputSource.handedness].visible = false;
}
});
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.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);
// 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
// ----------------------------------------------------------------------------
function defaultValues() {
return {
initialized: false,
drawControllersRay: false,
inputSourceToRay: {},
initCanvasSize: null,
initBackground: null,
renderWindow: null,
xrSession: null,
xrSessionType: 0,
xrReferenceSpace: null
};
}
// ----------------------------------------------------------------------------
function extend(publicAPI, model) {
let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
Object.assign(model, defaultValues(), initialValues);
// Build VTK API
macro.obj(publicAPI, model);
macro.event(publicAPI, model, 'event');
macro.get(publicAPI, model, ['xrSession']);
macro.setGet(publicAPI, model, ['renderWindow', 'drawControllersRay']);
// Object methods
vtkWebXRRenderWindowHelper(publicAPI, model);
}
// ----------------------------------------------------------------------------
const newInstance = macro.newInstance(extend, 'vtkWebXRRenderWindowHelper');
// ----------------------------------------------------------------------------
var vtkRenderWindowHelper = {
newInstance,
extend,
...Constants
};
export { vtkRenderWindowHelper as default, extend, newInstance };