From 10f0c5b04775e5a213d932fe8f2b212ff9c0a026 Mon Sep 17 00:00:00 2001 From: Ken Martin Date: Mon, 4 Dec 2017 16:01:59 -0500 Subject: [PATCH] feat(Rendering): Add initial support for WebVR Tested on Firefox with the Vive. Supports the notion of WorldToPhysical coordinate systems and uses the gamepad API to poll controller events. Currently the right controller trackpad is used to control movement. ResetCamera will compute reasonable values for PhysicalScale and PhysicalTranslation. Still needs to be tested against DayDream and other VR systems. Support for their gamepads needs to be added. --- .eslintrc.js | 1 + Examples/Geometry/VR/controller.html | 21 +++ Examples/Geometry/VR/index.js | 101 ++++++++++++++ .../InteractorStyleTrackballCamera/index.js | 59 ++++++++- Sources/Rendering/Core/Camera/index.js | 123 ++++++++++++++++-- .../Core/InteractorStyle/Constants.js | 1 + .../Rendering/Core/InteractorStyle/index.js | 7 +- .../Core/RenderWindowInteractor/Constants.js | 18 +++ .../Core/RenderWindowInteractor/index.js | 76 ++++++++++- Sources/Rendering/Core/Renderer/index.js | 8 +- .../Misc/FullScreenRenderWindow/index.js | 12 +- .../Rendering/OpenGL/RenderWindow/index.js | 76 +++++++++++ 12 files changed, 473 insertions(+), 30 deletions(-) create mode 100644 Examples/Geometry/VR/controller.html create mode 100644 Examples/Geometry/VR/index.js create mode 100644 Sources/Rendering/Core/RenderWindowInteractor/Constants.js 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');