├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist └── cardboard-vr-display.js ├── examples ├── custom-viewer.html ├── iframe.html ├── img │ └── box.png ├── index.html └── magicwindow.html ├── index.html ├── licenses.txt ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── assets │ └── rotate-instructions.js ├── base.js ├── cardboard-distorter.js ├── cardboard-ui.js ├── cardboard-vr-display.js ├── device-info.js ├── distortion.js ├── dpdb.js ├── math-util.js ├── options.js ├── pose-sensor.js ├── rotate-instructions.js ├── sensor-fusion │ ├── complementary-filter.js │ ├── fusion-pose-sensor.js │ ├── pose-predictor.js │ └── sensor-sample.js ├── util.js └── viewer-selector.js └── third_party └── three.js ├── LICENSE ├── METADATA ├── VRControls.js ├── VREffect.js └── three.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }] 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pid 3 | *.seed 4 | *.swp 5 | *.un~ 6 | *~ 7 | .DS_Store 8 | .netrwhist 9 | .node_repl_history 10 | .npm 11 | Session.vim 12 | [._]*.s[a-w][a-z] 13 | [._]s[a-w][a-z] 14 | logs 15 | node_modules 16 | pids 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pid 3 | *.seed 4 | *.un~ 5 | *~ 6 | .DS_Store 7 | .netrwhist 8 | .node_repl_history 9 | .npm 10 | Session.vim 11 | [._]*.s[a-w][a-z] 12 | [._]s[a-w][a-z] 13 | examples 14 | logs 15 | node_modules 16 | pids 17 | third_party 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Google Inc 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cardboard-vr-display 2 | 3 | [![Build Status](http://img.shields.io/travis/immersive-web/cardboard-vr-display.svg?style=flat-square)](https://travis-ci.org/immersive-web/cardboard-vr-display) 4 | [![Build Status](http://img.shields.io/npm/v/cardboard-vr-display.svg?style=flat-square)](https://www.npmjs.org/package/cardboard-vr-display) 5 | 6 | A JavaScript implementation of a [WebVR 1.1 VRDisplay][VRDisplay]. This is the magic 7 | behind rendering distorted stereoscopic views for browsers that do not support the [WebVR API] 8 | with the [webvr-polyfill]. 9 | 10 | Unless you're building a WebVR wrapper, you probably want to use [webvr-polyfill] directly 11 | rather than this. This component **does not** polyfill interfaces like `VRFrameData` and 12 | `navigator.getVRDisplays`, and up to the consumer, although trivial (see examples). 13 | 14 | ## How It Works 15 | 16 | As of [1.0.4](https://github.com/immersive-web/cardboard-vr-display/tree/v1.0.4), `CardboardVRDisplay` uses [RelativeOrientationSensor] for orientation tracking, 17 | falling back to [DeviceMotionEvents] using [sensor fusion and pose prediction][fusion]. 18 | [RelativeOrientationSensor] is a new API ([read more about the new Sensors on the web][sensors]) 19 | first implemented in Chrome M63. This API uses the new [Feature Policy] specification which allows 20 | developers to selectively enable or disable browser features. 21 | 22 | It can also render in stereo mode, and includes mesh-based 23 | lens distortion. This display also includes user interface elements in VR mode 24 | to make the VR experience more intuitive, including: 25 | 26 | * A gear icon to select your VR viewer. 27 | * A back button to exit VR mode. 28 | * An interstitial which only appears in portrait orientation, requesting you switch 29 | into landscape orientation (if [orientation lock][ol] is not available). 30 | 31 | ### iframes 32 | 33 | By default, main frames and same-origin iframes have access to Sensor APIs, 34 | but cross-origin iframes must specify feature policy and allow `gyroscope` and 35 | `accelerometer` features. If your experience is attempting to use the native 36 | [WebXR Device API] in an iframe, you'll have to specify that feature as well ([WebXR's 37 | feature name may change](https://github.com/immersive-web/webxr/issues/308)). All of these features require HTTPS to function, except for `localhost`, where HTTP is allowed. 38 | 39 | ```html 40 | 41 | ``` 42 | 43 | While `devicemotion` is a fallback for Sensors, eventually `devicemotion` will be behind the same 44 | Feature Policy as Sensors and it is encouraged to adhere to these policies in the meantime. 45 | If the Feature Policy for Sensors is denied, `CardboardVRDisplay` will **not** always attempt 46 | to fall back to `devicemotion`. Using Feature Policies now will guarantee a more future-proof experience. 47 | 48 | #### Caveats 49 | 50 | * On iOS, cross-origin iframes do not have access to the `devicemotion` events. 51 | The `CardboardVRDisplay` however does respond to events passed in from a parent 52 | frame via `postMessage`. See the [iframe example][iframe-example] to see how 53 | the events must be formatted. 54 | * Chrome M63 supports Sensors, although not the corresponding Feature Policy [until Chrome M65][sensors-main-frame]. 55 | This results in Chrome M63/M64 only supporting Sensors in main frames, and these browsers 56 | will fall back to using devicemotion if in iframes. 57 | * Using Sensors in a cross-origin iframe [requires the frame to be in focus](https://www.w3.org/TR/generic-sensor/#focused-area). In builds of Chrome prior to M69, this logic is [erroneously reversed](https://bugs.chromium.org/p/chromium/issues/detail?id=849501). If loading content via cross-origin iframe, you can disable Sensors, triggering the `devicemotion` fallback with this [hacky workaround](https://github.com/immersive-web/cardboard-vr-display/blob/c196e15a8c7ccf594fe6a5044fbdcb51cc2eff91/examples/index.html#L117-L124). More info in [#27](https://github.com/immersive-web/cardboard-vr-display/issues/27). 58 | 59 | ### Magic Window 60 | 61 | It is possible to have a magic window using a VRDisplay that isn't 100% width/height of the window, and can jump into fullscreen WebVR. See the [magic window][magicwindow-example] for usage. 62 | 63 | ## Installation 64 | 65 | ``` 66 | $ npm install --save cardboard-vr-display 67 | ``` 68 | 69 | ## Browser Support 70 | 71 | Should support most modern browsers (IE11 is missing a few, for example) and requires [ES5](https://kangax.github.io/compat-table/es5/) JavaScript support. If you want to support a non-ES5 browser, or browser lacking some DOM globals, you must use a transformation or provide polyfills to support older environments. 72 | 73 | Globals required: 74 | 75 | * [`Promise`](https://caniuse.com/#feat=promises) 76 | * [`CustomEvent`](https://caniuse.com/#feat=customevent) 77 | * [`requestAnimationFrame`](https://caniuse.com/#feat=requestanimationframe) 78 | 79 | Additionally, WebGL support, [`devicemotion`](https://caniuse.com/#feat=deviceorientation) events, and common browser globals (`window`, `navigator`, `document`) are also required in the environment. 80 | 81 | ## Usage 82 | 83 | `cardboard-vr-display` exposes a constructor for a `CardboardVRDisplay` that takes 84 | a single options configuration, detailed below. Check out [running the demo](#running-the-demo) 85 | to try the different options. 86 | 87 | ```js 88 | import CardboardVRDisplay from 'cardboard-vr-display'; 89 | 90 | // Default options 91 | const options = { 92 | // Optionally inject custom Viewer parameters as an option. Each item 93 | // in the array must be an object with the following properties; here is 94 | // an example of the built in CardboardV2 viewer: 95 | // 96 | // { 97 | // id: 'CardboardV2', 98 | // label: 'Cardboard I/O 2015', 99 | // fov: 60, 100 | // interLensDistance: 0.064, 101 | // baselineLensDistance: 0.035, 102 | // screenLensDistance: 0.039, 103 | // distortionCoefficients: [0.34, 0.55], 104 | // inverseCoefficients: [-0.33836704, -0.18162185, 0.862655, -1.2462051, 105 | // 1.0560602, -0.58208317, 0.21609078, -0.05444823, 0.009177956, 106 | // -9.904169E-4, 6.183535E-5, -1.6981803E-6] 107 | // } 108 | // Added in 1.0.12. 109 | ADDITIONAL_VIEWERS: [], 110 | 111 | // Select the viewer by ID. If unspecified, defaults to 'CardboardV1'. 112 | // Added in 1.0.12. 113 | DEFAULT_VIEWER: '', 114 | 115 | // By default, on mobile, a wakelock is necessary to prevent the device's screen 116 | // from turning off without user input. Disable if you're keeping the screen awake through 117 | // other means on mobile. A wakelock is never used on desktop. 118 | // Added in 1.0.3. 119 | MOBILE_WAKE_LOCK: true, 120 | 121 | // Whether or not CardboardVRDisplay is in debug mode. Logs extra 122 | // messages. Added in 1.0.2. 123 | DEBUG: false, 124 | 125 | // The URL to JSON of DPDB information. By default, uses the data 126 | // from https://github.com/WebVRRocks/webvr-polyfill-dpdb; if left 127 | // falsy, then no attempt is made. 128 | // Added in 1.0.1 129 | DPDB_URL: 'https://dpdb.webvr.rocks/dpdb.json', 130 | 131 | // Complementary filter coefficient. 0 for accelerometer, 1 for gyro. 132 | K_FILTER: 0.98, 133 | 134 | // How far into the future to predict during fast motion (in seconds). 135 | PREDICTION_TIME_S: 0.040, 136 | 137 | // Flag to disabled the UI in VR Mode. 138 | CARDBOARD_UI_DISABLED: false, 139 | 140 | // Flag to disable the instructions to rotate your device. 141 | ROTATE_INSTRUCTIONS_DISABLED: false, 142 | 143 | // Enable yaw panning only, disabling roll and pitch. This can be useful 144 | // for panoramas with nothing interesting above or below. 145 | YAW_ONLY: false, 146 | 147 | // Scales the recommended buffer size reported by WebVR, which can improve 148 | // performance. 149 | // UPDATE(2016-05-03): Setting this to 0.5 by default since 1.0 does not 150 | // perform well on many mobile devices. 151 | BUFFER_SCALE: 0.5, 152 | 153 | // Allow VRDisplay.submitFrame to change gl bindings, which is more 154 | // efficient if the application code will re-bind its resources on the 155 | // next frame anyway. This has been seen to cause rendering glitches with 156 | // THREE.js. 157 | // Dirty bindings include: gl.FRAMEBUFFER_BINDING, gl.CURRENT_PROGRAM, 158 | // gl.ARRAY_BUFFER_BINDING, gl.ELEMENT_ARRAY_BUFFER_BINDING, 159 | // and gl.TEXTURE_BINDING_2D for texture unit 0. 160 | DIRTY_SUBMIT_FRAME_BINDINGS: false, 161 | }; 162 | 163 | const display = new CardboardVRDisplay(options); 164 | 165 | function MockVRFrameData () { 166 | this.leftViewMatrix = new Float32Array(16); 167 | this.rightViewMatrix = new Float32Array(16); 168 | this.leftProjectionMatrix = new Float32Array(16); 169 | this.rightProjectionMatrix = new Float32Array(16); 170 | this.pose = null; 171 | }; 172 | 173 | const frame = new (window.VRFrameData || MockVRFrameData)(); 174 | 175 | display.isConnected; // true 176 | display.getFrameData(frame); 177 | 178 | frame.rightViewMatrix; // Float32Array 179 | frame.pose; // { orientation, position } 180 | ``` 181 | 182 | ## Development 183 | 184 | * `npm install`: installs the dependencies. 185 | * `npm run build`: builds the distributable. 186 | * `npm run watch`: watches `src/` for changes and rebuilds on change. 187 | 188 | ### Releasing a new version 189 | 190 | For maintainers only, to cut a new release for npm, use the [npm version] command. The `preversion`, `version` and `postversion` npm scripts will run tests, build, add built files and tag to git, push to github, and publish the new npm version. 191 | 192 | `npm version ` 193 | 194 | ## Running The Demo 195 | 196 | View the [example] to see a demo running the CardboardVRDisplay. This executes 197 | a minimal WebVR 1.1 polyfill and parses query params to inject configuration parameters. 198 | View some premade links at [index.html]. For example, to set the buffer scale to 1.0 199 | and limit rotation to yaw, go to [https://immersive-web.github.io/cardboard-vr-display/examples/index.html?YAW_ONLY=true&BUFFER_SCALE=1.0]. 200 | View all config options at `src/options.js`. 201 | 202 | ## License 203 | 204 | This program is free software for both commercial and non-commercial use, 205 | distributed under the [Apache 2.0 License](LICENSE). 206 | 207 | [VRDisplay]: https://immersive-web.github.io/webvr/spec/1.1/#interface-vrdisplay 208 | [WebVR API]: https://immersive-web.github.io/webvr/spec/1.1/ 209 | [WebXR Device API]: https://immersive-web.github.io/webxr/spec/latest/ 210 | [webvr-polyfill]: https://github.com/immersive-web/webvr-polyfill 211 | [example]: https://immersive-web.github.io/cardboard-vr-display/examples 212 | [iframe-example]: examples/iframe.html 213 | [magicwindow-example]: examples/magicwindow.html 214 | [index.html]: https://immersive-web.github.io/cardboard-vr-display 215 | [fusion]: http://smus.com/sensor-fusion-prediction-webvr/ 216 | [ol]: https://www.w3.org/TR/screen-orientation/ 217 | [sensors]: https://developers.google.com/web/updates/2017/09/sensors-for-the-web 218 | [DeviceMotionEvents]: https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent 219 | [RelativeOrientationSensor]: https://www.w3.org/TR/orientation-sensor/#relativeorientationsensor-model 220 | [Feature Policy]: https://wicg.github.io/feature-policy/ 221 | [sensors-main-frame]: https://developers.google.com/web/updates/2017/09/sensors-for-the-web#feature_policy_integration 222 | -------------------------------------------------------------------------------- /examples/custom-viewer.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | Cardboard VRDisplay Demo 23 | 24 | 25 | 26 | 27 | 28 | 52 | 53 | 54 | 55 | 56 |
57 | 58 | 59 | 60 |
61 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /examples/iframe.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | Cardboard VRDisplay iframe test 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 | 42 | 43 | 44 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /examples/img/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immersive-web/cardboard-vr-display/935b67866d8fb749dc0cd71f1f35cb120ce01a30/examples/img/box.png -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | Cardboard VRDisplay Demo 23 | 24 | 25 | 26 | 27 | 28 | 52 | 53 | 54 | 55 | 56 |
57 | 58 | 59 | 60 |
61 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /examples/magicwindow.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | Cardboard VRDisplay Magic Window Demo 23 | 24 | 25 | 26 | 27 | 28 | 58 | 59 | 60 | 61 | 62 |
63 | 64 | 65 | 66 |
67 |
68 | 69 | 70 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | Cardboard VRDisplay 22 | 23 | 24 | 25 | 26 | 27 | 28 | 56 | 57 | 58 | 59 |
60 |

cardboard-vr-display

61 |

62 | cardboard-vr-display 63 | 64 | is a JavaScript implementation of a WebVR 1.1 VRDisplay. This is the magic behind rendering distorted stereoscopic views for browsers that do not support the WebVR API with the webvr-polyfill. 65 |

66 |

Demos

67 |

The base example parses the query params in the URL to pass configuration into a CardboardVRDisplay. Some examples links below. 68 |

69 | 76 |

There are other examples for testing other use cases. They also respond to the query params passed into the URL.

77 | 82 |
83 | 84 | 85 | -------------------------------------------------------------------------------- /licenses.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * cardboard-vr-display 4 | * Copyright (c) 2015-2017 Google 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @license 20 | * gl-preserve-state 21 | * Copyright (c) 2016, Brandon Jones. 22 | * 23 | * Permission is hereby granted, free of charge, to any person obtaining a copy 24 | * of this software and associated documentation files (the "Software"), to deal 25 | * in the Software without restriction, including without limitation the rights 26 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 27 | * copies of the Software, and to permit persons to whom the Software is 28 | * furnished to do so, subject to the following conditions: 29 | * 30 | * The above copyright notice and this permission notice shall be included in 31 | * all copies or substantial portions of the Software. 32 | * 33 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 34 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 35 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 36 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 37 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 38 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 39 | * THE SOFTWARE. 40 | */ 41 | 42 | /** 43 | * @license 44 | * webvr-polyfill-dpdb 45 | * Copyright (c) 2015-2017 Google 46 | * Licensed under the Apache License, Version 2.0 (the "License"); 47 | * you may not use this file except in compliance with the License. 48 | * You may obtain a copy of the License at 49 | * 50 | * http://www.apache.org/licenses/LICENSE-2.0 51 | * 52 | * Unless required by applicable law or agreed to in writing, software 53 | * distributed under the License is distributed on an "AS IS" BASIS, 54 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 55 | * See the License for the specific language governing permissions and 56 | * limitations under the License. 57 | */ 58 | 59 | /** 60 | * @license 61 | * nosleep.js 62 | * Copyright (c) 2017, Rich Tibbett 63 | * 64 | * Permission is hereby granted, free of charge, to any person obtaining a copy 65 | * of this software and associated documentation files (the "Software"), to deal 66 | * in the Software without restriction, including without limitation the rights 67 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 68 | * copies of the Software, and to permit persons to whom the Software is 69 | * furnished to do so, subject to the following conditions: 70 | * 71 | * The above copyright notice and this permission notice shall be included in 72 | * all copies or substantial portions of the Software. 73 | * 74 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 75 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 76 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 77 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 78 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 79 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 80 | * THE SOFTWARE. 81 | */ 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cardboard-vr-display", 3 | "version": "1.0.19", 4 | "homepage": "https://github.com/immersive-web/cardboard-vr-display", 5 | "authors": [ 6 | "Boris Smus ", 7 | "Brandon Jones ", 8 | "Jordan Santell " 9 | ], 10 | "description": "A Cardboard VR implementation of a WebVR 1.1 VRDisplay for polyfilling the WebVR API", 11 | "devDependencies": { 12 | "babel-core": "^6.26.3", 13 | "babel-plugin-external-helpers": "^6.22.0", 14 | "babel-plugin-transform-class-properties": "^6.24.1", 15 | "babel-preset-env": "^1.7.0", 16 | "rollup": "^0.50.0", 17 | "rollup-plugin-babel": "^3.0.2", 18 | "rollup-plugin-cleanup": "^1.0.1", 19 | "rollup-plugin-commonjs": "^8.2.1", 20 | "rollup-plugin-json": "^2.3.0", 21 | "rollup-plugin-node-resolve": "^3.0.0" 22 | }, 23 | "main": "dist/cardboard-vr-display", 24 | "keywords": [ 25 | "vr", 26 | "webvr" 27 | ], 28 | "license": "Apache-2.0", 29 | "scripts": { 30 | "test": "echo \"No tests defined\"", 31 | "build": "rollup -c", 32 | "watch": "rollup -c -w", 33 | "preversion": "npm test", 34 | "version": "npm run build && git add dist/*", 35 | "postversion": "git push && git push --tags && npm publish" 36 | }, 37 | "repository": "immersive-web/cardboard-vr-display", 38 | "bugs": { 39 | "url": "https://github.com/immersive-web/cardboard-vr-display/issues" 40 | }, 41 | "dependencies": { 42 | "gl-preserve-state": "^1.0.0", 43 | "nosleep.js": "^0.7.0", 44 | "webvr-polyfill-dpdb": "^1.0.17" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import fs from 'fs'; 17 | import path from 'path'; 18 | import json from 'rollup-plugin-json'; 19 | import commonjs from 'rollup-plugin-commonjs'; 20 | import resolve from 'rollup-plugin-node-resolve'; 21 | import cleanup from 'rollup-plugin-cleanup'; 22 | const babel = require('rollup-plugin-babel'); 23 | 24 | const banner = fs.readFileSync(path.join(__dirname, 'licenses.txt')); 25 | 26 | export default { 27 | input: 'src/cardboard-vr-display.js', 28 | output: { 29 | file: './dist/cardboard-vr-display.js', 30 | format: 'umd', 31 | name: 'CardboardVRDisplay', 32 | }, 33 | banner: banner, 34 | plugins: [ 35 | json(), 36 | babel({ 37 | plugins: ['external-helpers'], 38 | exclude: 'node_modules/**', 39 | }), 40 | resolve(), 41 | commonjs(), 42 | cleanup(), 43 | ] 44 | }; 45 | -------------------------------------------------------------------------------- /src/base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import * as Util from './util.js'; 17 | import NoSleep from 'nosleep.js/dist/NoSleep.js'; 18 | 19 | // Start at a higher number to reduce chance of conflict. 20 | var nextDisplayId = 1000; 21 | 22 | var defaultLeftBounds = [0, 0, 0.5, 1]; 23 | var defaultRightBounds = [0.5, 0, 0.5, 1]; 24 | 25 | var raf = window.requestAnimationFrame; 26 | var caf = window.cancelAnimationFrame; 27 | 28 | /** 29 | * The base class for all VR frame data. 30 | */ 31 | 32 | export function VRFrameData() { 33 | this.leftProjectionMatrix = new Float32Array(16); 34 | this.leftViewMatrix = new Float32Array(16); 35 | this.rightProjectionMatrix = new Float32Array(16); 36 | this.rightViewMatrix = new Float32Array(16); 37 | this.pose = null; 38 | }; 39 | 40 | export function VRDisplayCapabilities (config) { 41 | Object.defineProperties(this, { 42 | hasPosition: { 43 | writable: false, enumerable: true, value: config.hasPosition, 44 | }, 45 | hasExternalDisplay: { 46 | writable: false, enumerable: true, value: config.hasExternalDisplay, 47 | }, 48 | canPresent: { 49 | writable: false, enumerable: true, value: config.canPresent, 50 | }, 51 | maxLayers: { 52 | writable: false, enumerable: true, value: config.maxLayers, 53 | }, 54 | hasOrientation: { 55 | enumerable: true, get: function() { 56 | Util.deprecateWarning('VRDisplayCapabilities.prototype.hasOrientation', 57 | 'VRDisplay.prototype.getFrameData'); 58 | return config.hasOrientation; 59 | }, 60 | }, 61 | }); 62 | } 63 | 64 | /** 65 | * The base class for all VR displays. 66 | */ 67 | export function VRDisplay(config) { 68 | config = config || {}; 69 | var USE_WAKELOCK = 'wakelock' in config ? config.wakelock : true; 70 | 71 | this.isPolyfilled = true; 72 | this.displayId = nextDisplayId++; 73 | this.displayName = ''; 74 | 75 | this.depthNear = 0.01; 76 | this.depthFar = 10000.0; 77 | 78 | this.isPresenting = false; 79 | 80 | Object.defineProperty(this, 'isConnected', { 81 | get: function() { 82 | Util.deprecateWarning('VRDisplay.prototype.isConnected', 83 | 'VRDisplayCapabilities.prototype.hasExternalDisplay'); 84 | return false; 85 | }, 86 | }); 87 | 88 | this.capabilities = new VRDisplayCapabilities({ 89 | hasPosition: false, 90 | hasOrientation: false, 91 | hasExternalDisplay: false, 92 | canPresent: false, 93 | maxLayers: 1 94 | }); 95 | 96 | this.stageParameters = null; 97 | 98 | // "Private" members. 99 | this.waitingForPresent_ = false; 100 | this.layer_ = null; 101 | // Keep track of the original parent of the source passed into 102 | // `requestPresent`. While the fullscreenWrapper will be a child of the parent 103 | // in most cases, we must keep track when there's no parent (like 104 | // when it was never in the DOM, e.g. WebXR polyfill), or if the `source` 105 | // changes during presentation (in which case we need something to track 106 | // the newer `source`'s parent when we clean up). 107 | this.originalParent_ = null; 108 | 109 | this.fullscreenElement_ = null; 110 | this.fullscreenWrapper_ = null; 111 | this.fullscreenElementCachedStyle_ = null; 112 | 113 | this.fullscreenEventTarget_ = null; 114 | this.fullscreenChangeHandler_ = null; 115 | this.fullscreenErrorHandler_ = null; 116 | 117 | // Get an appropriate wakelock for Android or iOS if MOBILE_WAKE_LOCK 118 | // is true. 119 | if (USE_WAKELOCK && Util.isMobile()) { 120 | this.wakelock_ = new NoSleep(); 121 | } 122 | } 123 | 124 | VRDisplay.prototype.getFrameData = function(frameData) { 125 | // TODO: Technically this should retain it's value for the duration of a frame 126 | // but I doubt that's practical to do in javascript. 127 | return Util.frameDataFromPose(frameData, this._getPose(), this); 128 | }; 129 | 130 | VRDisplay.prototype.getPose = function() { 131 | // TODO: Technically this should retain it's value for the duration of a frame 132 | // but I doubt that's practical to do in javascript. 133 | Util.deprecateWarning('VRDisplay.prototype.getPose', 134 | 'VRDisplay.prototype.getFrameData'); 135 | return this._getPose(); 136 | }; 137 | 138 | VRDisplay.prototype.resetPose = function() { 139 | Util.deprecateWarning('VRDisplay.prototype.resetPose'); 140 | return this._resetPose(); 141 | }; 142 | 143 | VRDisplay.prototype.getImmediatePose = function() { 144 | // TODO: Technically this should retain it's value for the duration of a frame 145 | // but I doubt that's practical to do in javascript. 146 | Util.deprecateWarning('VRDisplay.prototype.getImmediatePose', 147 | 'VRDisplay.prototype.getFrameData'); 148 | return this._getPose(); 149 | }; 150 | 151 | VRDisplay.prototype.requestAnimationFrame = function(callback) { 152 | return raf(callback); 153 | }; 154 | 155 | VRDisplay.prototype.cancelAnimationFrame = function(id) { 156 | return caf(id); 157 | }; 158 | 159 | VRDisplay.prototype.wrapForFullscreen = function(element) { 160 | // Don't wrap in iOS. 161 | if (Util.isIOS()) { 162 | return element; 163 | } 164 | if (!this.fullscreenWrapper_) { 165 | this.fullscreenWrapper_ = document.createElement('div'); 166 | var cssProperties = [ 167 | 'height: ' + Math.min(screen.height, screen.width) + 'px !important', 168 | 'top: 0 !important', 169 | 'left: 0 !important', 170 | 'right: 0 !important', 171 | 'border: 0', 172 | 'margin: 0', 173 | 'padding: 0', 174 | 'z-index: 999999 !important', 175 | 'position: fixed', 176 | ]; 177 | this.fullscreenWrapper_.setAttribute('style', cssProperties.join('; ') + ';'); 178 | this.fullscreenWrapper_.classList.add('webvr-polyfill-fullscreen-wrapper'); 179 | } 180 | 181 | if (this.fullscreenElement_ == element) { 182 | return this.fullscreenWrapper_; 183 | } 184 | 185 | // If fullscreenElement_ already exists, swap it out with the new element. 186 | // This is necessary for changing the layer's `source` context, beyond just 187 | // changing the bounds. This is used in the WebXRPolyfill, which calls requestPresent 188 | // twice -- once with a dummy canvas to call requestFullscreen immediately after 189 | // a user gesture, and again once an `XRSession`'s `baseLayer` is set (with the 190 | // real canvas). 191 | if (this.fullscreenElement_) { 192 | // Move the current fullscreenElement_ back to its originalParent_ 193 | if (this.originalParent_) { 194 | this.originalParent_.appendChild(this.fullscreenElement_); 195 | } else { 196 | this.fullscreenElement_.parentElement.removeChild(this.fullscreenElement_); 197 | } 198 | } 199 | 200 | this.fullscreenElement_ = element; 201 | this.originalParent_ = element.parentElement; 202 | // We may have to inject the canvas in the DOM 203 | if (!this.originalParent_) { 204 | document.body.appendChild(element); 205 | } 206 | 207 | // If the fullscreenWrapper is already in the DOM, don't move it. Otherwise, 208 | // make it a child of `element`'s parent. 209 | if (!this.fullscreenWrapper_.parentElement) { 210 | var parent = this.fullscreenElement_.parentElement; 211 | parent.insertBefore(this.fullscreenWrapper_, this.fullscreenElement_); 212 | parent.removeChild(this.fullscreenElement_); 213 | } 214 | 215 | this.fullscreenWrapper_.insertBefore(this.fullscreenElement_, this.fullscreenWrapper_.firstChild); 216 | this.fullscreenElementCachedStyle_ = this.fullscreenElement_.getAttribute('style'); 217 | 218 | var self = this; 219 | function applyFullscreenElementStyle() { 220 | if (!self.fullscreenElement_) { 221 | return; 222 | } 223 | 224 | var cssProperties = [ 225 | 'position: absolute', 226 | 'top: 0', 227 | 'left: 0', 228 | 'width: ' + Math.max(screen.width, screen.height) + 'px', 229 | 'height: ' + Math.min(screen.height, screen.width) + 'px', 230 | 'border: 0', 231 | 'margin: 0', 232 | 'padding: 0', 233 | ]; 234 | self.fullscreenElement_.setAttribute('style', cssProperties.join('; ') + ';'); 235 | } 236 | 237 | applyFullscreenElementStyle(); 238 | 239 | return this.fullscreenWrapper_; 240 | }; 241 | 242 | VRDisplay.prototype.removeFullscreenWrapper = function() { 243 | if (!this.fullscreenElement_) { 244 | return; 245 | } 246 | 247 | var element = this.fullscreenElement_; 248 | if (this.fullscreenElementCachedStyle_) { 249 | element.setAttribute('style', this.fullscreenElementCachedStyle_); 250 | } else { 251 | element.removeAttribute('style'); 252 | } 253 | 254 | this.fullscreenElement_ = null; 255 | this.fullscreenElementCachedStyle_ = null; 256 | 257 | var parent = this.fullscreenWrapper_.parentElement; 258 | this.fullscreenWrapper_.removeChild(element); 259 | 260 | if (this.originalParent_ === parent) { 261 | parent.insertBefore(element, this.fullscreenWrapper_); 262 | } 263 | // If it has an original parent but different than the wrapper parent, 264 | // make a best attempt at reinserting into the DOM. This occurs when swapping 265 | // canvases during a presentation. 266 | else if (this.originalParent_) { 267 | this.originalParent_.appendChild(element); 268 | } 269 | 270 | parent.removeChild(this.fullscreenWrapper_); 271 | 272 | return element; 273 | }; 274 | 275 | VRDisplay.prototype.requestPresent = function(layers) { 276 | var wasPresenting = this.isPresenting; 277 | var self = this; 278 | 279 | if (!(layers instanceof Array)) { 280 | Util.deprecateWarning('VRDisplay.prototype.requestPresent with non-array argument', 281 | 'an array of VRLayers as the first argument'); 282 | layers = [layers]; 283 | } 284 | 285 | return new Promise(function(resolve, reject) { 286 | if (!self.capabilities.canPresent) { 287 | reject(new Error('VRDisplay is not capable of presenting.')); 288 | return; 289 | } 290 | 291 | if (layers.length == 0 || layers.length > self.capabilities.maxLayers) { 292 | reject(new Error('Invalid number of layers.')); 293 | return; 294 | } 295 | 296 | var incomingLayer = layers[0]; 297 | if (!incomingLayer.source) { 298 | /* 299 | todo: figure out the correct behavior if the source is not provided. 300 | see https://github.com/w3c/webvr/issues/58 301 | */ 302 | resolve(); 303 | return; 304 | } 305 | 306 | var leftBounds = incomingLayer.leftBounds || defaultLeftBounds; 307 | var rightBounds = incomingLayer.rightBounds || defaultRightBounds; 308 | if (wasPresenting) { 309 | // Already presenting, just changing configuration 310 | var layer = self.layer_; 311 | if (layer.source !== incomingLayer.source) { 312 | layer.source = incomingLayer.source; 313 | } 314 | 315 | for (var i = 0; i < 4; i++) { 316 | layer.leftBounds[i] = leftBounds[i]; 317 | layer.rightBounds[i] = rightBounds[i]; 318 | } 319 | 320 | // Call another wrap to swap out canvases in the fullscreen wrapper. 321 | self.wrapForFullscreen(self.layer_.source); 322 | self.updatePresent_(); 323 | resolve(); 324 | return; 325 | } 326 | 327 | // Was not already presenting. 328 | self.layer_ = { 329 | predistorted: incomingLayer.predistorted, 330 | source: incomingLayer.source, 331 | leftBounds: leftBounds.slice(0), 332 | rightBounds: rightBounds.slice(0) 333 | }; 334 | 335 | self.waitingForPresent_ = false; 336 | if (self.layer_ && self.layer_.source) { 337 | 338 | var fullscreenElement = self.wrapForFullscreen(self.layer_.source); 339 | 340 | var onFullscreenChange = function() { 341 | var actualFullscreenElement = Util.getFullscreenElement(); 342 | 343 | self.isPresenting = (fullscreenElement === actualFullscreenElement); 344 | if (self.isPresenting) { 345 | if (screen.orientation && screen.orientation.lock) { 346 | screen.orientation.lock('landscape-primary').catch(function(error){ 347 | console.error('screen.orientation.lock() failed due to', error.message) 348 | }); 349 | } 350 | self.waitingForPresent_ = false; 351 | self.beginPresent_(); 352 | resolve(); 353 | } else { 354 | if (screen.orientation && screen.orientation.unlock) { 355 | screen.orientation.unlock(); 356 | } 357 | self.removeFullscreenWrapper(); 358 | self.disableWakeLock(); 359 | self.endPresent_(); 360 | self.removeFullscreenListeners_(); 361 | } 362 | self.fireVRDisplayPresentChange_(); 363 | } 364 | var onFullscreenError = function() { 365 | if (!self.waitingForPresent_) { 366 | return; 367 | } 368 | 369 | self.removeFullscreenWrapper(); 370 | self.removeFullscreenListeners_(); 371 | 372 | self.disableWakeLock(); 373 | self.waitingForPresent_ = false; 374 | self.isPresenting = false; 375 | 376 | reject(new Error('Unable to present.')); 377 | } 378 | 379 | self.addFullscreenListeners_(fullscreenElement, 380 | onFullscreenChange, onFullscreenError); 381 | 382 | if (Util.requestFullscreen(fullscreenElement)) { 383 | self.enableWakeLock(); 384 | self.waitingForPresent_ = true; 385 | } else if (Util.isIOS() || Util.isWebViewAndroid()) { 386 | // *sigh* Just fake it. 387 | self.enableWakeLock(); 388 | self.isPresenting = true; 389 | self.beginPresent_(); 390 | self.fireVRDisplayPresentChange_(); 391 | resolve(); 392 | } 393 | } 394 | 395 | if (!self.waitingForPresent_ && !Util.isIOS()) { 396 | Util.exitFullscreen(); 397 | reject(new Error('Unable to present.')); 398 | } 399 | }); 400 | }; 401 | 402 | VRDisplay.prototype.exitPresent = function() { 403 | var wasPresenting = this.isPresenting; 404 | var self = this; 405 | this.isPresenting = false; 406 | this.layer_ = null; 407 | this.disableWakeLock(); 408 | 409 | return new Promise(function(resolve, reject) { 410 | if (wasPresenting) { 411 | if (!Util.exitFullscreen() && Util.isIOS()) { 412 | self.endPresent_(); 413 | self.fireVRDisplayPresentChange_(); 414 | } 415 | 416 | if (Util.isWebViewAndroid()) { 417 | self.removeFullscreenWrapper(); 418 | self.removeFullscreenListeners_(); 419 | self.endPresent_(); 420 | self.fireVRDisplayPresentChange_(); 421 | } 422 | 423 | resolve(); 424 | } else { 425 | reject(new Error('Was not presenting to VRDisplay.')); 426 | } 427 | }); 428 | }; 429 | 430 | VRDisplay.prototype.getLayers = function() { 431 | if (this.layer_) { 432 | return [this.layer_]; 433 | } 434 | return []; 435 | }; 436 | 437 | VRDisplay.prototype.fireVRDisplayPresentChange_ = function() { 438 | // Important: unfortunately we cannot have full spec compliance here. 439 | // CustomEvent custom fields all go under e.detail (so the VRDisplay ends up 440 | // being e.detail.display, instead of e.display as per WebVR spec). 441 | var event = new CustomEvent('vrdisplaypresentchange', {detail: {display: this}}); 442 | window.dispatchEvent(event); 443 | }; 444 | 445 | VRDisplay.prototype.fireVRDisplayConnect_ = function() { 446 | // Important: unfortunately we cannot have full spec compliance here. 447 | // CustomEvent custom fields all go under e.detail (so the VRDisplay ends up 448 | // being e.detail.display, instead of e.display as per WebVR spec). 449 | var event = new CustomEvent('vrdisplayconnect', {detail: {display: this}}); 450 | window.dispatchEvent(event); 451 | }; 452 | 453 | VRDisplay.prototype.addFullscreenListeners_ = function(element, changeHandler, errorHandler) { 454 | this.removeFullscreenListeners_(); 455 | 456 | this.fullscreenEventTarget_ = element; 457 | this.fullscreenChangeHandler_ = changeHandler; 458 | this.fullscreenErrorHandler_ = errorHandler; 459 | 460 | if (changeHandler) { 461 | if (document.fullscreenEnabled) { 462 | element.addEventListener('fullscreenchange', changeHandler, false); 463 | } else if (document.webkitFullscreenEnabled) { 464 | element.addEventListener('webkitfullscreenchange', changeHandler, false); 465 | } else if (document.mozFullScreenEnabled) { 466 | document.addEventListener('mozfullscreenchange', changeHandler, false); 467 | } else if (document.msFullscreenEnabled) { 468 | element.addEventListener('msfullscreenchange', changeHandler, false); 469 | } 470 | } 471 | 472 | if (errorHandler) { 473 | if (document.fullscreenEnabled) { 474 | element.addEventListener('fullscreenerror', errorHandler, false); 475 | } else if (document.webkitFullscreenEnabled) { 476 | element.addEventListener('webkitfullscreenerror', errorHandler, false); 477 | } else if (document.mozFullScreenEnabled) { 478 | document.addEventListener('mozfullscreenerror', errorHandler, false); 479 | } else if (document.msFullscreenEnabled) { 480 | element.addEventListener('msfullscreenerror', errorHandler, false); 481 | } 482 | } 483 | }; 484 | 485 | VRDisplay.prototype.removeFullscreenListeners_ = function() { 486 | if (!this.fullscreenEventTarget_) 487 | return; 488 | 489 | var element = this.fullscreenEventTarget_; 490 | 491 | if (this.fullscreenChangeHandler_) { 492 | var changeHandler = this.fullscreenChangeHandler_; 493 | element.removeEventListener('fullscreenchange', changeHandler, false); 494 | element.removeEventListener('webkitfullscreenchange', changeHandler, false); 495 | document.removeEventListener('mozfullscreenchange', changeHandler, false); 496 | element.removeEventListener('msfullscreenchange', changeHandler, false); 497 | } 498 | 499 | if (this.fullscreenErrorHandler_) { 500 | var errorHandler = this.fullscreenErrorHandler_; 501 | element.removeEventListener('fullscreenerror', errorHandler, false); 502 | element.removeEventListener('webkitfullscreenerror', errorHandler, false); 503 | document.removeEventListener('mozfullscreenerror', errorHandler, false); 504 | element.removeEventListener('msfullscreenerror', errorHandler, false); 505 | } 506 | 507 | this.fullscreenEventTarget_ = null; 508 | this.fullscreenChangeHandler_ = null; 509 | this.fullscreenErrorHandler_ = null; 510 | }; 511 | 512 | VRDisplay.prototype.enableWakeLock = function() { 513 | if (this.wakelock_) { 514 | this.wakelock_.enable(); 515 | } 516 | } 517 | 518 | VRDisplay.prototype.disableWakeLock = function() { 519 | if (this.wakelock_) { 520 | this.wakelock_.disable(); 521 | } 522 | } 523 | 524 | VRDisplay.prototype.beginPresent_ = function() { 525 | // Override to add custom behavior when presentation begins. 526 | }; 527 | 528 | VRDisplay.prototype.endPresent_ = function() { 529 | // Override to add custom behavior when presentation ends. 530 | }; 531 | 532 | VRDisplay.prototype.submitFrame = function(pose) { 533 | // Override to add custom behavior for frame submission. 534 | }; 535 | 536 | VRDisplay.prototype.getEyeParameters = function(whichEye) { 537 | // Override to return accurate eye parameters if canPresent is true. 538 | return null; 539 | }; 540 | -------------------------------------------------------------------------------- /src/cardboard-ui.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import * as Util from './util.js'; 17 | import WGLUPreserveGLState from 'gl-preserve-state'; 18 | 19 | var uiVS = [ 20 | 'attribute vec2 position;', 21 | 22 | 'uniform mat4 projectionMat;', 23 | 24 | 'void main() {', 25 | ' gl_Position = projectionMat * vec4( position, -1.0, 1.0 );', 26 | '}', 27 | ].join('\n'); 28 | 29 | var uiFS = [ 30 | 'precision mediump float;', 31 | 32 | 'uniform vec4 color;', 33 | 34 | 'void main() {', 35 | ' gl_FragColor = color;', 36 | '}', 37 | ].join('\n'); 38 | 39 | var DEG2RAD = Math.PI/180.0; 40 | 41 | // The gear has 6 identical sections, each spanning 60 degrees. 42 | var kAnglePerGearSection = 60; 43 | 44 | // Half-angle of the span of the outer rim. 45 | var kOuterRimEndAngle = 12; 46 | 47 | // Angle between the middle of the outer rim and the start of the inner rim. 48 | var kInnerRimBeginAngle = 20; 49 | 50 | // Distance from center to outer rim, normalized so that the entire model 51 | // fits in a [-1, 1] x [-1, 1] square. 52 | var kOuterRadius = 1; 53 | 54 | // Distance from center to depressed rim, in model units. 55 | var kMiddleRadius = 0.75; 56 | 57 | // Radius of the inner hollow circle, in model units. 58 | var kInnerRadius = 0.3125; 59 | 60 | // Center line thickness in DP. 61 | var kCenterLineThicknessDp = 4; 62 | 63 | // Button width in DP. 64 | var kButtonWidthDp = 28; 65 | 66 | // Factor to scale the touch area that responds to the touch. 67 | var kTouchSlopFactor = 1.5; 68 | 69 | var Angles = [ 70 | 0, kOuterRimEndAngle, kInnerRimBeginAngle, 71 | kAnglePerGearSection - kInnerRimBeginAngle, 72 | kAnglePerGearSection - kOuterRimEndAngle 73 | ]; 74 | 75 | /** 76 | * Renders the alignment line and "options" gear. It is assumed that the canvas 77 | * this is rendered into covers the entire screen (or close to it.) 78 | */ 79 | function CardboardUI(gl) { 80 | this.gl = gl; 81 | 82 | this.attribs = { 83 | position: 0 84 | }; 85 | this.program = Util.linkProgram(gl, uiVS, uiFS, this.attribs); 86 | this.uniforms = Util.getProgramUniforms(gl, this.program); 87 | 88 | this.vertexBuffer = gl.createBuffer(); 89 | this.gearOffset = 0; 90 | this.gearVertexCount = 0; 91 | this.arrowOffset = 0; 92 | this.arrowVertexCount = 0; 93 | 94 | this.projMat = new Float32Array(16); 95 | 96 | this.listener = null; 97 | 98 | this.onResize(); 99 | }; 100 | 101 | /** 102 | * Tears down all the resources created by the UI renderer. 103 | */ 104 | CardboardUI.prototype.destroy = function() { 105 | var gl = this.gl; 106 | 107 | if (this.listener) { 108 | gl.canvas.removeEventListener('click', this.listener, false); 109 | } 110 | 111 | gl.deleteProgram(this.program); 112 | gl.deleteBuffer(this.vertexBuffer); 113 | }; 114 | 115 | /** 116 | * Adds a listener to clicks on the gear and back icons 117 | */ 118 | CardboardUI.prototype.listen = function(optionsCallback, backCallback) { 119 | var canvas = this.gl.canvas; 120 | this.listener = function(event) { 121 | var midline = canvas.clientWidth / 2; 122 | var buttonSize = kButtonWidthDp * kTouchSlopFactor; 123 | // Check to see if the user clicked on (or around) the gear icon 124 | if (event.clientX > midline - buttonSize && 125 | event.clientX < midline + buttonSize && 126 | event.clientY > canvas.clientHeight - buttonSize) { 127 | optionsCallback(event); 128 | } 129 | // Check to see if the user clicked on (or around) the back icon 130 | else if (event.clientX < buttonSize && event.clientY < buttonSize) { 131 | backCallback(event); 132 | } 133 | }; 134 | canvas.addEventListener('click', this.listener, false); 135 | }; 136 | 137 | /** 138 | * Builds the UI mesh. 139 | */ 140 | CardboardUI.prototype.onResize = function() { 141 | var gl = this.gl; 142 | var self = this; 143 | 144 | var glState = [ 145 | gl.ARRAY_BUFFER_BINDING 146 | ]; 147 | 148 | WGLUPreserveGLState(gl, glState, function(gl) { 149 | var vertices = []; 150 | 151 | var midline = gl.drawingBufferWidth / 2; 152 | 153 | // The gl buffer size will likely be smaller than the physical pixel count. 154 | // So we need to scale the dps down based on the actual buffer size vs physical pixel count. 155 | // This will properly size the ui elements no matter what the gl buffer resolution is 156 | var physicalPixels = Math.max(screen.width, screen.height) * window.devicePixelRatio; 157 | var scalingRatio = gl.drawingBufferWidth / physicalPixels; 158 | var dps = scalingRatio * window.devicePixelRatio; 159 | 160 | var lineWidth = kCenterLineThicknessDp * dps / 2; 161 | var buttonSize = kButtonWidthDp * kTouchSlopFactor * dps; 162 | var buttonScale = kButtonWidthDp * dps / 2; 163 | var buttonBorder = ((kButtonWidthDp * kTouchSlopFactor) - kButtonWidthDp) * dps; 164 | 165 | // Build centerline 166 | vertices.push(midline - lineWidth, buttonSize); 167 | vertices.push(midline - lineWidth, gl.drawingBufferHeight); 168 | vertices.push(midline + lineWidth, buttonSize); 169 | vertices.push(midline + lineWidth, gl.drawingBufferHeight); 170 | 171 | // Build gear 172 | self.gearOffset = (vertices.length / 2); 173 | 174 | function addGearSegment(theta, r) { 175 | var angle = (90 - theta) * DEG2RAD; 176 | var x = Math.cos(angle); 177 | var y = Math.sin(angle); 178 | vertices.push(kInnerRadius * x * buttonScale + midline, kInnerRadius * y * buttonScale + buttonScale); 179 | vertices.push(r * x * buttonScale + midline, r * y * buttonScale + buttonScale); 180 | } 181 | 182 | for (var i = 0; i <= 6; i++) { 183 | var segmentTheta = i * kAnglePerGearSection; 184 | 185 | addGearSegment(segmentTheta, kOuterRadius); 186 | addGearSegment(segmentTheta + kOuterRimEndAngle, kOuterRadius); 187 | addGearSegment(segmentTheta + kInnerRimBeginAngle, kMiddleRadius); 188 | addGearSegment(segmentTheta + (kAnglePerGearSection - kInnerRimBeginAngle), kMiddleRadius); 189 | addGearSegment(segmentTheta + (kAnglePerGearSection - kOuterRimEndAngle), kOuterRadius); 190 | } 191 | 192 | self.gearVertexCount = (vertices.length / 2) - self.gearOffset; 193 | 194 | // Build back arrow 195 | self.arrowOffset = (vertices.length / 2); 196 | 197 | function addArrowVertex(x, y) { 198 | vertices.push(buttonBorder + x, gl.drawingBufferHeight - buttonBorder - y); 199 | } 200 | 201 | var angledLineWidth = lineWidth / Math.sin(45 * DEG2RAD); 202 | 203 | addArrowVertex(0, buttonScale); 204 | addArrowVertex(buttonScale, 0); 205 | addArrowVertex(buttonScale + angledLineWidth, angledLineWidth); 206 | addArrowVertex(angledLineWidth, buttonScale + angledLineWidth); 207 | 208 | addArrowVertex(angledLineWidth, buttonScale - angledLineWidth); 209 | addArrowVertex(0, buttonScale); 210 | addArrowVertex(buttonScale, buttonScale * 2); 211 | addArrowVertex(buttonScale + angledLineWidth, (buttonScale * 2) - angledLineWidth); 212 | 213 | addArrowVertex(angledLineWidth, buttonScale - angledLineWidth); 214 | addArrowVertex(0, buttonScale); 215 | 216 | addArrowVertex(angledLineWidth, buttonScale - lineWidth); 217 | addArrowVertex(kButtonWidthDp * dps, buttonScale - lineWidth); 218 | addArrowVertex(angledLineWidth, buttonScale + lineWidth); 219 | addArrowVertex(kButtonWidthDp * dps, buttonScale + lineWidth); 220 | 221 | self.arrowVertexCount = (vertices.length / 2) - self.arrowOffset; 222 | 223 | // Buffer data 224 | gl.bindBuffer(gl.ARRAY_BUFFER, self.vertexBuffer); 225 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 226 | }); 227 | }; 228 | 229 | /** 230 | * Performs distortion pass on the injected backbuffer, rendering it to the real 231 | * backbuffer. 232 | */ 233 | CardboardUI.prototype.render = function() { 234 | var gl = this.gl; 235 | var self = this; 236 | 237 | var glState = [ 238 | gl.CULL_FACE, 239 | gl.DEPTH_TEST, 240 | gl.BLEND, 241 | gl.SCISSOR_TEST, 242 | gl.STENCIL_TEST, 243 | gl.COLOR_WRITEMASK, 244 | gl.VIEWPORT, 245 | 246 | gl.CURRENT_PROGRAM, 247 | gl.ARRAY_BUFFER_BINDING 248 | ]; 249 | 250 | WGLUPreserveGLState(gl, glState, function(gl) { 251 | // Make sure the GL state is in a good place 252 | gl.disable(gl.CULL_FACE); 253 | gl.disable(gl.DEPTH_TEST); 254 | gl.disable(gl.BLEND); 255 | gl.disable(gl.SCISSOR_TEST); 256 | gl.disable(gl.STENCIL_TEST); 257 | gl.colorMask(true, true, true, true); 258 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 259 | 260 | self.renderNoState(); 261 | }); 262 | }; 263 | 264 | CardboardUI.prototype.renderNoState = function() { 265 | var gl = this.gl; 266 | 267 | // Bind distortion program and mesh 268 | gl.useProgram(this.program); 269 | 270 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 271 | gl.enableVertexAttribArray(this.attribs.position); 272 | gl.vertexAttribPointer(this.attribs.position, 2, gl.FLOAT, false, 8, 0); 273 | 274 | gl.uniform4f(this.uniforms.color, 1.0, 1.0, 1.0, 1.0); 275 | 276 | Util.orthoMatrix(this.projMat, 0, gl.drawingBufferWidth, 0, gl.drawingBufferHeight, 0.1, 1024.0); 277 | gl.uniformMatrix4fv(this.uniforms.projectionMat, false, this.projMat); 278 | 279 | // Draws UI element 280 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 281 | gl.drawArrays(gl.TRIANGLE_STRIP, this.gearOffset, this.gearVertexCount); 282 | gl.drawArrays(gl.TRIANGLE_STRIP, this.arrowOffset, this.arrowVertexCount); 283 | }; 284 | 285 | export default CardboardUI; 286 | -------------------------------------------------------------------------------- /src/cardboard-vr-display.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import CardboardDistorter from './cardboard-distorter.js'; 17 | import CardboardUI from './cardboard-ui.js'; 18 | import DeviceInfo from './device-info.js'; 19 | import Dpdb from './dpdb.js'; 20 | import PoseSensor from './pose-sensor.js'; 21 | import RotateInstructions from './rotate-instructions.js'; 22 | import ViewerSelector from './viewer-selector.js'; 23 | import { VRFrameData, VRDisplay, VRDisplayCapabilities } from './base.js'; 24 | import * as Util from './util.js'; 25 | import Options from './options.js'; 26 | 27 | var Eye = { 28 | LEFT: 'left', 29 | RIGHT: 'right' 30 | }; 31 | 32 | /** 33 | * VRDisplay based on mobile device parameters and DeviceMotion APIs. 34 | */ 35 | function CardboardVRDisplay(config) { 36 | var defaults = Util.extend({}, Options); 37 | config = Util.extend(defaults, config || {}); 38 | 39 | VRDisplay.call(this, { 40 | wakelock: config.MOBILE_WAKE_LOCK, 41 | }); 42 | 43 | this.config = config; 44 | 45 | this.displayName = 'Cardboard VRDisplay'; 46 | 47 | this.capabilities = new VRDisplayCapabilities({ 48 | hasPosition: false, 49 | hasOrientation: true, 50 | hasExternalDisplay: false, 51 | canPresent: true, 52 | maxLayers: 1 53 | }); 54 | 55 | this.stageParameters = null; 56 | 57 | // "Private" members. 58 | this.bufferScale_ = this.config.BUFFER_SCALE; 59 | this.poseSensor_ = new PoseSensor(this.config); 60 | this.distorter_ = null; 61 | this.cardboardUI_ = null; 62 | 63 | this.dpdb_ = new Dpdb(this.config.DPDB_URL, this.onDeviceParamsUpdated_.bind(this)); 64 | this.deviceInfo_ = new DeviceInfo(this.dpdb_.getDeviceParams(), 65 | config.ADDITIONAL_VIEWERS); 66 | 67 | this.viewerSelector_ = new ViewerSelector(config.DEFAULT_VIEWER); 68 | this.viewerSelector_.onChange(this.onViewerChanged_.bind(this)); 69 | 70 | // Set the correct initial viewer. 71 | this.deviceInfo_.setViewer(this.viewerSelector_.getCurrentViewer()); 72 | 73 | if (!this.config.ROTATE_INSTRUCTIONS_DISABLED) { 74 | this.rotateInstructions_ = new RotateInstructions(); 75 | } 76 | 77 | if (Util.isIOS()) { 78 | // Listen for resize events to workaround this awful Safari bug. 79 | window.addEventListener('resize', this.onResize_.bind(this)); 80 | } 81 | } 82 | CardboardVRDisplay.prototype = Object.create(VRDisplay.prototype); 83 | 84 | CardboardVRDisplay.prototype._getPose = function() { 85 | return { 86 | position: null, 87 | orientation: this.poseSensor_.getOrientation(), 88 | linearVelocity: null, 89 | linearAcceleration: null, 90 | angularVelocity: null, 91 | angularAcceleration: null 92 | }; 93 | } 94 | 95 | CardboardVRDisplay.prototype._resetPose = function() { 96 | // The non-devicemotion PoseSensor does not have resetPose implemented 97 | // as it has been deprecated from spec. 98 | if (this.poseSensor_.resetPose) { 99 | this.poseSensor_.resetPose(); 100 | } 101 | }; 102 | 103 | CardboardVRDisplay.prototype._getFieldOfView = function(whichEye) { 104 | // TODO: FoV can be a little expensive to compute. Cache when device params change. 105 | var fieldOfView; 106 | if (whichEye == Eye.LEFT) { 107 | fieldOfView = this.deviceInfo_.getFieldOfViewLeftEye(); 108 | } else if (whichEye == Eye.RIGHT) { 109 | fieldOfView = this.deviceInfo_.getFieldOfViewRightEye(); 110 | } else { 111 | console.error('Invalid eye provided: %s', whichEye); 112 | return null; 113 | } 114 | 115 | return fieldOfView; 116 | }; 117 | 118 | CardboardVRDisplay.prototype._getEyeOffset = function(whichEye) { 119 | var offset; 120 | 121 | if (whichEye == Eye.LEFT) { 122 | offset = [-this.deviceInfo_.viewer.interLensDistance * 0.5, 0.0, 0.0]; 123 | } else if (whichEye == Eye.RIGHT) { 124 | offset = [this.deviceInfo_.viewer.interLensDistance * 0.5, 0.0, 0.0]; 125 | } else { 126 | console.error('Invalid eye provided: %s', whichEye); 127 | return null; 128 | } 129 | 130 | return offset; 131 | }; 132 | 133 | CardboardVRDisplay.prototype.getEyeParameters = function(whichEye) { 134 | var offset = this._getEyeOffset(whichEye); 135 | var fieldOfView = this._getFieldOfView(whichEye); 136 | 137 | var eyeParams = { 138 | offset: offset, 139 | // TODO: Should be able to provide better values than these. 140 | renderWidth: this.deviceInfo_.device.width * 0.5 * this.bufferScale_, 141 | renderHeight: this.deviceInfo_.device.height * this.bufferScale_, 142 | }; 143 | 144 | Object.defineProperty(eyeParams, 'fieldOfView', { 145 | enumerable: true, 146 | get: function() { 147 | Util.deprecateWarning('VRFieldOfView', 148 | 'VRFrameData\'s projection matrices'); 149 | return fieldOfView; 150 | }, 151 | }); 152 | 153 | return eyeParams; 154 | }; 155 | 156 | CardboardVRDisplay.prototype.onDeviceParamsUpdated_ = function(newParams) { 157 | if (this.config.DEBUG) { 158 | console.log('DPDB reported that device params were updated.'); 159 | } 160 | this.deviceInfo_.updateDeviceParams(newParams); 161 | 162 | if (this.distorter_) { 163 | this.distorter_.updateDeviceInfo(this.deviceInfo_); 164 | } 165 | }; 166 | 167 | CardboardVRDisplay.prototype.updateBounds_ = function () { 168 | if (this.layer_ && this.distorter_ && (this.layer_.leftBounds || this.layer_.rightBounds)) { 169 | this.distorter_.setTextureBounds(this.layer_.leftBounds, this.layer_.rightBounds); 170 | } 171 | }; 172 | 173 | CardboardVRDisplay.prototype.beginPresent_ = function() { 174 | var gl = this.layer_.source.getContext('webgl'); 175 | if (!gl) 176 | gl = this.layer_.source.getContext('experimental-webgl'); 177 | if (!gl) 178 | gl = this.layer_.source.getContext('webgl2'); 179 | 180 | if (!gl) 181 | return; // Can't do distortion without a WebGL context. 182 | 183 | // Provides a way to opt out of distortion 184 | if (this.layer_.predistorted) { 185 | if (!this.config.CARDBOARD_UI_DISABLED) { 186 | gl.canvas.width = Util.getScreenWidth() * this.bufferScale_; 187 | gl.canvas.height = Util.getScreenHeight() * this.bufferScale_; 188 | this.cardboardUI_ = new CardboardUI(gl); 189 | } 190 | } else { 191 | // Create a new distorter for the target context 192 | if (!this.config.CARDBOARD_UI_DISABLED) { 193 | this.cardboardUI_ = new CardboardUI(gl); 194 | } 195 | this.distorter_ = new CardboardDistorter(gl, this.cardboardUI_, 196 | this.config.BUFFER_SCALE, 197 | this.config.DIRTY_SUBMIT_FRAME_BINDINGS); 198 | this.distorter_.updateDeviceInfo(this.deviceInfo_); 199 | } 200 | 201 | if (this.cardboardUI_) { 202 | this.cardboardUI_.listen(function(e) { 203 | // Options clicked. 204 | this.viewerSelector_.show(this.layer_.source.parentElement); 205 | e.stopPropagation(); 206 | e.preventDefault(); 207 | }.bind(this), function(e) { 208 | // Back clicked. 209 | this.exitPresent(); 210 | e.stopPropagation(); 211 | e.preventDefault(); 212 | }.bind(this)); 213 | } 214 | 215 | if (this.rotateInstructions_) { 216 | if (Util.isLandscapeMode() && Util.isMobile()) { 217 | // In landscape mode, temporarily show the "put into Cardboard" 218 | // interstitial. Otherwise, do the default thing. 219 | this.rotateInstructions_.showTemporarily(3000, this.layer_.source.parentElement); 220 | } else { 221 | this.rotateInstructions_.update(); 222 | } 223 | } 224 | 225 | // Listen for orientation change events in order to show interstitial. 226 | this.orientationHandler = this.onOrientationChange_.bind(this); 227 | window.addEventListener('orientationchange', this.orientationHandler); 228 | 229 | // Listen for present display change events in order to update distorter dimensions 230 | this.vrdisplaypresentchangeHandler = this.updateBounds_.bind(this); 231 | window.addEventListener('vrdisplaypresentchange', this.vrdisplaypresentchangeHandler); 232 | 233 | // Fire this event initially, to give geometry-distortion clients the chance 234 | // to do something custom. 235 | this.fireVRDisplayDeviceParamsChange_(); 236 | }; 237 | 238 | CardboardVRDisplay.prototype.endPresent_ = function() { 239 | if (this.distorter_) { 240 | this.distorter_.destroy(); 241 | this.distorter_ = null; 242 | } 243 | if (this.cardboardUI_) { 244 | this.cardboardUI_.destroy(); 245 | this.cardboardUI_ = null; 246 | } 247 | 248 | if (this.rotateInstructions_) { 249 | this.rotateInstructions_.hide(); 250 | } 251 | this.viewerSelector_.hide(); 252 | 253 | window.removeEventListener('orientationchange', this.orientationHandler); 254 | window.removeEventListener('vrdisplaypresentchange', this.vrdisplaypresentchangeHandler); 255 | }; 256 | 257 | /** 258 | * Called when the layer's `source` changes to a new canvas. 259 | * Used to re-setup the distortions and UI with new context. 260 | */ 261 | CardboardVRDisplay.prototype.updatePresent_ = function() { 262 | this.endPresent_(); 263 | this.beginPresent_(); 264 | }; 265 | 266 | CardboardVRDisplay.prototype.submitFrame = function(pose) { 267 | if (this.distorter_) { 268 | this.updateBounds_(); 269 | this.distorter_.submitFrame(); 270 | } else if (this.cardboardUI_ && this.layer_) { 271 | // Hack for predistorted: true. 272 | var gl = this.layer_.source.getContext('webgl'); 273 | if (!gl) 274 | gl = this.layer_.source.getContext('experimental-webgl'); 275 | if (!gl) 276 | gl = this.layer_.source.getContext('webgl2'); 277 | 278 | var canvas = gl.canvas; 279 | if (canvas.width != this.lastWidth || canvas.height != this.lastHeight) { 280 | this.cardboardUI_.onResize(); 281 | } 282 | this.lastWidth = canvas.width; 283 | this.lastHeight = canvas.height; 284 | 285 | // Render the Cardboard UI. 286 | this.cardboardUI_.render(); 287 | } 288 | }; 289 | 290 | CardboardVRDisplay.prototype.onOrientationChange_ = function(e) { 291 | // Hide the viewer selector. 292 | this.viewerSelector_.hide(); 293 | 294 | // Update the rotate instructions. 295 | if (this.rotateInstructions_) { 296 | this.rotateInstructions_.update(); 297 | } 298 | 299 | this.onResize_(); 300 | }; 301 | 302 | CardboardVRDisplay.prototype.onResize_ = function(e) { 303 | if (this.layer_) { 304 | var gl = this.layer_.source.getContext('webgl'); 305 | if (!gl) gl = this.layer_.source.getContext('experimental-webgl'); 306 | if (!gl) gl = this.layer_.source.getContext('webgl2'); 307 | // Size the CSS canvas. 308 | // Added padding on right and bottom because iPhone 5 will not 309 | // hide the URL bar unless content is bigger than the screen. 310 | // This will not be visible as long as the container element (e.g. body) 311 | // is set to 'overflow: hidden'. 312 | // Additionally, 'box-sizing: content-box' ensures renderWidth = width + padding. 313 | // This is required when 'box-sizing: border-box' is used elsewhere in the page. 314 | var cssProperties = [ 315 | 'position: absolute', 316 | 'top: 0', 317 | 'left: 0', 318 | // Use vw/vh to handle implicitly devicePixelRatio; issue #282 319 | 'width: 100vw', 320 | 'height: 100vh', 321 | 'border: 0', 322 | 'margin: 0', 323 | // Set no padding in the case where you don't have control over 324 | // the content injection, like in Unity WebGL; issue #282 325 | 'padding: 0px', 326 | 'box-sizing: content-box', 327 | ]; 328 | gl.canvas.setAttribute('style', cssProperties.join('; ') + ';'); 329 | 330 | Util.safariCssSizeWorkaround(gl.canvas); 331 | } 332 | }; 333 | 334 | CardboardVRDisplay.prototype.onViewerChanged_ = function(viewer) { 335 | this.deviceInfo_.setViewer(viewer); 336 | 337 | if (this.distorter_) { 338 | // Update the distortion appropriately. 339 | this.distorter_.updateDeviceInfo(this.deviceInfo_); 340 | } 341 | 342 | // Fire a new event containing viewer and device parameters for clients that 343 | // want to implement their own geometry-based distortion. 344 | this.fireVRDisplayDeviceParamsChange_(); 345 | }; 346 | 347 | CardboardVRDisplay.prototype.fireVRDisplayDeviceParamsChange_ = function() { 348 | var event = new CustomEvent('vrdisplaydeviceparamschange', { 349 | detail: { 350 | vrdisplay: this, 351 | deviceInfo: this.deviceInfo_, 352 | } 353 | }); 354 | window.dispatchEvent(event); 355 | }; 356 | 357 | CardboardVRDisplay.VRFrameData = VRFrameData; 358 | CardboardVRDisplay.VRDisplay = VRDisplay; 359 | 360 | export default CardboardVRDisplay; 361 | -------------------------------------------------------------------------------- /src/device-info.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import Distortion from './distortion.js'; 17 | import { radToDeg, degToRad } from './math-util.js'; 18 | import * as Util from './util.js'; 19 | 20 | function Device(params) { 21 | this.width = params.width || Util.getScreenWidth(); 22 | this.height = params.height || Util.getScreenHeight(); 23 | this.widthMeters = params.widthMeters; 24 | this.heightMeters = params.heightMeters; 25 | this.bevelMeters = params.bevelMeters; 26 | } 27 | 28 | 29 | // Fallback Android device (based on Nexus 5 measurements) for use when 30 | // we can't recognize an Android device. 31 | var DEFAULT_ANDROID = new Device({ 32 | widthMeters: 0.110, 33 | heightMeters: 0.062, 34 | bevelMeters: 0.004 35 | }); 36 | 37 | // Fallback iOS device (based on iPhone6) for use when 38 | // we can't recognize an Android device. 39 | var DEFAULT_IOS = new Device({ 40 | widthMeters: 0.1038, 41 | heightMeters: 0.0584, 42 | bevelMeters: 0.004 43 | }); 44 | 45 | 46 | var Viewers = { 47 | CardboardV1: new CardboardViewer({ 48 | id: 'CardboardV1', 49 | label: 'Cardboard I/O 2014', 50 | fov: 40, 51 | interLensDistance: 0.060, 52 | baselineLensDistance: 0.035, 53 | screenLensDistance: 0.042, 54 | distortionCoefficients: [0.441, 0.156], 55 | inverseCoefficients: [-0.4410035, 0.42756155, -0.4804439, 0.5460139, 56 | -0.58821183, 0.5733938, -0.48303202, 0.33299083, -0.17573841, 57 | 0.0651772, -0.01488963, 0.001559834] 58 | }), 59 | CardboardV2: new CardboardViewer({ 60 | id: 'CardboardV2', 61 | label: 'Cardboard I/O 2015', 62 | fov: 60, 63 | interLensDistance: 0.064, 64 | baselineLensDistance: 0.035, 65 | screenLensDistance: 0.039, 66 | distortionCoefficients: [0.34, 0.55], 67 | inverseCoefficients: [-0.33836704, -0.18162185, 0.862655, -1.2462051, 68 | 1.0560602, -0.58208317, 0.21609078, -0.05444823, 0.009177956, 69 | -9.904169E-4, 6.183535E-5, -1.6981803E-6] 70 | }) 71 | }; 72 | 73 | 74 | var DEFAULT_LEFT_CENTER = {x: 0.5, y: 0.5}; 75 | var DEFAULT_RIGHT_CENTER = {x: 0.5, y: 0.5}; 76 | 77 | /** 78 | * Manages information about the device and the viewer. 79 | * 80 | * deviceParams indicates the parameters of the device to use (generally 81 | * obtained from dpdb.getDeviceParams()). Can be null to mean no device 82 | * params were found. 83 | */ 84 | function DeviceInfo(deviceParams, additionalViewers) { 85 | this.viewer = Viewers.CardboardV2; 86 | this.updateDeviceParams(deviceParams); 87 | this.distortion = new Distortion(this.viewer.distortionCoefficients); 88 | for (var i = 0; i < additionalViewers.length; i++) { 89 | var viewer = additionalViewers[i]; 90 | Viewers[viewer.id] = new CardboardViewer(viewer); 91 | } 92 | } 93 | 94 | DeviceInfo.prototype.updateDeviceParams = function(deviceParams) { 95 | this.device = this.determineDevice_(deviceParams) || this.device; 96 | }; 97 | 98 | DeviceInfo.prototype.getDevice = function() { 99 | return this.device; 100 | }; 101 | 102 | DeviceInfo.prototype.setViewer = function(viewer) { 103 | this.viewer = viewer; 104 | this.distortion = new Distortion(this.viewer.distortionCoefficients); 105 | }; 106 | 107 | DeviceInfo.prototype.determineDevice_ = function(deviceParams) { 108 | if (!deviceParams) { 109 | // No parameters, so use a default. 110 | if (Util.isIOS()) { 111 | console.warn('Using fallback iOS device measurements.'); 112 | return DEFAULT_IOS; 113 | } else { 114 | console.warn('Using fallback Android device measurements.'); 115 | return DEFAULT_ANDROID; 116 | } 117 | } 118 | 119 | // Compute device screen dimensions based on deviceParams. 120 | var METERS_PER_INCH = 0.0254; 121 | var metersPerPixelX = METERS_PER_INCH / deviceParams.xdpi; 122 | var metersPerPixelY = METERS_PER_INCH / deviceParams.ydpi; 123 | var width = Util.getScreenWidth(); 124 | var height = Util.getScreenHeight(); 125 | return new Device({ 126 | widthMeters: metersPerPixelX * width, 127 | heightMeters: metersPerPixelY * height, 128 | bevelMeters: deviceParams.bevelMm * 0.001, 129 | }); 130 | }; 131 | 132 | /** 133 | * Calculates field of view for the left eye. 134 | */ 135 | DeviceInfo.prototype.getDistortedFieldOfViewLeftEye = function() { 136 | var viewer = this.viewer; 137 | var device = this.device; 138 | var distortion = this.distortion; 139 | 140 | // Device.height and device.width for device in portrait mode, so transpose. 141 | var eyeToScreenDistance = viewer.screenLensDistance; 142 | 143 | var outerDist = (device.widthMeters - viewer.interLensDistance) / 2; 144 | var innerDist = viewer.interLensDistance / 2; 145 | var bottomDist = viewer.baselineLensDistance - device.bevelMeters; 146 | var topDist = device.heightMeters - bottomDist; 147 | 148 | var outerAngle = radToDeg * Math.atan( 149 | distortion.distort(outerDist / eyeToScreenDistance)); 150 | var innerAngle = radToDeg * Math.atan( 151 | distortion.distort(innerDist / eyeToScreenDistance)); 152 | var bottomAngle = radToDeg * Math.atan( 153 | distortion.distort(bottomDist / eyeToScreenDistance)); 154 | var topAngle = radToDeg * Math.atan( 155 | distortion.distort(topDist / eyeToScreenDistance)); 156 | 157 | return { 158 | leftDegrees: Math.min(outerAngle, viewer.fov), 159 | rightDegrees: Math.min(innerAngle, viewer.fov), 160 | downDegrees: Math.min(bottomAngle, viewer.fov), 161 | upDegrees: Math.min(topAngle, viewer.fov) 162 | }; 163 | }; 164 | 165 | /** 166 | * Calculates the tan-angles from the maximum FOV for the left eye for the 167 | * current device and screen parameters. 168 | */ 169 | DeviceInfo.prototype.getLeftEyeVisibleTanAngles = function() { 170 | var viewer = this.viewer; 171 | var device = this.device; 172 | var distortion = this.distortion; 173 | 174 | // Tan-angles from the max FOV. 175 | var fovLeft = Math.tan(-degToRad * viewer.fov); 176 | var fovTop = Math.tan(degToRad * viewer.fov); 177 | var fovRight = Math.tan(degToRad * viewer.fov); 178 | var fovBottom = Math.tan(-degToRad * viewer.fov); 179 | // Viewport size. 180 | var halfWidth = device.widthMeters / 4; 181 | var halfHeight = device.heightMeters / 2; 182 | // Viewport center, measured from left lens position. 183 | var verticalLensOffset = (viewer.baselineLensDistance - device.bevelMeters - halfHeight); 184 | var centerX = viewer.interLensDistance / 2 - halfWidth; 185 | var centerY = -verticalLensOffset; 186 | var centerZ = viewer.screenLensDistance; 187 | // Tan-angles of the viewport edges, as seen through the lens. 188 | var screenLeft = distortion.distort((centerX - halfWidth) / centerZ); 189 | var screenTop = distortion.distort((centerY + halfHeight) / centerZ); 190 | var screenRight = distortion.distort((centerX + halfWidth) / centerZ); 191 | var screenBottom = distortion.distort((centerY - halfHeight) / centerZ); 192 | // Compare the two sets of tan-angles and take the value closer to zero on each side. 193 | var result = new Float32Array(4); 194 | result[0] = Math.max(fovLeft, screenLeft); 195 | result[1] = Math.min(fovTop, screenTop); 196 | result[2] = Math.min(fovRight, screenRight); 197 | result[3] = Math.max(fovBottom, screenBottom); 198 | return result; 199 | }; 200 | 201 | /** 202 | * Calculates the tan-angles from the maximum FOV for the left eye for the 203 | * current device and screen parameters, assuming no lenses. 204 | */ 205 | DeviceInfo.prototype.getLeftEyeNoLensTanAngles = function() { 206 | var viewer = this.viewer; 207 | var device = this.device; 208 | var distortion = this.distortion; 209 | 210 | var result = new Float32Array(4); 211 | // Tan-angles from the max FOV. 212 | var fovLeft = distortion.distortInverse(Math.tan(-degToRad * viewer.fov)); 213 | var fovTop = distortion.distortInverse(Math.tan(degToRad * viewer.fov)); 214 | var fovRight = distortion.distortInverse(Math.tan(degToRad * viewer.fov)); 215 | var fovBottom = distortion.distortInverse(Math.tan(-degToRad * viewer.fov)); 216 | // Viewport size. 217 | var halfWidth = device.widthMeters / 4; 218 | var halfHeight = device.heightMeters / 2; 219 | // Viewport center, measured from left lens position. 220 | var verticalLensOffset = (viewer.baselineLensDistance - device.bevelMeters - halfHeight); 221 | var centerX = viewer.interLensDistance / 2 - halfWidth; 222 | var centerY = -verticalLensOffset; 223 | var centerZ = viewer.screenLensDistance; 224 | // Tan-angles of the viewport edges, as seen through the lens. 225 | var screenLeft = (centerX - halfWidth) / centerZ; 226 | var screenTop = (centerY + halfHeight) / centerZ; 227 | var screenRight = (centerX + halfWidth) / centerZ; 228 | var screenBottom = (centerY - halfHeight) / centerZ; 229 | // Compare the two sets of tan-angles and take the value closer to zero on each side. 230 | result[0] = Math.max(fovLeft, screenLeft); 231 | result[1] = Math.min(fovTop, screenTop); 232 | result[2] = Math.min(fovRight, screenRight); 233 | result[3] = Math.max(fovBottom, screenBottom); 234 | return result; 235 | }; 236 | 237 | /** 238 | * Calculates the screen rectangle visible from the left eye for the 239 | * current device and screen parameters. 240 | */ 241 | DeviceInfo.prototype.getLeftEyeVisibleScreenRect = function(undistortedFrustum) { 242 | var viewer = this.viewer; 243 | var device = this.device; 244 | 245 | var dist = viewer.screenLensDistance; 246 | var eyeX = (device.widthMeters - viewer.interLensDistance) / 2; 247 | var eyeY = viewer.baselineLensDistance - device.bevelMeters; 248 | var left = (undistortedFrustum[0] * dist + eyeX) / device.widthMeters; 249 | var top = (undistortedFrustum[1] * dist + eyeY) / device.heightMeters; 250 | var right = (undistortedFrustum[2] * dist + eyeX) / device.widthMeters; 251 | var bottom = (undistortedFrustum[3] * dist + eyeY) / device.heightMeters; 252 | return { 253 | x: left, 254 | y: bottom, 255 | width: right - left, 256 | height: top - bottom 257 | }; 258 | }; 259 | 260 | DeviceInfo.prototype.getFieldOfViewLeftEye = function(opt_isUndistorted) { 261 | return opt_isUndistorted ? this.getUndistortedFieldOfViewLeftEye() : 262 | this.getDistortedFieldOfViewLeftEye(); 263 | }; 264 | 265 | DeviceInfo.prototype.getFieldOfViewRightEye = function(opt_isUndistorted) { 266 | var fov = this.getFieldOfViewLeftEye(opt_isUndistorted); 267 | return { 268 | leftDegrees: fov.rightDegrees, 269 | rightDegrees: fov.leftDegrees, 270 | upDegrees: fov.upDegrees, 271 | downDegrees: fov.downDegrees 272 | }; 273 | }; 274 | 275 | /** 276 | * Calculates undistorted field of view for the left eye. 277 | */ 278 | DeviceInfo.prototype.getUndistortedFieldOfViewLeftEye = function() { 279 | var p = this.getUndistortedParams_(); 280 | 281 | return { 282 | leftDegrees: radToDeg * Math.atan(p.outerDist), 283 | rightDegrees: radToDeg * Math.atan(p.innerDist), 284 | downDegrees: radToDeg * Math.atan(p.bottomDist), 285 | upDegrees: radToDeg * Math.atan(p.topDist) 286 | }; 287 | }; 288 | 289 | DeviceInfo.prototype.getUndistortedViewportLeftEye = function() { 290 | var p = this.getUndistortedParams_(); 291 | var viewer = this.viewer; 292 | var device = this.device; 293 | 294 | // Distances stored in local variables are in tan-angle units unless otherwise 295 | // noted. 296 | var eyeToScreenDistance = viewer.screenLensDistance; 297 | var screenWidth = device.widthMeters / eyeToScreenDistance; 298 | var screenHeight = device.heightMeters / eyeToScreenDistance; 299 | var xPxPerTanAngle = device.width / screenWidth; 300 | var yPxPerTanAngle = device.height / screenHeight; 301 | 302 | var x = Math.round((p.eyePosX - p.outerDist) * xPxPerTanAngle); 303 | var y = Math.round((p.eyePosY - p.bottomDist) * yPxPerTanAngle); 304 | return { 305 | x: x, 306 | y: y, 307 | width: Math.round((p.eyePosX + p.innerDist) * xPxPerTanAngle) - x, 308 | height: Math.round((p.eyePosY + p.topDist) * yPxPerTanAngle) - y 309 | }; 310 | }; 311 | 312 | DeviceInfo.prototype.getUndistortedParams_ = function() { 313 | var viewer = this.viewer; 314 | var device = this.device; 315 | var distortion = this.distortion; 316 | 317 | // Most of these variables in tan-angle units. 318 | var eyeToScreenDistance = viewer.screenLensDistance; 319 | var halfLensDistance = viewer.interLensDistance / 2 / eyeToScreenDistance; 320 | var screenWidth = device.widthMeters / eyeToScreenDistance; 321 | var screenHeight = device.heightMeters / eyeToScreenDistance; 322 | 323 | var eyePosX = screenWidth / 2 - halfLensDistance; 324 | var eyePosY = (viewer.baselineLensDistance - device.bevelMeters) / eyeToScreenDistance; 325 | 326 | var maxFov = viewer.fov; 327 | var viewerMax = distortion.distortInverse(Math.tan(degToRad * maxFov)); 328 | var outerDist = Math.min(eyePosX, viewerMax); 329 | var innerDist = Math.min(halfLensDistance, viewerMax); 330 | var bottomDist = Math.min(eyePosY, viewerMax); 331 | var topDist = Math.min(screenHeight - eyePosY, viewerMax); 332 | 333 | return { 334 | outerDist: outerDist, 335 | innerDist: innerDist, 336 | topDist: topDist, 337 | bottomDist: bottomDist, 338 | eyePosX: eyePosX, 339 | eyePosY: eyePosY 340 | }; 341 | }; 342 | 343 | 344 | function CardboardViewer(params) { 345 | // A machine readable ID. 346 | this.id = params.id; 347 | // A human readable label. 348 | this.label = params.label; 349 | 350 | // Field of view in degrees (per side). 351 | this.fov = params.fov; 352 | 353 | // Distance between lens centers in meters. 354 | this.interLensDistance = params.interLensDistance; 355 | // Distance between viewer baseline and lens center in meters. 356 | this.baselineLensDistance = params.baselineLensDistance; 357 | // Screen-to-lens distance in meters. 358 | this.screenLensDistance = params.screenLensDistance; 359 | 360 | // Distortion coefficients. 361 | this.distortionCoefficients = params.distortionCoefficients; 362 | // Inverse distortion coefficients. 363 | // TODO: Calculate these from distortionCoefficients in the future. 364 | this.inverseCoefficients = params.inverseCoefficients; 365 | } 366 | 367 | // Export viewer information. 368 | DeviceInfo.Viewers = Viewers; 369 | export default DeviceInfo; 370 | -------------------------------------------------------------------------------- /src/distortion.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /** 17 | * TODO(smus): Implement coefficient inversion. 18 | */ 19 | function Distortion(coefficients) { 20 | this.coefficients = coefficients; 21 | } 22 | 23 | /** 24 | * Calculates the inverse distortion for a radius. 25 | *

26 | * Allows to compute the original undistorted radius from a distorted one. 27 | * See also getApproximateInverseDistortion() for a faster but potentially 28 | * less accurate method. 29 | * 30 | * @param {Number} radius Distorted radius from the lens center in tan-angle units. 31 | * @return {Number} The undistorted radius in tan-angle units. 32 | */ 33 | Distortion.prototype.distortInverse = function(radius) { 34 | // Secant method. 35 | var r0 = 0; 36 | var r1 = 1; 37 | var dr0 = radius - this.distort(r0); 38 | while (Math.abs(r1 - r0) > 0.0001 /** 0.1mm */) { 39 | var dr1 = radius - this.distort(r1); 40 | var r2 = r1 - dr1 * ((r1 - r0) / (dr1 - dr0)); 41 | r0 = r1; 42 | r1 = r2; 43 | dr0 = dr1; 44 | } 45 | return r1; 46 | }; 47 | 48 | /** 49 | * Distorts a radius by its distortion factor from the center of the lenses. 50 | * 51 | * @param {Number} radius Radius from the lens center in tan-angle units. 52 | * @return {Number} The distorted radius in tan-angle units. 53 | */ 54 | Distortion.prototype.distort = function(radius) { 55 | var r2 = radius * radius; 56 | var ret = 0; 57 | for (var i = 0; i < this.coefficients.length; i++) { 58 | ret = r2 * (ret + this.coefficients[i]); 59 | } 60 | return (ret + 1) * radius; 61 | }; 62 | 63 | export default Distortion; 64 | -------------------------------------------------------------------------------- /src/dpdb.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | // Offline cache of the DPDB, to be used until we load the online one (and 17 | // as a fallback in case we can't load the online one). 18 | import DPDB_CACHE from 'webvr-polyfill-dpdb'; 19 | import * as Util from './util.js'; 20 | 21 | /** 22 | * Calculates device parameters based on the DPDB (Device Parameter Database). 23 | * Initially, uses the cached DPDB values. 24 | * 25 | * If url defined, then this object tries to fetch the online version 26 | * of the DPDB and updates the device info if a better match is found. 27 | * Calls the onDeviceParamsUpdated callback when there is an update to the 28 | * device information. 29 | */ 30 | function Dpdb(url, onDeviceParamsUpdated) { 31 | // Start with the offline DPDB cache while we are loading the real one. 32 | this.dpdb = DPDB_CACHE; 33 | 34 | // Calculate device params based on the offline version of the DPDB. 35 | this.recalculateDeviceParams_(); 36 | 37 | // XHR to fetch online DPDB file, if requested. 38 | if (url) { 39 | // Set the callback. 40 | this.onDeviceParamsUpdated = onDeviceParamsUpdated; 41 | 42 | var xhr = new XMLHttpRequest(); 43 | var obj = this; 44 | xhr.open('GET', url, true); 45 | xhr.addEventListener('load', function() { 46 | obj.loading = false; 47 | if (xhr.status >= 200 && xhr.status <= 299) { 48 | // Success. 49 | obj.dpdb = JSON.parse(xhr.response); 50 | obj.recalculateDeviceParams_(); 51 | } else { 52 | // Error loading the DPDB. 53 | console.error('Error loading online DPDB!'); 54 | } 55 | }); 56 | xhr.send(); 57 | } 58 | } 59 | 60 | // Returns the current device parameters. 61 | Dpdb.prototype.getDeviceParams = function() { 62 | return this.deviceParams; 63 | }; 64 | 65 | // Recalculates this device's parameters based on the DPDB. 66 | Dpdb.prototype.recalculateDeviceParams_ = function() { 67 | var newDeviceParams = this.calcDeviceParams_(); 68 | if (newDeviceParams) { 69 | this.deviceParams = newDeviceParams; 70 | // Invoke callback, if it is set. 71 | if (this.onDeviceParamsUpdated) { 72 | this.onDeviceParamsUpdated(this.deviceParams); 73 | } 74 | } else { 75 | console.error('Failed to recalculate device parameters.'); 76 | } 77 | }; 78 | 79 | // Returns a DeviceParams object that represents the best guess as to this 80 | // device's parameters. Can return null if the device does not match any 81 | // known devices. 82 | Dpdb.prototype.calcDeviceParams_ = function() { 83 | var db = this.dpdb; // shorthand 84 | if (!db) { 85 | console.error('DPDB not available.'); 86 | return null; 87 | } 88 | if (db.format != 1) { 89 | console.error('DPDB has unexpected format version.'); 90 | return null; 91 | } 92 | if (!db.devices || !db.devices.length) { 93 | console.error('DPDB does not have a devices section.'); 94 | return null; 95 | } 96 | 97 | // Get the actual user agent and screen dimensions in pixels. 98 | var userAgent = navigator.userAgent || navigator.vendor || window.opera; 99 | var width = Util.getScreenWidth(); 100 | var height = Util.getScreenHeight(); 101 | 102 | if (!db.devices) { 103 | console.error('DPDB has no devices section.'); 104 | return null; 105 | } 106 | 107 | for (var i = 0; i < db.devices.length; i++) { 108 | var device = db.devices[i]; 109 | if (!device.rules) { 110 | console.warn('Device[' + i + '] has no rules section.'); 111 | continue; 112 | } 113 | 114 | if (device.type != 'ios' && device.type != 'android') { 115 | console.warn('Device[' + i + '] has invalid type.'); 116 | continue; 117 | } 118 | 119 | // See if this device is of the appropriate type. 120 | if (Util.isIOS() != (device.type == 'ios')) continue; 121 | 122 | // See if this device matches any of the rules: 123 | var matched = false; 124 | for (var j = 0; j < device.rules.length; j++) { 125 | var rule = device.rules[j]; 126 | if (this.ruleMatches_(rule, userAgent, width, height)) { 127 | matched = true; 128 | break; 129 | } 130 | } 131 | if (!matched) continue; 132 | 133 | // device.dpi might be an array of [ xdpi, ydpi] or just a scalar. 134 | var xdpi = device.dpi[0] || device.dpi; 135 | var ydpi = device.dpi[1] || device.dpi; 136 | 137 | return new DeviceParams({ xdpi: xdpi, ydpi: ydpi, bevelMm: device.bw }); 138 | } 139 | 140 | console.warn('No DPDB device match.'); 141 | return null; 142 | }; 143 | 144 | Dpdb.prototype.ruleMatches_ = function(rule, ua, screenWidth, screenHeight) { 145 | // We can only match 'ua' and 'res' rules, not other types like 'mdmh' 146 | // (which are meant for native platforms). 147 | if (!rule.ua && !rule.res) return false; 148 | 149 | // If this rule is for a Samsung device, generalize the rule name, such that 150 | // it can capture all variants of Samsung line e.g. all variants of the 151 | // Galaxy S8 (SM-G950A, SM-G950T, etc.) can be captured by "SM-G950". 152 | if (rule.ua && rule.ua.substring(0, 2) === 'SM') rule.ua = rule.ua.substring(0, 7); 153 | 154 | // If our user agent string doesn't contain the indicated user agent string, 155 | // the match fails. 156 | if (rule.ua && ua.indexOf(rule.ua) < 0) return false; 157 | 158 | // If the rule specifies screen dimensions that don't correspond to ours, 159 | // the match fails. 160 | if (rule.res) { 161 | if (!rule.res[0] || !rule.res[1]) return false; 162 | var resX = rule.res[0]; 163 | var resY = rule.res[1]; 164 | // Compare min and max so as to make the order not matter, i.e., it should 165 | // be true that 640x480 == 480x640. 166 | if (Math.min(screenWidth, screenHeight) != Math.min(resX, resY) || 167 | (Math.max(screenWidth, screenHeight) != Math.max(resX, resY))) { 168 | return false; 169 | } 170 | } 171 | 172 | return true; 173 | } 174 | 175 | function DeviceParams(params) { 176 | this.xdpi = params.xdpi; 177 | this.ydpi = params.ydpi; 178 | this.bevelMm = params.bevelMm; 179 | } 180 | 181 | export default Dpdb; 182 | -------------------------------------------------------------------------------- /src/math-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | export const degToRad = Math.PI / 180; 17 | export const radToDeg = 180 / Math.PI; 18 | 19 | // Some minimal math functionality borrowed from THREE.Math and stripped down 20 | // for the purposes of this library. 21 | 22 | 23 | export const Vector2 = function ( x, y ) { 24 | this.x = x || 0; 25 | this.y = y || 0; 26 | }; 27 | 28 | Vector2.prototype = { 29 | constructor: Vector2, 30 | 31 | set: function ( x, y ) { 32 | this.x = x; 33 | this.y = y; 34 | 35 | return this; 36 | }, 37 | 38 | copy: function ( v ) { 39 | this.x = v.x; 40 | this.y = v.y; 41 | 42 | return this; 43 | }, 44 | 45 | subVectors: function ( a, b ) { 46 | this.x = a.x - b.x; 47 | this.y = a.y - b.y; 48 | 49 | return this; 50 | }, 51 | }; 52 | 53 | export const Vector3 = function ( x, y, z ) { 54 | this.x = x || 0; 55 | this.y = y || 0; 56 | this.z = z || 0; 57 | }; 58 | 59 | Vector3.prototype = { 60 | constructor: Vector3, 61 | 62 | set: function ( x, y, z ) { 63 | this.x = x; 64 | this.y = y; 65 | this.z = z; 66 | 67 | return this; 68 | }, 69 | 70 | copy: function ( v ) { 71 | this.x = v.x; 72 | this.y = v.y; 73 | this.z = v.z; 74 | 75 | return this; 76 | }, 77 | 78 | length: function () { 79 | return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z ); 80 | }, 81 | 82 | normalize: function () { 83 | var scalar = this.length(); 84 | 85 | if ( scalar !== 0 ) { 86 | var invScalar = 1 / scalar; 87 | 88 | this.multiplyScalar(invScalar); 89 | } else { 90 | this.x = 0; 91 | this.y = 0; 92 | this.z = 0; 93 | } 94 | 95 | return this; 96 | }, 97 | 98 | multiplyScalar: function ( scalar ) { 99 | this.x *= scalar; 100 | this.y *= scalar; 101 | this.z *= scalar; 102 | }, 103 | 104 | applyQuaternion: function ( q ) { 105 | var x = this.x; 106 | var y = this.y; 107 | var z = this.z; 108 | 109 | var qx = q.x; 110 | var qy = q.y; 111 | var qz = q.z; 112 | var qw = q.w; 113 | 114 | // calculate quat * vector 115 | var ix = qw * x + qy * z - qz * y; 116 | var iy = qw * y + qz * x - qx * z; 117 | var iz = qw * z + qx * y - qy * x; 118 | var iw = - qx * x - qy * y - qz * z; 119 | 120 | // calculate result * inverse quat 121 | this.x = ix * qw + iw * - qx + iy * - qz - iz * - qy; 122 | this.y = iy * qw + iw * - qy + iz * - qx - ix * - qz; 123 | this.z = iz * qw + iw * - qz + ix * - qy - iy * - qx; 124 | 125 | return this; 126 | }, 127 | 128 | dot: function ( v ) { 129 | return this.x * v.x + this.y * v.y + this.z * v.z; 130 | }, 131 | 132 | crossVectors: function ( a, b ) { 133 | var ax = a.x, ay = a.y, az = a.z; 134 | var bx = b.x, by = b.y, bz = b.z; 135 | 136 | this.x = ay * bz - az * by; 137 | this.y = az * bx - ax * bz; 138 | this.z = ax * by - ay * bx; 139 | 140 | return this; 141 | }, 142 | }; 143 | 144 | export const Quaternion = function ( x, y, z, w ) { 145 | this.x = x || 0; 146 | this.y = y || 0; 147 | this.z = z || 0; 148 | this.w = ( w !== undefined ) ? w : 1; 149 | }; 150 | 151 | Quaternion.prototype = { 152 | constructor: Quaternion, 153 | 154 | set: function ( x, y, z, w ) { 155 | this.x = x; 156 | this.y = y; 157 | this.z = z; 158 | this.w = w; 159 | 160 | return this; 161 | }, 162 | 163 | copy: function ( quaternion ) { 164 | this.x = quaternion.x; 165 | this.y = quaternion.y; 166 | this.z = quaternion.z; 167 | this.w = quaternion.w; 168 | 169 | return this; 170 | }, 171 | 172 | setFromEulerXYZ: function( x, y, z ) { 173 | var c1 = Math.cos( x / 2 ); 174 | var c2 = Math.cos( y / 2 ); 175 | var c3 = Math.cos( z / 2 ); 176 | var s1 = Math.sin( x / 2 ); 177 | var s2 = Math.sin( y / 2 ); 178 | var s3 = Math.sin( z / 2 ); 179 | 180 | this.x = s1 * c2 * c3 + c1 * s2 * s3; 181 | this.y = c1 * s2 * c3 - s1 * c2 * s3; 182 | this.z = c1 * c2 * s3 + s1 * s2 * c3; 183 | this.w = c1 * c2 * c3 - s1 * s2 * s3; 184 | 185 | return this; 186 | }, 187 | 188 | setFromEulerYXZ: function( x, y, z ) { 189 | var c1 = Math.cos( x / 2 ); 190 | var c2 = Math.cos( y / 2 ); 191 | var c3 = Math.cos( z / 2 ); 192 | var s1 = Math.sin( x / 2 ); 193 | var s2 = Math.sin( y / 2 ); 194 | var s3 = Math.sin( z / 2 ); 195 | 196 | this.x = s1 * c2 * c3 + c1 * s2 * s3; 197 | this.y = c1 * s2 * c3 - s1 * c2 * s3; 198 | this.z = c1 * c2 * s3 - s1 * s2 * c3; 199 | this.w = c1 * c2 * c3 + s1 * s2 * s3; 200 | 201 | return this; 202 | }, 203 | 204 | setFromAxisAngle: function ( axis, angle ) { 205 | // http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm 206 | // assumes axis is normalized 207 | 208 | var halfAngle = angle / 2, s = Math.sin( halfAngle ); 209 | 210 | this.x = axis.x * s; 211 | this.y = axis.y * s; 212 | this.z = axis.z * s; 213 | this.w = Math.cos( halfAngle ); 214 | 215 | return this; 216 | }, 217 | 218 | multiply: function ( q ) { 219 | return this.multiplyQuaternions( this, q ); 220 | }, 221 | 222 | multiplyQuaternions: function ( a, b ) { 223 | // from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm 224 | 225 | var qax = a.x, qay = a.y, qaz = a.z, qaw = a.w; 226 | var qbx = b.x, qby = b.y, qbz = b.z, qbw = b.w; 227 | 228 | this.x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby; 229 | this.y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz; 230 | this.z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx; 231 | this.w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz; 232 | 233 | return this; 234 | }, 235 | 236 | inverse: function () { 237 | this.x *= -1; 238 | this.y *= -1; 239 | this.z *= -1; 240 | 241 | this.normalize(); 242 | 243 | return this; 244 | }, 245 | 246 | normalize: function () { 247 | var l = Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w ); 248 | 249 | if ( l === 0 ) { 250 | this.x = 0; 251 | this.y = 0; 252 | this.z = 0; 253 | this.w = 1; 254 | } else { 255 | l = 1 / l; 256 | 257 | this.x = this.x * l; 258 | this.y = this.y * l; 259 | this.z = this.z * l; 260 | this.w = this.w * l; 261 | } 262 | 263 | return this; 264 | }, 265 | 266 | slerp: function ( qb, t ) { 267 | if ( t === 0 ) return this; 268 | if ( t === 1 ) return this.copy( qb ); 269 | 270 | var x = this.x, y = this.y, z = this.z, w = this.w; 271 | 272 | // http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/ 273 | 274 | var cosHalfTheta = w * qb.w + x * qb.x + y * qb.y + z * qb.z; 275 | 276 | if ( cosHalfTheta < 0 ) { 277 | this.w = - qb.w; 278 | this.x = - qb.x; 279 | this.y = - qb.y; 280 | this.z = - qb.z; 281 | 282 | cosHalfTheta = - cosHalfTheta; 283 | } else { 284 | this.copy( qb ); 285 | } 286 | 287 | if ( cosHalfTheta >= 1.0 ) { 288 | this.w = w; 289 | this.x = x; 290 | this.y = y; 291 | this.z = z; 292 | 293 | return this; 294 | } 295 | 296 | var halfTheta = Math.acos( cosHalfTheta ); 297 | var sinHalfTheta = Math.sqrt( 1.0 - cosHalfTheta * cosHalfTheta ); 298 | 299 | if ( Math.abs( sinHalfTheta ) < 0.001 ) { 300 | this.w = 0.5 * ( w + this.w ); 301 | this.x = 0.5 * ( x + this.x ); 302 | this.y = 0.5 * ( y + this.y ); 303 | this.z = 0.5 * ( z + this.z ); 304 | 305 | return this; 306 | } 307 | 308 | var ratioA = Math.sin( ( 1 - t ) * halfTheta ) / sinHalfTheta, 309 | ratioB = Math.sin( t * halfTheta ) / sinHalfTheta; 310 | 311 | this.w = ( w * ratioA + this.w * ratioB ); 312 | this.x = ( x * ratioA + this.x * ratioB ); 313 | this.y = ( y * ratioA + this.y * ratioB ); 314 | this.z = ( z * ratioA + this.z * ratioB ); 315 | 316 | return this; 317 | }, 318 | 319 | setFromUnitVectors: function () { 320 | // http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final 321 | // assumes direction vectors vFrom and vTo are normalized 322 | 323 | var v1, r; 324 | var EPS = 0.000001; 325 | 326 | return function ( vFrom, vTo ) { 327 | if ( v1 === undefined ) v1 = new Vector3(); 328 | 329 | r = vFrom.dot( vTo ) + 1; 330 | 331 | if ( r < EPS ) { 332 | r = 0; 333 | 334 | if ( Math.abs( vFrom.x ) > Math.abs( vFrom.z ) ) { 335 | v1.set( - vFrom.y, vFrom.x, 0 ); 336 | } else { 337 | v1.set( 0, - vFrom.z, vFrom.y ); 338 | } 339 | } else { 340 | v1.crossVectors( vFrom, vTo ); 341 | } 342 | 343 | this.x = v1.x; 344 | this.y = v1.y; 345 | this.z = v1.z; 346 | this.w = r; 347 | 348 | this.normalize(); 349 | 350 | return this; 351 | } 352 | }(), 353 | }; 354 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | const config = { 17 | 18 | // Optionally inject custom Viewer parameters as an option. Each item 19 | // in the array must be an object with the following properties; here is 20 | // an example of the built in CardboardV2 viewer: 21 | // 22 | // { 23 | // id: 'CardboardV2', 24 | // label: 'Cardboard I/O 2015', 25 | // fov: 60, 26 | // interLensDistance: 0.064, 27 | // baselineLensDistance: 0.035, 28 | // screenLensDistance: 0.039, 29 | // distortionCoefficients: [0.34, 0.55], 30 | // inverseCoefficients: [-0.33836704, -0.18162185, 0.862655, -1.2462051, 31 | // 1.0560602, -0.58208317, 0.21609078, -0.05444823, 0.009177956, 32 | // -9.904169E-4, 6.183535E-5, -1.6981803E-6] 33 | // } 34 | // Added in 1.0.12. 35 | ADDITIONAL_VIEWERS: [], 36 | 37 | // Select the viewer by ID. If unspecified, defaults to 'CardboardV1'. 38 | // Added in 1.0.12. 39 | DEFAULT_VIEWER: '', 40 | 41 | // By default, on mobile, a wakelock is necessary to prevent the device's screen 42 | // from turning off without user input. Disable if you're keeping the screen awake through 43 | // other means on mobile. A wakelock is never used on desktop. 44 | // Added in 1.0.3. 45 | MOBILE_WAKE_LOCK: true, 46 | 47 | // Whether or not CardboardVRDisplay is in debug mode. Logs extra 48 | // messages. Added in 1.0.2. 49 | DEBUG: false, 50 | 51 | // The URL to JSON of DPDB information. By default, uses the data 52 | // from https://github.com/WebVRRocks/webvr-polyfill-dpdb; if left 53 | // falsy, then no attempt is made. 54 | // Added in 1.0.1 55 | DPDB_URL: 'https://dpdb.webvr.rocks/dpdb.json', 56 | 57 | // Complementary filter coefficient. 0 for accelerometer, 1 for gyro. 58 | K_FILTER: 0.98, 59 | 60 | // How far into the future to predict during fast motion (in seconds). 61 | PREDICTION_TIME_S: 0.040, 62 | 63 | // Flag to disabled the UI in VR Mode. 64 | CARDBOARD_UI_DISABLED: false, 65 | 66 | // Flag to disable the instructions to rotate your device. 67 | ROTATE_INSTRUCTIONS_DISABLED: false, 68 | 69 | // Enable yaw panning only, disabling roll and pitch. This can be useful 70 | // for panoramas with nothing interesting above or below. 71 | YAW_ONLY: false, 72 | 73 | // Scales the recommended buffer size reported by WebVR, which can improve 74 | // performance. 75 | // UPDATE(2016-05-03): Setting this to 0.5 by default since 1.0 does not 76 | // perform well on many mobile devices. 77 | BUFFER_SCALE: 0.5, 78 | 79 | // Allow VRDisplay.submitFrame to change gl bindings, which is more 80 | // efficient if the application code will re-bind its resources on the 81 | // next frame anyway. This has been seen to cause rendering glitches with 82 | // THREE.js. 83 | // Dirty bindings include: gl.FRAMEBUFFER_BINDING, gl.CURRENT_PROGRAM, 84 | // gl.ARRAY_BUFFER_BINDING, gl.ELEMENT_ARRAY_BUFFER_BINDING, 85 | // and gl.TEXTURE_BINDING_2D for texture unit 0. 86 | DIRTY_SUBMIT_FRAME_BINDINGS: false, 87 | }; 88 | 89 | export default config; 90 | -------------------------------------------------------------------------------- /src/pose-sensor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import FusionPoseSensor from './sensor-fusion/fusion-pose-sensor.js'; 17 | import { Vector3, Quaternion } from './math-util.js'; 18 | 19 | // Frequency which the Sensors will attempt to fire their 20 | // `reading` functions at. Use 60hz since we generally 21 | // can't get higher without native VR hardware. 22 | const SENSOR_FREQUENCY = 60; 23 | 24 | const X_AXIS = new Vector3(1, 0, 0); 25 | const Z_AXIS = new Vector3(0, 0, 1); 26 | 27 | // Quaternion to rotate from sensor coordinates to WebVR coordinates 28 | const SENSOR_TO_VR = new Quaternion(); 29 | SENSOR_TO_VR.setFromAxisAngle(X_AXIS, -Math.PI / 2); 30 | SENSOR_TO_VR.multiply(new Quaternion().setFromAxisAngle(Z_AXIS, Math.PI / 2)); 31 | 32 | /** 33 | * An abstraction class around either using the new RelativeOrientationSensor, 34 | * or `devicemotion` events with complimentary filter via fusion-pose-sensor.js. 35 | */ 36 | export default class PoseSensor { 37 | constructor(config) { 38 | this.config = config; 39 | this.sensor = null; 40 | this.fusionSensor = null; 41 | this._out = new Float32Array(4); 42 | 43 | // Can be 'sensor' (using RelativeOrientationSensor) or 44 | // 'devicemotion' (using devicemotion events via FusionPoseSensor), 45 | // or `null` if not yet set. 46 | this.api = null; 47 | 48 | // Store any errors from Sensors for debugging purposes 49 | this.errors = []; 50 | 51 | // Quaternions for caching transforms 52 | this._sensorQ = new Quaternion(); 53 | this._outQ = new Quaternion(); 54 | 55 | this._onSensorRead = this._onSensorRead.bind(this); 56 | this._onSensorError = this._onSensorError.bind(this); 57 | 58 | this.init(); 59 | } 60 | 61 | init() { 62 | // Attempt to use the RelativeOrientationSensor from Generic Sensor APIs. 63 | // First available in Chrome M63, this can fail for several reasons, and attempt 64 | // to fallback to devicemotion. Failure scenarios include: 65 | // 66 | // * Generic Sensor APIs do not exist; fallback to devicemotion. 67 | // * Underlying sensor does not exist; no fallback possible. 68 | // * Feature Policy failure (in an iframe); no fallback. 69 | // https://github.com/immersive-web/webxr/issues/86 70 | // * Permission to sensor data denied; respect user agent; no fallback to devicemotion. 71 | // Browsers are heading towards disabling devicemotion when sensors are denied as well. 72 | // https://www.chromestatus.com/feature/5023919287304192 73 | let sensor = null; 74 | try { 75 | sensor = new RelativeOrientationSensor({ 76 | frequency: SENSOR_FREQUENCY, 77 | // Use `referenceFrame: screen` so we don't have to manage the orientation 78 | // of the device. First available in Chrome m66 (in release at time of writing), 79 | // and this will fail in earlier versions, kicking off `devicemotion` fallback. 80 | // @see https://w3c.github.io/accelerometer/#screen-coordinate-system 81 | referenceFrame: 'screen', 82 | }); 83 | sensor.addEventListener('error', this._onSensorError); 84 | } catch (error) { 85 | this.errors.push(error); 86 | 87 | // Sensors are available in Chrome M63, however the Feature Policy 88 | // integration is not available until Chrome M65, resulting in Sensors 89 | // only being available in main frames. 90 | // https://developers.google.com/web/updates/2017/09/sensors-for-the-web#feature_policy_integration 91 | if (error.name === 'SecurityError') { 92 | console.error('Cannot construct sensors due to the Feature Policy'); 93 | console.warn('Attempting to fall back using "devicemotion"; however this will ' + 94 | 'fail in the future without correct permissions.'); 95 | this.useDeviceMotion(); 96 | } else if (error.name === 'ReferenceError') { 97 | // Fall back to devicemotion. 98 | this.useDeviceMotion(); 99 | } else { 100 | console.error(error); 101 | } 102 | } 103 | 104 | if (sensor) { 105 | this.api = 'sensor'; 106 | this.sensor = sensor; 107 | this.sensor.addEventListener('reading', this._onSensorRead); 108 | this.sensor.start(); 109 | } 110 | } 111 | 112 | useDeviceMotion() { 113 | this.api = 'devicemotion'; 114 | this.fusionSensor = new FusionPoseSensor(this.config.K_FILTER, 115 | this.config.PREDICTION_TIME_S, 116 | this.config.YAW_ONLY, 117 | this.config.DEBUG); 118 | if (this.sensor) { 119 | this.sensor.removeEventListener('reading', this._onSensorRead); 120 | this.sensor.removeEventListener('error', this._onSensorError); 121 | this.sensor = null; 122 | } 123 | } 124 | 125 | getOrientation() { 126 | if (this.fusionSensor) { 127 | return this.fusionSensor.getOrientation(); 128 | } 129 | 130 | if (!this.sensor || !this.sensor.quaternion) { 131 | this._out[0] = this._out[1] = this._out[2] = 0; 132 | this._out[3] = 1; 133 | return this._out; 134 | } 135 | 136 | // Convert to THREE coordinate system: -Z forward, Y up, X right. 137 | const q = this.sensor.quaternion; 138 | this._sensorQ.set(q[0], q[1], q[2], q[3]); 139 | 140 | const out = this._outQ; 141 | out.copy(SENSOR_TO_VR); 142 | out.multiply(this._sensorQ); 143 | 144 | // Handle the yaw-only case. 145 | if (this.config.YAW_ONLY) { 146 | // Make a quaternion that only turns around the Y-axis. 147 | out.x = out.z = 0; 148 | out.normalize(); 149 | } 150 | 151 | this._out[0] = out.x; 152 | this._out[1] = out.y; 153 | this._out[2] = out.z; 154 | this._out[3] = out.w; 155 | return this._out; 156 | } 157 | 158 | _onSensorError(event) { 159 | this.errors.push(event.error); 160 | if (event.error.name === 'NotAllowedError') { 161 | console.error('Permission to access sensor was denied'); 162 | } else if (event.error.name === 'NotReadableError') { 163 | console.error('Sensor could not be read'); 164 | } else { 165 | console.error(event.error); 166 | } 167 | this.useDeviceMotion(); 168 | } 169 | 170 | _onSensorRead() {} 171 | } 172 | -------------------------------------------------------------------------------- /src/rotate-instructions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import * as Util from './util.js'; 17 | import rotateInstructionsAsset from './assets/rotate-instructions.js'; 18 | 19 | function RotateInstructions() { 20 | this.loadIcon_(); 21 | 22 | var overlay = document.createElement('div'); 23 | var s = overlay.style; 24 | s.position = 'fixed'; 25 | s.top = 0; 26 | s.right = 0; 27 | s.bottom = 0; 28 | s.left = 0; 29 | s.backgroundColor = 'gray'; 30 | s.fontFamily = 'sans-serif'; 31 | // Force this to be above the fullscreen canvas, which is at zIndex: 999999. 32 | s.zIndex = 1000000; 33 | 34 | var img = document.createElement('img'); 35 | img.src = this.icon; 36 | var s = img.style; 37 | s.marginLeft = '25%'; 38 | s.marginTop = '25%'; 39 | s.width = '50%'; 40 | overlay.appendChild(img); 41 | 42 | var text = document.createElement('div'); 43 | var s = text.style; 44 | s.textAlign = 'center'; 45 | s.fontSize = '16px'; 46 | s.lineHeight = '24px'; 47 | s.margin = '24px 25%'; 48 | s.width = '50%'; 49 | text.innerHTML = 'Place your phone into your Cardboard viewer.'; 50 | overlay.appendChild(text); 51 | 52 | var snackbar = document.createElement('div'); 53 | var s = snackbar.style; 54 | s.backgroundColor = '#CFD8DC'; 55 | s.position = 'fixed'; 56 | s.bottom = 0; 57 | s.width = '100%'; 58 | s.height = '48px'; 59 | s.padding = '14px 24px'; 60 | s.boxSizing = 'border-box'; 61 | s.color = '#656A6B'; 62 | overlay.appendChild(snackbar); 63 | 64 | var snackbarText = document.createElement('div'); 65 | snackbarText.style.float = 'left'; 66 | snackbarText.innerHTML = 'No Cardboard viewer?'; 67 | 68 | var snackbarButton = document.createElement('a'); 69 | snackbarButton.href = 'https://www.google.com/get/cardboard/get-cardboard/'; 70 | snackbarButton.innerHTML = 'get one'; 71 | snackbarButton.target = '_blank'; 72 | var s = snackbarButton.style; 73 | s.float = 'right'; 74 | s.fontWeight = 600; 75 | s.textTransform = 'uppercase'; 76 | s.borderLeft = '1px solid gray'; 77 | s.paddingLeft = '24px'; 78 | s.textDecoration = 'none'; 79 | s.color = '#656A6B'; 80 | 81 | snackbar.appendChild(snackbarText); 82 | snackbar.appendChild(snackbarButton); 83 | 84 | this.overlay = overlay; 85 | this.text = text; 86 | 87 | this.hide(); 88 | } 89 | 90 | RotateInstructions.prototype.show = function(parent) { 91 | if (!parent && !this.overlay.parentElement) { 92 | document.body.appendChild(this.overlay); 93 | } else if (parent) { 94 | if (this.overlay.parentElement && this.overlay.parentElement != parent) 95 | this.overlay.parentElement.removeChild(this.overlay); 96 | 97 | parent.appendChild(this.overlay); 98 | } 99 | 100 | this.overlay.style.display = 'block'; 101 | 102 | var img = this.overlay.querySelector('img'); 103 | var s = img.style; 104 | 105 | if (Util.isLandscapeMode()) { 106 | s.width = '20%'; 107 | s.marginLeft = '40%'; 108 | s.marginTop = '3%'; 109 | } else { 110 | s.width = '50%'; 111 | s.marginLeft = '25%'; 112 | s.marginTop = '25%'; 113 | } 114 | }; 115 | 116 | RotateInstructions.prototype.hide = function() { 117 | this.overlay.style.display = 'none'; 118 | }; 119 | 120 | RotateInstructions.prototype.showTemporarily = function(ms, parent) { 121 | this.show(parent); 122 | this.timer = setTimeout(this.hide.bind(this), ms); 123 | }; 124 | 125 | RotateInstructions.prototype.disableShowTemporarily = function() { 126 | clearTimeout(this.timer); 127 | }; 128 | 129 | RotateInstructions.prototype.update = function() { 130 | this.disableShowTemporarily(); 131 | // In portrait VR mode, tell the user to rotate to landscape. Otherwise, hide 132 | // the instructions. 133 | if (!Util.isLandscapeMode() && Util.isMobile()) { 134 | this.show(); 135 | } else { 136 | this.hide(); 137 | } 138 | }; 139 | 140 | RotateInstructions.prototype.loadIcon_ = function() { 141 | this.icon = Util.dataUri('image/svg+xml', rotateInstructionsAsset); 142 | }; 143 | 144 | export default RotateInstructions; 145 | -------------------------------------------------------------------------------- /src/sensor-fusion/complementary-filter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import SensorSample from './sensor-sample.js'; 17 | import * as MathUtil from '../math-util.js'; 18 | import * as Util from '../util.js'; 19 | 20 | /** 21 | * An implementation of a simple complementary filter, which fuses gyroscope and 22 | * accelerometer data from the 'devicemotion' event. 23 | * 24 | * Accelerometer data is very noisy, but stable over the long term. 25 | * Gyroscope data is smooth, but tends to drift over the long term. 26 | * 27 | * This fusion is relatively simple: 28 | * 1. Get orientation estimates from accelerometer by applying a low-pass filter 29 | * on that data. 30 | * 2. Get orientation estimates from gyroscope by integrating over time. 31 | * 3. Combine the two estimates, weighing (1) in the long term, but (2) for the 32 | * short term. 33 | */ 34 | function ComplementaryFilter(kFilter, isDebug) { 35 | this.kFilter = kFilter; 36 | this.isDebug = isDebug; 37 | 38 | // Raw sensor measurements. 39 | this.currentAccelMeasurement = new SensorSample(); 40 | this.currentGyroMeasurement = new SensorSample(); 41 | this.previousGyroMeasurement = new SensorSample(); 42 | 43 | // Set default look direction to be in the correct direction. 44 | if (Util.isIOS()) { 45 | this.filterQ = new MathUtil.Quaternion(-1, 0, 0, 1); 46 | } else { 47 | this.filterQ = new MathUtil.Quaternion(1, 0, 0, 1); 48 | } 49 | this.previousFilterQ = new MathUtil.Quaternion(); 50 | this.previousFilterQ.copy(this.filterQ); 51 | 52 | // Orientation based on the accelerometer. 53 | this.accelQ = new MathUtil.Quaternion(); 54 | // Whether or not the orientation has been initialized. 55 | this.isOrientationInitialized = false; 56 | // Running estimate of gravity based on the current orientation. 57 | this.estimatedGravity = new MathUtil.Vector3(); 58 | // Measured gravity based on accelerometer. 59 | this.measuredGravity = new MathUtil.Vector3(); 60 | 61 | // Debug only quaternion of gyro-based orientation. 62 | this.gyroIntegralQ = new MathUtil.Quaternion(); 63 | } 64 | 65 | ComplementaryFilter.prototype.addAccelMeasurement = function(vector, timestampS) { 66 | this.currentAccelMeasurement.set(vector, timestampS); 67 | }; 68 | 69 | ComplementaryFilter.prototype.addGyroMeasurement = function(vector, timestampS) { 70 | this.currentGyroMeasurement.set(vector, timestampS); 71 | 72 | var deltaT = timestampS - this.previousGyroMeasurement.timestampS; 73 | if (Util.isTimestampDeltaValid(deltaT)) { 74 | this.run_(); 75 | } 76 | 77 | this.previousGyroMeasurement.copy(this.currentGyroMeasurement); 78 | }; 79 | 80 | ComplementaryFilter.prototype.run_ = function() { 81 | 82 | if (!this.isOrientationInitialized) { 83 | this.accelQ = this.accelToQuaternion_(this.currentAccelMeasurement.sample); 84 | this.previousFilterQ.copy(this.accelQ); 85 | this.isOrientationInitialized = true; 86 | return; 87 | } 88 | 89 | var deltaT = this.currentGyroMeasurement.timestampS - 90 | this.previousGyroMeasurement.timestampS; 91 | 92 | // Convert gyro rotation vector to a quaternion delta. 93 | var gyroDeltaQ = this.gyroToQuaternionDelta_(this.currentGyroMeasurement.sample, deltaT); 94 | this.gyroIntegralQ.multiply(gyroDeltaQ); 95 | 96 | // filter_1 = K * (filter_0 + gyro * dT) + (1 - K) * accel. 97 | this.filterQ.copy(this.previousFilterQ); 98 | this.filterQ.multiply(gyroDeltaQ); 99 | 100 | // Calculate the delta between the current estimated gravity and the real 101 | // gravity vector from accelerometer. 102 | var invFilterQ = new MathUtil.Quaternion(); 103 | invFilterQ.copy(this.filterQ); 104 | invFilterQ.inverse(); 105 | 106 | this.estimatedGravity.set(0, 0, -1); 107 | this.estimatedGravity.applyQuaternion(invFilterQ); 108 | this.estimatedGravity.normalize(); 109 | 110 | this.measuredGravity.copy(this.currentAccelMeasurement.sample); 111 | this.measuredGravity.normalize(); 112 | 113 | // Compare estimated gravity with measured gravity, get the delta quaternion 114 | // between the two. 115 | var deltaQ = new MathUtil.Quaternion(); 116 | deltaQ.setFromUnitVectors(this.estimatedGravity, this.measuredGravity); 117 | deltaQ.inverse(); 118 | 119 | if (this.isDebug) { 120 | console.log('Delta: %d deg, G_est: (%s, %s, %s), G_meas: (%s, %s, %s)', 121 | MathUtil.radToDeg * Util.getQuaternionAngle(deltaQ), 122 | (this.estimatedGravity.x).toFixed(1), 123 | (this.estimatedGravity.y).toFixed(1), 124 | (this.estimatedGravity.z).toFixed(1), 125 | (this.measuredGravity.x).toFixed(1), 126 | (this.measuredGravity.y).toFixed(1), 127 | (this.measuredGravity.z).toFixed(1)); 128 | } 129 | 130 | // Calculate the SLERP target: current orientation plus the measured-estimated 131 | // quaternion delta. 132 | var targetQ = new MathUtil.Quaternion(); 133 | targetQ.copy(this.filterQ); 134 | targetQ.multiply(deltaQ); 135 | 136 | // SLERP factor: 0 is pure gyro, 1 is pure accel. 137 | this.filterQ.slerp(targetQ, 1 - this.kFilter); 138 | 139 | this.previousFilterQ.copy(this.filterQ); 140 | }; 141 | 142 | ComplementaryFilter.prototype.getOrientation = function() { 143 | return this.filterQ; 144 | }; 145 | 146 | ComplementaryFilter.prototype.accelToQuaternion_ = function(accel) { 147 | var normAccel = new MathUtil.Vector3(); 148 | normAccel.copy(accel); 149 | normAccel.normalize(); 150 | var quat = new MathUtil.Quaternion(); 151 | quat.setFromUnitVectors(new MathUtil.Vector3(0, 0, -1), normAccel); 152 | quat.inverse(); 153 | return quat; 154 | }; 155 | 156 | ComplementaryFilter.prototype.gyroToQuaternionDelta_ = function(gyro, dt) { 157 | // Extract axis and angle from the gyroscope data. 158 | var quat = new MathUtil.Quaternion(); 159 | var axis = new MathUtil.Vector3(); 160 | axis.copy(gyro); 161 | axis.normalize(); 162 | quat.setFromAxisAngle(axis, gyro.length() * dt); 163 | return quat; 164 | }; 165 | 166 | 167 | export default ComplementaryFilter; 168 | -------------------------------------------------------------------------------- /src/sensor-fusion/fusion-pose-sensor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | import ComplementaryFilter from './complementary-filter.js'; 16 | import PosePredictor from './pose-predictor.js'; 17 | import * as MathUtil from '../math-util.js'; 18 | import * as Util from '../util.js'; 19 | 20 | /** 21 | * The pose sensor, implemented using DeviceMotion APIs. 22 | * 23 | * @param {number} kFilter 24 | * @param {number} predictionTime 25 | * @param {boolean} yawOnly 26 | * @param {boolean} isDebug 27 | */ 28 | function FusionPoseSensor(kFilter, predictionTime, yawOnly, isDebug) { 29 | this.yawOnly = yawOnly; 30 | 31 | this.accelerometer = new MathUtil.Vector3(); 32 | this.gyroscope = new MathUtil.Vector3(); 33 | 34 | this.filter = new ComplementaryFilter(kFilter, isDebug); 35 | this.posePredictor = new PosePredictor(predictionTime, isDebug); 36 | 37 | this.isFirefoxAndroid = Util.isFirefoxAndroid(); 38 | this.isIOS = Util.isIOS(); 39 | // Chrome as of m66 started reporting `rotationRate` in degrees rather 40 | // than radians, to be consistent with other browsers. 41 | // https://github.com/immersive-web/cardboard-vr-display/issues/18 42 | let chromeVersion = Util.getChromeVersion(); 43 | this.isDeviceMotionInRadians = !this.isIOS && chromeVersion && chromeVersion < 66; 44 | // In Chrome m65 and Safari 13.4 there's a regression of devicemotion events. Fallback 45 | // to using deviceorientation for these specific builds. More information 46 | // at `Util.isChromeWithoutDeviceMotion`. 47 | this.isWithoutDeviceMotion = Util.isChromeWithoutDeviceMotion() || Util.isSafariWithoutDeviceMotion(); 48 | 49 | this.filterToWorldQ = new MathUtil.Quaternion(); 50 | 51 | // Set the filter to world transform, depending on OS. 52 | if (Util.isIOS()) { 53 | this.filterToWorldQ.setFromAxisAngle(new MathUtil.Vector3(1, 0, 0), Math.PI / 2); 54 | } else { 55 | this.filterToWorldQ.setFromAxisAngle(new MathUtil.Vector3(1, 0, 0), -Math.PI / 2); 56 | } 57 | 58 | this.inverseWorldToScreenQ = new MathUtil.Quaternion(); 59 | this.worldToScreenQ = new MathUtil.Quaternion(); 60 | this.originalPoseAdjustQ = new MathUtil.Quaternion(); 61 | this.originalPoseAdjustQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1), 62 | -window.orientation * Math.PI / 180); 63 | 64 | this.setScreenTransform_(); 65 | // Adjust this filter for being in landscape mode. 66 | if (Util.isLandscapeMode()) { 67 | this.filterToWorldQ.multiply(this.inverseWorldToScreenQ); 68 | } 69 | 70 | // Keep track of a reset transform for resetSensor. 71 | this.resetQ = new MathUtil.Quaternion(); 72 | 73 | this.orientationOut_ = new Float32Array(4); 74 | 75 | this.start(); 76 | } 77 | 78 | FusionPoseSensor.prototype.getPosition = function() { 79 | // This PoseSensor doesn't support position 80 | return null; 81 | }; 82 | 83 | FusionPoseSensor.prototype.getOrientation = function() { 84 | let orientation; 85 | 86 | // Hack around using deviceorientation instead of devicemotion 87 | if (this.isWithoutDeviceMotion && this._deviceOrientationQ) { 88 | // We must rotate 90 (or -90, based on initial rotation) degrees 89 | // on the Y axis to get the correct orientation of looking down the -Z axis. 90 | this.deviceOrientationFixQ = this.deviceOrientationFixQ || (function () { 91 | const z = new MathUtil.Quaternion().setFromAxisAngle(new MathUtil.Vector3(0, 0, -1), 0); 92 | const y = new MathUtil.Quaternion() 93 | 94 | if (window.orientation === -90) { 95 | y.setFromAxisAngle(new MathUtil.Vector3(0, 1, 0), Math.PI / -2); 96 | } else { 97 | y.setFromAxisAngle(new MathUtil.Vector3(0, 1, 0), Math.PI / 2); 98 | } 99 | 100 | return z.multiply(y); 101 | })(); 102 | 103 | this.deviceOrientationFilterToWorldQ = this.deviceOrientationFilterToWorldQ || (function () { 104 | const q = new MathUtil.Quaternion(); 105 | q.setFromAxisAngle(new MathUtil.Vector3(1, 0, 0), -Math.PI / 2); 106 | return q; 107 | })(); 108 | 109 | orientation = this._deviceOrientationQ; 110 | var out = new MathUtil.Quaternion(); 111 | out.copy(orientation); 112 | out.multiply(this.deviceOrientationFilterToWorldQ); 113 | out.multiply(this.resetQ); 114 | out.multiply(this.worldToScreenQ); 115 | out.multiplyQuaternions(this.deviceOrientationFixQ, out); 116 | 117 | // Handle the yaw-only case. 118 | if (this.yawOnly) { 119 | // Make a quaternion that only turns around the Y-axis. 120 | out.x = 0; 121 | out.z = 0; 122 | out.normalize(); 123 | } 124 | 125 | this.orientationOut_[0] = out.x; 126 | this.orientationOut_[1] = out.y; 127 | this.orientationOut_[2] = out.z; 128 | this.orientationOut_[3] = out.w; 129 | return this.orientationOut_; 130 | } else { 131 | // Convert from filter space to the the same system used by the 132 | // deviceorientation event. 133 | let filterOrientation = this.filter.getOrientation(); 134 | 135 | // Predict orientation. 136 | orientation = this.posePredictor.getPrediction(filterOrientation, 137 | this.gyroscope, 138 | this.previousTimestampS); 139 | } 140 | 141 | // Convert to THREE coordinate system: -Z forward, Y up, X right. 142 | var out = new MathUtil.Quaternion(); 143 | out.copy(this.filterToWorldQ); 144 | out.multiply(this.resetQ); 145 | out.multiply(orientation); 146 | out.multiply(this.worldToScreenQ); 147 | 148 | // Handle the yaw-only case. 149 | if (this.yawOnly) { 150 | // Make a quaternion that only turns around the Y-axis. 151 | out.x = 0; 152 | out.z = 0; 153 | out.normalize(); 154 | } 155 | 156 | this.orientationOut_[0] = out.x; 157 | this.orientationOut_[1] = out.y; 158 | this.orientationOut_[2] = out.z; 159 | this.orientationOut_[3] = out.w; 160 | return this.orientationOut_; 161 | }; 162 | 163 | FusionPoseSensor.prototype.resetPose = function() { 164 | // Reduce to inverted yaw-only. 165 | this.resetQ.copy(this.filter.getOrientation()); 166 | this.resetQ.x = 0; 167 | this.resetQ.y = 0; 168 | this.resetQ.z *= -1; 169 | this.resetQ.normalize(); 170 | 171 | // Take into account extra transformations in landscape mode. 172 | if (Util.isLandscapeMode()) { 173 | this.resetQ.multiply(this.inverseWorldToScreenQ); 174 | } 175 | 176 | // Take into account original pose. 177 | this.resetQ.multiply(this.originalPoseAdjustQ); 178 | }; 179 | 180 | FusionPoseSensor.prototype.onDeviceOrientation_ = function(e) { 181 | this._deviceOrientationQ = this._deviceOrientationQ || new MathUtil.Quaternion(); 182 | let { alpha, beta, gamma } = e; 183 | alpha = (alpha || 0) * Math.PI / 180; 184 | beta = (beta || 0) * Math.PI / 180; 185 | gamma = (gamma || 0) * Math.PI / 180; 186 | this._deviceOrientationQ.setFromEulerYXZ(beta, alpha, -gamma); 187 | }; 188 | 189 | FusionPoseSensor.prototype.onDeviceMotion_ = function(deviceMotion) { 190 | this.updateDeviceMotion_(deviceMotion); 191 | }; 192 | 193 | FusionPoseSensor.prototype.updateDeviceMotion_ = function(deviceMotion) { 194 | var accGravity = deviceMotion.accelerationIncludingGravity; 195 | var rotRate = deviceMotion.rotationRate; 196 | var timestampS = deviceMotion.timeStamp / 1000; 197 | 198 | var deltaS = timestampS - this.previousTimestampS; 199 | 200 | // On Firefox/iOS the `timeStamp` properties can come in out of order. 201 | // so emit a warning about it and then stop. The rotation still ends up 202 | // working. 203 | // @TODO is there a better way to handle this with the `interval` property 204 | // from the device motion event? `timeStamp` seems to be non-standard. 205 | if (deltaS < 0) { 206 | Util.warnOnce('fusion-pose-sensor:invalid:non-monotonic', 207 | 'Invalid timestamps detected: non-monotonic timestamp from devicemotion'); 208 | this.previousTimestampS = timestampS; 209 | return; 210 | } else if (deltaS <= Util.MIN_TIMESTEP || deltaS > Util.MAX_TIMESTEP) { 211 | Util.warnOnce('fusion-pose-sensor:invalid:outside-threshold', 212 | 'Invalid timestamps detected: Timestamp from devicemotion outside expected range.'); 213 | this.previousTimestampS = timestampS; 214 | return; 215 | } 216 | 217 | this.accelerometer.set(-accGravity.x, -accGravity.y, -accGravity.z); 218 | if (rotRate) { 219 | if (Util.isR7()) { 220 | this.gyroscope.set(-rotRate.beta, rotRate.alpha, rotRate.gamma); 221 | } else { 222 | this.gyroscope.set(rotRate.alpha, rotRate.beta, rotRate.gamma); 223 | } 224 | 225 | // DeviceMotionEvents should report `rotationRate` in degrees, so we need 226 | // to convert to radians. However, some browsers (Android Chrome < m66) report 227 | // the rotation as radians, in which case no conversion is needed. 228 | if (!this.isDeviceMotionInRadians) { 229 | this.gyroscope.multiplyScalar(Math.PI / 180); 230 | } 231 | 232 | this.filter.addGyroMeasurement(this.gyroscope, timestampS); 233 | } 234 | 235 | this.filter.addAccelMeasurement(this.accelerometer, timestampS); 236 | 237 | this.previousTimestampS = timestampS; 238 | }; 239 | 240 | FusionPoseSensor.prototype.onOrientationChange_ = function(screenOrientation) { 241 | this.setScreenTransform_(); 242 | }; 243 | 244 | /** 245 | * This is only needed if we are in an cross origin iframe on iOS to work around 246 | * this issue: https://bugs.webkit.org/show_bug.cgi?id=152299. 247 | */ 248 | FusionPoseSensor.prototype.onMessage_ = function(event) { 249 | var message = event.data; 250 | 251 | // If there's no message type, ignore it. 252 | if (!message || !message.type) { 253 | return; 254 | } 255 | 256 | // Ignore all messages that aren't devicemotion. 257 | var type = message.type.toLowerCase(); 258 | if (type !== 'devicemotion') { 259 | return; 260 | } 261 | 262 | // Update device motion. 263 | this.updateDeviceMotion_(message.deviceMotionEvent); 264 | }; 265 | 266 | FusionPoseSensor.prototype.setScreenTransform_ = function() { 267 | this.worldToScreenQ.set(0, 0, 0, 1); 268 | switch (window.orientation) { 269 | case 0: 270 | break; 271 | case 90: 272 | this.worldToScreenQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1), -Math.PI / 2); 273 | break; 274 | case -90: 275 | this.worldToScreenQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1), Math.PI / 2); 276 | break; 277 | case 180: 278 | // TODO. 279 | break; 280 | } 281 | this.inverseWorldToScreenQ.copy(this.worldToScreenQ); 282 | this.inverseWorldToScreenQ.inverse(); 283 | }; 284 | 285 | FusionPoseSensor.prototype.start = function() { 286 | this.onDeviceMotionCallback_ = this.onDeviceMotion_.bind(this); 287 | this.onOrientationChangeCallback_ = this.onOrientationChange_.bind(this); 288 | this.onMessageCallback_ = this.onMessage_.bind(this); 289 | this.onDeviceOrientationCallback_ = this.onDeviceOrientation_.bind(this); 290 | 291 | // Only listen for postMessages if we're in an iOS and embedded inside a cross 292 | // origin IFrame. In this case, the polyfill can still work if the containing 293 | // page sends synthetic devicemotion events. For an example of this, see 294 | // the iframe example in the repo at `examples/iframe.html` 295 | if (Util.isIOS() && Util.isInsideCrossOriginIFrame()) { 296 | window.addEventListener('message', this.onMessageCallback_); 297 | } 298 | window.addEventListener('orientationchange', this.onOrientationChangeCallback_); 299 | if (this.isWithoutDeviceMotion) { 300 | window.addEventListener('deviceorientation', this.onDeviceOrientationCallback_); 301 | } else { 302 | window.addEventListener('devicemotion', this.onDeviceMotionCallback_); 303 | } 304 | }; 305 | 306 | FusionPoseSensor.prototype.stop = function() { 307 | window.removeEventListener('devicemotion', this.onDeviceMotionCallback_); 308 | window.removeEventListener('deviceorientation', this.onDeviceOrientationCallback_); 309 | window.removeEventListener('orientationchange', this.onOrientationChangeCallback_); 310 | window.removeEventListener('message', this.onMessageCallback_); 311 | }; 312 | 313 | export default FusionPoseSensor; 314 | -------------------------------------------------------------------------------- /src/sensor-fusion/pose-predictor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | import * as MathUtil from '../math-util.js'; 16 | 17 | /** 18 | * Given an orientation and the gyroscope data, predicts the future orientation 19 | * of the head. This makes rendering appear faster. 20 | * 21 | * Also see: http://msl.cs.uiuc.edu/~lavalle/papers/LavYerKatAnt14.pdf 22 | * 23 | * @param {Number} predictionTimeS time from head movement to the appearance of 24 | * the corresponding image. 25 | */ 26 | function PosePredictor(predictionTimeS, isDebug) { 27 | this.predictionTimeS = predictionTimeS; 28 | this.isDebug = isDebug; 29 | 30 | // The quaternion corresponding to the previous state. 31 | this.previousQ = new MathUtil.Quaternion(); 32 | // Previous time a prediction occurred. 33 | this.previousTimestampS = null; 34 | 35 | // The delta quaternion that adjusts the current pose. 36 | this.deltaQ = new MathUtil.Quaternion(); 37 | // The output quaternion. 38 | this.outQ = new MathUtil.Quaternion(); 39 | } 40 | 41 | PosePredictor.prototype.getPrediction = function(currentQ, gyro, timestampS) { 42 | if (!this.previousTimestampS) { 43 | this.previousQ.copy(currentQ); 44 | this.previousTimestampS = timestampS; 45 | return currentQ; 46 | } 47 | 48 | // Calculate axis and angle based on gyroscope rotation rate data. 49 | var axis = new MathUtil.Vector3(); 50 | axis.copy(gyro); 51 | axis.normalize(); 52 | 53 | var angularSpeed = gyro.length(); 54 | 55 | // If we're rotating slowly, don't do prediction. 56 | if (angularSpeed < MathUtil.degToRad * 20) { 57 | if (this.isDebug) { 58 | console.log('Moving slowly, at %s deg/s: no prediction', 59 | (MathUtil.radToDeg * angularSpeed).toFixed(1)); 60 | } 61 | this.outQ.copy(currentQ); 62 | this.previousQ.copy(currentQ); 63 | return this.outQ; 64 | } 65 | 66 | // Get the predicted angle based on the time delta and latency. 67 | var deltaT = timestampS - this.previousTimestampS; 68 | var predictAngle = angularSpeed * this.predictionTimeS; 69 | 70 | this.deltaQ.setFromAxisAngle(axis, predictAngle); 71 | this.outQ.copy(this.previousQ); 72 | this.outQ.multiply(this.deltaQ); 73 | 74 | this.previousQ.copy(currentQ); 75 | this.previousTimestampS = timestampS; 76 | 77 | return this.outQ; 78 | }; 79 | 80 | 81 | export default PosePredictor; 82 | -------------------------------------------------------------------------------- /src/sensor-fusion/sensor-sample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | function SensorSample(sample, timestampS) { 17 | this.set(sample, timestampS); 18 | }; 19 | 20 | SensorSample.prototype.set = function(sample, timestampS) { 21 | this.sample = sample; 22 | this.timestampS = timestampS; 23 | }; 24 | 25 | SensorSample.prototype.copy = function(sensorSample) { 26 | this.set(sensorSample.sample, sensorSample.timestampS); 27 | }; 28 | 29 | export default SensorSample; 30 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | export const MIN_TIMESTEP = 0.001; 17 | export const MAX_TIMESTEP = 1; 18 | 19 | export const dataUri = function(mimeType, svg) { 20 | return 'data:' + mimeType + ',' + encodeURIComponent(svg); 21 | }; 22 | 23 | export const clamp = function(value, min, max) { 24 | return Math.min(Math.max(min, value), max); 25 | }; 26 | 27 | export const lerp = function(a, b, t) { 28 | return a + ((b - a) * t); 29 | }; 30 | 31 | export const isIOS = (function() { 32 | var isIOS = /iPad|iPhone|iPod/.test(navigator.platform); 33 | return function() { 34 | return isIOS; 35 | }; 36 | })(); 37 | 38 | export const isWebViewAndroid = (function() { 39 | var isWebViewAndroid = navigator.userAgent.indexOf('Version') !== -1 && 40 | navigator.userAgent.indexOf('Android') !== -1 && 41 | navigator.userAgent.indexOf('Chrome') !== -1; 42 | return function() { 43 | return isWebViewAndroid; 44 | }; 45 | })(); 46 | 47 | export const isSafari = (function() { 48 | var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 49 | return function() { 50 | return isSafari; 51 | }; 52 | })(); 53 | 54 | export const isFirefoxAndroid = (function() { 55 | var isFirefoxAndroid = navigator.userAgent.indexOf('Firefox') !== -1 && 56 | navigator.userAgent.indexOf('Android') !== -1; 57 | return function() { 58 | return isFirefoxAndroid; 59 | }; 60 | })(); 61 | 62 | /** 63 | * Returns a number value indiciating the version of Chrome being used, 64 | * or otherwise `null` if not on Chrome. 65 | */ 66 | export const getChromeVersion = (function() { 67 | const match = navigator.userAgent.match(/.*Chrome\/([0-9]+)/); 68 | const value = match ? parseInt(match[1], 10) : null; 69 | return function() { 70 | return value; 71 | }; 72 | })(); 73 | 74 | /** 75 | * In Safari 13.4 for iOS `devicemotion` events are broken 76 | */ 77 | export const isSafariWithoutDeviceMotion = (function() { 78 | let value = false; 79 | value = isIOS() && isSafari() && navigator.userAgent.indexOf('13_4') !== -1; 80 | return function () { 81 | return value; 82 | }; 83 | })(); 84 | 85 | /** 86 | * In Chrome m65, `devicemotion` events are broken but subsequently fixed 87 | * in 65.0.3325.148. Since many browsers use Chromium, ensure that 88 | * we scope this detection by branch and build numbers to provide 89 | * a proper fallback. 90 | * https://github.com/immersive-web/webvr-polyfill/issues/307 91 | */ 92 | export const isChromeWithoutDeviceMotion = (function() { 93 | let value = false; 94 | if (getChromeVersion() === 65) { 95 | const match = navigator.userAgent.match(/.*Chrome\/([0-9\.]*)/); 96 | if (match) { 97 | const [major, minor, branch, build] = match[1].split('.'); 98 | value = parseInt(branch, 10) === 3325 && parseInt(build, 10) < 148; 99 | } 100 | } 101 | return function() { 102 | return value; 103 | }; 104 | })(); 105 | 106 | export const isR7 = (function() { 107 | var isR7 = navigator.userAgent.indexOf('R7 Build') !== -1; 108 | return function() { 109 | return isR7; 110 | }; 111 | })(); 112 | 113 | export const isLandscapeMode = function() { 114 | var rtn = (window.orientation == 90 || window.orientation == -90); 115 | return isR7() ? !rtn : rtn; 116 | }; 117 | 118 | // Helper method to validate the time steps of sensor timestamps. 119 | export const isTimestampDeltaValid = function(timestampDeltaS) { 120 | if (isNaN(timestampDeltaS)) { 121 | return false; 122 | } 123 | if (timestampDeltaS <= MIN_TIMESTEP) { 124 | return false; 125 | } 126 | if (timestampDeltaS > MAX_TIMESTEP) { 127 | return false; 128 | } 129 | return true; 130 | }; 131 | 132 | export const getScreenWidth = function() { 133 | return Math.max(window.screen.width, window.screen.height) * 134 | window.devicePixelRatio; 135 | }; 136 | 137 | export const getScreenHeight = function() { 138 | return Math.min(window.screen.width, window.screen.height) * 139 | window.devicePixelRatio; 140 | }; 141 | 142 | export const requestFullscreen = function(element) { 143 | if (isWebViewAndroid()) { 144 | return false; 145 | } 146 | if (element.requestFullscreen) { 147 | element.requestFullscreen(); 148 | } else if (element.webkitRequestFullscreen) { 149 | element.webkitRequestFullscreen(); 150 | } else if (element.mozRequestFullScreen) { 151 | element.mozRequestFullScreen(); 152 | } else if (element.msRequestFullscreen) { 153 | element.msRequestFullscreen(); 154 | } else { 155 | return false; 156 | } 157 | 158 | return true; 159 | }; 160 | 161 | export const exitFullscreen = function() { 162 | if (document.exitFullscreen) { 163 | document.exitFullscreen(); 164 | } else if (document.webkitExitFullscreen) { 165 | document.webkitExitFullscreen(); 166 | } else if (document.mozCancelFullScreen) { 167 | document.mozCancelFullScreen(); 168 | } else if (document.msExitFullscreen) { 169 | document.msExitFullscreen(); 170 | } else { 171 | return false; 172 | } 173 | 174 | return true; 175 | }; 176 | 177 | export const getFullscreenElement = function() { 178 | return document.fullscreenElement || 179 | document.webkitFullscreenElement || 180 | document.mozFullScreenElement || 181 | document.msFullscreenElement; 182 | }; 183 | 184 | export const linkProgram = function(gl, vertexSource, fragmentSource, attribLocationMap) { 185 | // No error checking for brevity. 186 | var vertexShader = gl.createShader(gl.VERTEX_SHADER); 187 | gl.shaderSource(vertexShader, vertexSource); 188 | gl.compileShader(vertexShader); 189 | 190 | var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 191 | gl.shaderSource(fragmentShader, fragmentSource); 192 | gl.compileShader(fragmentShader); 193 | 194 | var program = gl.createProgram(); 195 | gl.attachShader(program, vertexShader); 196 | gl.attachShader(program, fragmentShader); 197 | 198 | for (var attribName in attribLocationMap) 199 | gl.bindAttribLocation(program, attribLocationMap[attribName], attribName); 200 | 201 | gl.linkProgram(program); 202 | 203 | gl.deleteShader(vertexShader); 204 | gl.deleteShader(fragmentShader); 205 | 206 | return program; 207 | }; 208 | 209 | export const getProgramUniforms = function(gl, program) { 210 | var uniforms = {}; 211 | var uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); 212 | var uniformName = ''; 213 | for (var i = 0; i < uniformCount; i++) { 214 | var uniformInfo = gl.getActiveUniform(program, i); 215 | uniformName = uniformInfo.name.replace('[0]', ''); 216 | uniforms[uniformName] = gl.getUniformLocation(program, uniformName); 217 | } 218 | return uniforms; 219 | }; 220 | 221 | export const orthoMatrix = function (out, left, right, bottom, top, near, far) { 222 | var lr = 1 / (left - right), 223 | bt = 1 / (bottom - top), 224 | nf = 1 / (near - far); 225 | out[0] = -2 * lr; 226 | out[1] = 0; 227 | out[2] = 0; 228 | out[3] = 0; 229 | out[4] = 0; 230 | out[5] = -2 * bt; 231 | out[6] = 0; 232 | out[7] = 0; 233 | out[8] = 0; 234 | out[9] = 0; 235 | out[10] = 2 * nf; 236 | out[11] = 0; 237 | out[12] = (left + right) * lr; 238 | out[13] = (top + bottom) * bt; 239 | out[14] = (far + near) * nf; 240 | out[15] = 1; 241 | return out; 242 | }; 243 | 244 | export const copyArray = function (source, dest) { 245 | for (var i = 0, n = source.length; i < n; i++) { 246 | dest[i] = source[i]; 247 | } 248 | }; 249 | 250 | export const isMobile = function() { 251 | var check = false; 252 | (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); 253 | return check; 254 | }; 255 | 256 | export const extend = function(dest, src) { 257 | for (var key in src) { 258 | if (src.hasOwnProperty(key)) { 259 | dest[key] = src[key]; 260 | } 261 | } 262 | 263 | return dest; 264 | }; 265 | 266 | export const safariCssSizeWorkaround = function(canvas) { 267 | // TODO(smus): Remove this workaround when Safari for iOS is fixed. 268 | // iOS only workaround (for https://bugs.webkit.org/show_bug.cgi?id=152556). 269 | // 270 | // "To the last I grapple with thee; 271 | // from hell's heart I stab at thee; 272 | // for hate's sake I spit my last breath at thee." 273 | // -- Moby Dick, by Herman Melville 274 | if (isIOS()) { 275 | var width = canvas.style.width; 276 | var height = canvas.style.height; 277 | canvas.style.width = (parseInt(width) + 1) + 'px'; 278 | canvas.style.height = (parseInt(height)) + 'px'; 279 | setTimeout(function() { 280 | canvas.style.width = width; 281 | canvas.style.height = height; 282 | }, 100); 283 | } 284 | 285 | // Debug only. 286 | window.canvas = canvas; 287 | }; 288 | 289 | export const frameDataFromPose = (function() { 290 | var piOver180 = Math.PI / 180.0; 291 | var rad45 = Math.PI * 0.25; 292 | 293 | // Borrowed from glMatrix. 294 | function mat4_perspectiveFromFieldOfView(out, fov, near, far) { 295 | var upTan = Math.tan(fov ? (fov.upDegrees * piOver180) : rad45), 296 | downTan = Math.tan(fov ? (fov.downDegrees * piOver180) : rad45), 297 | leftTan = Math.tan(fov ? (fov.leftDegrees * piOver180) : rad45), 298 | rightTan = Math.tan(fov ? (fov.rightDegrees * piOver180) : rad45), 299 | xScale = 2.0 / (leftTan + rightTan), 300 | yScale = 2.0 / (upTan + downTan); 301 | 302 | out[0] = xScale; 303 | out[1] = 0.0; 304 | out[2] = 0.0; 305 | out[3] = 0.0; 306 | out[4] = 0.0; 307 | out[5] = yScale; 308 | out[6] = 0.0; 309 | out[7] = 0.0; 310 | out[8] = -((leftTan - rightTan) * xScale * 0.5); 311 | out[9] = ((upTan - downTan) * yScale * 0.5); 312 | out[10] = far / (near - far); 313 | out[11] = -1.0; 314 | out[12] = 0.0; 315 | out[13] = 0.0; 316 | out[14] = (far * near) / (near - far); 317 | out[15] = 0.0; 318 | return out; 319 | } 320 | 321 | function mat4_fromRotationTranslation(out, q, v) { 322 | // Quaternion math 323 | var x = q[0], y = q[1], z = q[2], w = q[3], 324 | x2 = x + x, 325 | y2 = y + y, 326 | z2 = z + z, 327 | 328 | xx = x * x2, 329 | xy = x * y2, 330 | xz = x * z2, 331 | yy = y * y2, 332 | yz = y * z2, 333 | zz = z * z2, 334 | wx = w * x2, 335 | wy = w * y2, 336 | wz = w * z2; 337 | 338 | out[0] = 1 - (yy + zz); 339 | out[1] = xy + wz; 340 | out[2] = xz - wy; 341 | out[3] = 0; 342 | out[4] = xy - wz; 343 | out[5] = 1 - (xx + zz); 344 | out[6] = yz + wx; 345 | out[7] = 0; 346 | out[8] = xz + wy; 347 | out[9] = yz - wx; 348 | out[10] = 1 - (xx + yy); 349 | out[11] = 0; 350 | out[12] = v[0]; 351 | out[13] = v[1]; 352 | out[14] = v[2]; 353 | out[15] = 1; 354 | 355 | return out; 356 | }; 357 | 358 | function mat4_translate(out, a, v) { 359 | var x = v[0], y = v[1], z = v[2], 360 | a00, a01, a02, a03, 361 | a10, a11, a12, a13, 362 | a20, a21, a22, a23; 363 | 364 | if (a === out) { 365 | out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; 366 | out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; 367 | out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; 368 | out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; 369 | } else { 370 | a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; 371 | a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; 372 | a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; 373 | 374 | out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03; 375 | out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13; 376 | out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23; 377 | 378 | out[12] = a00 * x + a10 * y + a20 * z + a[12]; 379 | out[13] = a01 * x + a11 * y + a21 * z + a[13]; 380 | out[14] = a02 * x + a12 * y + a22 * z + a[14]; 381 | out[15] = a03 * x + a13 * y + a23 * z + a[15]; 382 | } 383 | 384 | return out; 385 | }; 386 | 387 | function mat4_invert(out, a) { 388 | var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], 389 | a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], 390 | a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], 391 | a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], 392 | 393 | b00 = a00 * a11 - a01 * a10, 394 | b01 = a00 * a12 - a02 * a10, 395 | b02 = a00 * a13 - a03 * a10, 396 | b03 = a01 * a12 - a02 * a11, 397 | b04 = a01 * a13 - a03 * a11, 398 | b05 = a02 * a13 - a03 * a12, 399 | b06 = a20 * a31 - a21 * a30, 400 | b07 = a20 * a32 - a22 * a30, 401 | b08 = a20 * a33 - a23 * a30, 402 | b09 = a21 * a32 - a22 * a31, 403 | b10 = a21 * a33 - a23 * a31, 404 | b11 = a22 * a33 - a23 * a32, 405 | 406 | // Calculate the determinant 407 | det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 408 | 409 | if (!det) { 410 | return null; 411 | } 412 | det = 1.0 / det; 413 | 414 | out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; 415 | out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; 416 | out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; 417 | out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; 418 | out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; 419 | out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; 420 | out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; 421 | out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; 422 | out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; 423 | out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; 424 | out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; 425 | out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; 426 | out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; 427 | out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; 428 | out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; 429 | out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; 430 | 431 | return out; 432 | }; 433 | 434 | var defaultOrientation = new Float32Array([0, 0, 0, 1]); 435 | var defaultPosition = new Float32Array([0, 0, 0]); 436 | 437 | function updateEyeMatrices(projection, view, pose, fov, offset, vrDisplay) { 438 | mat4_perspectiveFromFieldOfView(projection, fov || null, vrDisplay.depthNear, vrDisplay.depthFar); 439 | 440 | var orientation = pose.orientation || defaultOrientation; 441 | var position = pose.position || defaultPosition; 442 | 443 | mat4_fromRotationTranslation(view, orientation, position); 444 | if (offset) 445 | mat4_translate(view, view, offset); 446 | mat4_invert(view, view); 447 | } 448 | 449 | return function(frameData, pose, vrDisplay) { 450 | if (!frameData || !pose) 451 | return false; 452 | 453 | frameData.pose = pose; 454 | frameData.timestamp = pose.timestamp; 455 | 456 | updateEyeMatrices( 457 | frameData.leftProjectionMatrix, frameData.leftViewMatrix, 458 | pose, vrDisplay._getFieldOfView("left"), vrDisplay._getEyeOffset("left"), vrDisplay); 459 | updateEyeMatrices( 460 | frameData.rightProjectionMatrix, frameData.rightViewMatrix, 461 | pose, vrDisplay._getFieldOfView("right"), vrDisplay._getEyeOffset("right"), vrDisplay); 462 | 463 | return true; 464 | }; 465 | })(); 466 | 467 | // via https://github.com/immersive-web/webvr-polyfill/issues/271 468 | export const isInsideCrossOriginIFrame = function() { 469 | var isFramed = (window.self !== window.top); 470 | var refOrigin = getOriginFromUrl(document.referrer); 471 | var thisOrigin = getOriginFromUrl(window.location.href); 472 | 473 | return isFramed && (refOrigin !== thisOrigin); 474 | }; 475 | 476 | // via https://github.com/immersive-web/webvr-polyfill/issues/271 477 | export const getOriginFromUrl = function(url) { 478 | var domainIdx; 479 | var protoSepIdx = url.indexOf("://"); 480 | if (protoSepIdx !== -1) { 481 | domainIdx = protoSepIdx + 3; 482 | } else { 483 | domainIdx = 0; 484 | } 485 | var domainEndIdx = url.indexOf('/', domainIdx); 486 | if (domainEndIdx === -1) { 487 | domainEndIdx = url.length; 488 | } 489 | return url.substring(0, domainEndIdx) 490 | }; 491 | 492 | export const getQuaternionAngle = function(quat) { 493 | // angle = 2 * acos(qw) 494 | // If w is greater than 1 (THREE.js, how can this be?), arccos is not defined. 495 | if (quat.w > 1) { 496 | console.warn('getQuaternionAngle: w > 1'); 497 | return 0; 498 | } 499 | var angle = 2 * Math.acos(quat.w); 500 | return angle; 501 | }; 502 | 503 | /** 504 | * Takes a key and a message and when executed, 505 | * prints a console.warn with the message if this is the first 506 | * of `key`'s warnings. 507 | */ 508 | export const warnOnce = (function() { 509 | var observedWarnings = {}; 510 | 511 | return function(key, message) { 512 | if (observedWarnings[key] === undefined) { 513 | console.warn('webvr-polyfill: ' + message); 514 | observedWarnings[key] = true; 515 | } 516 | }; 517 | })(); 518 | 519 | export const deprecateWarning = function(deprecated, suggested) { 520 | var alternative = suggested ? ('Please use ' + suggested + ' instead.') : ''; 521 | warnOnce(deprecated, deprecated + ' has been deprecated. ' + 522 | 'This may not work on native WebVR displays. ' + 523 | alternative); 524 | }; 525 | -------------------------------------------------------------------------------- /src/viewer-selector.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | import DeviceInfo from './device-info.js'; 17 | import * as Util from './util.js'; 18 | 19 | const DEFAULT_VIEWER = 'CardboardV1'; 20 | const VIEWER_KEY = 'WEBVR_CARDBOARD_VIEWER'; 21 | const CLASS_NAME = 'webvr-polyfill-viewer-selector'; 22 | 23 | /** 24 | * Creates a viewer selector with the options specified. Supports being shown 25 | * and hidden. Generates events when viewer parameters change. Also supports 26 | * saving the currently selected index in localStorage. 27 | */ 28 | function ViewerSelector(defaultViewer) { 29 | // Try to load the selected key from local storage. 30 | try { 31 | this.selectedKey = localStorage.getItem(VIEWER_KEY); 32 | } catch (error) { 33 | console.error('Failed to load viewer profile: %s', error); 34 | } 35 | 36 | //If none exists, or if localstorage is unavailable, use the default key. 37 | if (!this.selectedKey) { 38 | this.selectedKey = defaultViewer || DEFAULT_VIEWER; 39 | } 40 | 41 | this.dialog = this.createDialog_(DeviceInfo.Viewers); 42 | this.root = null; 43 | this.onChangeCallbacks_ = []; 44 | } 45 | 46 | ViewerSelector.prototype.show = function(root) { 47 | this.root = root; 48 | 49 | root.appendChild(this.dialog); 50 | 51 | // Ensure the currently selected item is checked. 52 | var selected = this.dialog.querySelector('#' + this.selectedKey); 53 | selected.checked = true; 54 | 55 | // Show the UI. 56 | this.dialog.style.display = 'block'; 57 | }; 58 | 59 | ViewerSelector.prototype.hide = function() { 60 | if (this.root && this.root.contains(this.dialog)) { 61 | this.root.removeChild(this.dialog); 62 | } 63 | this.dialog.style.display = 'none'; 64 | }; 65 | 66 | ViewerSelector.prototype.getCurrentViewer = function() { 67 | return DeviceInfo.Viewers[this.selectedKey]; 68 | }; 69 | 70 | ViewerSelector.prototype.getSelectedKey_ = function() { 71 | var input = this.dialog.querySelector('input[name=field]:checked'); 72 | if (input) { 73 | return input.id; 74 | } 75 | return null; 76 | }; 77 | 78 | ViewerSelector.prototype.onChange = function(cb) { 79 | this.onChangeCallbacks_.push(cb); 80 | }; 81 | 82 | ViewerSelector.prototype.fireOnChange_ = function(viewer) { 83 | for (var i = 0; i < this.onChangeCallbacks_.length; i++) { 84 | this.onChangeCallbacks_[i](viewer); 85 | } 86 | }; 87 | 88 | ViewerSelector.prototype.onSave_ = function() { 89 | this.selectedKey = this.getSelectedKey_(); 90 | if (!this.selectedKey || !DeviceInfo.Viewers[this.selectedKey]) { 91 | console.error('ViewerSelector.onSave_: this should never happen!'); 92 | return; 93 | } 94 | 95 | this.fireOnChange_(DeviceInfo.Viewers[this.selectedKey]); 96 | 97 | // Attempt to save the viewer profile, but fails in private mode. 98 | try { 99 | localStorage.setItem(VIEWER_KEY, this.selectedKey); 100 | } catch(error) { 101 | console.error('Failed to save viewer profile: %s', error); 102 | } 103 | this.hide(); 104 | }; 105 | 106 | /** 107 | * Creates the dialog. 108 | */ 109 | ViewerSelector.prototype.createDialog_ = function(options) { 110 | var container = document.createElement('div'); 111 | container.classList.add(CLASS_NAME); 112 | container.style.display = 'none'; 113 | // Create an overlay that dims the background, and which goes away when you 114 | // tap it. 115 | var overlay = document.createElement('div'); 116 | var s = overlay.style; 117 | s.position = 'fixed'; 118 | s.left = 0; 119 | s.top = 0; 120 | s.width = '100%'; 121 | s.height = '100%'; 122 | s.background = 'rgba(0, 0, 0, 0.3)'; 123 | overlay.addEventListener('click', this.hide.bind(this)); 124 | 125 | var width = 280; 126 | var dialog = document.createElement('div'); 127 | var s = dialog.style; 128 | s.boxSizing = 'border-box'; 129 | s.position = 'fixed'; 130 | s.top = '24px'; 131 | s.left = '50%'; 132 | s.marginLeft = (-width/2) + 'px'; 133 | s.width = width + 'px'; 134 | s.padding = '24px'; 135 | s.overflow = 'hidden'; 136 | s.background = '#fafafa'; 137 | s.fontFamily = "'Roboto', sans-serif"; 138 | s.boxShadow = '0px 5px 20px #666'; 139 | 140 | dialog.appendChild(this.createH1_('Select your viewer')); 141 | for (var id in options) { 142 | dialog.appendChild(this.createChoice_(id, options[id].label)); 143 | } 144 | dialog.appendChild(this.createButton_('Save', this.onSave_.bind(this))); 145 | 146 | container.appendChild(overlay); 147 | container.appendChild(dialog); 148 | 149 | return container; 150 | }; 151 | 152 | ViewerSelector.prototype.createH1_ = function(name) { 153 | var h1 = document.createElement('h1'); 154 | var s = h1.style; 155 | s.color = 'black'; 156 | s.fontSize = '20px'; 157 | s.fontWeight = 'bold'; 158 | s.marginTop = 0; 159 | s.marginBottom = '24px'; 160 | h1.innerHTML = name; 161 | return h1; 162 | }; 163 | 164 | ViewerSelector.prototype.createChoice_ = function(id, name) { 165 | /* 166 |

167 | 168 | 169 |
170 | */ 171 | var div = document.createElement('div'); 172 | div.style.marginTop = '8px'; 173 | div.style.color = 'black'; 174 | 175 | var input = document.createElement('input'); 176 | input.style.fontSize = '30px'; 177 | input.setAttribute('id', id); 178 | input.setAttribute('type', 'radio'); 179 | input.setAttribute('value', id); 180 | input.setAttribute('name', 'field'); 181 | 182 | var label = document.createElement('label'); 183 | label.style.marginLeft = '4px'; 184 | label.setAttribute('for', id); 185 | label.innerHTML = name; 186 | 187 | div.appendChild(input); 188 | div.appendChild(label); 189 | 190 | return div; 191 | }; 192 | 193 | ViewerSelector.prototype.createButton_ = function(label, onclick) { 194 | var button = document.createElement('button'); 195 | button.innerHTML = label; 196 | var s = button.style; 197 | s.float = 'right'; 198 | s.textTransform = 'uppercase'; 199 | s.color = '#1094f7'; 200 | s.fontSize = '14px'; 201 | s.letterSpacing = 0; 202 | s.border = 0; 203 | s.background = 'none'; 204 | s.marginTop = '16px'; 205 | 206 | button.addEventListener('click', onclick); 207 | 208 | return button; 209 | }; 210 | 211 | export default ViewerSelector; 212 | -------------------------------------------------------------------------------- /third_party/three.js/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2010-2017 three.js authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /third_party/three.js/METADATA: -------------------------------------------------------------------------------- 1 | name: "three.js" 2 | description: 3 | "JavaScript 3D Library" 4 | 5 | third_party { 6 | url { 7 | type: HOMEPAGE 8 | value: "https://github.com/mrdoob/three.js" 9 | } 10 | url { 11 | type: GIT 12 | value: "git@github.com:mrdoob/three.js.git" 13 | } 14 | version: "r87" 15 | last_upgrade_date { year: 2017 month: 9 day: 14 } 16 | } 17 | -------------------------------------------------------------------------------- /third_party/three.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 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 | -------------------------------------------------------------------------------- /third_party/three.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://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 | --------------------------------------------------------------------------------