/** * @author dmarcos / https://github.com/dmarcos * @author mrdoob / http://mrdoob.com * * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html * * Firefox: http://mozvr.com/downloads/ * Chromium: https://drive.google.com/folderview?id=0BzudLt22BqGRbW9WTHMtOWMzNjQ&usp=sharing#list * */ THREE.VREffect = function ( renderer, onError ) { var isWebVR1 = true; var vrDisplay, vrDisplays; var eyeTranslationL = new THREE.Vector3(); var eyeTranslationR = new THREE.Vector3(); var renderRectL, renderRectR; var eyeFOVL, eyeFOVR; function gotVRDisplays( displays ) { vrDisplays = displays; for ( var i = 0; i < displays.length; i ++ ) { if ( 'VRDisplay' in window && displays[ i ] instanceof VRDisplay ) { vrDisplay = displays[ i ]; isWebVR1 = true; break; // We keep the first we encounter } else if ( 'HMDVRDevice' in window && displays[ i ] instanceof HMDVRDevice ) { vrDisplay = displays[ i ]; isWebVR1 = false; break; // We keep the first we encounter } } if ( vrDisplay === undefined ) { if ( onError ) onError( 'HMD not available' ); } } if ( navigator.getVRDisplays ) { navigator.getVRDisplays().then( gotVRDisplays ); } else if ( navigator.getVRDevices ) { // Deprecated API. navigator.getVRDevices().then( gotVRDisplays ); } // this.isPresenting = false; this.scale = 1; var scope = this; var rendererSize = renderer.getSize(); var rendererPixelRatio = renderer.getPixelRatio(); this.getVRDisplay = function () { return vrDisplay; }; this.getVRDisplays = function () { return vrDisplays; }; this.setSize = function ( width, height ) { rendererSize = { width: width, height: height }; if ( scope.isPresenting ) { var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); renderer.setPixelRatio( 1 ); if ( isWebVR1 ) { renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); } else { renderer.setSize( eyeParamsL.renderRect.width * 2, eyeParamsL.renderRect.height, false ); } } else { renderer.setPixelRatio( rendererPixelRatio ); renderer.setSize( width, height ); } }; // fullscreen var canvas = renderer.domElement; var requestFullscreen; var exitFullscreen; var fullscreenElement; var leftBounds = [ 0.0, 0.0, 0.5, 1.0 ]; var rightBounds = [ 0.5, 0.0, 0.5, 1.0 ]; function onFullscreenChange () { var wasPresenting = scope.isPresenting; scope.isPresenting = vrDisplay !== undefined && ( vrDisplay.isPresenting || ( ! isWebVR1 && document[ fullscreenElement ] instanceof window.HTMLElement ) ); if ( scope.isPresenting ) { var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); var eyeWidth, eyeHeight; if ( isWebVR1 ) { eyeWidth = eyeParamsL.renderWidth; eyeHeight = eyeParamsL.renderHeight; if ( vrDisplay.getLayers ) { var layers = vrDisplay.getLayers(); if (layers.length) { leftBounds = layers[0].leftBounds || [ 0.0, 0.0, 0.5, 1.0 ]; rightBounds = layers[0].rightBounds || [ 0.5, 0.0, 0.5, 1.0 ]; } } } else { eyeWidth = eyeParamsL.renderRect.width; eyeHeight = eyeParamsL.renderRect.height; } if ( !wasPresenting ) { rendererPixelRatio = renderer.getPixelRatio(); rendererSize = renderer.getSize(); renderer.setPixelRatio( 1 ); renderer.setSize( eyeWidth * 2, eyeHeight, false ); } } else if ( wasPresenting ) { renderer.setPixelRatio( rendererPixelRatio ); renderer.setSize( rendererSize.width, rendererSize.height ); } } if ( canvas.requestFullscreen ) { requestFullscreen = 'requestFullscreen'; fullscreenElement = 'fullscreenElement'; exitFullscreen = 'exitFullscreen'; document.addEventListener( 'fullscreenchange', onFullscreenChange, false ); } else if ( canvas.mozRequestFullScreen ) { requestFullscreen = 'mozRequestFullScreen'; fullscreenElement = 'mozFullScreenElement'; exitFullscreen = 'mozCancelFullScreen'; document.addEventListener( 'mozfullscreenchange', onFullscreenChange, false ); } else { requestFullscreen = 'webkitRequestFullscreen'; fullscreenElement = 'webkitFullscreenElement'; exitFullscreen = 'webkitExitFullscreen'; document.addEventListener( 'webkitfullscreenchange', onFullscreenChange, false ); } window.addEventListener( 'vrdisplaypresentchange', onFullscreenChange, false ); this.setFullScreen = function ( boolean ) { return new Promise( function ( resolve, reject ) { if ( vrDisplay === undefined ) { reject( new Error( 'No VR hardware found.' ) ); return; } if ( scope.isPresenting === boolean ) { resolve(); return; } if ( isWebVR1 ) { if ( boolean ) { resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) ); } else { resolve( vrDisplay.exitPresent() ); } } else { if ( canvas[ requestFullscreen ] ) { canvas[ boolean ? requestFullscreen : exitFullscreen ]( { vrDisplay: vrDisplay } ); resolve(); } else { console.error( 'No compatible requestFullscreen method found.' ); reject( new Error( 'No compatible requestFullscreen method found.' ) ); } } } ); }; this.requestPresent = function () { return this.setFullScreen( true ); }; this.exitPresent = function () { return this.setFullScreen( false ); }; this.requestAnimationFrame = function ( f ) { if ( isWebVR1 && vrDisplay !== undefined ) { return vrDisplay.requestAnimationFrame( f ); } else { return window.requestAnimationFrame( f ); } }; this.cancelAnimationFrame = function ( h ) { if ( isWebVR1 && vrDisplay !== undefined ) { vrDisplay.cancelAnimationFrame( h ); } else { window.cancelAnimationFrame( h ); } }; this.submitFrame = function () { if ( isWebVR1 && vrDisplay !== undefined && scope.isPresenting ) { vrDisplay.submitFrame(); } }; this.autoSubmitFrame = true; // render var cameraL = new THREE.PerspectiveCamera(); cameraL.layers.enable( 1 ); var cameraR = new THREE.PerspectiveCamera(); cameraR.layers.enable( 2 ); this.render = function ( scene, camera, renderTarget, forceClear ) { if ( vrDisplay && scope.isPresenting ) { var autoUpdate = scene.autoUpdate; if ( autoUpdate ) { scene.updateMatrixWorld(); scene.autoUpdate = false; } var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); var eyeParamsR = vrDisplay.getEyeParameters( 'right' ); if ( isWebVR1 ) { eyeTranslationL.fromArray( eyeParamsL.offset ); eyeTranslationR.fromArray( eyeParamsR.offset ); eyeFOVL = eyeParamsL.fieldOfView; eyeFOVR = eyeParamsR.fieldOfView; } else { eyeTranslationL.copy( eyeParamsL.eyeTranslation ); eyeTranslationR.copy( eyeParamsR.eyeTranslation ); eyeFOVL = eyeParamsL.recommendedFieldOfView; eyeFOVR = eyeParamsR.recommendedFieldOfView; } if ( Array.isArray( scene ) ) { console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' ); scene = scene[ 0 ]; } // When rendering we don't care what the recommended size is, only what the actual size // of the backbuffer is. var size = renderer.getSize(); renderRectL = { x: Math.round( size.width * leftBounds[ 0 ] ), y: Math.round( size.height * leftBounds[ 1 ] ), width: Math.round( size.width * leftBounds[ 2 ] ), height: Math.round(size.height * leftBounds[ 3 ] ) }; renderRectR = { x: Math.round( size.width * rightBounds[ 0 ] ), y: Math.round( size.height * rightBounds[ 1 ] ), width: Math.round( size.width * rightBounds[ 2 ] ), height: Math.round(size.height * rightBounds[ 3 ] ) }; if (renderTarget) { renderer.setRenderTarget(renderTarget); renderTarget.scissorTest = true; } else { renderer.setScissorTest( true ); } if ( renderer.autoClear || forceClear ) renderer.clear(); if ( camera.parent === null ) camera.updateMatrixWorld(); cameraL.projectionMatrix = fovToProjection( eyeFOVL, true, camera.near, camera.far ); cameraR.projectionMatrix = fovToProjection( eyeFOVR, true, camera.near, camera.far ); camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); var scale = this.scale; cameraL.translateOnAxis( eyeTranslationL, scale ); cameraR.translateOnAxis( eyeTranslationR, scale ); // render left eye if ( renderTarget ) { renderTarget.viewport.set(renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height); renderTarget.scissor.set(renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height); } else { renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); } renderer.render( scene, cameraL, renderTarget, forceClear ); // render right eye if (renderTarget) { renderTarget.viewport.set(renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height); renderTarget.scissor.set(renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height); } else { renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); } renderer.render( scene, cameraR, renderTarget, forceClear ); if (renderTarget) { renderTarget.viewport.set( 0, 0, size.width, size.height ); renderTarget.scissor.set( 0, 0, size.width, size.height ); renderTarget.scissorTest = false; renderer.setRenderTarget( null ); } else { renderer.setScissorTest( false ); } if ( autoUpdate ) { scene.autoUpdate = true; } if ( scope.autoSubmitFrame ) { scope.submitFrame(); } return; } // Regular render mode if not HMD renderer.render( scene, camera, renderTarget, forceClear ); }; // function fovToNDCScaleOffset( fov ) { var pxscale = 2.0 / ( fov.leftTan + fov.rightTan ); var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5; var pyscale = 2.0 / ( fov.upTan + fov.downTan ); var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5; return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; } function fovPortToProjection( fov, rightHanded, zNear, zFar ) { rightHanded = rightHanded === undefined ? true : rightHanded; zNear = zNear === undefined ? 0.01 : zNear; zFar = zFar === undefined ? 10000.0 : zFar; var handednessScale = rightHanded ? - 1.0 : 1.0; // start with an identity matrix var mobj = new THREE.Matrix4(); var m = mobj.elements; // and with scale/offset info for normalized device coords var scaleAndOffset = fovToNDCScaleOffset( fov ); // X result, map clip edges to [-w,+w] m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ]; m[ 0 * 4 + 1 ] = 0.0; m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale; m[ 0 * 4 + 3 ] = 0.0; // Y result, map clip edges to [-w,+w] // Y offset is negated because this proj matrix transforms from world coords with Y=up, // but the NDC scaling has Y=down (thanks D3D?) m[ 1 * 4 + 0 ] = 0.0; m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ]; m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale; m[ 1 * 4 + 3 ] = 0.0; // Z result (up to the app) m[ 2 * 4 + 0 ] = 0.0; m[ 2 * 4 + 1 ] = 0.0; m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale; m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar ); // W result (= Z in) m[ 3 * 4 + 0 ] = 0.0; m[ 3 * 4 + 1 ] = 0.0; m[ 3 * 4 + 2 ] = handednessScale; m[ 3 * 4 + 3 ] = 0.0; mobj.transpose(); return mobj; } function fovToProjection( fov, rightHanded, zNear, zFar ) { var DEG2RAD = Math.PI / 180.0; var fovPort = { upTan: Math.tan( fov.upDegrees * DEG2RAD ), downTan: Math.tan( fov.downDegrees * DEG2RAD ), leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) }; return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); } };