├── examples ├── textures │ └── cube │ │ ├── Park2Small │ │ ├── negx.jpg │ │ ├── negy.jpg │ │ ├── negz.jpg │ │ ├── posx.jpg │ │ ├── posy.jpg │ │ ├── posz.jpg │ │ └── readme.txt │ │ └── Park3Small │ │ ├── negx.jpg │ │ ├── negy.jpg │ │ ├── negz.jpg │ │ ├── posx.jpg │ │ ├── posy.jpg │ │ ├── posz.jpg │ │ └── readme.txt ├── lib │ └── CSS3DRenderer.js └── vr_basic.html ├── README.md └── js └── DeviceOrientationController.js /examples/textures/cube/Park2Small/negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park2Small/negx.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park2Small/negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park2Small/negy.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park2Small/negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park2Small/negz.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park2Small/posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park2Small/posx.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park2Small/posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park2Small/posy.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park2Small/posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park2Small/posz.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park3Small/negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park3Small/negx.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park3Small/negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park3Small/negy.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park3Small/negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park3Small/negz.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park3Small/posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park3Small/posx.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park3Small/posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park3Small/posy.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park3Small/posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richtr/threeVR/HEAD/examples/textures/cube/Park3Small/posz.jpg -------------------------------------------------------------------------------- /examples/textures/cube/Park2Small/readme.txt: -------------------------------------------------------------------------------- 1 | Author 2 | ====== 3 | 4 | This is the work of Emil Persson, aka Humus. 5 | http://www.humus.name 6 | humus@comhem.se 7 | 8 | 9 | 10 | Legal stuff 11 | =========== 12 | 13 | This work is free and may be used by anyone for any purpose 14 | and may be distributed freely to anyone using any distribution 15 | media or distribution method as long as this file is included. 16 | Distribution without this file is allowed if it's distributed 17 | with free non-commercial software; however, fair credit of the 18 | original author is expected. 19 | Any commercial distribution of this software requires the written 20 | approval of Emil Persson. 21 | -------------------------------------------------------------------------------- /examples/textures/cube/Park3Small/readme.txt: -------------------------------------------------------------------------------- 1 | Author 2 | ====== 3 | 4 | This is the work of Emil Persson, aka Humus. 5 | http://www.humus.name 6 | humus@comhem.se 7 | 8 | 9 | 10 | Legal stuff 11 | =========== 12 | 13 | This work is free and may be used by anyone for any purpose 14 | and may be distributed freely to anyone using any distribution 15 | media or distribution method as long as this file is included. 16 | Distribution without this file is allowed if it's distributed 17 | with free non-commercial software; however, fair credit of the 18 | original author is expected. 19 | Any commercial distribution of this software requires the written 20 | approval of Emil Persson. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | threeVR 2 | ======= 3 | 4 | ####An orientation-aware Virtual Reality controller for [three.js](http://threejs.org) #### 5 | 6 | threeVR is a virtual reality controller that makes it easy to build device-orientation aware applications on top of the three.js library. 7 | 8 | threeVR listens for device orientation event changes and orients a three.js scene in the direction the user is facing. This library also provides manual user override controls so users can drag to look around a scene and pinch to zoom in on scene features. Once user interaction is complete, the threeVR library snaps the scene's camera back to the current device orientation position. threeVR also provides a set of custom event callbacks that web applications can use to build their own compelling user interfaces. 9 | 10 | [Live Demo](http://richtr.github.io/threeVR/examples/vr_basic.html) | [Basic Usage](#basic-usage) | [API](#api) | [Reference Material](#reference-material) | [License](#license) 11 | 12 | ### Basic Usage ### 13 | 14 | Add [three.js](https://github.com/mrdoob/three.js/) and [DeviceOrientationController.js](https://github.com/richtr/threeVR/blob/master/js/DeviceOrientationController.js) to your project: 15 | 16 | 17 | 18 | 19 | Create a new `DeviceOrientationController` object in JavaScript passing in your scene's `camera` (required) and the target `domElement` object (optional, defaults to the `document` object). 20 | 21 | Then call `connect()` to start the controller. 22 | 23 | 27 | 28 | ### API ### 29 | 30 | ##### connect() ##### 31 | 32 | Start the controller and register all required deviceorientation and manual interaction override event listeners 33 | 34 | Example: 35 | 36 | controls.connect(); // start listening for device orientation changes 37 | 38 | ##### disconnect() ##### 39 | 40 | Stop the controller and de-register all required deviceorientation and manual interaction override event listeners 41 | 42 | Example: 43 | 44 | controls.disconnect(); // stop listening for device orientation changes 45 | 46 | ##### addEventListener(type, callback) ##### 47 | 48 | Register an event handler when events fire in the `DeviceOrientationController` object. 49 | 50 | Available event types are: 51 | 52 | * `compassneedscalibration` - when the system compass indicates that it needs calibration 53 | * `orientationchange` - when the screen orientation changes (e.g. the user rotates their screen from portrait to landscape or vice-versa). The current screen orientation can subsequently be read from `controls.screenOrientation`. 54 | - `userinteractionstart` - when the user starts manually overriding deviceorientation controls by interacting with the renderer DOM element. 55 | - `userinteractionend` - when the user ends manually overriding deviceorientation controls by interacting with the renderer DOM element. 56 | - `zoomstart` - when the user manually starts zooming the scene in the renderer DOM element. 57 | - `zoomend` - when the user manually ends zooming the scene in the renderer DOM element. 58 | - `rotatestart` - when the user manually starts rotating the scene in the renderer DOM element. 59 | - `rotateend` - when the user manually ends rotating the scene in the renderer DOM element. 60 | 61 | Example usage: 62 | 63 | controls.addEventListener('userinteractionstart', function() { 64 | controls.element.style.cursor = 'move'; 65 | }); 66 | 67 | controls.addEventListener('userinteractionend', function() { 68 | controls.element.style.cursor = 'normal'; 69 | }); 70 | 71 | ##### removeEventListener(type, callback) ##### 72 | 73 | De-register an event handler previously registered with `addEventListener(type, callback)`. 74 | 75 | ##### freeze ##### 76 | 77 | Prevent device orientation from updating the `camera` position. 78 | 79 | Example: 80 | 81 | controls.freeze = true; // pause deviceorientation affecting camera rotation 82 | 83 | ##### enableManualDrag ##### 84 | 85 | Whether to allow the user to manually override the automatic deviceorientation controls by dragging the scene to rotate the camera manually. 86 | 87 | The camera will automatically snap back to the deviceorientation when the user stops interacting with the scene. 88 | 89 | Default is `true`. 90 | 91 | Example: 92 | 93 | controls.enableManualDrag = false; // disable user manual scene drag-to-rotate override 94 | 95 | ##### enableManualZoom ##### 96 | 97 | Whether to allow the user to manually override the automatic deviceorientation controls by pinching the scene to zoom manually. 98 | 99 | The camera will automatically snap back to the deviceorientation when the user stops interacting with the scene. 100 | 101 | Default is `true`. 102 | 103 | Example: 104 | 105 | controls.enableManualZoom = false; // disable user manual scene pinch-to-zoom override 106 | 107 | ##### useQuaternions ##### 108 | 109 | Whether to use quaternions to calculate the device orientation (`true`) or rotation matrices (`false`). 110 | 111 | Default is `true`. 112 | 113 | Example: 114 | 115 | controls.useQuaternions = false; // use rotation matrix math 116 | 117 | ### Reference Material ### 118 | 119 | * Article: [Practical application and usage of the W3C Device Orientation API](http://dev.opera.com/articles/view/w3c-device-orientation-usage/) 120 | * [W3C Spec](http://w3c.github.io/deviceorientation/spec-source-orientation.html) 121 | 122 | ### License ### 123 | 124 | MIT. Copyright (c) Rich Tibbett 125 | -------------------------------------------------------------------------------- /examples/lib/CSS3DRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs 3 | * @author mrdoob / http://mrdoob.com/ 4 | */ 5 | 6 | THREE.CSS3DObject = function ( element ) { 7 | 8 | THREE.Object3D.call( this ); 9 | 10 | this.element = element; 11 | this.element.style.position = 'absolute'; 12 | 13 | this.addEventListener( 'removed', function ( event ) { 14 | 15 | if ( this.element.parentNode !== null ) { 16 | 17 | this.element.parentNode.removeChild( this.element ); 18 | 19 | for ( var i = 0, l = this.children.length; i < l; i ++ ) { 20 | 21 | this.children[ i ].dispatchEvent( event ); 22 | 23 | } 24 | 25 | } 26 | 27 | } ); 28 | 29 | }; 30 | 31 | THREE.CSS3DObject.prototype = Object.create( THREE.Object3D.prototype ); 32 | 33 | THREE.CSS3DSprite = function ( element ) { 34 | 35 | THREE.CSS3DObject.call( this, element ); 36 | 37 | }; 38 | 39 | THREE.CSS3DSprite.prototype = Object.create( THREE.CSS3DObject.prototype ); 40 | 41 | // 42 | 43 | THREE.CSS3DRenderer = function () { 44 | 45 | console.log( 'THREE.CSS3DRenderer', THREE.REVISION ); 46 | 47 | var _width, _height; 48 | var _widthHalf, _heightHalf; 49 | 50 | var matrix = new THREE.Matrix4(); 51 | 52 | var domElement = document.createElement( 'div' ); 53 | domElement.style.overflow = 'hidden'; 54 | 55 | domElement.style.WebkitTransformStyle = 'preserve-3d'; 56 | domElement.style.MozTransformStyle = 'preserve-3d'; 57 | domElement.style.oTransformStyle = 'preserve-3d'; 58 | domElement.style.transformStyle = 'preserve-3d'; 59 | 60 | this.domElement = domElement; 61 | 62 | var cameraElement = document.createElement( 'div' ); 63 | 64 | cameraElement.style.WebkitTransformStyle = 'preserve-3d'; 65 | cameraElement.style.MozTransformStyle = 'preserve-3d'; 66 | cameraElement.style.oTransformStyle = 'preserve-3d'; 67 | cameraElement.style.transformStyle = 'preserve-3d'; 68 | 69 | domElement.appendChild( cameraElement ); 70 | 71 | this.setClearColor = function () { 72 | 73 | }; 74 | 75 | this.setSize = function ( width, height ) { 76 | 77 | _width = width; 78 | _height = height; 79 | 80 | _widthHalf = _width / 2; 81 | _heightHalf = _height / 2; 82 | 83 | domElement.style.width = width + 'px'; 84 | domElement.style.height = height + 'px'; 85 | 86 | cameraElement.style.width = width + 'px'; 87 | cameraElement.style.height = height + 'px'; 88 | 89 | }; 90 | 91 | var epsilon = function ( value ) { 92 | 93 | return Math.abs( value ) < 0.000001 ? 0 : value; 94 | 95 | }; 96 | 97 | var getCameraCSSMatrix = function ( matrix ) { 98 | 99 | var elements = matrix.elements; 100 | 101 | return 'matrix3d(' + 102 | epsilon( elements[ 0 ] ) + ',' + 103 | epsilon( - elements[ 1 ] ) + ',' + 104 | epsilon( elements[ 2 ] ) + ',' + 105 | epsilon( elements[ 3 ] ) + ',' + 106 | epsilon( elements[ 4 ] ) + ',' + 107 | epsilon( - elements[ 5 ] ) + ',' + 108 | epsilon( elements[ 6 ] ) + ',' + 109 | epsilon( elements[ 7 ] ) + ',' + 110 | epsilon( elements[ 8 ] ) + ',' + 111 | epsilon( - elements[ 9 ] ) + ',' + 112 | epsilon( elements[ 10 ] ) + ',' + 113 | epsilon( elements[ 11 ] ) + ',' + 114 | epsilon( elements[ 12 ] ) + ',' + 115 | epsilon( - elements[ 13 ] ) + ',' + 116 | epsilon( elements[ 14 ] ) + ',' + 117 | epsilon( elements[ 15 ] ) + 118 | ')'; 119 | 120 | }; 121 | 122 | var getObjectCSSMatrix = function ( matrix ) { 123 | 124 | var elements = matrix.elements; 125 | 126 | return 'translate3d(-50%,-50%,0) matrix3d(' + 127 | epsilon( elements[ 0 ] ) + ',' + 128 | epsilon( elements[ 1 ] ) + ',' + 129 | epsilon( elements[ 2 ] ) + ',' + 130 | epsilon( elements[ 3 ] ) + ',' + 131 | epsilon( - elements[ 4 ] ) + ',' + 132 | epsilon( - elements[ 5 ] ) + ',' + 133 | epsilon( - elements[ 6 ] ) + ',' + 134 | epsilon( - elements[ 7 ] ) + ',' + 135 | epsilon( elements[ 8 ] ) + ',' + 136 | epsilon( elements[ 9 ] ) + ',' + 137 | epsilon( elements[ 10 ] ) + ',' + 138 | epsilon( elements[ 11 ] ) + ',' + 139 | epsilon( elements[ 12 ] ) + ',' + 140 | epsilon( elements[ 13 ] ) + ',' + 141 | epsilon( elements[ 14 ] ) + ',' + 142 | epsilon( elements[ 15 ] ) + 143 | ')'; 144 | 145 | }; 146 | 147 | var renderObject = function ( object, camera ) { 148 | 149 | if ( object instanceof THREE.CSS3DObject ) { 150 | 151 | var style; 152 | 153 | if ( object instanceof THREE.CSS3DSprite ) { 154 | 155 | // http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/ 156 | 157 | matrix.copy( camera.matrixWorldInverse ); 158 | matrix.transpose(); 159 | matrix.copyPosition( object.matrixWorld ); 160 | matrix.scale( object.scale ); 161 | 162 | matrix.elements[ 3 ] = 0; 163 | matrix.elements[ 7 ] = 0; 164 | matrix.elements[ 11 ] = 0; 165 | matrix.elements[ 15 ] = 1; 166 | 167 | style = getObjectCSSMatrix( matrix ); 168 | 169 | } else { 170 | 171 | style = getObjectCSSMatrix( object.matrixWorld ); 172 | 173 | } 174 | 175 | var element = object.element; 176 | 177 | element.style.WebkitTransform = style; 178 | element.style.MozTransform = style; 179 | element.style.oTransform = style; 180 | element.style.transform = style; 181 | 182 | if ( element.parentNode !== cameraElement ) { 183 | 184 | cameraElement.appendChild( element ); 185 | 186 | } 187 | 188 | } 189 | 190 | for ( var i = 0, l = object.children.length; i < l; i ++ ) { 191 | 192 | renderObject( object.children[ i ], camera ); 193 | 194 | } 195 | 196 | }; 197 | 198 | this.render = function ( scene, camera ) { 199 | 200 | var fov = 0.5 / Math.tan( THREE.Math.degToRad( camera.fov * 0.5 ) ) * _height; 201 | 202 | domElement.style.WebkitPerspective = fov + "px"; 203 | domElement.style.MozPerspective = fov + "px"; 204 | domElement.style.oPerspective = fov + "px"; 205 | domElement.style.perspective = fov + "px"; 206 | 207 | scene.updateMatrixWorld(); 208 | 209 | if ( camera.parent === undefined ) camera.updateMatrixWorld(); 210 | 211 | camera.matrixWorldInverse.getInverse( camera.matrixWorld ); 212 | 213 | var style = "translate3d(0,0," + fov + "px)" + getCameraCSSMatrix( camera.matrixWorldInverse ) + 214 | " translate3d(" + _widthHalf + "px," + _heightHalf + "px, 0)"; 215 | 216 | cameraElement.style.WebkitTransform = style; 217 | cameraElement.style.MozTransform = style; 218 | cameraElement.style.oTransform = style; 219 | cameraElement.style.transform = style; 220 | 221 | renderObject( scene, camera ); 222 | 223 | }; 224 | 225 | }; 226 | -------------------------------------------------------------------------------- /examples/vr_basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | deviceorientation - quaternion & rotation matrix manipulation - w/ three.js 8 | 9 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 105 | 106 |
107 | DeviceOrientation Control: 108 | Quaternions 109 |
110 | 111 | 300 | 301 | 302 | Fork me on GitHub 303 | 304 | 305 | -------------------------------------------------------------------------------- /js/DeviceOrientationController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ------- 3 | * threeVR (https://github.com/richtr/threeVR) 4 | * ------- 5 | * 6 | * W3C Device Orientation control (http://www.w3.org/TR/orientation-event/) 7 | * with manual user drag (rotate) and pinch (zoom) override handling 8 | * 9 | * Author: Rich Tibbett (http://github.com/richtr) 10 | * License: The MIT License 11 | * 12 | **/ 13 | 14 | var DeviceOrientationController = function ( object, domElement ) { 15 | 16 | this.object = object; 17 | this.element = domElement || document; 18 | 19 | this.freeze = true; 20 | 21 | this.enableManualDrag = true; // enable manual user drag override control by default 22 | this.enableManualZoom = true; // enable manual user zoom override control by default 23 | 24 | this.useQuaternions = true; // use quaternions for orientation calculation by default 25 | 26 | this.deviceOrientation = {}; 27 | this.screenOrientation = window.orientation || 0; 28 | 29 | // Manual rotate override components 30 | var startX = 0, startY = 0, 31 | currentX = 0, currentY = 0, 32 | scrollSpeedX, scrollSpeedY, 33 | tmpQuat = new THREE.Quaternion(); 34 | 35 | // Manual zoom override components 36 | var zoomStart = 1, zoomCurrent = 1, 37 | zoomP1 = new THREE.Vector2(), 38 | zoomP2 = new THREE.Vector2(), 39 | tmpFOV; 40 | 41 | var CONTROLLER_STATE = { 42 | AUTO: 0, 43 | MANUAL_ROTATE: 1, 44 | MANUAL_ZOOM: 2 45 | }; 46 | 47 | var appState = CONTROLLER_STATE.AUTO; 48 | 49 | var CONTROLLER_EVENT = { 50 | CALIBRATE_COMPASS: 'compassneedscalibration', 51 | SCREEN_ORIENTATION: 'orientationchange', 52 | MANUAL_CONTROL: 'userinteraction', // userinteractionstart, userinteractionend 53 | ZOOM_CONTROL: 'zoom', // zoomstart, zoomend 54 | ROTATE_CONTROL: 'rotate', // rotatestart, rotateend 55 | }; 56 | 57 | // Consistent Object Field-Of-View fix components 58 | var startClientHeight = window.innerHeight, 59 | startFOVFrustrumHeight = 2000 * Math.tan( THREE.Math.degToRad( ( this.object.fov || 75 ) / 2 ) ), 60 | relativeFOVFrustrumHeight, relativeVerticalFOV; 61 | 62 | var deviceQuat = new THREE.Quaternion(); 63 | 64 | var fireEvent = function () { 65 | var eventData; 66 | 67 | return function ( name ) { 68 | eventData = arguments || {}; 69 | 70 | eventData.type = name; 71 | eventData.target = this; 72 | 73 | this.dispatchEvent( eventData ); 74 | }.bind( this ); 75 | }.bind( this )(); 76 | 77 | this.constrainObjectFOV = function () { 78 | relativeFOVFrustrumHeight = startFOVFrustrumHeight * ( window.innerHeight / startClientHeight ); 79 | 80 | relativeVerticalFOV = THREE.Math.radToDeg( 2 * Math.atan( relativeFOVFrustrumHeight / 2000 ) ); 81 | 82 | this.object.fov = relativeVerticalFOV; 83 | }.bind( this ); 84 | 85 | this.onDeviceOrientationChange = function ( event ) { 86 | this.deviceOrientation = event; 87 | }.bind( this ); 88 | 89 | this.onScreenOrientationChange = function () { 90 | this.screenOrientation = window.orientation || 0; 91 | 92 | fireEvent( CONTROLLER_EVENT.SCREEN_ORIENTATION ); 93 | }.bind( this ); 94 | 95 | this.onCompassNeedsCalibration = function ( event ) { 96 | event.preventDefault(); 97 | 98 | fireEvent( CONTROLLER_EVENT.CALIBRATE_COMPASS ); 99 | }.bind( this ); 100 | 101 | this.onDocumentMouseDown = function ( event ) { 102 | if ( this.enableManualDrag !== true ) return; 103 | 104 | event.preventDefault(); 105 | 106 | appState = CONTROLLER_STATE.MANUAL_ROTATE; 107 | 108 | this.freeze = true; 109 | 110 | tmpQuat.copy( this.object.quaternion ); 111 | 112 | startX = currentX = event.pageX; 113 | startY = currentY = event.pageY; 114 | 115 | // Set consistent scroll speed based on current viewport width/height 116 | scrollSpeedX = ( 1200 / window.innerWidth ) * 0.2; 117 | scrollSpeedY = ( 800 / window.innerHeight ) * 0.2; 118 | 119 | this.element.addEventListener( 'mousemove', this.onDocumentMouseMove, false ); 120 | this.element.addEventListener( 'mouseup', this.onDocumentMouseUp, false ); 121 | 122 | fireEvent( CONTROLLER_EVENT.MANUAL_CONTROL + 'start' ); 123 | fireEvent( CONTROLLER_EVENT.ROTATE_CONTROL + 'start' ); 124 | }.bind( this ); 125 | 126 | this.onDocumentMouseMove = function ( event ) { 127 | currentX = event.pageX; 128 | currentY = event.pageY; 129 | }.bind( this ); 130 | 131 | this.onDocumentMouseUp = function ( event ) { 132 | this.element.removeEventListener( 'mousemove', this.onDocumentMouseMove, false ); 133 | this.element.removeEventListener( 'mouseup', this.onDocumentMouseUp, false ); 134 | 135 | appState = CONTROLLER_STATE.AUTO; 136 | 137 | this.freeze = false; 138 | 139 | fireEvent( CONTROLLER_EVENT.MANUAL_CONTROL + 'end' ); 140 | fireEvent( CONTROLLER_EVENT.ROTATE_CONTROL + 'end' ); 141 | }.bind( this ); 142 | 143 | this.onDocumentTouchStart = function ( event ) { 144 | event.preventDefault(); 145 | event.stopPropagation(); 146 | 147 | switch ( event.touches.length ) { 148 | case 1: // ROTATE 149 | if ( this.enableManualDrag !== true ) return; 150 | 151 | appState = CONTROLLER_STATE.MANUAL_ROTATE; 152 | 153 | this.freeze = true; 154 | 155 | tmpQuat.copy( this.object.quaternion ); 156 | 157 | startX = currentX = event.touches[ 0 ].pageX; 158 | startY = currentY = event.touches[ 0 ].pageY; 159 | 160 | // Set consistent scroll speed based on current viewport width/height 161 | scrollSpeedX = ( 1200 / window.innerWidth ) * 0.1; 162 | scrollSpeedY = ( 800 / window.innerHeight ) * 0.1; 163 | 164 | this.element.addEventListener( 'touchmove', this.onDocumentTouchMove, false ); 165 | this.element.addEventListener( 'touchend', this.onDocumentTouchEnd, false ); 166 | 167 | fireEvent( CONTROLLER_EVENT.MANUAL_CONTROL + 'start' ); 168 | fireEvent( CONTROLLER_EVENT.ROTATE_CONTROL + 'start' ); 169 | 170 | break; 171 | 172 | case 2: // ZOOM 173 | if ( this.enableManualZoom !== true ) return; 174 | 175 | appState = CONTROLLER_STATE.MANUAL_ZOOM; 176 | 177 | this.freeze = true; 178 | 179 | tmpFOV = this.object.fov; 180 | 181 | zoomP1.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 182 | zoomP2.set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY ); 183 | 184 | zoomStart = zoomCurrent = zoomP1.distanceTo( zoomP2 ); 185 | 186 | this.element.addEventListener( 'touchmove', this.onDocumentTouchMove, false ); 187 | this.element.addEventListener( 'touchend', this.onDocumentTouchEnd, false ); 188 | 189 | fireEvent( CONTROLLER_EVENT.MANUAL_CONTROL + 'start' ); 190 | fireEvent( CONTROLLER_EVENT.ZOOM_CONTROL + 'start' ); 191 | 192 | break; 193 | } 194 | }.bind( this ); 195 | 196 | this.onDocumentTouchMove = function ( event ) { 197 | switch( event.touches.length ) { 198 | case 1: 199 | currentX = event.touches[ 0 ].pageX; 200 | currentY = event.touches[ 0 ].pageY; 201 | break; 202 | 203 | case 2: 204 | zoomP1.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 205 | zoomP2.set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY ); 206 | break; 207 | } 208 | }.bind( this ); 209 | 210 | this.onDocumentTouchEnd = function ( event ) { 211 | this.element.removeEventListener( 'touchmove', this.onDocumentTouchMove, false ); 212 | this.element.removeEventListener( 'touchend', this.onDocumentTouchEnd, false ); 213 | 214 | if ( appState === CONTROLLER_STATE.MANUAL_ROTATE ) { 215 | 216 | appState = CONTROLLER_STATE.AUTO; // reset control state 217 | 218 | this.freeze = false; 219 | 220 | fireEvent( CONTROLLER_EVENT.MANUAL_CONTROL + 'end' ); 221 | fireEvent( CONTROLLER_EVENT.ROTATE_CONTROL + 'end' ); 222 | 223 | } else if ( appState === CONTROLLER_STATE.MANUAL_ZOOM ) { 224 | 225 | this.constrainObjectFOV(); // re-instate original object FOV 226 | 227 | appState = CONTROLLER_STATE.AUTO; // reset control state 228 | 229 | this.freeze = false; 230 | 231 | fireEvent( CONTROLLER_EVENT.MANUAL_CONTROL + 'end' ); 232 | fireEvent( CONTROLLER_EVENT.ZOOM_CONTROL + 'end' ); 233 | 234 | } 235 | }.bind( this ); 236 | 237 | var createQuaternion = function () { 238 | 239 | var finalQuaternion = new THREE.Quaternion(); 240 | 241 | var deviceEuler = new THREE.Euler(); 242 | 243 | var screenTransform = new THREE.Quaternion(); 244 | 245 | var worldTransform = new THREE.Quaternion( - Math.sqrt(0.5), 0, 0, Math.sqrt(0.5) ); // - PI/2 around the x-axis 246 | 247 | var minusHalfAngle = 0; 248 | 249 | return function ( alpha, beta, gamma, screenOrientation ) { 250 | 251 | deviceEuler.set( beta, alpha, - gamma, 'YXZ' ); 252 | 253 | finalQuaternion.setFromEuler( deviceEuler ); 254 | 255 | minusHalfAngle = - screenOrientation / 2; 256 | 257 | screenTransform.set( 0, Math.sin( minusHalfAngle ), 0, Math.cos( minusHalfAngle ) ); 258 | 259 | finalQuaternion.multiply( screenTransform ); 260 | 261 | finalQuaternion.multiply( worldTransform ); 262 | 263 | return finalQuaternion; 264 | 265 | } 266 | 267 | }(); 268 | 269 | var createRotationMatrix = function () { 270 | 271 | var finalMatrix = new THREE.Matrix4(); 272 | 273 | var deviceEuler = new THREE.Euler(); 274 | var screenEuler = new THREE.Euler(); 275 | var worldEuler = new THREE.Euler( - Math.PI / 2, 0, 0, 'YXZ' ); // - PI/2 around the x-axis 276 | 277 | var screenTransform = new THREE.Matrix4(); 278 | 279 | var worldTransform = new THREE.Matrix4(); 280 | worldTransform.makeRotationFromEuler(worldEuler); 281 | 282 | return function (alpha, beta, gamma, screenOrientation) { 283 | 284 | deviceEuler.set( beta, alpha, - gamma, 'YXZ' ); 285 | 286 | finalMatrix.identity(); 287 | 288 | finalMatrix.makeRotationFromEuler( deviceEuler ); 289 | 290 | screenEuler.set( 0, - screenOrientation, 0, 'YXZ' ); 291 | 292 | screenTransform.identity(); 293 | 294 | screenTransform.makeRotationFromEuler( screenEuler ); 295 | 296 | finalMatrix.multiply( screenTransform ); 297 | 298 | finalMatrix.multiply( worldTransform ); 299 | 300 | return finalMatrix; 301 | 302 | } 303 | 304 | }(); 305 | 306 | this.updateManualMove = function () { 307 | 308 | var lat, lon; 309 | var phi, theta; 310 | 311 | var rotation = new THREE.Euler( 0, 0, 0, 'YXZ' ); 312 | 313 | var rotQuat = new THREE.Quaternion(); 314 | var objQuat = new THREE.Quaternion(); 315 | 316 | var tmpZ, objZ, realZ; 317 | 318 | var zoomFactor, minZoomFactor = 1; // maxZoomFactor = Infinity 319 | 320 | return function () { 321 | 322 | objQuat.copy( tmpQuat ); 323 | 324 | if ( appState === CONTROLLER_STATE.MANUAL_ROTATE ) { 325 | 326 | lat = ( startY - currentY ) * scrollSpeedY; 327 | lon = ( startX - currentX ) * scrollSpeedX; 328 | 329 | phi = THREE.Math.degToRad( lat ); 330 | theta = THREE.Math.degToRad( lon ); 331 | 332 | rotQuat.set( 0, Math.sin( theta / 2 ), 0, Math.cos( theta / 2 ) ); 333 | 334 | objQuat.multiply( rotQuat ); 335 | 336 | rotQuat.set( Math.sin( phi / 2 ), 0, 0, Math.cos( phi / 2 ) ); 337 | 338 | objQuat.multiply( rotQuat ); 339 | 340 | // Remove introduced z-axis rotation and add device's current z-axis rotation 341 | 342 | tmpZ = rotation.setFromQuaternion( tmpQuat, 'YXZ' ).z; 343 | objZ = rotation.setFromQuaternion( objQuat, 'YXZ' ).z; 344 | realZ = rotation.setFromQuaternion( deviceQuat || tmpQuat, 'YXZ' ).z; 345 | 346 | rotQuat.set( 0, 0, Math.sin( ( realZ - tmpZ ) / 2 ), Math.cos( ( realZ - tmpZ ) / 2 ) ); 347 | 348 | tmpQuat.multiply( rotQuat ); 349 | 350 | rotQuat.set( 0, 0, Math.sin( ( realZ - objZ ) / 2 ), Math.cos( ( realZ - objZ ) / 2 ) ); 351 | 352 | objQuat.multiply( rotQuat ); 353 | 354 | this.object.quaternion.copy( objQuat ); 355 | 356 | } else if ( appState === CONTROLLER_STATE.MANUAL_ZOOM ) { 357 | 358 | zoomCurrent = zoomP1.distanceTo( zoomP2 ); 359 | 360 | zoomFactor = zoomStart / zoomCurrent; 361 | 362 | if ( zoomFactor <= minZoomFactor ) { 363 | 364 | this.object.fov = tmpFOV * zoomFactor; 365 | 366 | this.object.updateProjectionMatrix(); 367 | 368 | } 369 | 370 | // Add device's current z-axis rotation 371 | 372 | if ( deviceQuat ) { 373 | 374 | tmpZ = rotation.setFromQuaternion( tmpQuat, 'YXZ' ).z; 375 | realZ = rotation.setFromQuaternion( deviceQuat, 'YXZ' ).z; 376 | 377 | rotQuat.set( 0, 0, Math.sin( ( realZ - tmpZ ) / 2 ), Math.cos( ( realZ - tmpZ ) / 2 ) ); 378 | 379 | tmpQuat.multiply( rotQuat ); 380 | 381 | this.object.quaternion.copy( tmpQuat ); 382 | 383 | } 384 | 385 | } 386 | 387 | }; 388 | 389 | }(); 390 | 391 | this.updateDeviceMove = function () { 392 | 393 | var alpha, beta, gamma, orient; 394 | 395 | var deviceMatrix; 396 | 397 | return function () { 398 | 399 | alpha = THREE.Math.degToRad( this.deviceOrientation.alpha || 0 ); // Z 400 | beta = THREE.Math.degToRad( this.deviceOrientation.beta || 0 ); // X' 401 | gamma = THREE.Math.degToRad( this.deviceOrientation.gamma || 0 ); // Y'' 402 | orient = THREE.Math.degToRad( this.screenOrientation || 0 ); // O 403 | 404 | // only process non-zero 3-axis data 405 | if ( alpha !== 0 && beta !== 0 && gamma !== 0) { 406 | 407 | if ( this.useQuaternions ) { 408 | 409 | deviceQuat = createQuaternion( alpha, beta, gamma, orient ); 410 | 411 | } else { 412 | 413 | deviceMatrix = createRotationMatrix( alpha, beta, gamma, orient ); 414 | 415 | deviceQuat.setFromRotationMatrix( deviceMatrix ); 416 | 417 | } 418 | 419 | if ( this.freeze ) return; 420 | 421 | //this.object.quaternion.slerp( deviceQuat, 0.07 ); // smoothing 422 | this.object.quaternion.copy( deviceQuat ); 423 | 424 | } 425 | 426 | }; 427 | 428 | }(); 429 | 430 | this.update = function () { 431 | this.updateDeviceMove(); 432 | 433 | if ( appState !== CONTROLLER_STATE.AUTO ) { 434 | this.updateManualMove(); 435 | } 436 | }; 437 | 438 | this.connect = function () { 439 | window.addEventListener( 'resize', this.constrainObjectFOV, false ); 440 | 441 | window.addEventListener( 'orientationchange', this.onScreenOrientationChange, false ); 442 | window.addEventListener( 'deviceorientation', this.onDeviceOrientationChange, false ); 443 | 444 | window.addEventListener( 'compassneedscalibration', this.onCompassNeedsCalibration, false ); 445 | 446 | this.element.addEventListener( 'mousedown', this.onDocumentMouseDown, false ); 447 | this.element.addEventListener( 'touchstart', this.onDocumentTouchStart, false ); 448 | 449 | this.freeze = false; 450 | }; 451 | 452 | this.disconnect = function () { 453 | this.freeze = true; 454 | 455 | window.removeEventListener( 'resize', this.constrainObjectFOV, false ); 456 | 457 | window.removeEventListener( 'orientationchange', this.onScreenOrientationChange, false ); 458 | window.removeEventListener( 'deviceorientation', this.onDeviceOrientationChange, false ); 459 | 460 | window.removeEventListener( 'compassneedscalibration', this.onCompassNeedsCalibration, false ); 461 | 462 | this.element.removeEventListener( 'mousedown', this.onDocumentMouseDown, false ); 463 | this.element.removeEventListener( 'touchstart', this.onDocumentTouchStart, false ); 464 | }; 465 | 466 | }; 467 | 468 | DeviceOrientationController.prototype = Object.create( THREE.EventDispatcher.prototype ); 469 | --------------------------------------------------------------------------------