diff --git a/.eslintrc.js b/.eslintrc.js index 834df991194..71fdc9a6ae6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -37,6 +37,7 @@ module.exports = { // ], globals: { __BASE_PATH__: false, + VRFrameData: true, }, 'settings': { 'import/resolver': 'webpack' diff --git a/Examples/Geometry/VR/controller.html b/Examples/Geometry/VR/controller.html new file mode 100644 index 00000000000..00e847cb3f8 --- /dev/null +++ b/Examples/Geometry/VR/controller.html @@ -0,0 +1,21 @@ + + + + + + + + + + +
+ +
+ +
+ +
diff --git a/Examples/Geometry/VR/index.js b/Examples/Geometry/VR/index.js new file mode 100644 index 00000000000..bb67b372d72 --- /dev/null +++ b/Examples/Geometry/VR/index.js @@ -0,0 +1,101 @@ +import 'vtk.js/Sources/favicon'; + +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkCalculator from 'vtk.js/Sources/Filters/General/Calculator'; +import vtkConeSource from 'vtk.js/Sources/Filters/Sources/ConeSource'; +import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; +import { AttributeTypes } from 'vtk.js/Sources/Common/DataModel/DataSetAttributes/Constants'; +import { FieldDataTypes } from 'vtk.js/Sources/Common/DataModel/DataSet/Constants'; + +import controlPanel from './controller.html'; + +// ---------------------------------------------------------------------------- +// 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: 100.0, radius: 50.0 }); +// 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(20.0, 0.0, 0.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 VR') { + fullScreenRenderer.getOpenGLRenderWindow().startVR(); + vrbutton.textContent = 'Return From VR'; + } else { + fullScreenRenderer.getOpenGLRenderWindow().stopVR(); + vrbutton.textContent = 'Send To VR'; + } +}); + +// ----------------------------------------------------------- +// 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/Sources/Interaction/Style/InteractorStyleTrackballCamera/index.js b/Sources/Interaction/Style/InteractorStyleTrackballCamera/index.js index a0a039dee07..95f44194695 100644 --- a/Sources/Interaction/Style/InteractorStyleTrackballCamera/index.js +++ b/Sources/Interaction/Style/InteractorStyleTrackballCamera/index.js @@ -1,7 +1,9 @@ import macro from 'vtk.js/Sources/macro'; import vtkInteractorStyle from 'vtk.js/Sources/Rendering/Core/InteractorStyle'; import vtkMath from 'vtk.js/Sources/Common/Core/Math'; -import { States } from 'vtk.js/Sources/Rendering/Core/InteractorStyle/Constants'; +import { Device, Input } from 'vtk.js/Sources/Rendering/Core/RenderWindowInteractor/Constants'; + +const { States } = vtkInteractorStyle; /* eslint-disable no-lonely-if */ @@ -47,6 +49,61 @@ function vtkInteractorStyleTrackballCamera(publicAPI, model) { } }; + publicAPI.handleButton3D = (arg) => { + const ed = arg.calldata; + publicAPI.findPokedRenderer(0, 0); + if (model.currentRenderer === null) { + return; + } + + if (ed && ed.pressed && + ed.device === Device.RightController && + ed.input === Input.TrackPad) { + publicAPI.startCameraPose(); + publicAPI.setAnimationStateOn(); + return; + } + if (ed && !ed.pressed && + ed.device === Device.RightController && + ed.input === Input.TrackPad && + model.state === States.IS_CAMERA_POSE) { + publicAPI.endCameraPose(); + publicAPI.setAnimationStateOff(); + // return; + } + }; + + publicAPI.handleMove3D = (arg) => { + const ed = arg.calldata; + switch (model.state) { + case States.IS_CAMERA_POSE: + publicAPI.updateCameraPose(ed); + break; + default: + } + }; + + publicAPI.updateCameraPose = (ed) => { + // move the world in the direction of the + // controller + const camera = model.currentRenderer.getActiveCamera(); + const oldTrans = camera.getPhysicalTranslation(); + + // look at the y axis to determine how fast / what direction to move + const speed = ed.gamepad.axes[1]; + + // 0.05 meters / frame movement + const pscale = speed * 0.05 / camera.getPhysicalScale(); + + // convert orientation to world coordinate direction + const dir = camera.physicalOrientationToWorldDirection(ed.orientation); + + camera.setPhysicalTranslation( + oldTrans[0] + (dir[0] * pscale), + oldTrans[1] + (dir[1] * pscale), + oldTrans[2] + (dir[2] * pscale)); + }; + //---------------------------------------------------------------------------- publicAPI.handleLeftButtonPress = () => { const pos = model.interactor.getEventPosition(model.interactor.getPointerIndex()); diff --git a/Sources/Rendering/Core/Camera/index.js b/Sources/Rendering/Core/Camera/index.js index 83313566d1c..4dc38f5c9ee 100644 --- a/Sources/Rendering/Core/Camera/index.js +++ b/Sources/Rendering/Core/Camera/index.js @@ -28,6 +28,13 @@ function vtkCamera(publicAPI, model) { // Set up private variables and methods const viewMatrix = mat4.create(); const projectionMatrix = mat4.create(); + const w2pMatrix = mat4.create(); + const origin = vec3.create(); + const dopbasis = vec3.fromValues(0.0, 0.0, -1.0); + const upbasis = vec3.fromValues(0.0, 1.0, 0.0); + const tmpvec1 = vec3.create(); + const tmpvec2 = vec3.create(); + const tmpvec3 = vec3.create(); publicAPI.orthogonalizeViewUp = () => { const vt = publicAPI.getViewTransformMatrix(); @@ -125,7 +132,7 @@ function vtkCamera(publicAPI, model) { publicAPI.computeViewPlaneNormal(); }; -//---------------------------------------------------------------------------- + //---------------------------------------------------------------------------- publicAPI.computeViewPlaneNormal = () => { // VPN is -DOP model.viewPlaneNormal[0] = -model.directionOfProjection[0]; @@ -279,6 +286,83 @@ function vtkCamera(publicAPI, model) { }; + publicAPI.physicalOrientationToWorldDirection = (ori) => { + // get the PhysicalToWorldMatrix + publicAPI.getPhysicalToWorldMatrix(w2pMatrix); + + // push the x axis through the orientation quat + const oriq = quat.fromValues(ori[0], ori[1], ori[2], ori[3]); + const coriq = quat.create(); + const qdir = quat.fromValues(0.0, 0.0, 1.0, 0.0); + quat.conjugate(coriq, oriq); + + // rotate the z axis by the quat + quat.multiply(qdir, oriq, qdir); + quat.multiply(qdir, qdir, coriq); + + // return the z axis in world coords + return [qdir[0], qdir[1], qdir[2]]; + }; + + publicAPI.getPhysicalToWorldMatrix = (result) => { + publicAPI.getWorldToPhysicalMatrix(result); + mat4.invert(result, result); + }; + + publicAPI.getWorldToPhysicalMatrix = (result) => { + mat4.identity(w2pMatrix); + vec3.set(tmpvec1, + model.physicalScale, model.physicalScale, model.physicalScale); + mat4.scale(w2pMatrix, w2pMatrix, tmpvec1); + mat4.translate(w2pMatrix, w2pMatrix, model.physicalTranslation); + + // now the physical to vtk world rotation tform + const physVRight = [3]; + vtkMath.cross(model.physicalViewNorth, model.physicalViewUp, physVRight); + const phystoworld = mat4.create(); + phystoworld[0] = physVRight[0]; + phystoworld[1] = physVRight[1]; + phystoworld[2] = physVRight[2]; + phystoworld[4] = model.physicalViewUp[0]; + phystoworld[5] = model.physicalViewUp[1]; + phystoworld[6] = model.physicalViewUp[2]; + phystoworld[8] = -model.physicalViewNorth[0]; + phystoworld[9] = -model.physicalViewNorth[1]; + phystoworld[10] = -model.physicalViewNorth[2]; + mat4.transpose(phystoworld, phystoworld); + mat4.multiply(result, w2pMatrix, phystoworld); + }; + + // the provided matrix should include + // translation and orientation only + publicAPI.computeViewParametersFromPhysicalMatrix = (mat) => { + // get the WorldToPhysicalMatrix + publicAPI.getWorldToPhysicalMatrix(w2pMatrix); + + // first convert the physical -> hmd matrix to be world -> hmd + mat4.multiply(viewMatrix, mat, w2pMatrix); + // invert to get hmd -> world + mat4.invert(viewMatrix, viewMatrix); + + // then extract the params position, orientation + // push 0,0,0 through to get a translation + vec3.transformMat4(tmpvec1, origin, viewMatrix); + publicAPI.computeDistance(); + const oldDist = model.distance; + publicAPI.setPosition(tmpvec1[0], tmpvec1[1], tmpvec1[2]); + + // push basis vectors to get orientation + vec3.transformMat4(tmpvec2, dopbasis, viewMatrix); + vec3.subtract(tmpvec2, tmpvec2, tmpvec1); + vec3.normalize(tmpvec2, tmpvec2); + publicAPI.setDirectionOfProjection(tmpvec2[0], tmpvec2[1], tmpvec2[2]); + vec3.transformMat4(tmpvec3, upbasis, viewMatrix); + vec3.subtract(tmpvec3, tmpvec3, tmpvec1); + publicAPI.setViewUp(tmpvec3[0], tmpvec3[1], tmpvec3[2]); + + publicAPI.setDistance(oldDist); + }; + publicAPI.getViewTransformMatrix = () => { const eye = model.position; const at = model.focalPoint; @@ -291,11 +375,29 @@ function vtkCamera(publicAPI, model) { vec3.fromValues(up[0], up[1], up[2])); // up mat4.transpose(viewMatrix, viewMatrix); + mat4.copy(result, viewMatrix); return result; }; + publicAPI.setProjectionMatrix = (mat) => { + model.projectionMatrix = mat; + }; + publicAPI.getProjectionTransformMatrix = (aspect, nearz, farz) => { + const result = mat4.create(); + + if (model.projectionMatrix) { + vec3.set(tmpvec1, + model.physicalScale, model.physicalScale, model.physicalScale); + + + mat4.copy(result, model.projectionMatrix); + mat4.scale(result, result, tmpvec1); + mat4.transpose(result, result); + return result; + } + mat4.identity(projectionMatrix); // FIXME: Not sure what to do about adjust z buffer here @@ -349,7 +451,6 @@ function vtkCamera(publicAPI, model) { projectionMatrix[15] = 0.0; } - const result = mat4.create(); mat4.copy(result, projectionMatrix); return result; @@ -502,17 +603,19 @@ export const DEFAULT_VALUES = { thickness: 1000, windowCenter: [0, 0], viewPlaneNormal: [0, 0, 1], - focalDisk: 1, useOffAxisProjection: false, screenBottomLeft: [-0.5, -0.5, -0.5], screenBottomRight: [0.5, -0.5, -0.5], screenTopRight: [0.5, 0.5, -0.5], - userViewTransform: null, - userTransform: null, freezeFocalPoint: false, useScissor: false, - physicalViewUp: [0.0, 1.0, 0.0], - physicalViewNorth: [0.0, 0.0, -1.0], + projectionMatrix: null, + + // used for world to physical transformations + physicalTranslation: [0, 0, 0], + physicalScale: 1.0, + physicalViewUp: [0, 1, 0], + physicalViewNorth: [0, 0, -1], }; // ---------------------------------------------------------------------------- @@ -523,11 +626,10 @@ export function extend(publicAPI, model, initialValues = {}) { // Build VTK API macro.obj(publicAPI, model); + model.viewMatrix = macro.get(publicAPI, model, [ 'distance', 'thickness', - 'userViewTransform', - 'userTransform', ]); macro.setGet(publicAPI, model, [ @@ -535,10 +637,10 @@ export function extend(publicAPI, model, initialValues = {}) { 'useHorizontalViewAngle', 'viewAngle', 'parallelScale', - 'focalDisk', 'useOffAxisProjection', 'freezeFocalPoint', 'useScissor', + 'physicalScale', ]); macro.getArray(publicAPI, model, [ @@ -558,6 +660,7 @@ export function extend(publicAPI, model, initialValues = {}) { 'screenBottomLeft', 'screenBottomRight', 'screenTopRight', + 'physicalTranslation', 'physicalViewUp', 'physicalViewNorth', ], 3); diff --git a/Sources/Rendering/Core/InteractorStyle/Constants.js b/Sources/Rendering/Core/InteractorStyle/Constants.js index 45bb1c53696..55aa71f9dbb 100644 --- a/Sources/Rendering/Core/InteractorStyle/Constants.js +++ b/Sources/Rendering/Core/InteractorStyle/Constants.js @@ -12,6 +12,7 @@ export const States = { IS_FORWARDFLY: 8, IS_REVERSEFLY: 9, IS_TWO_POINTER: 10, + IS_CAMERA_POSE: 11, IS_ANIM_OFF: 0, IS_ANIM_ON: 1, diff --git a/Sources/Rendering/Core/InteractorStyle/index.js b/Sources/Rendering/Core/InteractorStyle/index.js index 19dc947b6be..324b0fd287a 100644 --- a/Sources/Rendering/Core/InteractorStyle/index.js +++ b/Sources/Rendering/Core/InteractorStyle/index.js @@ -21,6 +21,7 @@ const stateNames = { Timer: States.IS_TIMER, TwoPointer: States.IS_TWO_POINTER, UniformScale: States.IS_USCALE, + CameraPose: States.IS_CAMERA_POSE, }; const events = [ @@ -49,6 +50,8 @@ const events = [ 'Tap', 'LongTap', 'Swipe', + 'Button3D', + 'Move3D', ]; // ---------------------------------------------------------------------------- @@ -77,9 +80,9 @@ function vtkInteractorStyle(publicAPI, model) { if (i) { events.forEach((eventName) => { model.unsubscribes.push( - i[`on${eventName}`](() => { + i[`on${eventName}`]((data) => { if (publicAPI[`handle${eventName}`]) { - publicAPI[`handle${eventName}`](); + publicAPI[`handle${eventName}`](data); } })); }); diff --git a/Sources/Rendering/Core/RenderWindowInteractor/Constants.js b/Sources/Rendering/Core/RenderWindowInteractor/Constants.js new file mode 100644 index 00000000000..79a4de2fd8a --- /dev/null +++ b/Sources/Rendering/Core/RenderWindowInteractor/Constants.js @@ -0,0 +1,18 @@ +export const Device = { + Unknown: 0, + LeftController: 1, + RightController: 2, +}; + +export const Input = { + Unknown: 0, + Trigger: 1, + TrackPad: 2, + Grip: 3, + ApplicationMenu: 4, +}; + +export default { + Device, + Input, +}; diff --git a/Sources/Rendering/Core/RenderWindowInteractor/index.js b/Sources/Rendering/Core/RenderWindowInteractor/index.js index aa326d1c3d6..d2c51cde643 100644 --- a/Sources/Rendering/Core/RenderWindowInteractor/index.js +++ b/Sources/Rendering/Core/RenderWindowInteractor/index.js @@ -1,13 +1,24 @@ import macro from 'vtk.js/Sources/macro'; import vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkInteractorStyleTrackballCamera from 'vtk.js/Sources/Interaction/Style/InteractorStyleTrackballCamera'; +import Constants from 'vtk.js/Sources/Rendering/Core/RenderWindowInteractor/Constants'; +const { Device, Input } = Constants; const { vtkWarningMacro, vtkErrorMacro } = macro; // ---------------------------------------------------------------------------- // Global methods // ---------------------------------------------------------------------------- +const deviceInputMap = { + 'OpenVR Gamepad': [ + Input.TrackPad, + Input.Trigger, + Input.Grip, + Input.ApplicationMenu, + ], +}; + const eventsWeHandle = [ 'Animation', 'Enter', @@ -40,6 +51,8 @@ const eventsWeHandle = [ 'Tap', 'LongTap', 'Swipe', + 'Button3D', + 'Move3D', ]; function preventDefault(event) { @@ -229,7 +242,7 @@ function vtkRenderWindowInteractor(publicAPI, model) { } }; - publicAPI.isAnimating = () => (model.animationRequest !== null); + publicAPI.isAnimating = () => (model.vrAnimation || model.animationRequest !== null); publicAPI.cancelAnimation = (requestor) => { model.requestAnimationCount -= 1; @@ -241,6 +254,57 @@ function vtkRenderWindowInteractor(publicAPI, model) { } }; + publicAPI.switchToVRAnimation = () => { + // cancel existing animation if any + if (model.animationRequest) { + cancelAnimationFrame(model.animationRequest); + model.animationRequest = null; + } + model.vrAnimation = true; + }; + + publicAPI.returnFromVRAnimation = () => { + model.vrAnimation = false; + }; + + publicAPI.updateGamepads = (displayId) => { + const gamepads = navigator.getGamepads(); + + // watch for when buttons change state and fire events + for (let i = 0; i < gamepads.length; ++i) { + const gp = gamepads[i]; + if (gp && gp.displayId === displayId) { + if (!(gp.index in model.lastGamepadValues)) { + model.lastGamepadValues[gp.index] = { buttons: {} }; + } + for (let b = 0; b < gp.buttons.length; ++b) { + if (!(b in model.lastGamepadValues[gp.index].buttons)) { + model.lastGamepadValues[gp.index].buttons[b] = false; + } + if (model.lastGamepadValues[gp.index].buttons[b] !== gp.buttons[b].pressed) { + publicAPI.button3DEvent({ + gamepad: gp, + position: gp.pose.position, + orientation: gp.pose.orientation, + pressed: gp.buttons[b].pressed, + device: (gp.hand === 'left' ? Device.LeftController : Device.RightController), + input: (deviceInputMap[gp.id] && deviceInputMap[gp.id][b] ? deviceInputMap[gp.id][b] : Input.Trigger), + }); + model.lastGamepadValues[gp.index].buttons[b] = gp.buttons[b].pressed; + } + if (model.lastGamepadValues[gp.index].buttons[b]) { + publicAPI.move3DEvent({ + gamepad: gp, + position: gp.pose.position, + orientation: gp.pose.orientation, + device: (gp.hand === 'left' ? Device.LeftController : Device.RightController), + }); + } + } + } + } + }; + publicAPI.handleMouseMove = (event) => { publicAPI.setEventPosition(event.clientX, model.canvas.clientHeight - event.clientY + 1, 0, 0); // Do not consume event for move @@ -416,9 +480,6 @@ function vtkRenderWindowInteractor(publicAPI, model) { //---------------------------------------------------------------------- publicAPI.forceRender = () => { - // if (model.renderWindow && model.enabled && model.enableRender) { - // model.renderWindow.render(); - // } if (model.view && model.enabled && model.enableRender) { model.view.traverseAllPasses(); } @@ -440,11 +501,11 @@ function vtkRenderWindowInteractor(publicAPI, model) { // create the generic Event methods eventsWeHandle.forEach((eventName) => { const lowerFirst = eventName.charAt(0).toLowerCase() + eventName.slice(1); - publicAPI[`${lowerFirst}Event`] = () => { + publicAPI[`${lowerFirst}Event`] = (arg) => { if (!model.enabled) { return; } - publicAPI[`invoke${eventName}`]({ type: eventName }); + publicAPI[`invoke${eventName}`]({ type: eventName, calldata: arg }); }; }); @@ -747,6 +808,7 @@ const DEFAULT_VALUES = { requestAnimationCount: 0, lastFrameTime: 0.1, wheelTimeoutID: 0, + lastGamepadValues: {}, }; // ---------------------------------------------------------------------------- @@ -819,4 +881,4 @@ export const newInstance = macro.newInstance(extend, 'vtkRenderWindowInteractor' // ---------------------------------------------------------------------------- -export default Object.assign({ newInstance, extend }); +export default Object.assign({ newInstance, extend }, Constants); diff --git a/Sources/Rendering/Core/Renderer/index.js b/Sources/Rendering/Core/Renderer/index.js index d4272a86e8d..4f484f22691 100644 --- a/Sources/Rendering/Core/Renderer/index.js +++ b/Sources/Rendering/Core/Renderer/index.js @@ -45,10 +45,6 @@ function vtkRenderer(publicAPI, model) { vtkDebugMacro('No cameras are on, creating one.'); // the get method will automagically create a camera // and reset it since one hasn't been specified yet. - // If is very unlikely that this can occur - if this - // renderer is part of a vtkRenderWindow, the camera - // will already have been created as part of the - // DoStereoRender() method. publicAPI.getActiveCameraAndResetIfCreated(); } @@ -394,6 +390,10 @@ function vtkRenderer(publicAPI, model) { // setup default parallel scale model.activeCamera.setParallelScale(parallelScale); + // update reasonable world to physical values + model.activeCamera.setPhysicalScale(1.0 / radius); + model.activeCamera.setPhysicalTranslation(-center[0], -center[1], -center[2]); + // Here to let parallel/distributed compositing intercept // and do the right thing. publicAPI.invokeEvent(RESET_CAMERA_EVENT); diff --git a/Sources/Rendering/Misc/FullScreenRenderWindow/index.js b/Sources/Rendering/Misc/FullScreenRenderWindow/index.js index d301c032f24..47276191b4c 100644 --- a/Sources/Rendering/Misc/FullScreenRenderWindow/index.js +++ b/Sources/Rendering/Misc/FullScreenRenderWindow/index.js @@ -62,13 +62,13 @@ function vtkFullScreenRenderWindow(publicAPI, model) { model.renderWindow.addRenderer(model.renderer); // OpenGlRenderWindow - model.openGlRenderWindow = vtkOpenGLRenderWindow.newInstance(); - model.openGlRenderWindow.setContainer(model.container); - model.renderWindow.addView(model.openGlRenderWindow); + model.openGLRenderWindow = vtkOpenGLRenderWindow.newInstance(); + model.openGLRenderWindow.setContainer(model.container); + model.renderWindow.addView(model.openGLRenderWindow); // Interactor model.interactor = vtkRenderWindowInteractor.newInstance(); - model.interactor.setView(model.openGlRenderWindow); + model.interactor.setView(model.openGLRenderWindow); model.interactor.initialize(); model.interactor.bindEvents(model.container); @@ -115,7 +115,7 @@ function vtkFullScreenRenderWindow(publicAPI, model) { // Handle window resize publicAPI.resize = () => { const dims = model.container.getBoundingClientRect(); - model.openGlRenderWindow.setSize(Math.floor(dims.width), Math.floor(dims.height)); + model.openGLRenderWindow.setSize(Math.floor(dims.width), Math.floor(dims.height)); if (model.resizeCallback) { model.resizeCallback(dims); } @@ -155,7 +155,7 @@ export function extend(publicAPI, model, initialValues = {}) { macro.get(publicAPI, model, [ 'renderWindow', 'renderer', - 'openGlRenderWindow', + 'openGLRenderWindow', 'interactor', 'container', 'controlContainer', diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index 89aaa579dcc..0debfd18bce 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -201,6 +201,19 @@ function vtkOpenGLRenderWindow(publicAPI, model) { || model.canvas.getContext('experimental-webgl', options); } + // Do we have webvr support + if (navigator.getVRDisplays) { + navigator.getVRDisplays().then((displays) => { + if (displays.length > 0) { + // take the first display for now + model.vrDisplay = displays[0]; + // set the clipping ranges + model.vrDisplay.depthNear = 0.01; // meters + model.vrDisplay.depthFar = 100.0; // meters + } + }); + } + // prevent default context lost handler model.canvas.addEventListener('webglcontextlost', (event) => { event.preventDefault(); @@ -212,6 +225,69 @@ function vtkOpenGLRenderWindow(publicAPI, model) { return result; }; + publicAPI.startVR = () => { + if (model.vrDisplay.isConnected) { + model.vrDisplay.requestPresent([{ source: model.canvas }]).then(() => { + model.oldCanvasSize = [model.canvas.width, model.canvas.height]; + + // const leftEye = model.vrDisplay.getEyeParameters('left'); + // const rightEye = model.vrDisplay.getEyeParameters('right'); + // model.canvas.width = Math.max(leftEye.renderWidth, rightEye.renderWidth) * 2; + // model.canvas.height = Math.max(leftEye.renderHeight, rightEye.renderHeight); + const ren = model.renderable.getRenderers()[0]; + ren.resetCamera(); + model.vrFrameData = new VRFrameData(); + model.renderable.getInteractor().switchToVRAnimation(); + + publicAPI.vrRender(); + }); + } else { + vtkErrorMacro('vrDisplay is not connected'); + } + }; + + publicAPI.stopVR = () => { + model.renderable.getInteractor().returnFromVRAnimation(); + model.vrDisplay.exitPresent(); + model.vrDisplay.cancelAnimationFrame(model.vrSceneFrame); + + model.canvas.width = model.oldCanvasSize[0]; + model.canvas.height = model.oldCanvasSize[1]; + + + const ren = model.renderable.getRenderers()[0]; + ren.getActiveCamera().setProjectionMatrix(null); + + ren.setViewport(0.0, 0, 1.0, 1.0); + publicAPI.traverseAllPasses(); + }; + + publicAPI.vrRender = () => { + model.renderable.getInteractor().updateGamepads(model.vrDisplay.displayId); + model.vrSceneFrame = model.vrDisplay.requestAnimationFrame(publicAPI.vrRender); + model.vrDisplay.getFrameData(model.vrFrameData); + + // get the first renderer + const ren = model.renderable.getRenderers()[0]; + + // do the left eye + ren.setViewport(0, 0, 0.5, 1.0); + ren.getActiveCamera().computeViewParametersFromPhysicalMatrix( + model.vrFrameData.leftViewMatrix); + ren.getActiveCamera().setProjectionMatrix( + model.vrFrameData.leftProjectionMatrix); + publicAPI.traverseAllPasses(); + + ren.setViewport(0.5, 0, 1.0, 1.0); + ren.getActiveCamera().computeViewParametersFromPhysicalMatrix( + model.vrFrameData.rightViewMatrix); + ren.getActiveCamera().setProjectionMatrix( + model.vrFrameData.rightProjectionMatrix); + publicAPI.traverseAllPasses(); + + model.vrDisplay.submitFrame(); + }; + publicAPI.restoreContext = () => { const rp = vtkRenderPass.newInstance(); rp.setCurrentOperation('Release');