├── .gitignore ├── mockups-on-cylinders ├── images │ ├── background.png │ └── mockup.png ├── index.html └── js │ ├── VRControls.js │ ├── VREffect.js │ └── three.min.js ├── readme.md └── threejs-vr-boilerplate ├── index.html └── js ├── VRControls.js ├── VREffect.js └── three.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /mockups-on-cylinders/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/vr-web-examples/96333f53953535d44bb5b34e6f062377d9407a8e/mockups-on-cylinders/images/background.png -------------------------------------------------------------------------------- /mockups-on-cylinders/images/mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/vr-web-examples/96333f53953535d44bb5b34e6f062377d9407a8e/mockups-on-cylinders/images/mockup.png -------------------------------------------------------------------------------- /mockups-on-cylinders/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VR Mockups on Cylinders Boilerplate 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 31 | 32 | 33 | 36 | 37 | 38 | 218 | 219 | -------------------------------------------------------------------------------- /mockups-on-cylinders/js/VRControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author dmarcos / https://github.com/dmarcos 3 | * @author mrdoob / http://mrdoob.com 4 | */ 5 | 6 | THREE.VRControls = function ( object, onError ) { 7 | 8 | var scope = this; 9 | 10 | var vrInputs = []; 11 | 12 | function filterInvalidDevices( devices ) { 13 | 14 | // Exclude Cardboard position sensor if Oculus exists. 15 | 16 | var oculusDevices = devices.filter( function ( device ) { 17 | 18 | return device.deviceName.toLowerCase().indexOf('oculus') !== -1; 19 | 20 | } ); 21 | 22 | if ( oculusDevices.length >= 1 ) { 23 | 24 | return devices.filter( function ( device ) { 25 | 26 | return device.deviceName.toLowerCase().indexOf('cardboard') === -1; 27 | 28 | } ); 29 | 30 | } else { 31 | 32 | return devices; 33 | 34 | } 35 | 36 | } 37 | 38 | function gotVRDevices( devices ) { 39 | 40 | devices = filterInvalidDevices( devices ); 41 | 42 | for ( var i = 0; i < devices.length; i ++ ) { 43 | 44 | if ( devices[ i ] instanceof PositionSensorVRDevice ) { 45 | 46 | vrInputs.push( devices[ i ] ); 47 | 48 | } 49 | 50 | } 51 | 52 | if ( onError ) onError( 'HMD not available' ); 53 | 54 | } 55 | 56 | if ( navigator.getVRDevices ) { 57 | 58 | navigator.getVRDevices().then( gotVRDevices ); 59 | 60 | } 61 | 62 | // the Rift SDK returns the position in meters 63 | // this scale factor allows the user to define how meters 64 | // are converted to scene units. 65 | 66 | this.scale = 1; 67 | 68 | this.update = function () { 69 | 70 | for ( var i = 0; i < vrInputs.length; i ++ ) { 71 | 72 | var vrInput = vrInputs[ i ]; 73 | 74 | var state = vrInput.getState(); 75 | 76 | if ( state.orientation !== null ) { 77 | 78 | object.quaternion.copy( state.orientation ); 79 | 80 | } 81 | 82 | if ( state.position !== null ) { 83 | 84 | object.position.copy( state.position ).multiplyScalar( scope.scale ); 85 | 86 | } 87 | 88 | } 89 | 90 | }; 91 | 92 | this.resetSensor = function () { 93 | 94 | for ( var i = 0; i < vrInputs.length; i ++ ) { 95 | 96 | var vrInput = vrInputs[ i ]; 97 | 98 | if ( vrInput.resetSensor !== undefined ) { 99 | 100 | vrInput.resetSensor(); 101 | 102 | } else if ( vrInput.zeroSensor !== undefined ) { 103 | 104 | vrInput.zeroSensor(); 105 | 106 | } 107 | 108 | } 109 | 110 | }; 111 | 112 | this.zeroSensor = function () { 113 | 114 | THREE.warn( 'THREE.VRControls: .zeroSensor() is now .resetSensor().' ); 115 | this.resetSensor(); 116 | 117 | }; 118 | 119 | }; 120 | -------------------------------------------------------------------------------- /mockups-on-cylinders/js/VREffect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author dmarcos / https://github.com/dmarcos 3 | * @author mrdoob / http://mrdoob.com 4 | * 5 | * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html 6 | * 7 | * Firefox: http://mozvr.com/downloads/ 8 | * Chromium: https://drive.google.com/folderview?id=0BzudLt22BqGRbW9WTHMtOWMzNjQ&usp=sharing#list 9 | * 10 | */ 11 | 12 | THREE.VREffect = function ( renderer, onError ) { 13 | 14 | var vrHMD; 15 | var eyeTranslationL, eyeFOVL; 16 | var eyeTranslationR, eyeFOVR; 17 | 18 | function gotVRDevices( devices ) { 19 | 20 | for ( var i = 0; i < devices.length; i ++ ) { 21 | 22 | if ( devices[ i ] instanceof HMDVRDevice ) { 23 | 24 | vrHMD = devices[ i ]; 25 | 26 | if ( vrHMD.getEyeParameters !== undefined ) { 27 | 28 | var eyeParamsL = vrHMD.getEyeParameters( 'left' ); 29 | var eyeParamsR = vrHMD.getEyeParameters( 'right' ); 30 | 31 | eyeTranslationL = eyeParamsL.eyeTranslation; 32 | eyeTranslationR = eyeParamsR.eyeTranslation; 33 | eyeFOVL = eyeParamsL.recommendedFieldOfView; 34 | eyeFOVR = eyeParamsR.recommendedFieldOfView; 35 | 36 | } else { 37 | 38 | // TODO: This is an older code path and not spec compliant. 39 | // It should be removed at some point in the near future. 40 | eyeTranslationL = vrHMD.getEyeTranslation( 'left' ); 41 | eyeTranslationR = vrHMD.getEyeTranslation( 'right' ); 42 | eyeFOVL = vrHMD.getRecommendedEyeFieldOfView( 'left' ); 43 | eyeFOVR = vrHMD.getRecommendedEyeFieldOfView( 'right' ); 44 | 45 | } 46 | 47 | break; // We keep the first we encounter 48 | 49 | } 50 | 51 | } 52 | 53 | if ( vrHMD === undefined ) { 54 | 55 | if ( onError ) onError( 'HMD not available' ); 56 | 57 | } 58 | 59 | } 60 | 61 | if ( navigator.getVRDevices ) { 62 | 63 | navigator.getVRDevices().then( gotVRDevices ); 64 | 65 | } 66 | 67 | // 68 | 69 | this.scale = 1; 70 | 71 | this.setSize = function( width, height ) { 72 | 73 | renderer.setSize( width, height ); 74 | 75 | }; 76 | 77 | // fullscreen 78 | 79 | var isFullscreen = false; 80 | 81 | var canvas = renderer.domElement; 82 | var fullscreenchange = canvas.mozRequestFullScreen ? 'mozfullscreenchange' : 'webkitfullscreenchange'; 83 | 84 | document.addEventListener( fullscreenchange, function ( event ) { 85 | 86 | isFullscreen = document.mozFullScreenElement || document.webkitFullscreenElement; 87 | 88 | }, false ); 89 | 90 | this.setFullScreen = function ( boolean ) { 91 | 92 | if ( vrHMD === undefined ) return; 93 | if ( isFullscreen === boolean ) return; 94 | 95 | if ( canvas.mozRequestFullScreen ) { 96 | 97 | canvas.mozRequestFullScreen( { vrDisplay: vrHMD } ); 98 | 99 | } else if ( canvas.webkitRequestFullscreen ) { 100 | 101 | canvas.webkitRequestFullscreen( { vrDisplay: vrHMD } ); 102 | 103 | } 104 | 105 | }; 106 | 107 | // render 108 | 109 | var cameraL = new THREE.PerspectiveCamera(); 110 | var cameraR = new THREE.PerspectiveCamera(); 111 | 112 | this.render = function ( scene, camera ) { 113 | 114 | if ( vrHMD ) { 115 | 116 | var sceneL, sceneR; 117 | 118 | if ( scene instanceof Array ) { 119 | 120 | sceneL = scene[ 0 ]; 121 | sceneR = scene[ 1 ]; 122 | 123 | } else { 124 | 125 | sceneL = scene; 126 | sceneR = scene; 127 | 128 | } 129 | 130 | var size = renderer.getSize(); 131 | size.width /= 2; 132 | 133 | renderer.enableScissorTest( true ); 134 | renderer.clear(); 135 | 136 | if ( camera.parent === undefined ) camera.updateMatrixWorld(); 137 | 138 | cameraL.projectionMatrix = fovToProjection( eyeFOVL, true, camera.near, camera.far ); 139 | cameraR.projectionMatrix = fovToProjection( eyeFOVR, true, camera.near, camera.far ); 140 | 141 | camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); 142 | camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); 143 | 144 | cameraL.translateX( eyeTranslationL.x * this.scale ); 145 | cameraR.translateX( eyeTranslationR.x * this.scale ); 146 | 147 | // render left eye 148 | renderer.setViewport( 0, 0, size.width, size.height ); 149 | renderer.setScissor( 0, 0, size.width, size.height ); 150 | renderer.render( sceneL, cameraL ); 151 | 152 | // render right eye 153 | renderer.setViewport( size.width, 0, size.width, size.height ); 154 | renderer.setScissor( size.width, 0, size.width, size.height ); 155 | renderer.render( sceneR, cameraR ); 156 | 157 | renderer.enableScissorTest( false ); 158 | 159 | return; 160 | 161 | } 162 | 163 | // Regular render mode if not HMD 164 | 165 | if ( scene instanceof Array ) scene = scene[ 0 ]; 166 | 167 | renderer.render( scene, camera ); 168 | 169 | }; 170 | 171 | // 172 | 173 | function fovToNDCScaleOffset( fov ) { 174 | 175 | var pxscale = 2.0 / (fov.leftTan + fov.rightTan); 176 | var pxoffset = (fov.leftTan - fov.rightTan) * pxscale * 0.5; 177 | var pyscale = 2.0 / (fov.upTan + fov.downTan); 178 | var pyoffset = (fov.upTan - fov.downTan) * pyscale * 0.5; 179 | return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; 180 | 181 | } 182 | 183 | function fovPortToProjection( fov, rightHanded, zNear, zFar ) { 184 | 185 | rightHanded = rightHanded === undefined ? true : rightHanded; 186 | zNear = zNear === undefined ? 0.01 : zNear; 187 | zFar = zFar === undefined ? 10000.0 : zFar; 188 | 189 | var handednessScale = rightHanded ? -1.0 : 1.0; 190 | 191 | // start with an identity matrix 192 | var mobj = new THREE.Matrix4(); 193 | var m = mobj.elements; 194 | 195 | // and with scale/offset info for normalized device coords 196 | var scaleAndOffset = fovToNDCScaleOffset(fov); 197 | 198 | // X result, map clip edges to [-w,+w] 199 | m[0 * 4 + 0] = scaleAndOffset.scale[0]; 200 | m[0 * 4 + 1] = 0.0; 201 | m[0 * 4 + 2] = scaleAndOffset.offset[0] * handednessScale; 202 | m[0 * 4 + 3] = 0.0; 203 | 204 | // Y result, map clip edges to [-w,+w] 205 | // Y offset is negated because this proj matrix transforms from world coords with Y=up, 206 | // but the NDC scaling has Y=down (thanks D3D?) 207 | m[1 * 4 + 0] = 0.0; 208 | m[1 * 4 + 1] = scaleAndOffset.scale[1]; 209 | m[1 * 4 + 2] = -scaleAndOffset.offset[1] * handednessScale; 210 | m[1 * 4 + 3] = 0.0; 211 | 212 | // Z result (up to the app) 213 | m[2 * 4 + 0] = 0.0; 214 | m[2 * 4 + 1] = 0.0; 215 | m[2 * 4 + 2] = zFar / (zNear - zFar) * -handednessScale; 216 | m[2 * 4 + 3] = (zFar * zNear) / (zNear - zFar); 217 | 218 | // W result (= Z in) 219 | m[3 * 4 + 0] = 0.0; 220 | m[3 * 4 + 1] = 0.0; 221 | m[3 * 4 + 2] = handednessScale; 222 | m[3 * 4 + 3] = 0.0; 223 | 224 | mobj.transpose(); 225 | 226 | return mobj; 227 | } 228 | 229 | function fovToProjection( fov, rightHanded, zNear, zFar ) { 230 | 231 | var DEG2RAD = Math.PI / 180.0; 232 | 233 | var fovPort = { 234 | upTan: Math.tan( fov.upDegrees * DEG2RAD ), 235 | downTan: Math.tan( fov.downDegrees * DEG2RAD ), 236 | leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), 237 | rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) 238 | }; 239 | 240 | return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); 241 | 242 | } 243 | 244 | }; 245 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## VR Web Examples 2 | 3 | A collection of boilerplate web VR scenes from the Mozilla Research VR team. For additional demos, tutorials and resources, visit [mozvr.com](http://mozvr.com) 4 | 5 | * [Basic Three.js VR boilerplate](http://mozvr.github.io/vr-web-examples/threejs-vr-boilerplate/) 6 | * [Mockups on cylinders](http://mozvr.github.io/vr-web-examples/mockups-on-cylinders/) 7 | 8 | To view these examples in virtual reality mode requires a VR-enabled browser and a compatible headset. Currently the Oculus Rift DK1 and DK2 are supported, with support for additional devices coming soon. 9 | 10 | Download experimental builds of Firefox with VR support for Mac and Windows: 11 | 12 | [http://mozvr.com/downloads](http://mozvr.com/downloads) 13 | 14 | If using web VR for the first time, make sure to properly configure your display settings, as described on the mozvr.com downloads page [Read Me](http://mozvr.com/downloads). Not doing so can cause the browser to not display VR mode properly or create high levels of judder. 15 | 16 | ### Controls 17 | 18 | * `Double click` or press `F` to enter full-screen virtual reality mode. 19 | * Press `Z` to zero sensor (sets the forward direction of the scene). 20 | -------------------------------------------------------------------------------- /threejs-vr-boilerplate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Basic Three.js VR boilerplate 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 31 | 32 | 33 | 36 | 37 | 38 | 150 | 151 | -------------------------------------------------------------------------------- /threejs-vr-boilerplate/js/VRControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author dmarcos / https://github.com/dmarcos 3 | * @author mrdoob / http://mrdoob.com 4 | */ 5 | 6 | THREE.VRControls = function ( object, onError ) { 7 | 8 | var scope = this; 9 | 10 | var vrInputs = []; 11 | 12 | function filterInvalidDevices( devices ) { 13 | 14 | // Exclude Cardboard position sensor if Oculus exists. 15 | 16 | var oculusDevices = devices.filter( function ( device ) { 17 | 18 | return device.deviceName.toLowerCase().indexOf('oculus') !== -1; 19 | 20 | } ); 21 | 22 | if ( oculusDevices.length >= 1 ) { 23 | 24 | return devices.filter( function ( device ) { 25 | 26 | return device.deviceName.toLowerCase().indexOf('cardboard') === -1; 27 | 28 | } ); 29 | 30 | } else { 31 | 32 | return devices; 33 | 34 | } 35 | 36 | } 37 | 38 | function gotVRDevices( devices ) { 39 | 40 | devices = filterInvalidDevices( devices ); 41 | 42 | for ( var i = 0; i < devices.length; i ++ ) { 43 | 44 | if ( devices[ i ] instanceof PositionSensorVRDevice ) { 45 | 46 | vrInputs.push( devices[ i ] ); 47 | 48 | } 49 | 50 | } 51 | 52 | if ( onError ) onError( 'HMD not available' ); 53 | 54 | } 55 | 56 | if ( navigator.getVRDevices ) { 57 | 58 | navigator.getVRDevices().then( gotVRDevices ); 59 | 60 | } 61 | 62 | // the Rift SDK returns the position in meters 63 | // this scale factor allows the user to define how meters 64 | // are converted to scene units. 65 | 66 | this.scale = 1; 67 | 68 | this.update = function () { 69 | 70 | for ( var i = 0; i < vrInputs.length; i ++ ) { 71 | 72 | var vrInput = vrInputs[ i ]; 73 | 74 | var state = vrInput.getState(); 75 | 76 | if ( state.orientation !== null ) { 77 | 78 | object.quaternion.copy( state.orientation ); 79 | 80 | } 81 | 82 | if ( state.position !== null ) { 83 | 84 | object.position.copy( state.position ).multiplyScalar( scope.scale ); 85 | 86 | } 87 | 88 | } 89 | 90 | }; 91 | 92 | this.resetSensor = function () { 93 | 94 | for ( var i = 0; i < vrInputs.length; i ++ ) { 95 | 96 | var vrInput = vrInputs[ i ]; 97 | 98 | if ( vrInput.resetSensor !== undefined ) { 99 | 100 | vrInput.resetSensor(); 101 | 102 | } else if ( vrInput.zeroSensor !== undefined ) { 103 | 104 | vrInput.zeroSensor(); 105 | 106 | } 107 | 108 | } 109 | 110 | }; 111 | 112 | this.zeroSensor = function () { 113 | 114 | THREE.warn( 'THREE.VRControls: .zeroSensor() is now .resetSensor().' ); 115 | this.resetSensor(); 116 | 117 | }; 118 | 119 | }; 120 | -------------------------------------------------------------------------------- /threejs-vr-boilerplate/js/VREffect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author dmarcos / https://github.com/dmarcos 3 | * @author mrdoob / http://mrdoob.com 4 | * 5 | * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html 6 | * 7 | * Firefox: http://mozvr.com/downloads/ 8 | * Chromium: https://drive.google.com/folderview?id=0BzudLt22BqGRbW9WTHMtOWMzNjQ&usp=sharing#list 9 | * 10 | */ 11 | 12 | THREE.VREffect = function ( renderer, onError ) { 13 | 14 | var vrHMD; 15 | var eyeTranslationL, eyeFOVL; 16 | var eyeTranslationR, eyeFOVR; 17 | 18 | function gotVRDevices( devices ) { 19 | 20 | for ( var i = 0; i < devices.length; i ++ ) { 21 | 22 | if ( devices[ i ] instanceof HMDVRDevice ) { 23 | 24 | vrHMD = devices[ i ]; 25 | 26 | if ( vrHMD.getEyeParameters !== undefined ) { 27 | 28 | var eyeParamsL = vrHMD.getEyeParameters( 'left' ); 29 | var eyeParamsR = vrHMD.getEyeParameters( 'right' ); 30 | 31 | eyeTranslationL = eyeParamsL.eyeTranslation; 32 | eyeTranslationR = eyeParamsR.eyeTranslation; 33 | eyeFOVL = eyeParamsL.recommendedFieldOfView; 34 | eyeFOVR = eyeParamsR.recommendedFieldOfView; 35 | 36 | } else { 37 | 38 | // TODO: This is an older code path and not spec compliant. 39 | // It should be removed at some point in the near future. 40 | eyeTranslationL = vrHMD.getEyeTranslation( 'left' ); 41 | eyeTranslationR = vrHMD.getEyeTranslation( 'right' ); 42 | eyeFOVL = vrHMD.getRecommendedEyeFieldOfView( 'left' ); 43 | eyeFOVR = vrHMD.getRecommendedEyeFieldOfView( 'right' ); 44 | 45 | } 46 | 47 | break; // We keep the first we encounter 48 | 49 | } 50 | 51 | } 52 | 53 | if ( vrHMD === undefined ) { 54 | 55 | if ( onError ) onError( 'HMD not available' ); 56 | 57 | } 58 | 59 | } 60 | 61 | if ( navigator.getVRDevices ) { 62 | 63 | navigator.getVRDevices().then( gotVRDevices ); 64 | 65 | } 66 | 67 | // 68 | 69 | this.scale = 1; 70 | 71 | this.setSize = function( width, height ) { 72 | 73 | renderer.setSize( width, height ); 74 | 75 | }; 76 | 77 | // fullscreen 78 | 79 | var isFullscreen = false; 80 | 81 | var canvas = renderer.domElement; 82 | var fullscreenchange = canvas.mozRequestFullScreen ? 'mozfullscreenchange' : 'webkitfullscreenchange'; 83 | 84 | document.addEventListener( fullscreenchange, function ( event ) { 85 | 86 | isFullscreen = document.mozFullScreenElement || document.webkitFullscreenElement; 87 | 88 | }, false ); 89 | 90 | this.setFullScreen = function ( boolean ) { 91 | 92 | if ( vrHMD === undefined ) return; 93 | if ( isFullscreen === boolean ) return; 94 | 95 | if ( canvas.mozRequestFullScreen ) { 96 | 97 | canvas.mozRequestFullScreen( { vrDisplay: vrHMD } ); 98 | 99 | } else if ( canvas.webkitRequestFullscreen ) { 100 | 101 | canvas.webkitRequestFullscreen( { vrDisplay: vrHMD } ); 102 | 103 | } 104 | 105 | }; 106 | 107 | // render 108 | 109 | var cameraL = new THREE.PerspectiveCamera(); 110 | var cameraR = new THREE.PerspectiveCamera(); 111 | 112 | this.render = function ( scene, camera ) { 113 | 114 | if ( vrHMD ) { 115 | 116 | var sceneL, sceneR; 117 | 118 | if ( scene instanceof Array ) { 119 | 120 | sceneL = scene[ 0 ]; 121 | sceneR = scene[ 1 ]; 122 | 123 | } else { 124 | 125 | sceneL = scene; 126 | sceneR = scene; 127 | 128 | } 129 | 130 | var size = renderer.getSize(); 131 | size.width /= 2; 132 | 133 | renderer.enableScissorTest( true ); 134 | renderer.clear(); 135 | 136 | if ( camera.parent === undefined ) camera.updateMatrixWorld(); 137 | 138 | cameraL.projectionMatrix = fovToProjection( eyeFOVL, true, camera.near, camera.far ); 139 | cameraR.projectionMatrix = fovToProjection( eyeFOVR, true, camera.near, camera.far ); 140 | 141 | camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); 142 | camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); 143 | 144 | cameraL.translateX( eyeTranslationL.x * this.scale ); 145 | cameraR.translateX( eyeTranslationR.x * this.scale ); 146 | 147 | // render left eye 148 | renderer.setViewport( 0, 0, size.width, size.height ); 149 | renderer.setScissor( 0, 0, size.width, size.height ); 150 | renderer.render( sceneL, cameraL ); 151 | 152 | // render right eye 153 | renderer.setViewport( size.width, 0, size.width, size.height ); 154 | renderer.setScissor( size.width, 0, size.width, size.height ); 155 | renderer.render( sceneR, cameraR ); 156 | 157 | renderer.enableScissorTest( false ); 158 | 159 | return; 160 | 161 | } 162 | 163 | // Regular render mode if not HMD 164 | 165 | if ( scene instanceof Array ) scene = scene[ 0 ]; 166 | 167 | renderer.render( scene, camera ); 168 | 169 | }; 170 | 171 | // 172 | 173 | function fovToNDCScaleOffset( fov ) { 174 | 175 | var pxscale = 2.0 / (fov.leftTan + fov.rightTan); 176 | var pxoffset = (fov.leftTan - fov.rightTan) * pxscale * 0.5; 177 | var pyscale = 2.0 / (fov.upTan + fov.downTan); 178 | var pyoffset = (fov.upTan - fov.downTan) * pyscale * 0.5; 179 | return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; 180 | 181 | } 182 | 183 | function fovPortToProjection( fov, rightHanded, zNear, zFar ) { 184 | 185 | rightHanded = rightHanded === undefined ? true : rightHanded; 186 | zNear = zNear === undefined ? 0.01 : zNear; 187 | zFar = zFar === undefined ? 10000.0 : zFar; 188 | 189 | var handednessScale = rightHanded ? -1.0 : 1.0; 190 | 191 | // start with an identity matrix 192 | var mobj = new THREE.Matrix4(); 193 | var m = mobj.elements; 194 | 195 | // and with scale/offset info for normalized device coords 196 | var scaleAndOffset = fovToNDCScaleOffset(fov); 197 | 198 | // X result, map clip edges to [-w,+w] 199 | m[0 * 4 + 0] = scaleAndOffset.scale[0]; 200 | m[0 * 4 + 1] = 0.0; 201 | m[0 * 4 + 2] = scaleAndOffset.offset[0] * handednessScale; 202 | m[0 * 4 + 3] = 0.0; 203 | 204 | // Y result, map clip edges to [-w,+w] 205 | // Y offset is negated because this proj matrix transforms from world coords with Y=up, 206 | // but the NDC scaling has Y=down (thanks D3D?) 207 | m[1 * 4 + 0] = 0.0; 208 | m[1 * 4 + 1] = scaleAndOffset.scale[1]; 209 | m[1 * 4 + 2] = -scaleAndOffset.offset[1] * handednessScale; 210 | m[1 * 4 + 3] = 0.0; 211 | 212 | // Z result (up to the app) 213 | m[2 * 4 + 0] = 0.0; 214 | m[2 * 4 + 1] = 0.0; 215 | m[2 * 4 + 2] = zFar / (zNear - zFar) * -handednessScale; 216 | m[2 * 4 + 3] = (zFar * zNear) / (zNear - zFar); 217 | 218 | // W result (= Z in) 219 | m[3 * 4 + 0] = 0.0; 220 | m[3 * 4 + 1] = 0.0; 221 | m[3 * 4 + 2] = handednessScale; 222 | m[3 * 4 + 3] = 0.0; 223 | 224 | mobj.transpose(); 225 | 226 | return mobj; 227 | } 228 | 229 | function fovToProjection( fov, rightHanded, zNear, zFar ) { 230 | 231 | var DEG2RAD = Math.PI / 180.0; 232 | 233 | var fovPort = { 234 | upTan: Math.tan( fov.upDegrees * DEG2RAD ), 235 | downTan: Math.tan( fov.downDegrees * DEG2RAD ), 236 | leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), 237 | rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) 238 | }; 239 | 240 | return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); 241 | 242 | } 243 | 244 | }; 245 | --------------------------------------------------------------------------------