├── .gitignore ├── README.md ├── bower.json ├── examples ├── basic.html ├── depth.html ├── fuse.html ├── gazeable.html ├── groups.html ├── img │ └── interactivepatterns_displayreticle.png ├── js │ └── lib │ │ ├── VRControls.js │ │ ├── VREffect.js │ │ ├── device-info-test.js │ │ ├── three.js │ │ ├── webvr-manager.js │ │ └── webvr-polyfill.js ├── proximity.html └── visibility.html ├── index.html ├── package.json └── reticulum.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile ~/.gitignore_global 6 | 7 | # Ignore bundler config 8 | /.bundle 9 | 10 | # Ignore the build directory 11 | **/build 12 | **/tmp 13 | 14 | # Ignore TDS 15 | **/typings 16 | 17 | # Ignore Sass' cache 18 | /.sass-cache 19 | 20 | # Ignore .DS_store file 21 | .DS_Store 22 | 23 | # Ignore Bower files 24 | /bower_components 25 | 26 | # Ignore Node files 27 | **/node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Reticulum 2 | 3 | > A simple gaze interaction manager for VR with Three.js. [See examples](https://skezo.github.io/Reticulum/) 4 | 5 | ![Material Design](https://skezo.github.io/Reticulum/examples/img/interactivepatterns_displayreticle.png) 6 | 7 | ##Purpose 8 | Reticulum attempts to follow Google's interactive pattern for the [display reticle](http://www.google.com/design/spec-vr/interactive-patterns/display-reticle.html). It creates the illusion of depth by projecting spatially onto targeted objects while maintaining a fixed size so that it is easy to see at all times. 9 | 10 | 11 | ### Features: 12 | - Avoids double vision and depth issues by projecting spatially onto targeted objects 13 | - Gaze and click events for targeted objects `onGazeOver`, `onGazeOut`, `onGazeLong` and `onGazeClick` 14 | - Set different fuze durations for targeted objects 15 | - Built in [fuse support](http://www.google.com/design/spec-vr/interactive-patterns/controls.html#controls-fuse-buttons) 16 | - Display the reticle only when the camera can see a targeted object 17 | - Works in the browser with Three.js (r73) 18 | 19 | 20 | ### 1. Getting Started 21 | 22 | Load Three.js and include the Reticulum.js file. You might also want to use the [Web VR boilerplate](https://github.com/borismus/webvr-boilerplate): 23 | 24 | ```html 25 | 26 | 27 | 28 | ``` 29 | 30 | ### 2. Initiate and set options 31 | 32 | Call the Reticulum initializer function and set your options. Options can be set globally or per targeted object. 33 | 34 | **Note:** You must define the `camera`... it is required. 35 | 36 | ```javascript 37 | Reticulum.init(camera, { 38 | proximity: false, 39 | clickevents: true, 40 | near: null, //near factor of the raycaster (shouldn't be negative and should be smaller than the far property) 41 | far: null, //far factor of the raycaster (shouldn't be negative and should be larger than the near property) 42 | reticle: { 43 | visible: true, 44 | restPoint: 1000, //Defines the reticle's resting point when no object has been targeted 45 | color: 0xcc0000, 46 | innerRadius: 0.0001, 47 | outerRadius: 0.003, 48 | hover: { 49 | color: 0xcc0000, 50 | innerRadius: 0.02, 51 | outerRadius: 0.024, 52 | speed: 5, 53 | vibrate: 50 //Set to 0 or [] to disable 54 | } 55 | }, 56 | fuse: { 57 | visible: true, 58 | duration: 2.5, 59 | color: 0x00fff6, 60 | innerRadius: 0.045, 61 | outerRadius: 0.06, 62 | vibrate: 100, //Set to 0 or [] to disable 63 | clickCancelFuse: false //If users clicks on targeted object fuse is canceled 64 | } 65 | }); 66 | ``` 67 | 68 | ### 3. Define targeted objects and options 69 | 70 | Add the three.js objects you want to be targeted objects. Override global options by setting local ones. 71 | 72 | ```javascript 73 | 74 | Reticulum.add( object, { 75 | clickCancelFuse: true, // Overrides global setting for fuse's clickCancelFuse 76 | reticleHoverColor: 0x00fff6, // Overrides global reticle hover color 77 | fuseVisible: true, // Overrides global fuse visibility 78 | fuseDuration: 1.5, // Overrides global fuse duration 79 | fuseColor: 0xcc0000, // Overrides global fuse color 80 | onGazeOver: function(){ 81 | // do something when user targets object 82 | this.material.emissive.setHex( 0xffcc00 ); 83 | }, 84 | onGazeOut: function(){ 85 | // do something when user moves reticle off targeted object 86 | this.material.emissive.setHex( 0xcc0000 ); 87 | }, 88 | onGazeLong: function(){ 89 | // do something user targetes object for specific time 90 | this.material.emissive.setHex( 0x0000cc ); 91 | }, 92 | onGazeClick: function(){ 93 | // have the object react when user clicks / taps on targeted object 94 | this.material.emissive.setHex( 0x0000cc ); 95 | } 96 | }); 97 | ``` 98 | 99 | You can also remove targeted objects. 100 | ```javascript 101 | Reticulum.remove( object ); 102 | ``` 103 | 104 | 105 | ### 4. Add to animation loop 106 | 107 | Add Reticulum to your animation loop 108 | 109 | ```javascript 110 | Reticulum.update() 111 | ``` 112 | 113 | 114 | ### 5. Add Camera to scene 115 | 116 | If you require to display the reticle you will need to add the `camera` to the `scene`. 117 | 118 | **Note:** See Known Issues below if ghosting occurs. 119 | 120 | ```javascript 121 | scene.add(camera); 122 | ``` 123 | 124 | ## Demos 125 | 126 | - [Basic](https://skezo.github.io/Reticulum/examples/basic.html) 127 | - [Proximity](https://skezo.github.io/Reticulum/examples/proximity.html) - only display reticle if targeted object is visible 128 | - [Depth Test](https://skezo.github.io/Reticulum/examples/depth.html) - hit moving targets 129 | - [Objects in Groups](https://skezo.github.io/Reticulum/examples/groups.html) - hit object in group, get world values 130 | - [Fuse](https://skezo.github.io/Reticulum/examples/fuse.html) - selective objects have fuse 131 | - [Visibility](https://skezo.github.io/Reticulum/examples/visibility.html) - test for hitting only visible objects 132 | - [Gazeable](https://skezo.github.io/Reticulum/examples/gazeable.html) - test for hitting only gazeable objects 133 | 134 | ## Known Issues 135 | - Ghosting occurs to the reticle and fuse when in VR mode. More details on the issue can found [here](https://github.com/mrdoob/three.js/issues/7041). **A quick workaround** to this issue is adding `camera.updateMatrixWorld();` before the render call (e.g. `manager.render(scene, camera, timestamp);` to the callback function of the `requestAnimationFrame()` method. 136 | 137 | 138 | ## Acknowledgements: 139 | Reticulum was inspired by the work done by [neuman](https://github.com/neuman/vreticle) 140 | 141 | ## License 142 | The MIT License (MIT) -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reticulum", 3 | "version": "1.0.0", 4 | "main": "reticulum.js", 5 | "ignore": [ 6 | "example" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reticulum | Basic VR Example 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 222 | 223 | -------------------------------------------------------------------------------- /examples/depth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reticulum | Depth VR Example 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 88 | 89 | 258 | 259 | -------------------------------------------------------------------------------- /examples/fuse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reticulum | Fuse VR Example 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 230 | 231 | -------------------------------------------------------------------------------- /examples/gazeable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reticulum | Basic VR Example 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 237 | 238 | -------------------------------------------------------------------------------- /examples/groups.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reticulum | Groups VR Example 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 236 | 237 | -------------------------------------------------------------------------------- /examples/img/interactivepatterns_displayreticle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skezo/Reticulum/360e80af3fa079f8c8a893fe0b0d7a8039d9e027/examples/img/interactivepatterns_displayreticle.png -------------------------------------------------------------------------------- /examples/js/lib/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 vrDisplay, vrDisplays; 11 | 12 | var standingMatrix = new THREE.Matrix4(); 13 | 14 | var frameData = null; 15 | 16 | if ( 'VRFrameData' in window ) { 17 | 18 | frameData = new VRFrameData(); 19 | 20 | } 21 | 22 | function gotVRDisplays( displays ) { 23 | 24 | vrDisplays = displays; 25 | 26 | if ( displays.length > 0 ) { 27 | 28 | vrDisplay = displays[ 0 ]; 29 | 30 | } else { 31 | 32 | if ( onError ) onError( 'VR input not available.' ); 33 | 34 | } 35 | 36 | } 37 | 38 | if ( navigator.getVRDisplays ) { 39 | 40 | navigator.getVRDisplays().then( gotVRDisplays ).catch( function () { 41 | 42 | console.warn( 'THREE.VRControls: Unable to get VR Displays' ); 43 | 44 | } ); 45 | 46 | } 47 | 48 | // the Rift SDK returns the position in meters 49 | // this scale factor allows the user to define how meters 50 | // are converted to scene units. 51 | 52 | this.scale = 1; 53 | 54 | // If true will use "standing space" coordinate system where y=0 is the 55 | // floor and x=0, z=0 is the center of the room. 56 | this.standing = false; 57 | 58 | // Distance from the users eyes to the floor in meters. Used when 59 | // standing=true but the VRDisplay doesn't provide stageParameters. 60 | this.userHeight = 1.6; 61 | 62 | this.getVRDisplay = function () { 63 | 64 | return vrDisplay; 65 | 66 | }; 67 | 68 | this.setVRDisplay = function ( value ) { 69 | 70 | vrDisplay = value; 71 | 72 | }; 73 | 74 | this.getVRDisplays = function () { 75 | 76 | console.warn( 'THREE.VRControls: getVRDisplays() is being deprecated.' ); 77 | return vrDisplays; 78 | 79 | }; 80 | 81 | this.getStandingMatrix = function () { 82 | 83 | return standingMatrix; 84 | 85 | }; 86 | 87 | this.update = function () { 88 | 89 | if ( vrDisplay ) { 90 | 91 | var pose; 92 | 93 | if ( vrDisplay.getFrameData ) { 94 | 95 | vrDisplay.getFrameData( frameData ); 96 | pose = frameData.pose; 97 | 98 | } else if ( vrDisplay.getPose ) { 99 | 100 | pose = vrDisplay.getPose(); 101 | 102 | } 103 | 104 | if ( pose.orientation !== null ) { 105 | 106 | object.quaternion.fromArray( pose.orientation ); 107 | 108 | } 109 | 110 | if ( pose.position !== null ) { 111 | 112 | object.position.fromArray( pose.position ); 113 | 114 | } else { 115 | 116 | object.position.set( 0, 0, 0 ); 117 | 118 | } 119 | 120 | if ( this.standing ) { 121 | 122 | if ( vrDisplay.stageParameters ) { 123 | 124 | object.updateMatrix(); 125 | 126 | standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform ); 127 | object.applyMatrix( standingMatrix ); 128 | 129 | } else { 130 | 131 | object.position.setY( object.position.y + this.userHeight ); 132 | 133 | } 134 | 135 | } 136 | 137 | object.position.multiplyScalar( scope.scale ); 138 | 139 | } 140 | 141 | }; 142 | 143 | this.dispose = function () { 144 | 145 | vrDisplay = null; 146 | 147 | }; 148 | 149 | }; 150 | -------------------------------------------------------------------------------- /examples/js/lib/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://webvr.info/get-chrome 9 | */ 10 | 11 | THREE.VREffect = function ( renderer, onError ) { 12 | 13 | var vrDisplay, vrDisplays; 14 | var eyeTranslationL = new THREE.Vector3(); 15 | var eyeTranslationR = new THREE.Vector3(); 16 | var renderRectL, renderRectR; 17 | var headMatrix = new THREE.Matrix4(); 18 | var eyeMatrixL = new THREE.Matrix4(); 19 | var eyeMatrixR = new THREE.Matrix4(); 20 | 21 | var frameData = null; 22 | 23 | if ( 'VRFrameData' in window ) { 24 | 25 | frameData = new window.VRFrameData(); 26 | 27 | } 28 | 29 | function gotVRDisplays( displays ) { 30 | 31 | vrDisplays = displays; 32 | 33 | if ( displays.length > 0 ) { 34 | 35 | vrDisplay = displays[ 0 ]; 36 | 37 | } else { 38 | 39 | if ( onError ) onError( 'HMD not available' ); 40 | 41 | } 42 | 43 | } 44 | 45 | if ( navigator.getVRDisplays ) { 46 | 47 | navigator.getVRDisplays().then( gotVRDisplays ).catch( function () { 48 | 49 | console.warn( 'THREE.VREffect: Unable to get VR Displays' ); 50 | 51 | } ); 52 | 53 | } 54 | 55 | // 56 | 57 | this.isPresenting = false; 58 | 59 | var scope = this; 60 | 61 | var rendererSize = renderer.getSize(); 62 | var rendererUpdateStyle = false; 63 | var rendererPixelRatio = renderer.getPixelRatio(); 64 | 65 | this.getVRDisplay = function () { 66 | 67 | return vrDisplay; 68 | 69 | }; 70 | 71 | this.setVRDisplay = function ( value ) { 72 | 73 | vrDisplay = value; 74 | 75 | }; 76 | 77 | this.getVRDisplays = function () { 78 | 79 | console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' ); 80 | return vrDisplays; 81 | 82 | }; 83 | 84 | this.setSize = function ( width, height, updateStyle ) { 85 | 86 | rendererSize = { width: width, height: height }; 87 | rendererUpdateStyle = updateStyle; 88 | 89 | if ( scope.isPresenting ) { 90 | 91 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); 92 | renderer.setPixelRatio( 1 ); 93 | renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); 94 | 95 | } else { 96 | 97 | renderer.setPixelRatio( rendererPixelRatio ); 98 | renderer.setSize( width, height, updateStyle ); 99 | 100 | } 101 | 102 | }; 103 | 104 | // VR presentation 105 | 106 | var canvas = renderer.domElement; 107 | var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ]; 108 | var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ]; 109 | 110 | function onVRDisplayPresentChange() { 111 | 112 | var wasPresenting = scope.isPresenting; 113 | scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting; 114 | 115 | if ( scope.isPresenting ) { 116 | 117 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); 118 | var eyeWidth = eyeParamsL.renderWidth; 119 | var eyeHeight = eyeParamsL.renderHeight; 120 | 121 | if ( ! wasPresenting ) { 122 | 123 | rendererPixelRatio = renderer.getPixelRatio(); 124 | rendererSize = renderer.getSize(); 125 | 126 | renderer.setPixelRatio( 1 ); 127 | renderer.setSize( eyeWidth * 2, eyeHeight, false ); 128 | 129 | } 130 | 131 | } else if ( wasPresenting ) { 132 | 133 | renderer.setPixelRatio( rendererPixelRatio ); 134 | renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle ); 135 | 136 | } 137 | 138 | } 139 | 140 | window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); 141 | 142 | this.setFullScreen = function ( boolean ) { 143 | 144 | return new Promise( function ( resolve, reject ) { 145 | 146 | if ( vrDisplay === undefined ) { 147 | 148 | reject( new Error( 'No VR hardware found.' ) ); 149 | return; 150 | 151 | } 152 | 153 | if ( scope.isPresenting === boolean ) { 154 | 155 | resolve(); 156 | return; 157 | 158 | } 159 | 160 | if ( boolean ) { 161 | 162 | resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) ); 163 | 164 | } else { 165 | 166 | resolve( vrDisplay.exitPresent() ); 167 | 168 | } 169 | 170 | } ); 171 | 172 | }; 173 | 174 | this.requestPresent = function () { 175 | 176 | return this.setFullScreen( true ); 177 | 178 | }; 179 | 180 | this.exitPresent = function () { 181 | 182 | return this.setFullScreen( false ); 183 | 184 | }; 185 | 186 | this.requestAnimationFrame = function ( f ) { 187 | 188 | if ( vrDisplay !== undefined ) { 189 | 190 | return vrDisplay.requestAnimationFrame( f ); 191 | 192 | } else { 193 | 194 | return window.requestAnimationFrame( f ); 195 | 196 | } 197 | 198 | }; 199 | 200 | this.cancelAnimationFrame = function ( h ) { 201 | 202 | if ( vrDisplay !== undefined ) { 203 | 204 | vrDisplay.cancelAnimationFrame( h ); 205 | 206 | } else { 207 | 208 | window.cancelAnimationFrame( h ); 209 | 210 | } 211 | 212 | }; 213 | 214 | this.submitFrame = function () { 215 | 216 | if ( vrDisplay !== undefined && scope.isPresenting ) { 217 | 218 | vrDisplay.submitFrame(); 219 | 220 | } 221 | 222 | }; 223 | 224 | this.autoSubmitFrame = true; 225 | 226 | // render 227 | 228 | var cameraL = new THREE.PerspectiveCamera(); 229 | cameraL.layers.enable( 1 ); 230 | 231 | var cameraR = new THREE.PerspectiveCamera(); 232 | cameraR.layers.enable( 2 ); 233 | 234 | this.render = function ( scene, camera, renderTarget, forceClear ) { 235 | 236 | if ( vrDisplay && scope.isPresenting ) { 237 | 238 | var autoUpdate = scene.autoUpdate; 239 | 240 | if ( autoUpdate ) { 241 | 242 | scene.updateMatrixWorld(); 243 | scene.autoUpdate = false; 244 | 245 | } 246 | 247 | if ( Array.isArray( scene ) ) { 248 | 249 | console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' ); 250 | scene = scene[ 0 ]; 251 | 252 | } 253 | 254 | // When rendering we don't care what the recommended size is, only what the actual size 255 | // of the backbuffer is. 256 | var size = renderer.getSize(); 257 | var layers = vrDisplay.getLayers(); 258 | var leftBounds; 259 | var rightBounds; 260 | 261 | if ( layers.length ) { 262 | 263 | var layer = layers[ 0 ]; 264 | 265 | leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds; 266 | rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds; 267 | 268 | } else { 269 | 270 | leftBounds = defaultLeftBounds; 271 | rightBounds = defaultRightBounds; 272 | 273 | } 274 | 275 | renderRectL = { 276 | x: Math.round( size.width * leftBounds[ 0 ] ), 277 | y: Math.round( size.height * leftBounds[ 1 ] ), 278 | width: Math.round( size.width * leftBounds[ 2 ] ), 279 | height: Math.round( size.height * leftBounds[ 3 ] ) 280 | }; 281 | renderRectR = { 282 | x: Math.round( size.width * rightBounds[ 0 ] ), 283 | y: Math.round( size.height * rightBounds[ 1 ] ), 284 | width: Math.round( size.width * rightBounds[ 2 ] ), 285 | height: Math.round( size.height * rightBounds[ 3 ] ) 286 | }; 287 | 288 | if ( renderTarget ) { 289 | 290 | renderer.setRenderTarget( renderTarget ); 291 | renderTarget.scissorTest = true; 292 | 293 | } else { 294 | 295 | renderer.setRenderTarget( null ); 296 | renderer.setScissorTest( true ); 297 | 298 | } 299 | 300 | if ( renderer.autoClear || forceClear ) renderer.clear(); 301 | 302 | if ( camera.parent === null ) camera.updateMatrixWorld(); 303 | 304 | camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); 305 | 306 | cameraR.position.copy( cameraL.position ); 307 | cameraR.quaternion.copy( cameraL.quaternion ); 308 | cameraR.scale.copy( cameraL.scale ); 309 | 310 | if ( vrDisplay.getFrameData ) { 311 | 312 | vrDisplay.depthNear = camera.near; 313 | vrDisplay.depthFar = camera.far; 314 | 315 | vrDisplay.getFrameData( frameData ); 316 | 317 | cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix; 318 | cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix; 319 | 320 | getEyeMatrices( frameData ); 321 | 322 | cameraL.updateMatrix(); 323 | cameraL.matrix.multiply( eyeMatrixL ); 324 | cameraL.matrix.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); 325 | 326 | cameraR.updateMatrix(); 327 | cameraR.matrix.multiply( eyeMatrixR ); 328 | cameraR.matrix.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); 329 | 330 | } else { 331 | 332 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); 333 | var eyeParamsR = vrDisplay.getEyeParameters( 'right' ); 334 | 335 | cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far ); 336 | cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far ); 337 | 338 | eyeTranslationL.fromArray( eyeParamsL.offset ); 339 | eyeTranslationR.fromArray( eyeParamsR.offset ); 340 | 341 | cameraL.translateOnAxis( eyeTranslationL, cameraL.scale.x ); 342 | cameraR.translateOnAxis( eyeTranslationR, cameraR.scale.x ); 343 | 344 | } 345 | 346 | // render left eye 347 | if ( renderTarget ) { 348 | 349 | renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 350 | renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 351 | 352 | } else { 353 | 354 | renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 355 | renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 356 | 357 | } 358 | renderer.render( scene, cameraL, renderTarget, forceClear ); 359 | 360 | // render right eye 361 | if ( renderTarget ) { 362 | 363 | renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 364 | renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 365 | 366 | } else { 367 | 368 | renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 369 | renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 370 | 371 | } 372 | renderer.render( scene, cameraR, renderTarget, forceClear ); 373 | 374 | if ( renderTarget ) { 375 | 376 | renderTarget.viewport.set( 0, 0, size.width, size.height ); 377 | renderTarget.scissor.set( 0, 0, size.width, size.height ); 378 | renderTarget.scissorTest = false; 379 | renderer.setRenderTarget( null ); 380 | 381 | } else { 382 | 383 | renderer.setViewport( 0, 0, size.width, size.height ); 384 | renderer.setScissorTest( false ); 385 | 386 | } 387 | 388 | if ( autoUpdate ) { 389 | 390 | scene.autoUpdate = true; 391 | 392 | } 393 | 394 | if ( scope.autoSubmitFrame ) { 395 | 396 | scope.submitFrame(); 397 | 398 | } 399 | 400 | return; 401 | 402 | } 403 | 404 | // Regular render mode if not HMD 405 | 406 | renderer.render( scene, camera, renderTarget, forceClear ); 407 | 408 | }; 409 | 410 | this.dispose = function () { 411 | 412 | window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); 413 | 414 | }; 415 | 416 | // 417 | 418 | var poseOrientation = new THREE.Quaternion(); 419 | var posePosition = new THREE.Vector3(); 420 | 421 | // Compute model matrices of the eyes with respect to the head. 422 | function getEyeMatrices( frameData ) { 423 | 424 | // Compute the matrix for the position of the head based on the pose 425 | if ( frameData.pose.orientation ) { 426 | 427 | poseOrientation.fromArray( frameData.pose.orientation ); 428 | headMatrix.makeRotationFromQuaternion( poseOrientation ); 429 | 430 | } else { 431 | 432 | headMatrix.identity(); 433 | 434 | } 435 | 436 | if ( frameData.pose.position ) { 437 | 438 | posePosition.fromArray( frameData.pose.position ); 439 | headMatrix.setPosition( posePosition ); 440 | 441 | } 442 | 443 | // The view matrix transforms vertices from sitting space to eye space. As such, the view matrix can be thought of as a product of two matrices: 444 | // headToEyeMatrix * sittingToHeadMatrix 445 | 446 | // The headMatrix that we've calculated above is the model matrix of the head in sitting space, which is the inverse of sittingToHeadMatrix. 447 | // So when we multiply the view matrix with headMatrix, we're left with headToEyeMatrix: 448 | // viewMatrix * headMatrix = headToEyeMatrix * sittingToHeadMatrix * headMatrix = headToEyeMatrix 449 | 450 | eyeMatrixL.fromArray( frameData.leftViewMatrix ); 451 | eyeMatrixL.multiply( headMatrix ); 452 | eyeMatrixR.fromArray( frameData.rightViewMatrix ); 453 | eyeMatrixR.multiply( headMatrix ); 454 | 455 | // The eye's model matrix in head space is the inverse of headToEyeMatrix we calculated above. 456 | 457 | eyeMatrixL.getInverse( eyeMatrixL ); 458 | eyeMatrixR.getInverse( eyeMatrixR ); 459 | 460 | } 461 | 462 | function fovToNDCScaleOffset( fov ) { 463 | 464 | var pxscale = 2.0 / ( fov.leftTan + fov.rightTan ); 465 | var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5; 466 | var pyscale = 2.0 / ( fov.upTan + fov.downTan ); 467 | var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5; 468 | return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; 469 | 470 | } 471 | 472 | function fovPortToProjection( fov, rightHanded, zNear, zFar ) { 473 | 474 | rightHanded = rightHanded === undefined ? true : rightHanded; 475 | zNear = zNear === undefined ? 0.01 : zNear; 476 | zFar = zFar === undefined ? 10000.0 : zFar; 477 | 478 | var handednessScale = rightHanded ? - 1.0 : 1.0; 479 | 480 | // start with an identity matrix 481 | var mobj = new THREE.Matrix4(); 482 | var m = mobj.elements; 483 | 484 | // and with scale/offset info for normalized device coords 485 | var scaleAndOffset = fovToNDCScaleOffset( fov ); 486 | 487 | // X result, map clip edges to [-w,+w] 488 | m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ]; 489 | m[ 0 * 4 + 1 ] = 0.0; 490 | m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale; 491 | m[ 0 * 4 + 3 ] = 0.0; 492 | 493 | // Y result, map clip edges to [-w,+w] 494 | // Y offset is negated because this proj matrix transforms from world coords with Y=up, 495 | // but the NDC scaling has Y=down (thanks D3D?) 496 | m[ 1 * 4 + 0 ] = 0.0; 497 | m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ]; 498 | m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale; 499 | m[ 1 * 4 + 3 ] = 0.0; 500 | 501 | // Z result (up to the app) 502 | m[ 2 * 4 + 0 ] = 0.0; 503 | m[ 2 * 4 + 1 ] = 0.0; 504 | m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale; 505 | m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar ); 506 | 507 | // W result (= Z in) 508 | m[ 3 * 4 + 0 ] = 0.0; 509 | m[ 3 * 4 + 1 ] = 0.0; 510 | m[ 3 * 4 + 2 ] = handednessScale; 511 | m[ 3 * 4 + 3 ] = 0.0; 512 | 513 | mobj.transpose(); 514 | return mobj; 515 | 516 | } 517 | 518 | function fovToProjection( fov, rightHanded, zNear, zFar ) { 519 | 520 | var DEG2RAD = Math.PI / 180.0; 521 | 522 | var fovPort = { 523 | upTan: Math.tan( fov.upDegrees * DEG2RAD ), 524 | downTan: Math.tan( fov.downDegrees * DEG2RAD ), 525 | leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), 526 | rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) 527 | }; 528 | 529 | return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); 530 | 531 | } 532 | 533 | }; 534 | -------------------------------------------------------------------------------- /examples/js/lib/device-info-test.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o

301 | * Allows to compute the original undistorted radius from a distorted one. 302 | * See also getApproximateInverseDistortion() for a faster but potentially 303 | * less accurate method. 304 | * 305 | * @param {Number} radius Distorted radius from the lens center in tan-angle units. 306 | * @return {Number} The undistorted radius in tan-angle units. 307 | */ 308 | Distortion.prototype.distortInverse = function(radius) { 309 | // Secant method. 310 | var r0 = radius / 0.9; 311 | var r1 = radius * 0.9; 312 | var dr0 = radius - this.distort(r0); 313 | while (Math.abs(r1 - r0) > 0.0001 /** 0.1mm */) { 314 | var dr1 = radius - this.distort(r1); 315 | var r2 = r1 - dr1 * ((r1 - r0) / (dr1 - dr0)); 316 | r0 = r1; 317 | r1 = r2; 318 | dr0 = dr1; 319 | } 320 | return r1; 321 | } 322 | 323 | 324 | /** 325 | * Distorts a radius by its distortion factor from the center of the lenses. 326 | * 327 | * @param {Number} radius Radius from the lens center in tan-angle units. 328 | * @return {Number} The distorted radius in tan-angle units. 329 | */ 330 | Distortion.prototype.distort = function(radius) { 331 | return radius * this.distortionFactor_(radius); 332 | } 333 | 334 | /** 335 | * Returns the distortion factor of a point. 336 | * 337 | * @param {Number} radius Radius of the point from the lens center in tan-angle units. 338 | * @return {Number} The distortion factor. Multiply by this factor to distort points. 339 | */ 340 | Distortion.prototype.distortionFactor_ = function(radius) { 341 | var result = 1.0; 342 | var rFactor = 1.0; 343 | var rSquared = radius * radius; 344 | 345 | for (var i = 0; i < this.coefficients.length; i++) { 346 | var ki = this.coefficients[i]; 347 | rFactor *= rSquared; 348 | result += ki * rFactor; 349 | } 350 | 351 | return result; 352 | } 353 | 354 | module.exports = Distortion; 355 | 356 | },{}],3:[function(require,module,exports){ 357 | /* 358 | * Copyright 2015 Google Inc. All Rights Reserved. 359 | * Licensed under the Apache License, Version 2.0 (the "License"); 360 | * you may not use this file except in compliance with the License. 361 | * You may obtain a copy of the License at 362 | * 363 | * http://www.apache.org/licenses/LICENSE-2.0 364 | * 365 | * Unless required by applicable law or agreed to in writing, software 366 | * distributed under the License is distributed on an "AS IS" BASIS, 367 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 368 | * See the License for the specific language governing permissions and 369 | * limitations under the License. 370 | */ 371 | 372 | var Util = {}; 373 | 374 | Util.base64 = function(mimeType, base64) { 375 | return 'data:' + mimeType + ';base64,' + base64; 376 | }; 377 | 378 | Util.isMobile = function() { 379 | var check = false; 380 | (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera); 381 | return check; 382 | }; 383 | 384 | Util.isFirefox = function() { 385 | return /firefox/i.test(navigator.userAgent); 386 | }; 387 | 388 | Util.isIOS = function() { 389 | return /(iPad|iPhone|iPod)/g.test(navigator.userAgent); 390 | }; 391 | 392 | Util.isIFrame = function() { 393 | try { 394 | return window.self !== window.top; 395 | } catch (e) { 396 | return true; 397 | } 398 | }; 399 | 400 | Util.appendQueryParameter = function(url, key, value) { 401 | // Determine delimiter based on if the URL already GET parameters in it. 402 | var delimiter = (url.indexOf('?') < 0 ? '?' : '&'); 403 | url += delimiter + key + '=' + value; 404 | return url; 405 | }; 406 | 407 | // From http://goo.gl/4WX3tg 408 | Util.getQueryParameter = function(name) { 409 | name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); 410 | var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), 411 | results = regex.exec(location.search); 412 | return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); 413 | }; 414 | 415 | Util.isLandscapeMode = function() { 416 | return (window.orientation == 90 || window.orientation == -90); 417 | }; 418 | 419 | Util.getScreenWidth = function() { 420 | return Math.max(window.screen.width, window.screen.height) * 421 | window.devicePixelRatio; 422 | }; 423 | 424 | Util.getScreenHeight = function() { 425 | return Math.min(window.screen.width, window.screen.height) * 426 | window.devicePixelRatio; 427 | }; 428 | 429 | /** 430 | * Utility to convert the projection matrix to a vector accepted by the shader. 431 | * 432 | * @param {Object} opt_params A rectangle to scale this vector by. 433 | */ 434 | Util.projectionMatrixToVector_ = function(matrix, opt_params) { 435 | var params = opt_params || {}; 436 | var xScale = params.xScale || 1; 437 | var yScale = params.yScale || 1; 438 | var xTrans = params.xTrans || 0; 439 | var yTrans = params.yTrans || 0; 440 | 441 | var elements = matrix.elements; 442 | var vec = new THREE.Vector4(); 443 | vec.set(elements[4*0 + 0] * xScale, 444 | elements[4*1 + 1] * yScale, 445 | elements[4*2 + 0] - 1 - xTrans, 446 | elements[4*2 + 1] - 1 - yTrans).divideScalar(2); 447 | return vec; 448 | }; 449 | 450 | Util.leftProjectionVectorToRight_ = function(left) { 451 | //projectionLeft + vec4(0.0, 0.0, 1.0, 0.0)) * vec4(1.0, 1.0, -1.0, 1.0); 452 | var out = new THREE.Vector4(0, 0, 1, 0); 453 | out.add(left); // out = left + (0, 0, 1, 0). 454 | out.z *= -1; // Flip z. 455 | 456 | return out; 457 | }; 458 | 459 | 460 | module.exports = Util; 461 | 462 | },{}],4:[function(require,module,exports){ 463 | var DeviceInfo = require('../src/device-info.js'); 464 | 465 | var di = new DeviceInfo(); 466 | var centroid = di.getLeftEyeCenter(); 467 | 468 | // Size the canvas. Render the centroid. 469 | var canvas = document.querySelector('canvas'); 470 | var w = window.innerWidth; 471 | var h = window.innerHeight; 472 | var x = centroid.x * w/2; 473 | var y = centroid.y * h; 474 | var size = 10; 475 | 476 | canvas.width = w; 477 | canvas.height = h; 478 | 479 | var ctx = canvas.getContext('2d'); 480 | ctx.clearRect(0, 0, w, h); 481 | ctx.fillStyle = 'black'; 482 | ctx.fillRect(x - size/2, y - size/2, size, size); 483 | 484 | console.log('Placing eye at (%d, %d).', x, y); 485 | 486 | },{"../src/device-info.js":1}]},{},[4]); 487 | -------------------------------------------------------------------------------- /examples/js/lib/webvr-manager.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.WebVRManager = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o %s', this.mode, mode); 462 | this.mode = mode; 463 | this.button.setMode(mode, this.isVRCompatible); 464 | 465 | // Emit an event indicating the mode changed. 466 | this.emit('modechange', mode, oldMode); 467 | }; 468 | 469 | /** 470 | * Main button was clicked. 471 | */ 472 | WebVRManager.prototype.onFSClick_ = function() { 473 | switch (this.mode) { 474 | case Modes.NORMAL: 475 | // TODO: Remove this hack if/when iOS gets real fullscreen mode. 476 | // If this is an iframe on iOS, break out and open in no_fullscreen mode. 477 | if (Util.isIOS() && Util.isIFrame()) { 478 | if (this.fullscreenCallback) { 479 | this.fullscreenCallback(); 480 | } else { 481 | var url = window.location.href; 482 | url = Util.appendQueryParameter(url, 'no_fullscreen', 'true'); 483 | url = Util.appendQueryParameter(url, 'start_mode', Modes.MAGIC_WINDOW); 484 | top.location.href = url; 485 | return; 486 | } 487 | } 488 | this.setMode_(Modes.MAGIC_WINDOW); 489 | this.requestFullscreen_(); 490 | break; 491 | case Modes.MAGIC_WINDOW: 492 | if (this.isFullscreenDisabled) { 493 | window.history.back(); 494 | return; 495 | } 496 | if (this.exitFullscreenCallback) { 497 | this.exitFullscreenCallback(); 498 | } 499 | this.setMode_(Modes.NORMAL); 500 | this.exitFullscreen_(); 501 | break; 502 | } 503 | }; 504 | 505 | /** 506 | * The VR button was clicked. 507 | */ 508 | WebVRManager.prototype.onVRClick_ = function() { 509 | // TODO: Remove this hack when iOS has fullscreen mode. 510 | // If this is an iframe on iOS, break out and open in no_fullscreen mode. 511 | if (this.mode == Modes.NORMAL && Util.isIOS() && Util.isIFrame()) { 512 | if (this.vrCallback) { 513 | this.vrCallback(); 514 | } else { 515 | var url = window.location.href; 516 | url = Util.appendQueryParameter(url, 'no_fullscreen', 'true'); 517 | url = Util.appendQueryParameter(url, 'start_mode', Modes.VR); 518 | top.location.href = url; 519 | return; 520 | } 521 | } 522 | this.enterVRMode_(); 523 | }; 524 | 525 | WebVRManager.prototype.requestFullscreen_ = function() { 526 | var canvas = document.body; 527 | //var canvas = this.renderer.domElement; 528 | if (canvas.requestFullscreen) { 529 | canvas.requestFullscreen(); 530 | } else if (canvas.mozRequestFullScreen) { 531 | canvas.mozRequestFullScreen(); 532 | } else if (canvas.webkitRequestFullscreen) { 533 | canvas.webkitRequestFullscreen(); 534 | } else if (canvas.msRequestFullscreen) { 535 | canvas.msRequestFullscreen(); 536 | } 537 | }; 538 | 539 | WebVRManager.prototype.exitFullscreen_ = function() { 540 | if (document.exitFullscreen) { 541 | document.exitFullscreen(); 542 | } else if (document.mozCancelFullScreen) { 543 | document.mozCancelFullScreen(); 544 | } else if (document.webkitExitFullscreen) { 545 | document.webkitExitFullscreen(); 546 | } else if (document.msExitFullscreen) { 547 | document.msExitFullscreen(); 548 | } 549 | }; 550 | 551 | WebVRManager.prototype.onVRDisplayPresentChange_ = function(e) { 552 | console.log('onVRDisplayPresentChange_', e); 553 | if (this.hmd.isPresenting) { 554 | this.setMode_(Modes.VR); 555 | } else { 556 | this.setMode_(Modes.NORMAL); 557 | } 558 | }; 559 | 560 | WebVRManager.prototype.onVRDisplayDeviceParamsChange_ = function(e) { 561 | console.log('onVRDisplayDeviceParamsChange_', e); 562 | }; 563 | 564 | WebVRManager.prototype.onFullscreenChange_ = function(e) { 565 | // If we leave full-screen, go back to normal mode. 566 | if (document.webkitFullscreenElement === null || 567 | document.mozFullScreenElement === null) { 568 | this.setMode_(Modes.NORMAL); 569 | } 570 | }; 571 | 572 | module.exports = WebVRManager; 573 | 574 | },{"./button-manager.js":1,"./emitter.js":2,"./modes.js":3,"./util.js":4}]},{},[5])(5) 575 | }); -------------------------------------------------------------------------------- /examples/proximity.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reticulum | Proximity Example 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 224 | 225 | -------------------------------------------------------------------------------- /examples/visibility.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reticulum | Basic VR Example 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 217 | 218 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | VR Reticulum | A cursor for your vr environment 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 |

VR Reticulum

23 |

VR Reticulum attempts to follow Google's interactive pattern for the display reticle. It creates the illusion of depth by projecting spatially onto targeted objects while maintaining a fixed size so that it is easy to see at all times.

24 | 27 | 28 |

Examples

29 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reticulum", 3 | "title": "Reticulum", 4 | "description": "A gaze interaction manager for VR with three.js, supporting depth.", 5 | "version": "1.0.0", 6 | "homepage": "https://github.com/skezo/Reticulum", 7 | "license": "MIT", 8 | "keywords": [ 9 | "vr", 10 | "reticle", 11 | "gaze", 12 | "cardboard" 13 | ], 14 | "author": { 15 | "name": "Skezo", 16 | "email": "admin@gqpbj.com" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/GQPBJ/Reticulum.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/skezo/Reticulum/issues" 24 | }, 25 | "dependencies": {}, 26 | "devDependencies": {}, 27 | "main": "reticulum.js", 28 | "engines": { 29 | "node": ">=0.8.0" 30 | }, 31 | "scripts": {} 32 | } 33 | -------------------------------------------------------------------------------- /reticulum.js: -------------------------------------------------------------------------------- 1 | /*! Reticulum - v2.1.2 2 | * http://skezo.github.io/examples/basic.html 3 | * 4 | * Copyright (c) 2015 Skezo; 5 | * Licensed under the MIT license */ 6 | 7 | var Reticulum = (function () { 8 | var INTERSECTED = null; 9 | 10 | var collisionList = []; 11 | var raycaster; 12 | var vector; 13 | var clock; 14 | var reticle = {}; 15 | var fuse = {}; 16 | 17 | var frustum; 18 | var cameraViewProjectionMatrix; 19 | 20 | var parentContainer 21 | 22 | //Settings from user 23 | var settings = { 24 | camera: null, //Required 25 | proximity: false, 26 | isClickEnabled: true, 27 | lockDistance: false 28 | }; 29 | 30 | //Utilities 31 | var utilities = { 32 | clampBottom: function ( x, a ) { 33 | return x < a ? a : x; 34 | } 35 | } 36 | 37 | //Vibrate 38 | var vibrate = navigator.vibrate ? navigator.vibrate.bind(navigator) : function(){}; 39 | 40 | //Fuse 41 | fuse.initiate = function( options ) { 42 | var parameters = options || {}; 43 | 44 | this.visible = parameters.visible !== false; //default to true; 45 | this.globalDuration = parameters.duration || 2.5; 46 | this.vibratePattern = parameters.vibrate || 100; 47 | this.color = parameters.color || 0x00fff6; 48 | this.innerRadius = parameters.innerRadius || reticle.innerRadiusTo; 49 | this.outerRadius = parameters.outerRadius || reticle.outerRadiusTo; 50 | this.clickCancel = parameters.clickCancelFuse === undefined ? false : parameters.clickCancelFuse; //default to false; 51 | this.phiSegments = 3; 52 | this.thetaSegments = 32; 53 | this.thetaStart = Math.PI/2; 54 | this.duration = this.globalDuration; 55 | this.timeDone = false; 56 | //var geometry = new THREE.CircleGeometry( reticle.outerRadiusTo, 32, Math.PI/2, 0 ); 57 | var geometry = new THREE.RingGeometry( this.innerRadius, this.outerRadius, this.thetaSegments, this.phiSegments, this.thetaStart, Math.PI/2 ); 58 | 59 | //Make Mesh 60 | this.mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { 61 | color: this.color, 62 | side: THREE.BackSide, 63 | fog: false 64 | //depthWrite: false, 65 | //depthTest: false 66 | })); 67 | 68 | //Set mesh visibility 69 | this.mesh.visible = this.visible; 70 | 71 | //Change position and rotation of fuse 72 | this.mesh.position.z = 0.0001; // Keep in front of reticle 73 | this.mesh.rotation.y = 180*(Math.PI/180); //Make it clockwise 74 | 75 | //Add to reticle 76 | //reticle.mesh.add( this.mesh ); 77 | parentContainer.add( this.mesh ); 78 | //geometry.dispose(); 79 | }; 80 | 81 | fuse.out = function() { 82 | this.active = false; 83 | this.mesh.visible = false; 84 | this.timeDone = false; 85 | this.update(0); 86 | } 87 | 88 | fuse.over = function(duration, visible) { 89 | this.duration = duration || this.globalDuration; 90 | this.active = true; 91 | this.update(0); 92 | this.mesh.visible = visible || this.visible; 93 | } 94 | 95 | fuse.update = function(elapsed) { 96 | 97 | if(!this.active || fuse.timeDone) return; 98 | 99 | //--RING 100 | var gazedTime = elapsed/this.duration; 101 | var thetaLength = gazedTime * (Math.PI*2); 102 | 103 | var vertices = this.mesh.geometry.vertices; 104 | var radius = this.innerRadius; 105 | var radiusStep = ( ( this.outerRadius - this.innerRadius ) / this.phiSegments ); 106 | var count = 0; 107 | 108 | for ( var i = 0; i <= this.phiSegments; i ++ ) { 109 | 110 | for ( var o = 0; o <= this.thetaSegments; o++ ) { 111 | var vertex = vertices[ count ]; 112 | var segment = this.thetaStart + o / this.thetaSegments * thetaLength; 113 | vertex.x = radius * Math.cos( segment ); 114 | vertex.y = radius * Math.sin( segment ); 115 | count++; 116 | } 117 | radius += radiusStep; 118 | } 119 | 120 | this.mesh.geometry.verticesNeedUpdate = true; 121 | 122 | //Disable fuse if reached 100% 123 | if(gazedTime >= 1) { 124 | this.active = false; 125 | } 126 | //--RING EOF 127 | 128 | 129 | } 130 | 131 | //Reticle 132 | reticle.initiate = function( options ) { 133 | var parameters = options || {}; 134 | 135 | parameters.hover = parameters.hover || {}; 136 | parameters.click = parameters.click || {}; 137 | 138 | this.active = true; 139 | this.visible = parameters.visible !== false; //default to true; 140 | this.restPoint = parameters.restPoint || settings.camera.far-10.0; 141 | this.globalColor = parameters.color || 0xcc0000; 142 | this.innerRadius = parameters.innerRadius || 0.0004; 143 | this.outerRadius = parameters.outerRadius || 0.003; 144 | this.worldPosition = new THREE.Vector3(); 145 | this.ignoreInvisible = parameters.ignoreInvisible !== false; //default to true; 146 | 147 | //Hover 148 | this.innerRadiusTo = parameters.hover.innerRadius || 0.02; 149 | this.outerRadiusTo = parameters.hover.outerRadius || 0.024; 150 | this.globalColorTo = parameters.hover.color || this.color; 151 | this.vibrateHover = parameters.hover.vibrate || 50; 152 | this.hit = false; 153 | //Click 154 | this.vibrateClick = parameters.click.vibrate || 50; 155 | //Animation options 156 | this.speed = parameters.hover.speed || 5; 157 | this.moveSpeed = 0; 158 | 159 | //Colors 160 | this.globalColor = new THREE.Color( this.globalColor ); 161 | this.color = this.globalColor.clone(); 162 | this.globalColorTo = new THREE.Color( this.globalColorTo ); 163 | this.colorTo = this.globalColorTo.clone(); 164 | 165 | //Geometry 166 | var geometry = new THREE.RingGeometry( this.innerRadius, this.outerRadius, 32, 3, 0, Math.PI * 2 ); 167 | var geometryScale = new THREE.RingGeometry( this.innerRadiusTo, this.outerRadiusTo, 32, 3, 0, Math.PI * 2 ); 168 | 169 | //Add Morph Targets for scale animation 170 | geometry.morphTargets.push( { name: "target1", vertices: geometryScale.vertices } ); 171 | 172 | //Make Mesh 173 | this.mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { 174 | color: this.color, 175 | morphTargets: true, 176 | fog: false 177 | //depthWrite: false, 178 | //depthTest: false 179 | })); 180 | this.mesh.visible = this.visible; 181 | 182 | //set depth and scale 183 | this.setDepthAndScale(); 184 | 185 | //Add to camera 186 | //settings.camera.add( this.mesh ); 187 | parentContainer.add( this.mesh ); 188 | 189 | }; 190 | 191 | //Sets the depth and scale of the reticle - reduces eyestrain and depth issues 192 | reticle.setDepthAndScale = function( depth ) { 193 | //var crosshair = this.mesh; 194 | var crosshair = parentContainer; 195 | var z = Math.abs( depth || this.restPoint ); //Default to user far setting 196 | var cameraZ = settings.camera.position.z; 197 | //Force reticle to appear the same size - scale 198 | //http://answers.unity3d.com/questions/419342/make-gameobject-size-always-be-the-same.html 199 | var scale = Math.abs( cameraZ - z ) - Math.abs( cameraZ ); 200 | 201 | //Set Depth 202 | crosshair.position.x = 0; 203 | crosshair.position.y = 0; 204 | crosshair.position.z = utilities.clampBottom( z, settings.camera.near+0.1 ) * -1; 205 | 206 | //Set Scale 207 | crosshair.scale.set( scale, scale, scale ); 208 | }; 209 | 210 | reticle.update = function(delta) { 211 | //If not active 212 | if(!this.active) return; 213 | 214 | var accel = delta * this.speed; 215 | 216 | if( this.hit ) { 217 | this.moveSpeed += accel; 218 | this.moveSpeed = Math.min(this.moveSpeed, 1); 219 | } else { 220 | this.moveSpeed -= accel; 221 | this.moveSpeed = Math.max(this.moveSpeed, 0); 222 | } 223 | //Morph 224 | this.mesh.morphTargetInfluences[ 0 ] = this.moveSpeed; 225 | //Set Color 226 | this.color = this.globalColor.clone(); 227 | //console.log( this.color.lerp( this.colorTo, this.moveSpeed ) ) 228 | this.mesh.material.color = this.color.lerp( this.colorTo, this.moveSpeed ); 229 | }; 230 | 231 | var initiate = function (camera, options) { 232 | //Update Settings: 233 | options = options || {}; 234 | 235 | settings.camera = camera; //required 236 | settings.proximity = options.proximity || settings.proximity; 237 | settings.lockDistance = options.lockDistance || settings.lockDistance; 238 | settings.isClickEnabled = options.clickevents || settings.isClickEnabled; 239 | options.reticle = options.reticle || {}; 240 | options.fuse = options.fuse || {}; 241 | 242 | //Raycaster Setup 243 | raycaster = new THREE.Raycaster(); 244 | vector = new THREE.Vector2(0, 0); 245 | //Update Raycaster 246 | if(options.near && options.near >= 0 ) { 247 | raycaster.near = options.near; 248 | } 249 | if(options.far && options.far >= 0 ) { 250 | raycaster.far = options.far; 251 | } 252 | 253 | //Create Parent Object for reticle and fuse 254 | parentContainer = new THREE.Object3D(); 255 | settings.camera.add( parentContainer ); 256 | 257 | //Proximity Setup 258 | if( settings.proximity ) { 259 | frustum = new THREE.Frustum(); 260 | cameraViewProjectionMatrix = new THREE.Matrix4(); 261 | } 262 | 263 | //Enable Click / Tap Events 264 | if( settings.isClickEnabled ) { 265 | document.body.addEventListener('touchend', touchClickHandler, false); 266 | document.body.addEventListener('click', touchClickHandler, false); 267 | } 268 | 269 | //Clock Setup 270 | clock = new THREE.Clock(true); 271 | 272 | //Initiate Reticle 273 | reticle.initiate(options.reticle); 274 | 275 | //Initiate Fuse 276 | fuse.initiate(options.fuse); 277 | }; 278 | 279 | var proximity = function() { 280 | var camera = settings.camera; 281 | var showReticle = false; 282 | 283 | //Use frustum to see if any targetable object is visible 284 | //http://stackoverflow.com/questions/17624021/determine-if-a-mesh-is-visible-on-the-viewport-according-to-current-camera 285 | camera.updateMatrixWorld(); 286 | camera.matrixWorldInverse.getInverse( camera.matrixWorld ); 287 | cameraViewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ); 288 | 289 | frustum.setFromMatrix( cameraViewProjectionMatrix ); 290 | 291 | 292 | for( var i =0, l=collisionList.length; i= fuse.duration && !fuse.active && !fuse.timeDone) { 450 | //Vibrate 451 | fuse.timeDone=true; 452 | fuse.mesh.visible = false; 453 | vibrate( fuse.vibratePattern ); 454 | //Does object have an action assigned to it? 455 | if (threeObject.onGazeLong != null) { 456 | threeObject.onGazeLong(); 457 | } 458 | //Reset the clock 459 | threeObject.userData.hitTime = elapsed; 460 | } else { 461 | fuse.update(gazeTime); 462 | } 463 | }; 464 | 465 | var gazeClick = function( threeObject ) { 466 | var clickCancelFuse = threeObject.reticulumData.clickCancelFuse != null ? threeObject.reticulumData.clickCancelFuse : fuse.clickCancel; 467 | //Cancel Fuse 468 | if( clickCancelFuse ) { 469 | //Reset the clock 470 | threeObject.userData.hitTime = clock.getElapsedTime(); 471 | //Force gaze to end...this might be to assumptions 472 | fuse.update( fuse.duration ); 473 | } 474 | 475 | //Does object have an action assigned to it? 476 | if (threeObject.onGazeClick != null) { 477 | threeObject.onGazeClick(); 478 | } 479 | }; 480 | 481 | //This function is called on click or touch events 482 | var touchClickHandler = function(e) { 483 | if( reticle.hit && INTERSECTED ) { 484 | e.preventDefault(); 485 | gazeClick(INTERSECTED); 486 | } 487 | } 488 | 489 | 490 | return { 491 | add: function (threeObject, options) { 492 | var parameters = options || {}; 493 | 494 | //Stores object options for reticulum 495 | threeObject.reticulumData = {}; 496 | threeObject.reticulumData.gazeable = true; 497 | //Reticle 498 | threeObject.reticulumData.reticleHoverColor = null; 499 | if(parameters.reticleHoverColor) { 500 | threeObject.reticulumData.reticleHoverColor = new THREE.Color(parameters.reticleHoverColor); 501 | } 502 | //Fuse 503 | threeObject.reticulumData.fuseDuration = parameters.fuseDuration || null; 504 | threeObject.reticulumData.fuseColor = parameters.fuseColor || null; 505 | threeObject.reticulumData.fuseVisible = parameters.fuseVisible === undefined ? null : parameters.fuseVisible; 506 | threeObject.reticulumData.clickCancelFuse = parameters.clickCancelFuse === undefined ? null : parameters.clickCancelFuse; 507 | //Events 508 | threeObject.onGazeOver = parameters.onGazeOver || null; 509 | threeObject.onGazeOut = parameters.onGazeOut || null; 510 | threeObject.onGazeLong = parameters.onGazeLong || null; 511 | threeObject.onGazeClick = parameters.onGazeClick || null; 512 | 513 | 514 | //Add object to list 515 | collisionList.push(threeObject); 516 | }, 517 | remove: function (threeObject) { 518 | var index = collisionList.indexOf(threeObject); 519 | threeObject.reticulumData.gazeable = false; 520 | if (index > -1) { 521 | collisionList.splice(index, 1); 522 | } 523 | }, 524 | update: function () { 525 | var delta = clock.getDelta(); // 526 | detectHit(); 527 | 528 | //Proximity 529 | if(settings.proximity) { 530 | proximity(); 531 | } 532 | 533 | //Animation 534 | reticle.update(delta); 535 | 536 | }, 537 | init: function (camera, options) { 538 | var c = camera || null; 539 | var o = options || {}; 540 | if ( !c instanceof THREE.Camera ) { 541 | console.error("ERROR: Camera was not correctly defined. Unable to initiate Reticulum."); 542 | return; 543 | } 544 | initiate(c, o); 545 | } 546 | }; 547 | })(); 548 | 549 | export default Reticulum --------------------------------------------------------------------------------