├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── webxr-polyfill.js ├── webxr-polyfill.min.js └── webxr-polyfill.module.js ├── examples ├── css │ └── common.css ├── js │ ├── cottontail.js │ ├── gl-matrix-min.js │ └── webxr-button.js ├── magic-window.html ├── media │ └── textures │ │ └── cube-sea.png ├── mirroring.html ├── offscreen-canvas.html └── xr-presentation.html ├── licenses.txt ├── package-lock.json ├── package.json ├── rollup.config.js ├── rollup.config.min.js ├── rollup.config.module.js ├── src ├── WebXRPolyfill.js ├── api │ ├── XRFrame.js │ ├── XRInputSource.js │ ├── XRInputSourceEvent.js │ ├── XRInputSourcesChangeEvent.js │ ├── XRPose.js │ ├── XRReferenceSpace.js │ ├── XRReferenceSpaceEvent.js │ ├── XRRenderState.js │ ├── XRRigidTransform.js │ ├── XRSession.js │ ├── XRSessionEvent.js │ ├── XRSpace.js │ ├── XRSystem.js │ ├── XRView.js │ ├── XRViewerPose.js │ ├── XRViewport.js │ ├── XRWebGLLayer.js │ └── index.js ├── constants.js ├── devices.js ├── devices │ ├── CardboardXRDevice.js │ ├── GamepadMappings.js │ ├── GamepadXRInputSource.js │ ├── InlineDevice.js │ ├── WebVRDevice.js │ └── XRDevice.js ├── index.js ├── lib │ ├── DOMPointReadOnly.js │ ├── EventTarget.js │ ├── OrientationArmModel.js │ ├── global.js │ └── now.js ├── polyfill-globals.js └── utils.js └── test ├── api ├── test-event-target.js ├── test-xr-device-pose.js ├── test-xr-device.js ├── test-xr-frame-of-reference.js ├── test-xr-frame.js ├── test-xr-layer.js ├── test-xr-ray.js ├── test-xr-session.js ├── test-xr-stage-bounds.js ├── test-xr-view.js └── test-xr.js ├── lib ├── MockVRDisplay.js ├── globals.js └── utils.js ├── setup.js ├── test-devices.js ├── test-utils.js └── test-webxr-polyfill.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | * When building, we do not want to transpile modules in Babel, and instead handle 4 | * them via Rollup. When running tests, we want Babel to transpile and convert modules 5 | * to commonjs, since they're run in Node, and do not use rollup, so we need to use 6 | * a module import system that Node can understand. We need to make two environments 7 | * since the options are merged with the top-level configuration. 8 | */ 9 | "env": { 10 | "production": { 11 | "presets": [ 12 | ["env", { 13 | "targets": { 14 | "browsers": [ 15 | ">1%", 16 | "Firefox ESR", 17 | ] 18 | }, 19 | "debug": true, 20 | "modules": false, 21 | // Disable polyfilling features for now, 22 | // let consumer handle which features to support. Just 23 | // handle transpiling of code for the UMD build. 24 | // "useBuiltIns": "usage", 25 | "exclude": [ 26 | "web.dom.iterable", 27 | "web.immediate", 28 | "web.timers", 29 | // Ignore @babel/preset-env's async/await transformations, 30 | // we're using the `fast-async` plugin to reduce build size and 31 | // not use Regenerator's runtime. 32 | "transform-regenerator", 33 | "transform-async-to-generator", 34 | ], 35 | }] 36 | ], 37 | "plugins": [ 38 | ["fast-async", { 39 | "spec": true, 40 | "useRuntimeModule": false, 41 | }], 42 | "external-helpers", 43 | ], 44 | }, 45 | "test": { 46 | "presets": [ 47 | ["env", { 48 | "targets": { 49 | "node": "8", 50 | }, 51 | "debug": true, 52 | }] 53 | ], 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "google"], 3 | "rules": { 4 | "max-len": ["error", { "code": 100 }], 5 | "object-curly-spacing": ["error", "always"], 6 | "arrow-parens": ["error", "as-needed"], 7 | "no-console": ["error", { "allow": ["warn", "error"] }] 8 | }, 9 | "env": { 10 | "browser": true, 11 | "es6": true 12 | }, 13 | "globals": { 14 | "THREE": false, 15 | "VRFrameData": false 16 | }, 17 | "parserOptions": { 18 | "ecmaVersion": 6, 19 | "sourceType": "module" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | webvr2-samples 2 | *.log 3 | *.pid 4 | *.seed 5 | *.swp 6 | *.un~ 7 | *~ 8 | .DS_Store 9 | .netrwhist 10 | .node_repl_history 11 | .npm 12 | Session.vim 13 | [._]*.s[a-w][a-z] 14 | [._]s[a-w][a-z] 15 | logs 16 | node_modules 17 | pids 18 | build 19 | webxr-samples 20 | media 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Developing 2 | 3 | If you're interested in developing and contributing on the polyfill itself, you'll need to 4 | have [npm] installed and familiarize yourself with some commands below. For full list 5 | of commands available, see `package.json` scripts. 6 | 7 | ``` 8 | $ git clone git@github.com:immersive-web/webxr-polyfill.git 9 | $ cd webxr-polyfill/ 10 | 11 | # Install dependencies 12 | $ npm install 13 | 14 | # Build transpiled ES5 script 15 | $ npm run build-script 16 | 17 | # Build compressed ES5 script 18 | $ npm run build-min 19 | 20 | # Build ES module 21 | $ npm run build-module 22 | 23 | # Build all builds 24 | $ npm run build 25 | 26 | # Run tests 27 | $ npm test 28 | 29 | # Watch src/* directory and auto-rebuild on changes 30 | $ npm watch 31 | ``` 32 | 33 | ### Testing 34 | 35 | Right now there are some unit tests in the configuration and logic for how things get polyfilled. 36 | Be sure to run tests before submitting any PRs, and bonus points for having new tests! 37 | 38 | ``` 39 | $ npm test 40 | ``` 41 | 42 | Due to the nature of the polyfill, be also sure to test the examples with your changes where appropriate. 43 | 44 | ### Releasing a new version 45 | 46 | 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. 47 | 48 | `npm version ` 49 | 50 | ## License 51 | 52 | This program is free software for both commercial and non-commercial use, 53 | distributed under the [Apache 2.0 License](LICENSE). 54 | 55 | [rollup]: https://rollupjs.org 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebXR Polyfill 2 | 3 | [![Build Status](http://img.shields.io/travis/immersive-web/webxr-polyfill.svg?style=flat-square)](https://travis-ci.org/immersive-web/webxr-polyfill) 4 | [![Build Status](http://img.shields.io/npm/v/webxr-polyfill.svg?style=flat-square)](https://www.npmjs.org/package/webxr-polyfill) 5 | 6 | A JavaScript implementation of the [WebXR Device API][webxr-spec], as well as the [WebXR Gamepad Module][webxr-gamepad-module]. This polyfill allows developers to write against the latest specification, providing support when run on browsers that implement the [WebVR 1.1 spec][webvr-spec], or on mobile devices with no WebVR/WebXR support at all. 7 | 8 | The polyfill reflects the stable version of the API which has shipped in multiple browsers. 9 | 10 | --- 11 | 12 | If you are writing code against the [WebVR 1.1 spec][webvr-spec], use [webvr-polyfill], which supports browsers with the 1.0 spec, or no implementation at all. It is recommended to write your code targeting the [WebXR Device API spec][webxr-spec] however and use this polyfill as browsers begin to implement the latest changes. 13 | 14 | The minimal input controls currently supported by WebXR is polyfilled here as well, using the [Gamepad API][gamepad-api]. 15 | 16 | ## Setup 17 | 18 | ### Installing 19 | 20 | Download the build at [build/webxr-polyfill.js](build/webxr-polyfill.js) and include it as a script tag, 21 | or use a CDN. You can also use the minified file in the same location as `webxr-polyfill.min.js`. 22 | 23 | ```html 24 | 25 | 26 | 27 | ``` 28 | 29 | Or if you're using a build tool like [browserify] or [webpack], install it via [npm]. 30 | 31 | ``` 32 | $ npm install --save webxr-polyfill 33 | ``` 34 | 35 | ### Building Locally 36 | 37 | ``` 38 | $ npm run build 39 | ``` 40 | 41 | ### Using 42 | 43 | The webxr-polyfill exposes a single constructor, `WebXRPolyfill` that takes an 44 | object for configuration. See full configuration options at [API](#api). 45 | 46 | Be sure to instantiate the polyfill before calling any of your XR code! The 47 | polyfill needs to patch the API if it does not exist so your content code can 48 | assume that the WebXR API will just work. 49 | 50 | If using script tags, a `WebXRPolyfill` global constructor will exist. 51 | 52 | ```js 53 | var polyfill = new WebXRPolyfill(); 54 | ``` 55 | 56 | In a modular ES6 world, import and instantiate the constructor similarly. 57 | 58 | ```js 59 | import WebXRPolyfill from 'webxr-polyfill'; 60 | const polyfill = new WebXRPolyfill(); 61 | ``` 62 | 63 | ## API 64 | 65 | ### new WebXRPolyfill(config) 66 | 67 | Takes a `config` object with the following options: 68 | 69 | * `global`: What global should be used to find needed types. (default: `window` on browsers) 70 | * `webvr`: Whether or not there should be an attempt to fall back to a 71 | WebVR 1.1 VRDisplay. (default: `true`). 72 | * `cardboard`: Whether or not there should be an attempt to fall back to a 73 | JavaScript implementation of the WebXR API only on mobile. (default: `true`) 74 | * `cardboardConfig`: The configuration to be used for CardboardVRDisplay when used. Has no effect when `cardboard` is `false`, or another XRDevice is used. Possible configuration options can be found [here in the cardboard-vr-display repo](https://github.com/immersive-web/cardboard-vr-display/blob/master/src/options.js). (default: `null`) 75 | * `allowCardboardOnDesktop`: Whether or not to allow cardboard's stereoscopic rendering and pose via sensors on desktop. This is most likely only helpful for development and debugging. (default: `false`) 76 | 77 | ## Browser Support 78 | 79 | **Development note: babel support is currently removed, handle definitively in [#63](https://github.com/immersive-web/webxr-polyfill/issues/63)** 80 | 81 | There are 3 builds provided: [build/webxr-polyfill.js](build/webxr-polyfill.js), an ES5 transpiled build, its minified counterpart [build/webxr-polyfill.min.js](build/webxr-polyfill.min.js), and an untranspiled [ES Modules] version [build/webxr-polyfill.module.js](build/webxr-polyfill.module.js). If using the transpiled ES5 build, its up to developers to decide which browser features to polyfill based on their support, as no extra polyfills are included. Some browser features this library uses include: 82 | 83 | * TypedArrays 84 | * Object.assign 85 | * Promise 86 | * Symbol 87 | * Map 88 | * Array#includes 89 | 90 | Check the [.babelrc](.babelrc) configuration and ensure the polyfill runs in whatever browsers you choose to support. 91 | 92 | ## Polyfilling Rules 93 | 94 | * If `'xr' in navigator === false`: 95 | * WebXR classes (e.g. `XRDevice`, `XRSession`) will be added to the global 96 | * `navigator.xr` will be polyfilled. 97 | * If the platform has a `VRDisplay` from the [WebVR 1.1 spec][webvr-spec] available: 98 | * `navigator.xr.requestDevice()` will return a polyfilled `XRDevice` wrapping the `VRDisplay`. 99 | * If the platform does not have a `VRDisplay`, `config.cardboard === true`, and on mobile: 100 | * `navigator.xr.requestDevice()` will return a polyfilled `XRDevice` based on [CardboardVRDisplay]. 101 | * If `WebGLRenderingContext.prototype.setCompatibleXRDevice` is not a function: 102 | * Polyfill all `WebGLRenderingContext.prototype.setCompatibleXRDevice` and a creation attribute 103 | for `{ compatibleXrDevice }`. 104 | * Polyfills `HTMLCanvasElement.prototype.getContext` to support a `xrpresent` type. Returns a polyfilled `XRPresentationContext` (via `CanvasRenderingContext2D` or `ImageBitmapRenderingContext` if supported) used for mirroring and magic window. 105 | * If `'xr' in navigator === true`, `config.cardboard === true` and on mobile: 106 | * Overwrite `navigator.xr.requestDevice` so that a native `XRDevice` is returned if it exists, and if not, return a polyfilled `XRDevice` based on [CardboardVRDisplay]. 107 | 108 | In the future, when the WebXR API is implemented on a platform but inconsistent with spec (due to new spec changes or inconsistencies), the polyfill will attempt to patch these differences without overwriting the native behavior. 109 | 110 | ## Not supported/Caveats 111 | 112 | * `XRWebGLLayer.framebufferScaleFactor` 113 | 114 | ## License 115 | 116 | This program is free software for both commercial and non-commercial use, 117 | distributed under the [Apache 2.0 License](LICENSE). 118 | 119 | [webxr-spec]: https://immersive-web.github.io/webxr/ 120 | [webvr-spec]: https://immersive-web.github.io/webvr/spec/1.1/ 121 | [webvr-polyfill]: https://github.com/immersive-web/webvr-polyfill 122 | [npm]: https://www.npmjs.com 123 | [browserify]: http://browserify.org/ 124 | [webpack]: https://webpack.github.io/ 125 | [ES Modules]: https://jakearchibald.com/2017/es-modules-in-browsers/ 126 | [CardboardVRDisplay]: https://immersive-web.github.io/cardboard-vr-display 127 | [gamepad-api]: https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API 128 | [webxr-gamepad-module]: https://immersive-web.github.io/webxr-gamepads-module/ 129 | -------------------------------------------------------------------------------- /examples/css/common.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #F0F0F0; 3 | font: 1rem/1.4 -apple-system, BlinkMacSystemFont, 4 | Segoe UI, Roboto, Oxygen, 5 | Ubuntu, Cantarell, Fira Sans, 6 | Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | header { 10 | position: relative; 11 | z-index: 2; 12 | left: 0px; 13 | text-align: left; 14 | max-width: 420px; 15 | padding: 0.5em; 16 | background-color: rgba(255, 255, 255, 0.90); 17 | margin-bottom: 0.5em; 18 | border-radius: 2px; 19 | } 20 | 21 | details summary { 22 | font-size: 1.0em; 23 | font-weight: bold; 24 | } 25 | 26 | details[open] summary { 27 | font-size: 1.4em; 28 | font-weight: bold; 29 | } 30 | 31 | header h1 { 32 | margin-top: 0px; 33 | } 34 | 35 | canvas { 36 | position: absolute; 37 | z-index: 0; 38 | width: 100%; 39 | height: 100%; 40 | left: 0; 41 | top: 0; 42 | right: 0; 43 | bottom: 0; 44 | margin: 0; 45 | touch-action: none; 46 | } 47 | -------------------------------------------------------------------------------- /examples/magic-window.html: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Magic Window 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | Magic Window 45 |

46 | This sample demonstrates use of a non-immersive XRSession to present 47 | 'Magic Window' content prior to entering XR presentation with an 48 | immersive. 49 |

50 |
51 |
52 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /examples/media/textures/cube-sea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immersive-web/webxr-polyfill/7a6090614e226e2d2839f251a451f559bb2358fc/examples/media/textures/cube-sea.png -------------------------------------------------------------------------------- /examples/mirroring.html: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Mirroring 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 |
43 |
44 | Mirroring 45 |

46 | This sample demonstrates how to present a simple WebGL scene to a 47 | XRDevice while mirroring to a context. The scene is not rendered to 48 | the page prior to XR presentation. Mirroring has no effect on mobile 49 | or standalone devices. 50 |

51 |
52 |
53 |
54 |

Click 'Enter XR' to see content

55 |
56 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /examples/offscreen-canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Offscreen Canvas 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | Offscreen Canvas 45 |

46 | This sample demonstrates use of a non-immersive XRSession to present 47 | 'Magic Window' rendered from an OffscreenCanvas. 48 |

49 |
50 |
51 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /examples/xr-presentation.html: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | XR Presentation 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 |
44 |
45 | XR Presentation 46 |

47 | This sample demonstrates how to present a simple WebGL scene to a 48 | XRDevice. The scene is not rendered to the page prior to XR 49 | presentation, nor is it mirrored during presentation. 50 |

51 |
52 |
53 |
54 |

Click 'Enter XR' to see content

55 |
56 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /licenses.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * webxr-polyfill 4 | * Copyright (c) 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 | * cardboard-vr-display 21 | * Copyright (c) 2015-2017 Google 22 | * Licensed under the Apache License, Version 2.0 (the "License"); 23 | * you may not use this file except in compliance with the License. 24 | * You may obtain a copy of the License at 25 | * 26 | * http://www.apache.org/licenses/LICENSE-2.0 27 | * 28 | * Unless required by applicable law or agreed to in writing, software 29 | * distributed under the License is distributed on an "AS IS" BASIS, 30 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 | * See the License for the specific language governing permissions and 32 | * limitations under the License. 33 | */ 34 | 35 | /** 36 | * @license 37 | * webvr-polyfill-dpdb 38 | * Copyright (c) 2017 Google 39 | * Licensed under the Apache License, Version 2.0 (the "License"); 40 | * you may not use this file except in compliance with the License. 41 | * You may obtain a copy of the License at 42 | * 43 | * http://www.apache.org/licenses/LICENSE-2.0 44 | * 45 | * Unless required by applicable law or agreed to in writing, software 46 | * distributed under the License is distributed on an "AS IS" BASIS, 47 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 48 | * See the License for the specific language governing permissions and 49 | * limitations under the License. 50 | */ 51 | 52 | /** 53 | * @license 54 | * wglu-preserve-state 55 | * Copyright (c) 2016, Brandon Jones. 56 | * 57 | * Permission is hereby granted, free of charge, to any person obtaining a copy 58 | * of this software and associated documentation files (the "Software"), to deal 59 | * in the Software without restriction, including without limitation the rights 60 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 61 | * copies of the Software, and to permit persons to whom the Software is 62 | * furnished to do so, subject to the following conditions: 63 | * 64 | * The above copyright notice and this permission notice shall be included in 65 | * all copies or substantial portions of the Software. 66 | * 67 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 68 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 69 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 70 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 71 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 72 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 73 | * THE SOFTWARE. 74 | */ 75 | 76 | /** 77 | * @license 78 | * nosleep.js 79 | * Copyright (c) 2017, Rich Tibbett 80 | * 81 | * Permission is hereby granted, free of charge, to any person obtaining a copy 82 | * of this software and associated documentation files (the "Software"), to deal 83 | * in the Software without restriction, including without limitation the rights 84 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 85 | * copies of the Software, and to permit persons to whom the Software is 86 | * furnished to do so, subject to the following conditions: 87 | * 88 | * The above copyright notice and this permission notice shall be included in 89 | * all copies or substantial portions of the Software. 90 | * 91 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 92 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 93 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 94 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 95 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 96 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 97 | * THE SOFTWARE. 98 | */ 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webxr-polyfill", 3 | "version": "2.0.3", 4 | "homepage": "https://github.com/immersive-web/webxr-polyfill", 5 | "main": "build/webxr-polyfill.js", 6 | "module": "build/webxr-polyfill.module.js", 7 | "authors": [ 8 | "Jordan Santell ", 9 | "Brandon Jones " 10 | ], 11 | "description": "Use the WebXR Device API today, providing fallbacks to native WebVR 1.1 and Cardboard.", 12 | "devDependencies": { 13 | "babel-core": "^6.26.3", 14 | "babel-plugin-external-helpers": "^6.22.0", 15 | "babel-polyfill": "^6.26.0", 16 | "babel-preset-env": "^1.7.0", 17 | "babel-register": "^6.26.0", 18 | "chai": "^3.5.0", 19 | "cross-env": "^5.1.3", 20 | "eslint": "^4.16.0", 21 | "eslint-config-google": "^0.9.1", 22 | "fast-async": "^6.3.0", 23 | "jsdom": "^11.11.0", 24 | "mocha": "^5.0.0", 25 | "mock-require": "^3.0.1", 26 | "raf": "^3.4.0", 27 | "rollup": "^0.55.3", 28 | "rollup-plugin-babel": "^3.0.2", 29 | "rollup-plugin-cleanup": "^1.0.1", 30 | "rollup-plugin-commonjs": "^8.3.0", 31 | "rollup-plugin-node-resolve": "^3.0.0", 32 | "rollup-plugin-replace": "^2.0.0", 33 | "rollup-plugin-uglify": "^3.0.0", 34 | "semver": "^5.5.0", 35 | "serve": "^11.0.2", 36 | "uglify-es": "^3.3.9" 37 | }, 38 | "keywords": [ 39 | "vr", 40 | "webvr", 41 | "webxr" 42 | ], 43 | "license": "Apache-2.0", 44 | "scripts": { 45 | "build-script": "cross-env NODE_ENV=production rollup -c", 46 | "build-module": "cross-env NODE_ENV=production rollup -c rollup.config.module.js", 47 | "build-min": "cross-env NODE_ENV=production rollup -c rollup.config.min.js", 48 | "build": "npm run build-script && npm run build-min && npm run build-module", 49 | "test": "cross-env NODE_ENV=test mocha --require ./test/setup.js --exit --recursive", 50 | "preversion": "npm install && echo \"NO TESTS RUNNING\"", 51 | "version": "npm run build && git add build/*", 52 | "postversion": "git push && git push --tags && npm publish" 53 | }, 54 | "repository": "immersive-web/webxr-polyfill", 55 | "bugs": { 56 | "url": "https://github.com/immersive-web/webxr-polyfill/issues" 57 | }, 58 | "dependencies": { 59 | "cardboard-vr-display": "^1.0.19", 60 | "gl-matrix": "^2.8.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /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 commonjs from 'rollup-plugin-commonjs'; 19 | import replace from 'rollup-plugin-replace'; 20 | import resolve from 'rollup-plugin-node-resolve'; 21 | import cleanup from 'rollup-plugin-cleanup'; 22 | import babel from 'rollup-plugin-babel'; 23 | const banner = fs.readFileSync(path.join(__dirname, 'licenses.txt')); 24 | 25 | export default { 26 | input: 'src/index.js', 27 | output: { 28 | file: './build/webxr-polyfill.js', 29 | format: 'umd', 30 | name: 'WebXRPolyfill', 31 | banner: banner, 32 | }, 33 | plugins: [ 34 | replace({ 35 | 'process.env.NODE_ENV': JSON.stringify('production'), 36 | }), 37 | /* 38 | babel({ 39 | include: [ 40 | 'src/**', 41 | 'node_modules/gl-matrix/**' 42 | ] 43 | }), 44 | */ 45 | resolve(), 46 | commonjs(), 47 | cleanup({ 48 | comments: 'none', 49 | }), 50 | ], 51 | }; 52 | -------------------------------------------------------------------------------- /rollup.config.min.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 defaultConfig from './rollup.config'; 17 | import uglify from 'rollup-plugin-uglify'; 18 | import { minify } from 'uglify-es'; 19 | 20 | export default Object.assign({}, defaultConfig, { 21 | output: { 22 | file: './build/webxr-polyfill.min.js', 23 | format: defaultConfig.output.format, 24 | name: defaultConfig.output.name, 25 | banner: defaultConfig.output.banner, 26 | }, 27 | plugins: [...defaultConfig.plugins, uglify({ 28 | output: { 29 | // Preserve license commenting in minified build: 30 | // https://github.com/TrySound/rollup-plugin-uglify/blob/master/README.md#comments 31 | comments: function(node, comment) { 32 | const { value, type } = comment; 33 | if (type == "comment2") { 34 | // multiline comment 35 | return /@preserve|@license|@cc_on/i.test(value); 36 | } 37 | } 38 | } 39 | }, minify)], 40 | }); 41 | -------------------------------------------------------------------------------- /rollup.config.module.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 commonjs from 'rollup-plugin-commonjs'; 19 | import replace from 'rollup-plugin-replace'; 20 | import resolve from 'rollup-plugin-node-resolve'; 21 | import cleanup from 'rollup-plugin-cleanup'; 22 | const banner = fs.readFileSync(path.join(__dirname, 'licenses.txt')); 23 | 24 | export default { 25 | input: 'src/WebXRPolyfill.js', 26 | output: { 27 | file: './build/webxr-polyfill.module.js', 28 | format: 'es', 29 | banner: banner, 30 | }, 31 | plugins: [ 32 | replace({ 33 | 'process.env.NODE_ENV': JSON.stringify('production'), 34 | }), 35 | resolve(), 36 | commonjs(), 37 | cleanup({ 38 | comments: 'none', 39 | }), 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /src/WebXRPolyfill.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 GLOBAL from './lib/global'; 17 | import API from './api/index'; 18 | import { 19 | polyfillMakeXRCompatible, 20 | polyfillGetContext 21 | } from './polyfill-globals'; 22 | import { isImageBitmapSupported, isMobile } from './utils'; 23 | import { requestXRDevice } from './devices'; 24 | 25 | const CONFIG_DEFAULTS = { 26 | // The default global to use for needed APIs. 27 | global: GLOBAL, 28 | // Whether support for a browser implementing WebVR 1.1 is enabled. 29 | // If enabled, XR support is powered by native WebVR 1.1 VRDisplays, 30 | // exposed as XRDevices. 31 | webvr: true, 32 | // Whether a CardboardXRDevice should be discoverable if on 33 | // a mobile device, and no other native (1.1 VRDisplay if `webvr` on, 34 | // or XRDevice) found. 35 | cardboard: true, 36 | // The configuration to be used for CardboardVRDisplay when used. 37 | // Has no effect if `cardboard: false` or another XRDevice is used. 38 | // Configuration can be found: https://github.com/immersive-web/cardboard-vr-display/blob/master/src/options.js 39 | cardboardConfig: null, 40 | // Whether a CardboardXRDevice should be created if no WebXR API found 41 | // on desktop or not. Stereoscopic rendering with a gyro often does not make sense on desktop, and probably only useful for debugging. 42 | allowCardboardOnDesktop: false, 43 | }; 44 | 45 | const partials = ['navigator', 'HTMLCanvasElement', 'WebGLRenderingContext']; 46 | 47 | export default class WebXRPolyfill { 48 | /** 49 | * @param {object?} config 50 | */ 51 | constructor(config={}) { 52 | this.config = Object.freeze(Object.assign({}, CONFIG_DEFAULTS, config)); 53 | this.global = this.config.global; 54 | this.nativeWebXR = 'xr' in this.global.navigator; 55 | this.injected = false; 56 | 57 | // If no native WebXR implementation found, inject one 58 | if (!this.nativeWebXR) { 59 | this._injectPolyfill(this.global); 60 | } else { 61 | this._injectCompatibilityShims(this.global); 62 | } 63 | } 64 | 65 | _injectPolyfill(global) { 66 | if (!partials.every(iface => !!global[iface])) { 67 | throw new Error(`Global must have the following attributes : ${partials}`); 68 | } 69 | 70 | // Apply classes as globals 71 | for (const className of Object.keys(API)) { 72 | if (global[className] !== undefined) { 73 | console.warn(`${className} already defined on global.`); 74 | } else { 75 | global[className] = API[className]; 76 | } 77 | } 78 | 79 | // Test environment does not have rendering contexts 80 | if (process.env.NODE_ENV !== 'test') { 81 | // Attempts to polyfill WebGLRenderingContext's `makeXRCompatible` 82 | // if it does not exist. 83 | const polyfilledCtx = polyfillMakeXRCompatible(global.WebGLRenderingContext); 84 | 85 | // If we polyfilled `makeXRCompatible`, also polyfill the context creation 86 | // parameter `{ xrCompatible }`. 87 | if (polyfilledCtx) { 88 | polyfillGetContext(global.HTMLCanvasElement); 89 | 90 | // If OffscreenCanvas is available, patch its `getContext` method as well 91 | // for the compatible XRDevice bit. 92 | if (global.OffscreenCanvas) { 93 | polyfillGetContext(global.OffscreenCanvas); 94 | } 95 | 96 | // If we needed to polyfill WebGLRenderingContext, do the same 97 | // for WebGL2 contexts if it exists. 98 | if (global.WebGL2RenderingContext){ 99 | polyfillMakeXRCompatible(global.WebGL2RenderingContext); 100 | } 101 | 102 | if (!window.isSecureContext) { 103 | console.warn(`WebXR Polyfill Warning: 104 | This page is not running in a secure context (https:// or localhost)! 105 | This means that although the page may be able to use the WebXR Polyfill it will 106 | not be able to use native WebXR implementations, and as such will not be able to 107 | access dedicated VR or AR hardware, and will not be able to take advantage of 108 | any performance improvements a native WebXR implementation may offer. Please 109 | host this content on a secure origin for the best user experience. 110 | `); 111 | } 112 | } 113 | } 114 | 115 | this.injected = true; 116 | 117 | this._patchNavigatorXR(); 118 | } 119 | 120 | _patchNavigatorXR() { 121 | // Request a polyfilled XRDevice. 122 | let devicePromise = requestXRDevice(this.global, this.config); 123 | 124 | // Create `navigator.xr` instance populated with the XRDevice promise 125 | // requested above. The promise resolve will be monitored by the XR object. 126 | this.xr = new API.XRSystem(devicePromise); 127 | Object.defineProperty(this.global.navigator, 'xr', { 128 | value: this.xr, 129 | configurable: true, 130 | }); 131 | } 132 | 133 | _injectCompatibilityShims(global) { 134 | if (!partials.every(iface => !!global[iface])) { 135 | throw new Error(`Global must have the following attributes : ${partials}`); 136 | } 137 | 138 | // Patch for Chrome 76-78: exposed supportsSession rather than 139 | // isSessionSupported. Wraps the function to ensure the promise properly 140 | // resolves with a boolean. 141 | if (global.navigator.xr && 142 | 'supportsSession' in global.navigator.xr && 143 | !('isSessionSupported' in global.navigator.xr)) { 144 | let originalSupportsSession = global.navigator.xr.supportsSession; 145 | global.navigator.xr.isSessionSupported = function(mode) { 146 | return originalSupportsSession.call(this, mode).then(() => { 147 | return true; 148 | }).catch(() => { 149 | return false; 150 | }); 151 | } 152 | 153 | global.navigator.xr.supportsSession = function(mode) { 154 | console.warn("navigator.xr.supportsSession() is deprecated. Please " + 155 | "call navigator.xr.isSessionSupported() instead and check the boolean " + 156 | "value returned when the promise resolves."); 157 | return originalSupportsSession.call(this, mode); 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/api/XRFrame.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 {PRIVATE as SESSION_PRIVATE} from './XRSession'; 17 | import XRViewerPose from './XRViewerPose'; 18 | import XRView from './XRView'; 19 | 20 | export const PRIVATE = Symbol('@@webxr-polyfill/XRFrame'); 21 | 22 | const NON_ACTIVE_MSG = "XRFrame access outside the callback that produced it is invalid."; 23 | const NON_ANIMFRAME_MSG = "getViewerPose can only be called on XRFrame objects passed to XRSession.requestAnimationFrame callbacks."; 24 | 25 | let NEXT_FRAME_ID = 0; 26 | 27 | export default class XRFrame { 28 | /** 29 | * @param {XRDevice} device 30 | * @param {XRSession} session 31 | * @param {number} sessionId 32 | */ 33 | constructor(device, session, sessionId) { 34 | this[PRIVATE] = { 35 | id: ++NEXT_FRAME_ID, 36 | active: false, 37 | animationFrame: false, 38 | device, 39 | session, 40 | sessionId 41 | }; 42 | } 43 | 44 | /** 45 | * @return {XRSession} session 46 | */ 47 | get session() { return this[PRIVATE].session; } 48 | 49 | /** 50 | * @param {XRReferenceSpace} referenceSpace 51 | * @return {XRViewerPose?} 52 | */ 53 | getViewerPose(referenceSpace) { 54 | if (!this[PRIVATE].animationFrame) { 55 | throw new DOMException(NON_ANIMFRAME_MSG, 'InvalidStateError'); 56 | } 57 | if (!this[PRIVATE].active) { 58 | throw new DOMException(NON_ACTIVE_MSG, 'InvalidStateError'); 59 | } 60 | 61 | const device = this[PRIVATE].device; 62 | const session = this[PRIVATE].session; 63 | 64 | session[SESSION_PRIVATE].viewerSpace._ensurePoseUpdated(device, this[PRIVATE].id); 65 | referenceSpace._ensurePoseUpdated(device, this[PRIVATE].id); 66 | 67 | let viewerTransform = referenceSpace._getSpaceRelativeTransform(session[SESSION_PRIVATE].viewerSpace); 68 | 69 | const views = []; 70 | for (const viewSpace of session[SESSION_PRIVATE].viewSpaces) { 71 | viewSpace._ensurePoseUpdated(device, this[PRIVATE].id); 72 | let viewTransform = referenceSpace._getSpaceRelativeTransform(viewSpace); 73 | let view = new XRView(device, viewTransform, viewSpace.eye, this[PRIVATE].sessionId, viewSpace.viewIndex); 74 | views.push(view); 75 | } 76 | let viewerPose = new XRViewerPose(viewerTransform, views, false /* TODO: emulatedPosition */); 77 | 78 | return viewerPose; 79 | } 80 | 81 | /** 82 | * @param {XRSpace} space 83 | * @param {XRSpace} baseSpace 84 | * @return {XRPose?} pose 85 | */ 86 | getPose(space, baseSpace) { 87 | if (!this[PRIVATE].active) { 88 | throw new DOMException(NON_ACTIVE_MSG, 'InvalidStateError'); 89 | } 90 | 91 | const device = this[PRIVATE].device; 92 | if (space._specialType === "target-ray" || space._specialType === "grip") { 93 | // TODO: Stop special-casing input. 94 | return device.getInputPose( 95 | space._inputSource, baseSpace, space._specialType); 96 | } else { 97 | space._ensurePoseUpdated(device, this[PRIVATE].id); 98 | baseSpace._ensurePoseUpdated(device, this[PRIVATE].id); 99 | let transform = baseSpace._getSpaceRelativeTransform(space); 100 | if (!transform) { return null; } 101 | return new XRPose(transform, false /* TODO: emulatedPosition */); 102 | } 103 | 104 | return null; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/api/XRInputSource.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 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 XRSpace from './XRSpace'; 17 | 18 | export const PRIVATE = Symbol('@@webxr-polyfill/XRInputSource'); 19 | 20 | export default class XRInputSource { 21 | /** 22 | * @param {GamepadXRInputSource} impl 23 | */ 24 | constructor(impl) { 25 | this[PRIVATE] = { 26 | impl, 27 | gripSpace: new XRSpace("grip", this), 28 | targetRaySpace: new XRSpace("target-ray", this) 29 | }; 30 | } 31 | 32 | /** 33 | * @return {XRHandedness} 34 | */ 35 | get handedness() { return this[PRIVATE].impl.handedness; } 36 | 37 | /** 38 | * @return {XRTargetRayMode} 39 | */ 40 | get targetRayMode() { return this[PRIVATE].impl.targetRayMode; } 41 | 42 | /** 43 | * @return {XRSpace} 44 | */ 45 | get gripSpace() { 46 | let mode = this[PRIVATE].impl.targetRayMode; 47 | if (mode === "gaze" || mode === "screen") { 48 | // grip space must be null for non-trackable input sources 49 | return null; 50 | } 51 | return this[PRIVATE].gripSpace; 52 | } 53 | 54 | /** 55 | * @return {XRSpace} 56 | */ 57 | get targetRaySpace() { return this[PRIVATE].targetRaySpace; } 58 | 59 | /** 60 | * @return {Array} 61 | */ 62 | get profiles() { return this[PRIVATE].impl.profiles; } 63 | 64 | /** 65 | * @return {Gamepad} 66 | */ 67 | get gamepad() { return this[PRIVATE].impl.gamepad; } 68 | } 69 | -------------------------------------------------------------------------------- /src/api/XRInputSourceEvent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 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 PRIVATE = Symbol('@@webxr-polyfill/XRInputSourceEvent'); 17 | 18 | export default class XRInputSourceEvent extends Event { 19 | /** 20 | * @param {string} type 21 | * @param {Object} eventInitDict 22 | */ 23 | constructor(type, eventInitDict) { 24 | super(type, eventInitDict); 25 | this[PRIVATE] = { 26 | frame: eventInitDict.frame, 27 | inputSource: eventInitDict.inputSource 28 | }; 29 | // safari bug: super() seems to return object of type Event, with Event as prototype 30 | Object.setPrototypeOf(this, XRInputSourceEvent.prototype); 31 | } 32 | 33 | /** 34 | * @return {XRFrame} 35 | */ 36 | get frame() { return this[PRIVATE].frame; } 37 | 38 | /** 39 | * @return {XRInputSource} 40 | */ 41 | get inputSource() { return this[PRIVATE].inputSource; } 42 | } -------------------------------------------------------------------------------- /src/api/XRInputSourcesChangeEvent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 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 PRIVATE = Symbol('@@webxr-polyfill/XRInputSourcesChangeEvent'); 17 | 18 | export default class XRInputSourcesChangeEvent extends Event { 19 | /** 20 | * @param {string} type 21 | * @param {Object} eventInitDict 22 | */ 23 | constructor(type, eventInitDict) { 24 | super(type, eventInitDict); 25 | this[PRIVATE] = { 26 | session: eventInitDict.session, 27 | added: eventInitDict.added, 28 | removed: eventInitDict.removed 29 | }; 30 | // safari bug: super() seems to return object of type Event, with Event as prototype 31 | Object.setPrototypeOf(this, XRInputSourcesChangeEvent.prototype); 32 | } 33 | 34 | /** 35 | * @return {XRSession} 36 | */ 37 | get session() { return this[PRIVATE].session; } 38 | 39 | /** 40 | * @return {Array} 41 | */ 42 | get added() { return this[PRIVATE].added; } 43 | 44 | /** 45 | * @return {Array} 46 | */ 47 | get removed() { return this[PRIVATE].removed; } 48 | } -------------------------------------------------------------------------------- /src/api/XRPose.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 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 PRIVATE = Symbol('@@webxr-polyfill/XRPose'); 17 | 18 | export default class XRPose { 19 | /** 20 | * @param {XRRigidTransform} transform 21 | * @param {boolean} emulatedPosition 22 | */ 23 | constructor(transform, emulatedPosition) { 24 | this[PRIVATE] = { 25 | transform, 26 | emulatedPosition, 27 | }; 28 | } 29 | 30 | /** 31 | * @return {XRRigidTransform} 32 | */ 33 | get transform() { return this[PRIVATE].transform; } 34 | 35 | /** 36 | * @return {bool} 37 | */ 38 | get emulatedPosition() { return this[PRIVATE].emulatedPosition; } 39 | } 40 | -------------------------------------------------------------------------------- /src/api/XRReferenceSpace.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 XRSpace from './XRSpace'; 17 | import * as mat4 from 'gl-matrix/src/gl-matrix/mat4'; 18 | 19 | const DEFAULT_EMULATION_HEIGHT = 1.6; 20 | 21 | export const PRIVATE = Symbol('@@webxr-polyfill/XRReferenceSpace'); 22 | 23 | export const XRReferenceSpaceTypes = [ 24 | 'viewer', 25 | 'local', 26 | 'local-floor', 27 | 'bounded-floor', 28 | 'unbounded' // TODO: 'unbounded' is not supported by the polyfill. 29 | ]; 30 | 31 | /** 32 | * @param {string} type 33 | * @return {boolean} 34 | */ 35 | function isFloor(type) { 36 | return type === 'bounded-floor' || type === 'local-floor'; 37 | } 38 | 39 | export default class XRReferenceSpace extends XRSpace { 40 | /** 41 | * Optionally takes a `transform` from a device's requestFrameOfReferenceMatrix 42 | * so device's can provide their own transforms for stage (or if they 43 | * wanted to override eye-level/head-model). 44 | * 45 | * @param {XRReferenceSpaceType} type 46 | * @param {Float32Array?} transform 47 | */ 48 | constructor(type, transform = null) { 49 | if (!XRReferenceSpaceTypes.includes(type)) { 50 | throw new Error(`XRReferenceSpaceType must be one of ${XRReferenceSpaceTypes}`); 51 | } 52 | 53 | super(type); 54 | 55 | // If stage emulation is disabled, and this is a stage frame of reference, 56 | // and the XRDevice did not provide a transform, this is an invalid 57 | // configuration and we shouldn't emulate here. XRSession.requestFrameOfReference 58 | // should check this as well. 59 | if (type === 'bounded-floor' && !transform) { 60 | throw new Error(`XRReferenceSpace cannot use 'bounded-floor' type if the platform does not provide the floor level`); 61 | } 62 | 63 | // If we're using floor-level reference and no transform, we're emulating. 64 | // Set emulated height from option or use the default 65 | if (isFloor(type) && !transform) { 66 | // Apply an emulated height to the `y` translation 67 | transform = mat4.identity(new Float32Array(16)); 68 | transform[13] = DEFAULT_EMULATION_HEIGHT; 69 | } 70 | 71 | this._inverseBaseMatrix = transform || mat4.identity(new Float32Array(16)); 72 | 73 | this[PRIVATE] = { 74 | type, 75 | transform, 76 | originOffset : mat4.identity(new Float32Array(16)), 77 | }; 78 | } 79 | 80 | /** 81 | * NON-STANDARD 82 | * Takes a base pose model matrix and transforms it by the 83 | * frame of reference. 84 | * 85 | * @param {Float32Array} out 86 | * @param {Float32Array} pose 87 | */ 88 | _transformBasePoseMatrix(out, pose) { 89 | mat4.multiply(out, this._inverseBaseMatrix, pose); 90 | } 91 | 92 | /** 93 | * NON-STANDARD 94 | * 95 | * @return {Float32Array} 96 | */ 97 | _originOffsetMatrix() { 98 | return this[PRIVATE].originOffset; 99 | } 100 | 101 | /** 102 | * transformMatrix = Inv(OriginOffsetMatrix) * transformMatrix 103 | * @param {Float32Array} transformMatrix 104 | */ 105 | _adjustForOriginOffset(transformMatrix) { 106 | let inverseOriginOffsetMatrix = new Float32Array(16); 107 | mat4.invert(inverseOriginOffsetMatrix, this[PRIVATE].originOffset); 108 | mat4.multiply(transformMatrix, inverseOriginOffsetMatrix, transformMatrix); 109 | } 110 | 111 | /** 112 | * Gets the transform of the given space in this space 113 | * 114 | * @param {XRSpace} space 115 | * @return {XRRigidTransform} 116 | */ 117 | _getSpaceRelativeTransform(space) { 118 | let transform = super._getSpaceRelativeTransform(space); 119 | // TODO: Optimize away double creation of XRRigidTransform 120 | this._adjustForOriginOffset(transform.matrix) 121 | return new XRRigidTransform(transform.matrix); 122 | } 123 | 124 | /** 125 | * Doesn't update the bound geometry for bounded reference spaces. 126 | * @param {XRRigidTransform} additionalOffset 127 | * @return {XRReferenceSpace} 128 | */ 129 | getOffsetReferenceSpace(additionalOffset) { 130 | let newSpace = new XRReferenceSpace( 131 | this[PRIVATE].type, 132 | this[PRIVATE].transform, 133 | this[PRIVATE].bounds); 134 | 135 | mat4.multiply(newSpace[PRIVATE].originOffset, this[PRIVATE].originOffset, additionalOffset.matrix); 136 | return newSpace; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/api/XRReferenceSpaceEvent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 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 PRIVATE = Symbol('@@webxr-polyfill/XRReferenceSpaceEvent'); 17 | 18 | export default class XRReferenceSpaceEvent extends Event { 19 | /** 20 | * @param {string} type 21 | * @param {Object} eventInitDict 22 | */ 23 | constructor(type, eventInitDict) { 24 | super(type, eventInitDict); 25 | this[PRIVATE] = { 26 | referenceSpace: eventInitDict.referenceSpace, 27 | transform: eventInitDict.transform || null 28 | }; 29 | // safari bug: super() seems to return object of type Event, with Event as prototype 30 | Object.setPrototypeOf(this, XRReferenceSpaceEvent.prototype); 31 | } 32 | 33 | /** 34 | * @return {XRFrame} 35 | */ 36 | get referenceSpace() { return this[PRIVATE].referenceSpace; } 37 | 38 | /** 39 | * @return {XRInputSource} 40 | */ 41 | get transform() { return this[PRIVATE].transform; } 42 | } -------------------------------------------------------------------------------- /src/api/XRRenderState.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 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 PRIVATE = Symbol('@@webxr-polyfill/XRRenderState'); 17 | 18 | export const XRRenderStateInit = Object.freeze({ 19 | depthNear: 0.1, 20 | depthFar: 1000.0, 21 | inlineVerticalFieldOfView: null, 22 | baseLayer: null 23 | }); 24 | 25 | export default class XRRenderState { 26 | /** 27 | * @param {Object?} stateInit 28 | */ 29 | constructor(stateInit = {}) { 30 | const config = Object.assign({}, XRRenderStateInit, stateInit); 31 | this[PRIVATE] = { config }; 32 | } 33 | 34 | /** 35 | * @return {number} 36 | */ 37 | get depthNear() { return this[PRIVATE].config.depthNear; } 38 | 39 | /** 40 | * @return {number} 41 | */ 42 | get depthFar() { return this[PRIVATE].config.depthFar; } 43 | 44 | /** 45 | * @return {number?} 46 | */ 47 | get inlineVerticalFieldOfView() { return this[PRIVATE].config.inlineVerticalFieldOfView; } 48 | 49 | /** 50 | * @return {XRWebGLLayer} 51 | */ 52 | get baseLayer() { return this[PRIVATE].config.baseLayer; } 53 | } 54 | -------------------------------------------------------------------------------- /src/api/XRRigidTransform.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 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 mat4 from 'gl-matrix/src/gl-matrix/mat4'; 17 | import * as vec3 from 'gl-matrix/src/gl-matrix/vec3'; 18 | import * as quat from 'gl-matrix/src/gl-matrix/quat'; 19 | 20 | export const PRIVATE = Symbol('@@webxr-polyfill/XRRigidTransform'); 21 | 22 | export default class XRRigidTransform { 23 | // no arguments: identity transform 24 | // (Float32Array): transform based on matrix 25 | // (DOMPointReadOnly): transform based on position without any rotation 26 | // (DOMPointReadOnly, DOMPointReadOnly): transform based on position and 27 | // orientation quaternion 28 | constructor() { 29 | this[PRIVATE] = { 30 | matrix: null, 31 | position: null, 32 | orientation: null, 33 | inverse: null, 34 | }; 35 | 36 | if (arguments.length === 0) { 37 | this[PRIVATE].matrix = mat4.identity(new Float32Array(16)); 38 | } else if (arguments.length === 1) { 39 | if (arguments[0] instanceof Float32Array) { 40 | this[PRIVATE].matrix = arguments[0]; 41 | } else { 42 | this[PRIVATE].position = this._getPoint(arguments[0]); 43 | this[PRIVATE].orientation = DOMPointReadOnly.fromPoint({ 44 | x: 0, y: 0, z: 0, w: 1 45 | }); 46 | } 47 | } else if (arguments.length === 2) { 48 | this[PRIVATE].position = this._getPoint(arguments[0]); 49 | this[PRIVATE].orientation = this._getPoint(arguments[1]); 50 | } else { 51 | throw new Error("Too many arguments!"); 52 | } 53 | 54 | if (this[PRIVATE].matrix) { 55 | // Decompose matrix into position and orientation. 56 | let position = vec3.create(); 57 | mat4.getTranslation(position, this[PRIVATE].matrix); 58 | this[PRIVATE].position = DOMPointReadOnly.fromPoint({ 59 | x: position[0], 60 | y: position[1], 61 | z: position[2] 62 | }); 63 | 64 | let orientation = quat.create(); 65 | mat4.getRotation(orientation, this[PRIVATE].matrix); 66 | this[PRIVATE].orientation = DOMPointReadOnly.fromPoint({ 67 | x: orientation[0], 68 | y: orientation[1], 69 | z: orientation[2], 70 | w: orientation[3] 71 | }); 72 | } else { 73 | // Compose matrix from position and orientation. 74 | this[PRIVATE].matrix = mat4.identity(new Float32Array(16)); 75 | mat4.fromRotationTranslation( 76 | this[PRIVATE].matrix, 77 | quat.fromValues( 78 | this[PRIVATE].orientation.x, 79 | this[PRIVATE].orientation.y, 80 | this[PRIVATE].orientation.z, 81 | this[PRIVATE].orientation.w), 82 | vec3.fromValues( 83 | this[PRIVATE].position.x, 84 | this[PRIVATE].position.y, 85 | this[PRIVATE].position.z) 86 | ); 87 | } 88 | } 89 | 90 | /** 91 | * Try to convert arg to a DOMPointReadOnly if it isn't already one. 92 | * @param {*} arg 93 | * @return {DOMPointReadOnly} 94 | */ 95 | _getPoint(arg) { 96 | if (arg instanceof DOMPointReadOnly) { 97 | return arg; 98 | } 99 | 100 | return DOMPointReadOnly.fromPoint(arg); 101 | } 102 | 103 | /** 104 | * @return {Float32Array} 105 | */ 106 | get matrix() { return this[PRIVATE].matrix; } 107 | 108 | /** 109 | * @return {DOMPointReadOnly} 110 | */ 111 | get position() { return this[PRIVATE].position; } 112 | 113 | /** 114 | * @return {DOMPointReadOnly} 115 | */ 116 | get orientation() { return this[PRIVATE].orientation; } 117 | 118 | /** 119 | * @return {XRRigidTransform} 120 | */ 121 | get inverse() { 122 | if (this[PRIVATE].inverse === null) { 123 | let invMatrix = mat4.identity(new Float32Array(16)); 124 | mat4.invert(invMatrix, this[PRIVATE].matrix); 125 | this[PRIVATE].inverse = new XRRigidTransform(invMatrix); 126 | this[PRIVATE].inverse[PRIVATE].inverse = this; 127 | } 128 | 129 | return this[PRIVATE].inverse; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/api/XRSessionEvent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 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 PRIVATE = Symbol('@@webxr-polyfill/XRSessionEvent'); 17 | 18 | export default class XRSessionEvent extends Event { 19 | /** 20 | * @param {string} type 21 | * @param {Object} eventInitDict 22 | */ 23 | constructor(type, eventInitDict) { 24 | super(type, eventInitDict); 25 | this[PRIVATE] = { 26 | session: eventInitDict.session 27 | }; 28 | // safari bug: super() seems to return object of type Event, with Event as prototype 29 | Object.setPrototypeOf(this, XRSessionEvent.prototype); 30 | } 31 | 32 | /** 33 | * @return {XRSession} 34 | */ 35 | get session() { return this[PRIVATE].session; } 36 | } -------------------------------------------------------------------------------- /src/api/XRSpace.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 XRRigidTransform from './XRRigidTransform'; 16 | import * as mat4 from 'gl-matrix/src/gl-matrix/mat4'; 17 | 18 | export const PRIVATE = Symbol('@@webxr-polyfill/XRSpace'); 19 | 20 | // Not exposed, for reference only 21 | export const XRSpaceSpecialTypes = [ 22 | "grip", 23 | "target-ray" 24 | ]; 25 | 26 | export default class XRSpace { 27 | /** 28 | * @param {string?} specialType 29 | * @param {XRInputSource?} inputSource 30 | */ 31 | constructor(specialType = null, inputSource = null) { 32 | this[PRIVATE] = { 33 | specialType, 34 | inputSource, 35 | // The transform for the space in the base space, along with it's inverse 36 | baseMatrix: null, 37 | inverseBaseMatrix: null, 38 | lastFrameId: -1 39 | }; 40 | } 41 | 42 | /** 43 | * @return {string?} 44 | */ 45 | get _specialType() { 46 | return this[PRIVATE].specialType; 47 | } 48 | 49 | /** 50 | * @return {XRInputSource?} 51 | */ 52 | get _inputSource() { 53 | return this[PRIVATE].inputSource; 54 | } 55 | 56 | /** 57 | * NON-STANDARD 58 | * Trigger an update for this space's base pose if necessary 59 | * @param {XRDevice} device 60 | * @param {Number} frameId 61 | */ 62 | _ensurePoseUpdated(device, frameId) { 63 | if (frameId == this[PRIVATE].lastFrameId) return; 64 | this[PRIVATE].lastFrameId = frameId; 65 | this._onPoseUpdate(device); 66 | } 67 | 68 | /** 69 | * NON-STANDARD 70 | * Called when this space's base pose needs to be updated 71 | * @param {XRDevice} device 72 | */ 73 | _onPoseUpdate(device) { 74 | if (this[PRIVATE].specialType == 'viewer') { 75 | this._baseMatrix = device.getBasePoseMatrix(); 76 | } 77 | } 78 | 79 | /** 80 | * NON-STANDARD 81 | * @param {Float32Array(16)} matrix 82 | */ 83 | set _baseMatrix(matrix) { 84 | this[PRIVATE].baseMatrix = matrix; 85 | this[PRIVATE].inverseBaseMatrix = null; 86 | } 87 | 88 | /** 89 | * NON-STANDARD 90 | * @return {Float32Array(16)} 91 | */ 92 | get _baseMatrix() { 93 | if (!this[PRIVATE].baseMatrix) { 94 | if (this[PRIVATE].inverseBaseMatrix) { 95 | this[PRIVATE].baseMatrix = new Float32Array(16); 96 | mat4.invert(this[PRIVATE].baseMatrix, this[PRIVATE].inverseBaseMatrix); 97 | } 98 | } 99 | return this[PRIVATE].baseMatrix; 100 | } 101 | 102 | /** 103 | * NON-STANDARD 104 | * @param {Float32Array(16)} matrix 105 | */ 106 | set _inverseBaseMatrix(matrix) { 107 | this[PRIVATE].inverseBaseMatrix = matrix; 108 | this[PRIVATE].baseMatrix = null; 109 | } 110 | 111 | /** 112 | * NON-STANDARD 113 | * @return {Float32Array(16)} 114 | */ 115 | get _inverseBaseMatrix() { 116 | if (!this[PRIVATE].inverseBaseMatrix) { 117 | if (this[PRIVATE].baseMatrix) { 118 | this[PRIVATE].inverseBaseMatrix = new Float32Array(16); 119 | mat4.invert(this[PRIVATE].inverseBaseMatrix, this[PRIVATE].baseMatrix); 120 | } 121 | } 122 | return this[PRIVATE].inverseBaseMatrix; 123 | } 124 | 125 | /** 126 | * NON-STANDARD 127 | * Gets the transform of the given space in this space 128 | * 129 | * @param {XRSpace} space 130 | * @return {XRRigidTransform} 131 | */ 132 | _getSpaceRelativeTransform(space) { 133 | if (!this._inverseBaseMatrix || !space._baseMatrix) { 134 | return null; 135 | } 136 | let out = new Float32Array(16); 137 | mat4.multiply(out, this._inverseBaseMatrix, space._baseMatrix); 138 | return new XRRigidTransform(out); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/api/XRSystem.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 EventTarget from '../lib/EventTarget'; 17 | import { XRReferenceSpaceTypes } from './XRReferenceSpace'; 18 | 19 | export const PRIVATE = Symbol('@@webxr-polyfill/XR'); 20 | 21 | export const XRSessionModes = ['inline', 'immersive-vr', 'immersive-ar']; 22 | 23 | const DEFAULT_SESSION_OPTIONS = { 24 | 'inline': { 25 | requiredFeatures: ['viewer'], 26 | optionalFeatures: [], 27 | }, 28 | 'immersive-vr': { 29 | requiredFeatures: ['viewer', 'local'], 30 | optionalFeatures: [], 31 | }, 32 | 'immersive-ar': { 33 | requiredFeatures: ['viewer', 'local'], 34 | optionalFeatures: [], 35 | } 36 | }; 37 | 38 | const POLYFILL_REQUEST_SESSION_ERROR = 39 | `Polyfill Error: Must call navigator.xr.isSessionSupported() with any XRSessionMode 40 | or navigator.xr.requestSession('inline') prior to requesting an immersive 41 | session. This is a limitation specific to the WebXR Polyfill and does not apply 42 | to native implementations of the API.` 43 | 44 | export default class XRSystem extends EventTarget { 45 | /** 46 | * Receives a promise of an XRDevice, so that the polyfill 47 | * can pass in some initial checks to asynchronously provide XRDevices 48 | * if content immediately requests `requestDevice()`. 49 | * 50 | * @param {Promise} devicePromise 51 | */ 52 | constructor(devicePromise) { 53 | super(); 54 | this[PRIVATE] = { 55 | device: null, 56 | devicePromise, 57 | immersiveSession: null, 58 | inlineSessions: new Set(), 59 | }; 60 | 61 | devicePromise.then((device) => { this[PRIVATE].device = device; }); 62 | } 63 | 64 | /** 65 | * @param {XRSessionMode} mode 66 | * @return {Promise} 67 | */ 68 | async isSessionSupported(mode) { 69 | // Always ensure that we wait for the device promise to resolve. 70 | if (!this[PRIVATE].device) { 71 | await this[PRIVATE].devicePromise; 72 | } 73 | 74 | // 'inline' is always guaranteed to be supported. 75 | if (mode != 'inline') { 76 | return Promise.resolve(this[PRIVATE].device.isSessionSupported(mode)); 77 | } 78 | 79 | return Promise.resolve(true); 80 | } 81 | 82 | /** 83 | * @param {XRSessionMode} mode 84 | * @param {XRSessionInit} options 85 | * @return {Promise} 86 | */ 87 | async requestSession(mode, options) { 88 | // If the device hasn't resolved yet, wait for it and try again. 89 | if (!this[PRIVATE].device) { 90 | if (mode != 'inline') { 91 | // Because requesting immersive modes requires a user gesture, we can't 92 | // wait for a promise to resolve before making the real session request. 93 | // For that reason, we'll throw a polyfill-specific error here. 94 | throw new Error(POLYFILL_REQUEST_SESSION_ERROR); 95 | } else { 96 | await this[PRIVATE].devicePromise; 97 | } 98 | } 99 | 100 | if (!XRSessionModes.includes(mode)) { 101 | throw new TypeError( 102 | `The provided value '${mode}' is not a valid enum value of type XRSessionMode`); 103 | } 104 | 105 | // Resolve which of the requested features are supported and reject if a 106 | // required feature isn't available. 107 | const defaultOptions = DEFAULT_SESSION_OPTIONS[mode]; 108 | const requiredFeatures = defaultOptions.requiredFeatures.concat( 109 | options && options.requiredFeatures ? options.requiredFeatures : []); 110 | const optionalFeatures = defaultOptions.optionalFeatures.concat( 111 | options && options.optionalFeatures ? options.optionalFeatures : []); 112 | const enabledFeatures = new Set(); 113 | 114 | let requirementsFailed = false; 115 | for (let feature of requiredFeatures) { 116 | if (!this[PRIVATE].device.isFeatureSupported(feature)) { 117 | console.error(`The required feature '${feature}' is not supported`); 118 | requirementsFailed = true; 119 | } else { 120 | enabledFeatures.add(feature); 121 | } 122 | } 123 | 124 | if (requirementsFailed) { 125 | throw new DOMException('Session does not support some required features', 'NotSupportedError'); 126 | } 127 | 128 | for (let feature of optionalFeatures) { 129 | if (!this[PRIVATE].device.isFeatureSupported(feature)) { 130 | console.log(`The optional feature '${feature}' is not supported`); 131 | } else { 132 | enabledFeatures.add(feature); 133 | } 134 | } 135 | 136 | // Call device's requestSession, which does some initialization (1.1 137 | // fallback calls `vrDisplay.requestPresent()` for example). Could throw 138 | // due to missing user gesture. 139 | const sessionId = await this[PRIVATE].device.requestSession(mode, enabledFeatures); 140 | const session = new XRSession(this[PRIVATE].device, mode, sessionId); 141 | 142 | if (mode == 'inline') { 143 | this[PRIVATE].inlineSessions.add(session); 144 | } else { 145 | this[PRIVATE].immersiveSession = session; 146 | } 147 | 148 | const onSessionEnd = () => { 149 | if (mode == 'inline') { 150 | this[PRIVATE].inlineSessions.delete(session); 151 | } else { 152 | this[PRIVATE].immersiveSession = null; 153 | } 154 | session.removeEventListener('end', onSessionEnd); 155 | }; 156 | session.addEventListener('end', onSessionEnd); 157 | 158 | return session; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/api/XRView.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 XRViewport from './XRViewport'; 17 | import XRRigidTransform from './XRRigidTransform'; 18 | 19 | const XREyes = ['left', 'right', 'none']; 20 | 21 | export const PRIVATE = Symbol('@@webxr-polyfill/XRView'); 22 | 23 | export default class XRView { 24 | /** 25 | * @param {XRDevice} device 26 | * @param {XREye} eye 27 | * @param {number} sessionId 28 | * @param {number} viewIndex 29 | */ 30 | constructor(device, transform, eye, sessionId, viewIndex) { 31 | if (!XREyes.includes(eye)) { 32 | throw new Error(`XREye must be one of: ${XREyes}`); 33 | } 34 | 35 | // Create a shared object that can be updated by other code 36 | // that can update XRViewport values to adhere to API. 37 | // Ugly but it works. 38 | const temp = Object.create(null); 39 | const viewport = new XRViewport(temp); 40 | 41 | this[PRIVATE] = { 42 | device, 43 | eye, 44 | viewport, 45 | temp, 46 | sessionId, 47 | transform, 48 | viewIndex, 49 | }; 50 | } 51 | 52 | /** 53 | * @return {XREye} 54 | */ 55 | get eye() { return this[PRIVATE].eye; } 56 | 57 | /** 58 | * @return {Float32Array} 59 | */ 60 | get projectionMatrix() { 61 | return this[PRIVATE].device.getProjectionMatrix(this.eye, this[PRIVATE].viewIndex); 62 | } 63 | 64 | /** 65 | * @return {XRRigidTransform} 66 | */ 67 | get transform() { return this[PRIVATE].transform; } 68 | 69 | /** 70 | * NON-STANDARD 71 | * 72 | * `getViewport` is now exposed via XRWebGLLayer instead of XRView. 73 | * XRWebGLLayer delegates all the actual work to this function. 74 | * 75 | * @param {XRWebGLLayer} layer 76 | * @return {XRViewport?} 77 | */ 78 | _getViewport(layer) { 79 | if (this[PRIVATE].device.getViewport(this[PRIVATE].sessionId, 80 | this.eye, 81 | layer, 82 | this[PRIVATE].temp, 83 | this[PRIVATE].viewIndex)) { 84 | return this[PRIVATE].viewport; 85 | } 86 | return undefined; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/api/XRViewerPose.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 XRPose from './XRPose'; 17 | 18 | export const PRIVATE = Symbol('@@webxr-polyfill/XRViewerPose'); 19 | 20 | export default class XRViewerPose extends XRPose { 21 | /** 22 | * @param {XRDevice} device 23 | */ 24 | constructor(transform, views, emulatedPosition = false) { 25 | super(transform, emulatedPosition); 26 | this[PRIVATE] = { 27 | views 28 | }; 29 | } 30 | 31 | /** 32 | * @return {Array} 33 | */ 34 | get views() { 35 | return this[PRIVATE].views; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/api/XRViewport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 PRIVATE = Symbol('@@webxr-polyfill/XRViewport'); 17 | 18 | export default class XRViewport { 19 | /** 20 | * Takes a proxy object that this viewport's XRView 21 | * updates and we serve here to match API. 22 | * 23 | * @param {Object} target 24 | */ 25 | constructor(target) { 26 | this[PRIVATE] = { target }; 27 | } 28 | 29 | /** 30 | * @return {number} 31 | */ 32 | get x() { return this[PRIVATE].target.x; } 33 | 34 | /** 35 | * @return {number} 36 | */ 37 | get y() { return this[PRIVATE].target.y; } 38 | 39 | /** 40 | * @return {number} 41 | */ 42 | get width() { return this[PRIVATE].target.width; } 43 | 44 | /** 45 | * @return {number} 46 | */ 47 | get height() { return this[PRIVATE].target.height; } 48 | } 49 | -------------------------------------------------------------------------------- /src/api/XRWebGLLayer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 XRSession, { PRIVATE as SESSION_PRIVATE } from './XRSession'; 17 | import { 18 | POLYFILLED_XR_COMPATIBLE, 19 | XR_COMPATIBLE, 20 | } from '../constants'; 21 | 22 | export const PRIVATE = Symbol('@@webxr-polyfill/XRWebGLLayer'); 23 | 24 | export const XRWebGLLayerInit = Object.freeze({ 25 | antialias: true, 26 | depth: true, 27 | stencil: false, 28 | alpha: true, 29 | multiview: false, 30 | ignoreDepthValues: false, 31 | framebufferScaleFactor: 1.0, 32 | }); 33 | 34 | export default class XRWebGLLayer { 35 | /** 36 | * @param {XRSession} session 37 | * @param {XRWebGLRenderingContext} context 38 | * @param {Object?} layerInit 39 | */ 40 | constructor(session, context, layerInit={}) { 41 | const config = Object.assign({}, XRWebGLLayerInit, layerInit); 42 | 43 | if (!(session instanceof XRSession)) { 44 | throw new Error('session must be a XRSession'); 45 | } 46 | 47 | if (session.ended) { 48 | throw new Error(`InvalidStateError`); 49 | } 50 | 51 | // Since we're polyfilling, we're probably polyfilling 52 | // the compatible XR device bit as well. It'd be 53 | // unusual for this bit to not be polyfilled. 54 | if (context[POLYFILLED_XR_COMPATIBLE]) { 55 | if (session[SESSION_PRIVATE].immersive && context[XR_COMPATIBLE] !== true) { 56 | throw new Error(`InvalidStateError`); 57 | } 58 | } 59 | 60 | this[PRIVATE] = { 61 | context, 62 | config, 63 | session, 64 | }; 65 | } 66 | 67 | /** 68 | * @return {WebGLRenderingContext} 69 | */ 70 | get context() { return this[PRIVATE].context; } 71 | 72 | /** 73 | * @return {boolean} 74 | */ 75 | get antialias() { return this[PRIVATE].config.antialias; } 76 | 77 | /** 78 | * The polyfill will always ignore depth values. 79 | * 80 | * @return {boolean} 81 | */ 82 | get ignoreDepthValues() { return true; } 83 | 84 | /** 85 | * @return {WebGLFramebuffer} 86 | */ 87 | get framebuffer() { 88 | // Use the default framebuffer 89 | return null; 90 | } 91 | 92 | /** 93 | * @return {number} 94 | */ 95 | get framebufferWidth() { return this[PRIVATE].context.drawingBufferWidth; } 96 | 97 | /** 98 | * @return {number} 99 | */ 100 | get framebufferHeight() { return this[PRIVATE].context.drawingBufferHeight; } 101 | 102 | /** 103 | * @return {XRSession} 104 | */ 105 | get _session() { return this[PRIVATE].session; } 106 | 107 | /** 108 | * @TODO No mention in spec on not reusing the XRViewport on every frame. 109 | * 110 | * @TODO In the future maybe all this logic should be handled here instead of 111 | * delegated to the XRView? 112 | * 113 | * @param {XRView} view 114 | * @return {XRViewport?} 115 | */ 116 | getViewport(view) { 117 | return view._getViewport(this); 118 | } 119 | 120 | /** 121 | * Gets the scale factor to be requested if you want to match the device 122 | * resolution at the center of the user's vision. The polyfill will always 123 | * report 1.0. 124 | * 125 | * @param {XRSession} session 126 | * @return {number} 127 | */ 128 | static getNativeFramebufferScaleFactor(session) { 129 | if (!session) { 130 | throw new TypeError('getNativeFramebufferScaleFactor must be passed a session.') 131 | } 132 | 133 | if (session[SESSION_PRIVATE].ended) { return 0.0; } 134 | 135 | return 1.0; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 XRSystem from './XRSystem'; 17 | import XRSession from './XRSession'; 18 | import XRSessionEvent from './XRSessionEvent'; 19 | import XRFrame from './XRFrame'; 20 | import XRView from './XRView'; 21 | import XRViewport from './XRViewport'; 22 | import XRViewerPose from './XRViewerPose'; 23 | import XRInputSource from './XRInputSource'; 24 | import XRInputSourceEvent from './XRInputSourceEvent'; 25 | import XRInputSourcesChangeEvent from './XRInputSourcesChangeEvent'; 26 | import XRWebGLLayer from './XRWebGLLayer'; 27 | import XRSpace from './XRSpace'; 28 | import XRReferenceSpace from './XRReferenceSpace'; 29 | import XRReferenceSpaceEvent from './XRReferenceSpaceEvent'; 30 | import XRRenderState from './XRRenderState'; 31 | import XRRigidTransform from './XRRigidTransform'; 32 | import XRPose from './XRPose'; 33 | 34 | /** 35 | * Everything exposed here will also be attached to the window 36 | */ 37 | export default { 38 | XRSystem, 39 | XRSession, 40 | XRSessionEvent, 41 | XRFrame, 42 | XRView, 43 | XRViewport, 44 | XRViewerPose, 45 | XRWebGLLayer, 46 | XRSpace, 47 | XRReferenceSpace, 48 | XRReferenceSpaceEvent, 49 | XRInputSource, 50 | XRInputSourceEvent, 51 | XRInputSourcesChangeEvent, 52 | XRRenderState, 53 | XRRigidTransform, 54 | XRPose, 55 | }; 56 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 | * Token for storing whether or not a WebGLRenderingContext has 18 | * a polyfill for `xrCompatible`/`makeXRCompatible()` 19 | */ 20 | export const POLYFILLED_XR_COMPATIBLE = Symbol('@@webxr-polyfill/polyfilled-xr-compatible'); 21 | 22 | /** 23 | * Token for storing the XR compatible boolean set on a WebGLRenderingContext 24 | * via `gl.makeXRCompatible()` or via creation 25 | * parameters like `canvas.getContext('webgl', { xrCompatible })` 26 | */ 27 | export const XR_COMPATIBLE = Symbol('@@webxr-polyfill/xr-compatible'); 28 | -------------------------------------------------------------------------------- /src/devices.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 CardboardXRDevice from './devices/CardboardXRDevice'; 17 | import InlineDevice from './devices/InlineDevice'; 18 | import WebVRDevice from './devices/WebVRDevice'; 19 | 20 | import { isMobile } from './utils'; 21 | 22 | /** 23 | * Queries browser to see if any VRDisplay exists. 24 | * Resolves to a polyfilled XRDevice or null. 25 | */ 26 | const getWebVRDevice = async function (global) { 27 | let device = null; 28 | if ('getVRDisplays' in global.navigator) { 29 | try { 30 | const displays = await global.navigator.getVRDisplays(); 31 | if (displays && displays.length) { 32 | device = new WebVRDevice(global, displays[0]); 33 | } 34 | } catch (e) {} 35 | } 36 | 37 | return device; 38 | }; 39 | 40 | /** 41 | * Return an XRDevice interface based off of configuration 42 | * and platform. 43 | * 44 | * @param {Object} global 45 | * @param {Object} config 46 | * @return {Promise} 47 | */ 48 | export const requestXRDevice = async function (global, config) { 49 | // Check for a 1.1 VRDisplay. 50 | if (config.webvr) { 51 | let xr = await getWebVRDevice(global); 52 | if (xr) { 53 | return xr; 54 | } 55 | } 56 | 57 | // If no WebVR devices are present, check to see if a Cardboard device is 58 | // allowed and if so return that. 59 | // TODO: This probably requires more changes to allow creating an 60 | // immersive session in a headset that gets connected later. 61 | let mobile = isMobile(global); 62 | if ((mobile && config.cardboard) || 63 | (!mobile && config.allowCardboardOnDesktop)) { 64 | // If we're on Cardboard, make sure that VRFrameData is a global 65 | if (!global.VRFrameData) { 66 | global.VRFrameData = function () { 67 | this.rightViewMatrix = new Float32Array(16); 68 | this.leftViewMatrix = new Float32Array(16); 69 | this.rightProjectionMatrix = new Float32Array(16); 70 | this.leftProjectionMatrix = new Float32Array(16); 71 | this.pose = null; 72 | }; 73 | } 74 | 75 | return new CardboardXRDevice(global, config.cardboardConfig); 76 | } 77 | 78 | // Inline sessions are always allowed, so if no other device is available 79 | // create one that only supports sensorless inline sessions. 80 | return new InlineDevice(global); 81 | } 82 | -------------------------------------------------------------------------------- /src/devices/CardboardXRDevice.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 CardboardVRDisplay from 'cardboard-vr-display'; 17 | import WebVRDevice from './WebVRDevice'; 18 | 19 | export default class CardboardXRDevice extends WebVRDevice { 20 | /** 21 | * Takes a VRDisplay instance and a VRFrameData 22 | * constructor from the WebVR 1.1 spec. 23 | * 24 | * @param {VRDisplay} display 25 | * @param {Object?} cardboardConfig 26 | */ 27 | constructor(global, cardboardConfig) { 28 | const display = new CardboardVRDisplay(cardboardConfig || {}); 29 | super(global, display); 30 | 31 | this.display = display; 32 | this.frame = { 33 | rightViewMatrix: new Float32Array(16), 34 | leftViewMatrix: new Float32Array(16), 35 | rightProjectionMatrix: new Float32Array(16), 36 | leftProjectionMatrix: new Float32Array(16), 37 | pose: null, 38 | timestamp: null, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/devices/GamepadMappings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Immersive Web Community Group. 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 | Example Gamepad mapping. Any of the values may be omitted for the original 18 | gamepad values to pass through unchanged. 19 | 20 | "Gamepad ID String": { // The Gamepad.id that this entry maps to. 21 | mapping: 'xr-standard', // Overrides the Gamepad.mapping that is reported 22 | profiles: ['gamepad-id-string'], // The profiles array that should be reported 23 | displayProfiles: { 24 | // Alternative profiles arrays to report if the VRDevice.displayName matches 25 | "WebVR Display Name": ['gamepad-id-string'] 26 | }, 27 | axes: { // Remaps the reported axes 28 | length: 2, // Overrides the length of the reported axes array 29 | invert: [0] // List of mapped axes who's value should be inverted 30 | 0: 2, // Remaps axes[0] to report axis[2] from the original gamepad object 31 | 1: null, // Remaps axes[1] to a placeholder axis (always reports 0) 32 | }, 33 | buttons: { // Remaps the reported buttons 34 | length: 2, // Overrides the length of the reported buttons array 35 | 0: 2, // Remaps buttons[0] to report buttons[2] from the original gamepad object 36 | 1: null // Remaps buttons[1] to a placeholder button (always reports 0/false) 37 | }, 38 | gripTransform: { // An additional transform to apply to the gripSpace's pose 39 | position: [0, 0, 0.5], // Additional translation vector to apply 40 | orientation: [0, 0, 0, 1] // Additional rotation quaternion to apply 41 | }, 42 | targetRayTransform: { // An additional transform to apply to the targetRaySpace's pose 43 | position: [0, 0, 0.5], // Additional translation vector to apply 44 | orientation: [0, 0, 0, 1] // Additional rotation quaternion to apply 45 | } 46 | } 47 | */ 48 | 49 | let daydream = { 50 | mapping: '', 51 | profiles: ['google-daydream', 'generic-trigger-touchpad'], 52 | buttons: { 53 | length: 3, 54 | 0: null, 55 | 1: null, 56 | 2: 0 57 | }, 58 | }; 59 | 60 | let viveFocus = { 61 | mapping: 'xr-standard', 62 | profiles: ['htc-vive-focus', 'generic-trigger-touchpad'], 63 | buttons: { 64 | length: 3, 65 | 0: 1, 66 | 1: null, 67 | 2: 0 68 | }, 69 | }; 70 | 71 | let oculusGo = { 72 | mapping: 'xr-standard', 73 | profiles: ['oculus-go', 'generic-trigger-touchpad'], 74 | buttons: { 75 | length: 3, 76 | 0: 1, 77 | 1: null, 78 | 2: 0 79 | }, 80 | // Grip adjustments determined experimentally. 81 | gripTransform: { 82 | orientation: [Math.PI * 0.11, 0, 0, 1] 83 | } 84 | }; 85 | 86 | // Applies to both left and right Oculus Touch controllers. 87 | let oculusTouch = { 88 | mapping: 'xr-standard', 89 | displayProfiles: { 90 | 'Oculus Quest': ['oculus-touch-v2', 'oculus-touch', 'generic-trigger-squeeze-thumbstick'] 91 | }, 92 | profiles: ['oculus-touch', 'generic-trigger-squeeze-thumbstick'], 93 | axes: { 94 | length: 4, 95 | 0: 2, 96 | 1: 3, 97 | 2: 0, 98 | 3: 1 99 | }, 100 | buttons: { 101 | length: 7, 102 | 0: 1, 103 | 1: 2, 104 | 2: null, 105 | 3: 0, 106 | 4: 3, 107 | 5: 4, 108 | 6: null 109 | }, 110 | // Grip adjustments determined experimentally. 111 | gripTransform: { 112 | position: [0, -0.02, 0.04, 1], 113 | orientation: [Math.PI * 0.11, 0, 0, 1] 114 | } 115 | }; 116 | 117 | // Applies to both left and right Oculus Touch controllers. 118 | let oculusTouchV2 = { 119 | mapping: 'xr-standard', 120 | displayProfiles: { 121 | 'Oculus Quest': ['oculus-touch-v2', 'oculus-touch', 'generic-trigger-squeeze-thumbstick'] 122 | }, 123 | profiles: ['oculus-touch-v2', 'oculus-touch', 'generic-trigger-squeeze-thumbstick'], 124 | axes: { 125 | length: 4, 126 | 0: 2, 127 | 1: 3, 128 | 2: 0, 129 | 3: 1 130 | }, 131 | buttons: { 132 | length: 7, 133 | 0: 1, 134 | 1: 2, 135 | 2: null, 136 | 3: 0, 137 | 4: 3, 138 | 5: 4, 139 | 6: null 140 | }, 141 | // Grip adjustments determined experimentally. 142 | gripTransform: { 143 | position: [0, -0.02, 0.04, 1], 144 | orientation: [Math.PI * 0.11, 0, 0, 1] 145 | } 146 | }; 147 | 148 | // Applies to both left and right Oculus Touch controllers. 149 | let oculusTouchV3 = { 150 | mapping: 'xr-standard', 151 | displayProfiles: { 152 | 'Oculus Quest': ['oculus-touch-v2', 'oculus-touch', 'generic-trigger-squeeze-thumbstick'] 153 | }, 154 | profiles: ['oculus-touch-v3', 'oculus-touch-v2', 'oculus-touch', 'generic-trigger-squeeze-thumbstick'], 155 | axes: { 156 | length: 4, 157 | 0: 2, 158 | 1: 3, 159 | 2: 0, 160 | 3: 1 161 | }, 162 | buttons: { 163 | length: 7, 164 | 0: 1, 165 | 1: 2, 166 | 2: null, 167 | 3: 0, 168 | 4: 3, 169 | 5: 4, 170 | 6: null 171 | }, 172 | // Grip adjustments determined experimentally. 173 | gripTransform: { 174 | position: [0, -0.02, 0.04, 1], 175 | orientation: [Math.PI * 0.11, 0, 0, 1] 176 | } 177 | }; 178 | 179 | let openVr = { 180 | mapping: 'xr-standard', 181 | profiles: ['htc-vive', 'generic-trigger-squeeze-touchpad'], 182 | displayProfiles: { 183 | 'HTC Vive': ['htc-vive', 'generic-trigger-squeeze-touchpad'], 184 | 'HTC Vive DVT': ['htc-vive', 'generic-trigger-squeeze-touchpad'], 185 | 'Valve Index': ['valve-index', 'generic-trigger-squeeze-touchpad-thumbstick'] 186 | }, 187 | buttons: { 188 | length: 3, 189 | 0: 1, 190 | 1: 2, 191 | 2: 0 192 | }, 193 | // Transform adjustments determined experimentally. 194 | gripTransform: { 195 | position: [0, 0, 0.05, 1], 196 | }, 197 | targetRayTransform: { 198 | orientation: [Math.PI * -0.08, 0, 0, 1] 199 | }, 200 | userAgentOverrides: { 201 | "Firefox": { 202 | axes: { 203 | invert: [1, 3] 204 | } 205 | } 206 | } 207 | }; 208 | 209 | let samsungGearVR = { 210 | mapping: 'xr-standard', 211 | profiles: ['samsung-gearvr', 'generic-trigger-touchpad'], 212 | buttons: { 213 | length: 3, 214 | 0: 1, 215 | 1: null, 216 | 2: 0 217 | }, 218 | gripTransform: { 219 | orientation: [Math.PI * 0.11, 0, 0, 1] 220 | } 221 | }; 222 | 223 | let samsungOdyssey = { 224 | mapping: 'xr-standard', 225 | profiles: ['samsung-odyssey', 'microsoft-mixed-reality', 'generic-trigger-squeeze-touchpad-thumbstick'], 226 | buttons: { 227 | length: 4, 228 | 0: 1, // index finger trigger 229 | 1: 0, // pressable joystick 230 | 2: 2, // grip trigger 231 | 3: 4, // pressable touchpad 232 | }, 233 | // Grip adjustments determined experimentally. 234 | gripTransform: { 235 | position: [0, -0.02, 0.04, 1], 236 | orientation: [Math.PI * 0.11, 0, 0, 1] 237 | } 238 | }; 239 | 240 | let windowsMixedReality = { 241 | mapping: 'xr-standard', 242 | profiles: ['microsoft-mixed-reality', 'generic-trigger-squeeze-touchpad-thumbstick'], 243 | buttons: { 244 | length: 4, 245 | 0: 1, // index finger trigger 246 | 1: 0, // pressable joystick 247 | 2: 2, // grip trigger 248 | 3: 4, // pressable touchpad 249 | }, 250 | // Grip adjustments determined experimentally. 251 | gripTransform: { 252 | position: [0, -0.02, 0.04, 1], 253 | orientation: [Math.PI * 0.11, 0, 0, 1] 254 | } 255 | }; 256 | 257 | let GamepadMappings = { 258 | 'Daydream Controller': daydream, 259 | 'Gear VR Controller': samsungGearVR, 260 | 'HTC Vive Focus Controller': viveFocus, 261 | 'Oculus Go Controller': oculusGo, 262 | 'Oculus Touch (Right)': oculusTouch, 263 | 'Oculus Touch (Left)': oculusTouch, 264 | 'Oculus Touch V2 (Right)': oculusTouchV2, 265 | 'Oculus Touch V2 (Left)': oculusTouchV2, 266 | 'Oculus Touch V3 (Right)': oculusTouchV3, 267 | 'Oculus Touch V3 (Left)': oculusTouchV3, 268 | 'OpenVR Gamepad': openVr, 269 | 'Spatial Controller (Spatial Interaction Source) 045E-065A': windowsMixedReality, 270 | 'Spatial Controller (Spatial Interaction Source) 045E-065D': samsungOdyssey, 271 | 'Windows Mixed Reality (Right)': windowsMixedReality, 272 | 'Windows Mixed Reality (Left)': windowsMixedReality, 273 | }; 274 | 275 | export default GamepadMappings; 276 | -------------------------------------------------------------------------------- /src/devices/GamepadXRInputSource.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 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 GamepadMappings from './GamepadMappings'; 17 | import XRInputSource from '../api/XRInputSource'; 18 | import OrientationArmModel from '../lib/OrientationArmModel'; 19 | import * as mat4 from 'gl-matrix/src/gl-matrix/mat4'; 20 | import * as vec3 from 'gl-matrix/src/gl-matrix/vec3'; 21 | import * as quat from 'gl-matrix/src/gl-matrix/quat'; 22 | 23 | export const PRIVATE = Symbol('@@webxr-polyfill/XRRemappedGamepad'); 24 | 25 | const PLACEHOLDER_BUTTON = { pressed: false, touched: false, value: 0.0 }; 26 | Object.freeze(PLACEHOLDER_BUTTON); 27 | 28 | class XRRemappedGamepad { 29 | constructor(gamepad, display, map) { 30 | if (!map) { 31 | map = {}; 32 | } 33 | 34 | // Apply user-agent-specific overrides to the mapping when applicable. 35 | if (map.userAgentOverrides) { 36 | for (let agent in map.userAgentOverrides) { 37 | if (navigator.userAgent.includes(agent)) { 38 | let override = map.userAgentOverrides[agent]; 39 | 40 | for (let key in override) { 41 | if (key in map) { 42 | // If the key already exists, merge the override values into the 43 | // existing dictionary. 44 | Object.assign(map[key], override[key]); 45 | } else { 46 | // If the base mapping doesn't have this key, insert the override 47 | // values wholesale. 48 | map[key] = override[key]; 49 | } 50 | } 51 | break; 52 | } 53 | } 54 | } 55 | 56 | let axes = new Array(map.axes && map.axes.length ? map.axes.length : gamepad.axes.length); 57 | let buttons = new Array(map.buttons && map.buttons.length ? map.buttons.length : gamepad.buttons.length); 58 | 59 | let gripTransform = null; 60 | if (map.gripTransform) { 61 | let orientation = map.gripTransform.orientation || [0, 0, 0, 1]; 62 | gripTransform = mat4.create(); 63 | mat4.fromRotationTranslation( 64 | gripTransform, 65 | quat.normalize(orientation, orientation), 66 | map.gripTransform.position || [0, 0, 0] 67 | ); 68 | } 69 | 70 | let targetRayTransform = null; 71 | if (map.targetRayTransform) { 72 | let orientation = map.targetRayTransform.orientation || [0, 0, 0, 1]; 73 | targetRayTransform = mat4.create(); 74 | mat4.fromRotationTranslation( 75 | targetRayTransform, 76 | quat.normalize(orientation, orientation), 77 | map.targetRayTransform.position || [0, 0, 0] 78 | ); 79 | } 80 | 81 | let profiles = map.profiles; 82 | if (map.displayProfiles) { 83 | if (display.displayName in map.displayProfiles) { 84 | profiles = map.displayProfiles[display.displayName]; 85 | } 86 | } 87 | 88 | this[PRIVATE] = { 89 | gamepad, 90 | map, 91 | profiles: profiles || [gamepad.id], 92 | mapping: map.mapping || gamepad.mapping, 93 | axes, 94 | buttons, 95 | gripTransform, 96 | targetRayTransform, 97 | }; 98 | 99 | this._update(); 100 | } 101 | 102 | _update() { 103 | let gamepad = this[PRIVATE].gamepad; 104 | let map = this[PRIVATE].map; 105 | 106 | let axes = this[PRIVATE].axes; 107 | for (let i = 0; i < axes.length; ++i) { 108 | if (map.axes && i in map.axes) { 109 | if (map.axes[i] === null) { 110 | axes[i] = 0; 111 | } else { 112 | axes[i] = gamepad.axes[map.axes[i]]; 113 | } 114 | } else { 115 | axes[i] = gamepad.axes[i]; 116 | } 117 | } 118 | 119 | if (map.axes && map.axes.invert) { 120 | for (let axis of map.axes.invert) { 121 | if (axis < axes.length) { 122 | axes[axis] *= -1; 123 | } 124 | } 125 | } 126 | 127 | let buttons = this[PRIVATE].buttons; 128 | for (let i = 0; i < buttons.length; ++i) { 129 | if (map.buttons && i in map.buttons) { 130 | if (map.buttons[i] === null) { 131 | buttons[i] = PLACEHOLDER_BUTTON; 132 | } else { 133 | buttons[i] = gamepad.buttons[map.buttons[i]]; 134 | } 135 | } else { 136 | buttons[i] = gamepad.buttons[i]; 137 | } 138 | } 139 | } 140 | 141 | get id() { 142 | return ''; 143 | } 144 | 145 | get _profiles() { 146 | return this[PRIVATE].profiles; 147 | } 148 | 149 | get index() { 150 | return -1; 151 | } 152 | 153 | get connected() { 154 | return this[PRIVATE].gamepad.connected; 155 | } 156 | 157 | get timestamp() { 158 | return this[PRIVATE].gamepad.timestamp; 159 | } 160 | 161 | get mapping() { 162 | return this[PRIVATE].mapping; 163 | } 164 | 165 | get axes() { 166 | return this[PRIVATE].axes; 167 | } 168 | 169 | get buttons() { 170 | return this[PRIVATE].buttons; 171 | } 172 | 173 | // Non-standard extension 174 | get hapticActuators() { 175 | return this[PRIVATE].gamepad.hapticActuators; 176 | } 177 | } 178 | 179 | export default class GamepadXRInputSource { 180 | constructor(polyfill, display, primaryButtonIndex = 0, primarySqueezeButtonIndex = -1) { 181 | this.polyfill = polyfill; 182 | this.display = display; 183 | this.nativeGamepad = null; 184 | this.gamepad = null; 185 | this.inputSource = new XRInputSource(this); 186 | this.lastPosition = vec3.create(); 187 | this.emulatedPosition = false; 188 | this.basePoseMatrix = mat4.create(); 189 | this.outputMatrix = mat4.create(); 190 | this.primaryButtonIndex = primaryButtonIndex; 191 | this.primaryActionPressed = false; 192 | this.primarySqueezeButtonIndex = primarySqueezeButtonIndex; 193 | this.primarySqueezeActionPressed = false; 194 | this.handedness = ''; 195 | this.targetRayMode = 'gaze'; 196 | this.armModel = null; 197 | } 198 | 199 | get profiles() { 200 | return this.gamepad ? this.gamepad._profiles : []; 201 | } 202 | 203 | updateFromGamepad(gamepad) { 204 | if (this.nativeGamepad !== gamepad) { 205 | this.nativeGamepad = gamepad; 206 | if (gamepad) { 207 | this.gamepad = new XRRemappedGamepad(gamepad, this.display, GamepadMappings[gamepad.id]); 208 | } else { 209 | this.gamepad = null; 210 | } 211 | } 212 | this.handedness = gamepad.hand === '' ? 'none' : gamepad.hand 213 | 214 | if (this.gamepad) { 215 | this.gamepad._update(); 216 | } 217 | 218 | if (gamepad.pose) { 219 | this.targetRayMode = 'tracked-pointer'; 220 | this.emulatedPosition = !gamepad.pose.hasPosition; 221 | } else if (gamepad.hand === '') { 222 | this.targetRayMode = 'gaze'; 223 | this.emulatedPosition = false; 224 | } 225 | } 226 | 227 | updateBasePoseMatrix() { 228 | if (this.nativeGamepad && this.nativeGamepad.pose) { 229 | let pose = this.nativeGamepad.pose; 230 | let position = pose.position; 231 | let orientation = pose.orientation; 232 | // On initialization, we might not have any values 233 | if (!position && !orientation) { 234 | return; 235 | } 236 | if (!position) { 237 | if (!pose.hasPosition) { 238 | if (!this.armModel) { 239 | this.armModel = new OrientationArmModel(); 240 | } 241 | 242 | this.armModel.setHandedness(this.nativeGamepad.hand); 243 | this.armModel.update(orientation, this.polyfill.getBasePoseMatrix()); 244 | position = this.armModel.getPosition(); 245 | } else { 246 | position = this.lastPosition; 247 | } 248 | } else { 249 | // This is if we temporarily lose tracking, so the controller doesn't 250 | // snap back to the origin. 251 | this.lastPosition[0] = position[0]; 252 | this.lastPosition[1] = position[1]; 253 | this.lastPosition[2] = position[2]; 254 | } 255 | mat4.fromRotationTranslation(this.basePoseMatrix, orientation, position); 256 | } else { 257 | mat4.copy(this.basePoseMatrix, this.polyfill.getBasePoseMatrix()); 258 | } 259 | return this.basePoseMatrix; 260 | } 261 | 262 | /** 263 | * @param {XRReferenceSpace} coordinateSystem 264 | * @param {string} poseType 265 | * @return {XRPose?} 266 | */ 267 | getXRPose(coordinateSystem, poseType) { 268 | this.updateBasePoseMatrix(); 269 | 270 | switch(poseType) { 271 | case "target-ray": 272 | coordinateSystem._transformBasePoseMatrix(this.outputMatrix, this.basePoseMatrix); 273 | if (this.gamepad && this.gamepad[PRIVATE].targetRayTransform) { 274 | mat4.multiply(this.outputMatrix, this.outputMatrix, this.gamepad[PRIVATE].targetRayTransform); 275 | } 276 | break; 277 | case "grip": 278 | if (!this.nativeGamepad || !this.nativeGamepad.pose) { 279 | return null; 280 | } 281 | // TODO: Does the grip matrix need to be tweaked? 282 | coordinateSystem._transformBasePoseMatrix(this.outputMatrix, this.basePoseMatrix); 283 | if (this.gamepad && this.gamepad[PRIVATE].gripTransform) { 284 | mat4.multiply(this.outputMatrix, this.outputMatrix, this.gamepad[PRIVATE].gripTransform); 285 | } 286 | break; 287 | default: 288 | return null; 289 | } 290 | 291 | coordinateSystem._adjustForOriginOffset(this.outputMatrix); 292 | 293 | return new XRPose(new XRRigidTransform(this.outputMatrix), this.emulatedPosition); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/devices/InlineDevice.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 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 mat4 from 'gl-matrix/src/gl-matrix/mat4'; 17 | import XRDevice from './XRDevice'; 18 | 19 | const TEST_ENV = process.env.NODE_ENV === 'test'; 20 | 21 | /** 22 | * A Session helper class to mirror an XRSession and correlate 23 | * between an XRSession, and tracking sessions in a XRDevice. 24 | * Mostly referenced via `session.id` due to needing to verify 25 | * session creation is possible on the XRDevice before 26 | * the XRSession can be created. 27 | */ 28 | let SESSION_ID = 0; 29 | class Session { 30 | constructor(mode, enabledFeatures) { 31 | this.mode = mode; 32 | this.enabledFeatures = enabledFeatures; 33 | this.ended = null; 34 | this.baseLayer = null; 35 | this.id = ++SESSION_ID; 36 | } 37 | }; 38 | 39 | /** 40 | * An XRDevice which only supports sensorless inline sessions, used as a 41 | * fallback when no other type of XRDevice is available as a way to satisfy the 42 | * spec requirement that inline sessions are always supported. 43 | */ 44 | export default class InlineDevice extends XRDevice { 45 | /** 46 | * Constructs an inline-only XRDevice 47 | */ 48 | constructor(global) { 49 | super(global); 50 | 51 | this.sessions = new Map(); 52 | this.projectionMatrix = mat4.create(); 53 | this.identityMatrix = mat4.create(); 54 | } 55 | 56 | /** 57 | * Called when a XRSession has a `baseLayer` property set. 58 | * 59 | * @param {number} sessionId 60 | * @param {XRWebGLLayer} layer 61 | */ 62 | onBaseLayerSet(sessionId, layer) { 63 | const session = this.sessions.get(sessionId); 64 | session.baseLayer = layer; 65 | } 66 | 67 | /** 68 | * Returns true if the requested mode is inline 69 | * 70 | * @param {XRSessionMode} mode 71 | * @return {boolean} 72 | */ 73 | isSessionSupported(mode) { 74 | return mode == 'inline'; 75 | } 76 | 77 | /** 78 | * @param {string} featureDescriptor 79 | * @return {boolean} 80 | */ 81 | isFeatureSupported(featureDescriptor) { 82 | switch(featureDescriptor) { 83 | // Only viewer reference spaces are supported 84 | case 'viewer': return true; 85 | default: return false; 86 | } 87 | } 88 | 89 | /** 90 | * Returns a promise of a session ID if creating a session is successful. 91 | * 92 | * @param {XRSessionMode} mode 93 | * @param {Set} enabledFeatures 94 | * @return {Promise} 95 | */ 96 | async requestSession(mode, enabledFeatures) { 97 | if (!this.isSessionSupported(mode)) { 98 | return Promise.reject(); 99 | } 100 | 101 | const session = new Session(mode, enabledFeatures); 102 | 103 | this.sessions.set(session.id, session); 104 | 105 | return Promise.resolve(session.id); 106 | } 107 | 108 | /** 109 | * @return {Function} 110 | */ 111 | requestAnimationFrame(callback) { 112 | return window.requestAnimationFrame(callback); 113 | } 114 | 115 | /** 116 | * @param {number} handle 117 | */ 118 | cancelAnimationFrame(handle) { 119 | window.cancelAnimationFrame(handle); 120 | } 121 | 122 | onFrameStart(sessionId, renderState) { 123 | // @TODO Our test environment doesn't have the canvas package for now, 124 | // but this could be something we add to the tests. 125 | if (TEST_ENV) { 126 | return; 127 | } 128 | 129 | const session = this.sessions.get(sessionId); 130 | 131 | // If the session is inline make sure the projection matrix matches the 132 | // aspect ratio of the underlying WebGL canvas. 133 | if (session.baseLayer) { 134 | const canvas = session.baseLayer.context.canvas; 135 | // Update the projection matrix. 136 | mat4.perspective(this.projectionMatrix, renderState.inlineVerticalFieldOfView, 137 | canvas.width/canvas.height, renderState.depthNear, renderState.depthFar); 138 | } 139 | } 140 | 141 | onFrameEnd(sessionId) { 142 | // Nothing to do here because inline always renders to the canvas backbuffer 143 | // directly. 144 | } 145 | 146 | /** 147 | * @TODO Spec 148 | */ 149 | async endSession(sessionId) { 150 | const session = this.sessions.get(sessionId); 151 | session.ended = true; 152 | } 153 | 154 | /** 155 | * @param {number} sessionId 156 | * @param {XRReferenceSpaceType} type 157 | * @return {boolean} 158 | */ 159 | doesSessionSupportReferenceSpace(sessionId, type) { 160 | const session = this.sessions.get(sessionId); 161 | if (session.ended) { 162 | return false; 163 | } 164 | 165 | return session.enabledFeatures.has(type); 166 | } 167 | 168 | /** 169 | * Inline sessions don't have stage bounds 170 | * 171 | * @return {Object?} 172 | */ 173 | requestStageBounds() { 174 | return null; 175 | } 176 | 177 | /** 178 | * Inline sessions don't have multiple frames of reference 179 | * 180 | * @param {XRFrameOfReferenceType} type 181 | * @param {XRFrameOfReferenceOptions} options 182 | * @return {Promise} 183 | */ 184 | async requestFrameOfReferenceTransform(type, options) { 185 | return null; 186 | } 187 | 188 | /** 189 | * @param {XREye} eye 190 | * @return {Float32Array} 191 | */ 192 | getProjectionMatrix(eye) { 193 | return this.projectionMatrix; 194 | } 195 | 196 | /** 197 | * Takes a XREye and a target to apply properties of 198 | * `x`, `y`, `width` and `height` on. Returns a boolean 199 | * indicating if it successfully was able to populate 200 | * target's values. 201 | * 202 | * @param {number} sessionId 203 | * @param {XREye} eye 204 | * @param {XRWebGLLayer} layer 205 | * @param {Object?} target 206 | * @return {boolean} 207 | */ 208 | getViewport(sessionId, eye, layer, target) { 209 | // @TODO can we have another layer passed in that 210 | // wasn't the same one as the `baseLayer`? 211 | 212 | const session = this.sessions.get(sessionId); 213 | const { width, height } = layer.context.canvas; 214 | 215 | // Inline sessions return the whole canvas as the viewport 216 | target.x = target.y = 0; 217 | target.width = width; 218 | target.height = height; 219 | return true; 220 | } 221 | 222 | /** 223 | * Get model matrix unaffected by frame of reference. 224 | * 225 | * @return {Float32Array} 226 | */ 227 | getBasePoseMatrix() { 228 | return this.identityMatrix; 229 | } 230 | 231 | /** 232 | * Get view matrix unaffected by frame of reference. 233 | * 234 | * @param {XREye} eye 235 | * @return {Float32Array} 236 | */ 237 | getBaseViewMatrix(eye) { 238 | return this.identityMatrix; 239 | } 240 | 241 | /** 242 | * No persistent input sources for the inline session 243 | */ 244 | getInputSources() { 245 | return []; 246 | } 247 | 248 | getInputPose(inputSource, coordinateSystem, poseType) { 249 | return null; 250 | } 251 | 252 | /** 253 | * Triggered on window resize. 254 | */ 255 | onWindowResize() { 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/devices/XRDevice.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 EventTarget from '../lib/EventTarget'; 17 | import XRReferenceSpace from '../api/XRReferenceSpace'; 18 | 19 | export default class XRDevice extends EventTarget { 20 | /** 21 | * Takes a VRDisplay object from the WebVR 1.1 spec. 22 | * 23 | * @param {Object} global 24 | */ 25 | constructor(global) { 26 | super(); 27 | 28 | this.global = global; 29 | this.onWindowResize = this.onWindowResize.bind(this); 30 | 31 | this.global.window.addEventListener('resize', this.onWindowResize); 32 | 33 | // Value is used for `XRSession.prototype.environmentBlendMode` 34 | // and should be one of XREnvironmentBlendMode types: 'opaque', 'additive', 35 | // or 'alpha-blend'. 36 | this.environmentBlendMode = 'opaque'; 37 | } 38 | 39 | /** 40 | * Called when a XRSession has a `baseLayer` property set. 41 | * 42 | * @param {number} sessionId 43 | * @param {XRWebGLLayer} layer 44 | */ 45 | onBaseLayerSet(sessionId, layer) { throw new Error('Not implemented'); } 46 | 47 | /** 48 | * @param {XRSessionMode} mode 49 | * @return {boolean} 50 | */ 51 | isSessionSupported(mode) { throw new Error('Not implemented'); } 52 | 53 | /** 54 | * @param {string} featureDescriptor 55 | * @return {boolean} 56 | */ 57 | isFeatureSupported(featureDescriptor) { throw new Error('Not implemented'); } 58 | 59 | /** 60 | * Returns a promise if creating a session is successful. 61 | * Usually used to set up presentation in the device. 62 | * 63 | * @param {XRSessionMode} mode 64 | * @param {Set} enabledFeatures 65 | * @return {Promise} 66 | */ 67 | async requestSession(mode, enabledFeatures) { throw new Error('Not implemented'); } 68 | 69 | /** 70 | * @return {Function} 71 | */ 72 | requestAnimationFrame(callback) { throw new Error('Not implemented'); } 73 | 74 | /** 75 | * @param {number} sessionId 76 | */ 77 | onFrameStart(sessionId) { throw new Error('Not implemented'); } 78 | 79 | /** 80 | * @param {number} sessionId 81 | */ 82 | onFrameEnd(sessionId) { throw new Error('Not implemented'); } 83 | 84 | /** 85 | * @param {number} sessionId 86 | * @param {XRReferenceSpaceType} type 87 | * @return {boolean} 88 | */ 89 | doesSessionSupportReferenceSpace(sessionId, type) { throw new Error('Not implemented'); } 90 | 91 | /** 92 | * @return {Object?} 93 | */ 94 | requestStageBounds() { throw new Error('Not implemented'); } 95 | 96 | /** 97 | * Returns a promise resolving to a transform if XRDevice 98 | * can support frame of reference and provides its own values. 99 | * Can resolve to `undefined` if the polyfilled API can provide 100 | * a default. Rejects if this XRDevice cannot 101 | * support the frame of reference. 102 | * 103 | * @param {XRFrameOfReferenceType} type 104 | * @param {XRFrameOfReferenceOptions} options 105 | * @return {Promise} 106 | */ 107 | async requestFrameOfReferenceTransform(type, options) { 108 | return undefined; 109 | } 110 | 111 | /** 112 | * @param {number} handle 113 | */ 114 | cancelAnimationFrame(handle) { throw new Error('Not implemented'); } 115 | 116 | /** 117 | * @param {number} sessionId 118 | */ 119 | endSession(sessionId) { throw new Error('Not implemented'); } 120 | 121 | /** 122 | * Allows the XRDevice to override the XRSession's view spaces. 123 | * 124 | * @param {XRSessionMode} mode 125 | * @return {Array | undefined} 126 | */ 127 | getViewSpaces(mode) { return undefined; } 128 | 129 | /** 130 | * Takes a XREye and a target to apply properties of 131 | * `x`, `y`, `width` and `height` on. Returns a boolean 132 | * indicating if it successfully was able to populate 133 | * target's values. 134 | * 135 | * @param {number} sessionId 136 | * @param {XREye} eye 137 | * @param {XRWebGLLayer} layer 138 | * @param {Object?} target 139 | * @param {number} viewIndex 140 | * @return {boolean} 141 | */ 142 | getViewport(sessionId, eye, layer, target, viewIndex) { throw new Error('Not implemented'); } 143 | 144 | /** 145 | * @param {XREye} eye 146 | * @param {number} viewIndex 147 | * @return {Float32Array} 148 | */ 149 | getProjectionMatrix(eye, viewIndex) { throw new Error('Not implemented'); } 150 | 151 | /** 152 | * Get model matrix unaffected by frame of reference. 153 | * 154 | * @return {Float32Array} 155 | */ 156 | getBasePoseMatrix() { throw new Error('Not implemented'); } 157 | 158 | /** 159 | * Get view matrix unaffected by frame of reference. 160 | * 161 | * @param {XREye} eye 162 | * @return {Float32Array} 163 | */ 164 | getBaseViewMatrix(eye) { throw new Error('Not implemented'); } 165 | 166 | /** 167 | * Get a list of input sources. 168 | * 169 | * @return {Array} 170 | */ 171 | getInputSources() { throw new Error('Not implemented'); } 172 | 173 | /** 174 | * Get the current pose of an input source. 175 | * 176 | * @param {XRInputSource} inputSource 177 | * @param {XRCoordinateSystem} coordinateSystem 178 | * @param {String} poseType 179 | * @return {XRPose} 180 | */ 181 | getInputPose(inputSource, coordinateSystem, poseType) { throw new Error('Not implemented'); } 182 | 183 | /** 184 | * Called on window resize. 185 | */ 186 | onWindowResize() { 187 | // Bound by XRDevice and called on resize, but 188 | // this will call child class onWindowResize (or, if not defined, 189 | // call an infinite loop I guess) 190 | this.onWindowResize(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 | * index.js is used as an entry point for building a rollup bundle 18 | * form of the polyfill in UMD style. 19 | */ 20 | 21 | // Do not use any polyfills for the time being; let the consumers 22 | // decide which features to support 23 | // import 'babel-polyfill'; 24 | import WebXRPolyfill from './WebXRPolyfill'; 25 | 26 | export default WebXRPolyfill; 27 | -------------------------------------------------------------------------------- /src/lib/DOMPointReadOnly.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 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 GLOBAL from './global'; 17 | 18 | let domPointROExport = ('DOMPointReadOnly' in GLOBAL) ? DOMPointReadOnly : null; 19 | 20 | if (!domPointROExport) { 21 | const PRIVATE = Symbol('@@webxr-polyfill/DOMPointReadOnly'); 22 | 23 | domPointROExport = class DOMPointReadOnly { 24 | constructor(x, y, z, w) { 25 | if (arguments.length === 1) { 26 | this[PRIVATE] = { 27 | x: x.x, 28 | y: x.y, 29 | z: x.z, 30 | w: x.w 31 | }; 32 | } else if (arguments.length === 4) { 33 | this[PRIVATE] = { 34 | x: x, 35 | y: y, 36 | z: z, 37 | w: w 38 | }; 39 | } else { 40 | throw new TypeError('Must supply either 1 or 4 arguments') 41 | } 42 | } 43 | 44 | /** 45 | * @return {number} 46 | */ 47 | get x() { return this[PRIVATE].x } 48 | 49 | /** 50 | * @return {number} 51 | */ 52 | get y() { return this[PRIVATE].y } 53 | 54 | /** 55 | * @return {number} 56 | */ 57 | get z() { return this[PRIVATE].z } 58 | 59 | /** 60 | * @return {number} 61 | */ 62 | get w() { return this[PRIVATE].w } 63 | } 64 | } 65 | 66 | export default domPointROExport; 67 | -------------------------------------------------------------------------------- /src/lib/EventTarget.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 PRIVATE = Symbol('@@webxr-polyfill/EventTarget'); 17 | 18 | export default class EventTarget { 19 | constructor() { 20 | this[PRIVATE] = { 21 | listeners: new Map(), 22 | }; 23 | } 24 | 25 | /** 26 | * @param {string} type 27 | * @param {Function} listener 28 | */ 29 | addEventListener(type, listener) { 30 | if (typeof type !== 'string') { throw new Error('`type` must be a string'); } 31 | if (typeof listener !== 'function') { throw new Error('`listener` must be a function'); } 32 | 33 | const typedListeners = this[PRIVATE].listeners.get(type) || []; 34 | typedListeners.push(listener); 35 | this[PRIVATE].listeners.set(type, typedListeners); 36 | } 37 | 38 | /** 39 | * @param {string} type 40 | * @param {Function} listener 41 | */ 42 | removeEventListener(type, listener) { 43 | if (typeof type !== 'string') { throw new Error('`type` must be a string'); } 44 | if (typeof listener !== 'function') { throw new Error('`listener` must be a function'); } 45 | 46 | const typedListeners = this[PRIVATE].listeners.get(type) || []; 47 | 48 | for (let i = typedListeners.length; i >= 0; i--) { 49 | if (typedListeners[i] === listener) { 50 | typedListeners.pop(); 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * @param {string} type 57 | * @param {object} event 58 | */ 59 | dispatchEvent(type, event) { 60 | const typedListeners = this[PRIVATE].listeners.get(type) || []; 61 | 62 | // Copy over all the listeners because a callback could remove 63 | // an event listener, preventing all listeners from firing when 64 | // the event was first dispatched. 65 | const queue = []; 66 | for (let i = 0; i < typedListeners.length; i++) { 67 | queue[i] = typedListeners[i]; 68 | } 69 | 70 | for (let listener of queue) { 71 | listener(event); 72 | } 73 | 74 | // Also fire if this EventTarget has an `on${EVENT_TYPE}` property 75 | // that's a function 76 | if (typeof this[`on${type}`] === 'function') { 77 | this[`on${type}`](event); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/lib/OrientationArmModel.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 now from './now'; 17 | import * as mat4 from 'gl-matrix/src/gl-matrix/mat4'; 18 | import * as vec3 from 'gl-matrix/src/gl-matrix/vec3'; 19 | import * as quat from 'gl-matrix/src/gl-matrix/quat'; 20 | 21 | const HEAD_ELBOW_OFFSET_RIGHTHANDED = vec3.fromValues(0.155, -0.465, -0.15); 22 | const HEAD_ELBOW_OFFSET_LEFTHANDED = vec3.fromValues(-0.155, -0.465, -0.15); 23 | const ELBOW_WRIST_OFFSET = vec3.fromValues(0, 0, -0.25); 24 | const WRIST_CONTROLLER_OFFSET = vec3.fromValues(0, 0, 0.05); 25 | const ARM_EXTENSION_OFFSET = vec3.fromValues(-0.08, 0.14, 0.08); 26 | 27 | const ELBOW_BEND_RATIO = 0.4; // 40% elbow, 60% wrist. 28 | const EXTENSION_RATIO_WEIGHT = 0.4; 29 | 30 | const MIN_ANGULAR_SPEED = 0.61; // 35 degrees per second (in radians). 31 | const MIN_ANGLE_DELTA = 0.175; // 10 degrees (in radians). 32 | 33 | const MIN_EXTENSION_COS = 0.12; // cos of 83 degrees. 34 | const MAX_EXTENSION_COS = 0.87; // cos of 30 degrees. 35 | 36 | const RAD_TO_DEG = 180 / Math.PI; 37 | 38 | function eulerFromQuaternion(out, q, order) { 39 | function clamp(value, min, max) { 40 | return (value < min ? min : (value > max ? max : value)); 41 | } 42 | // Borrowed from Three.JS :) 43 | // q is assumed to be normalized 44 | // http://www.mathworks.com/matlabcentral/fileexchange/20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/content/SpinCalc.m 45 | var sqx = q[0] * q[0]; 46 | var sqy = q[1] * q[1]; 47 | var sqz = q[2] * q[2]; 48 | var sqw = q[3] * q[3]; 49 | 50 | if ( order === 'XYZ' ) { 51 | out[0] = Math.atan2( 2 * ( q[0] * q[3] - q[1] * q[2] ), ( sqw - sqx - sqy + sqz ) ); 52 | out[1] = Math.asin( clamp( 2 * ( q[0] * q[2] + q[1] * q[3] ), -1, 1 ) ); 53 | out[2] = Math.atan2( 2 * ( q[2] * q[3] - q[0] * q[1] ), ( sqw + sqx - sqy - sqz ) ); 54 | } else if ( order === 'YXZ' ) { 55 | out[0] = Math.asin( clamp( 2 * ( q[0] * q[3] - q[1] * q[2] ), -1, 1 ) ); 56 | out[1] = Math.atan2( 2 * ( q[0] * q[2] + q[1] * q[3] ), ( sqw - sqx - sqy + sqz ) ); 57 | out[2] = Math.atan2( 2 * ( q[0] * q[1] + q[2] * q[3] ), ( sqw - sqx + sqy - sqz ) ); 58 | } else if ( order === 'ZXY' ) { 59 | out[0] = Math.asin( clamp( 2 * ( q[0] * q[3] + q[1] * q[2] ), -1, 1 ) ); 60 | out[1] = Math.atan2( 2 * ( q[1] * q[3] - q[2] * q[0] ), ( sqw - sqx - sqy + sqz ) ); 61 | out[2] = Math.atan2( 2 * ( q[2] * q[3] - q[0] * q[1] ), ( sqw - sqx + sqy - sqz ) ); 62 | } else if ( order === 'ZYX' ) { 63 | out[0] = Math.atan2( 2 * ( q[0] * q[3] + q[2] * q[1] ), ( sqw - sqx - sqy + sqz ) ); 64 | out[1] = Math.asin( clamp( 2 * ( q[1] * q[3] - q[0] * q[2] ), -1, 1 ) ); 65 | out[2] = Math.atan2( 2 * ( q[0] * q[1] + q[2] * q[3] ), ( sqw + sqx - sqy - sqz ) ); 66 | } else if ( order === 'YZX' ) { 67 | out[0] = Math.atan2( 2 * ( q[0] * q[3] - q[2] * q[1] ), ( sqw - sqx + sqy - sqz ) ); 68 | out[1] = Math.atan2( 2 * ( q[1] * q[3] - q[0] * q[2] ), ( sqw + sqx - sqy - sqz ) ); 69 | out[2] = Math.asin( clamp( 2 * ( q[0] * q[1] + q[2] * q[3] ), -1, 1 ) ); 70 | } else if ( order === 'XZY' ) { 71 | out[0] = Math.atan2( 2 * ( q[0] * q[3] + q[1] * q[2] ), ( sqw - sqx + sqy - sqz ) ); 72 | out[1] = Math.atan2( 2 * ( q[0] * q[2] + q[1] * q[3] ), ( sqw + sqx - sqy - sqz ) ); 73 | out[2] = Math.asin( clamp( 2 * ( q[2] * q[3] - q[0] * q[1] ), -1, 1 ) ); 74 | } else { 75 | console.log('No order given for quaternion to euler conversion.'); 76 | return; 77 | } 78 | } 79 | 80 | /** 81 | * Represents the arm model for the Daydream controller. Feed it a camera and 82 | * the controller. Update it on a RAF. 83 | * 84 | * Get the model's pose using getPose(). 85 | */ 86 | export default class OrientationArmModel { 87 | constructor() { 88 | this.hand = 'right'; 89 | this.headElbowOffset = HEAD_ELBOW_OFFSET_RIGHTHANDED; 90 | 91 | // Current and previous controller orientations. 92 | this.controllerQ = quat.create(); 93 | this.lastControllerQ = quat.create(); 94 | 95 | // Current and previous head orientations. 96 | this.headQ = quat.create(); 97 | 98 | // Current head position. 99 | this.headPos = vec3.create(); 100 | 101 | // Positions of other joints (mostly for debugging). 102 | this.elbowPos = vec3.create(); 103 | this.wristPos = vec3.create(); 104 | 105 | // Current and previous times the model was updated. 106 | this.time = null; 107 | this.lastTime = null; 108 | 109 | // Root rotation. 110 | this.rootQ = quat.create(); 111 | 112 | // Current position that this arm model calculates. 113 | this.position = vec3.create(); 114 | } 115 | 116 | setHandedness(hand) { 117 | if (this.hand != hand) { 118 | this.hand = hand; 119 | if (this.hand == 'left') { 120 | this.headElbowOffset = HEAD_ELBOW_OFFSET_LEFTHANDED; 121 | } else { 122 | this.headElbowOffset = HEAD_ELBOW_OFFSET_RIGHTHANDED; 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * Called on a RAF. 129 | */ 130 | update(controllerOrientation, headPoseMatrix) { 131 | this.time = now(); 132 | 133 | // Update the internal copies of the controller and head pose. 134 | if (controllerOrientation) { 135 | quat.copy(this.lastControllerQ, this.controllerQ); 136 | quat.copy(this.controllerQ, controllerOrientation); 137 | } 138 | 139 | if (headPoseMatrix) { 140 | mat4.getTranslation(this.headPos, headPoseMatrix); 141 | mat4.getRotation(this.headQ, headPoseMatrix); 142 | } 143 | 144 | // If the controller's angular velocity is above a certain amount, we can 145 | // assume torso rotation and move the elbow joint relative to the 146 | // camera orientation. 147 | let headYawQ = this.getHeadYawOrientation_(); 148 | let angleDelta = this.quatAngle_(this.lastControllerQ, this.controllerQ); 149 | let timeDelta = (this.time - this.lastTime) / 1000; 150 | let controllerAngularSpeed = angleDelta / timeDelta; 151 | if (controllerAngularSpeed > MIN_ANGULAR_SPEED) { 152 | // Attenuate the Root rotation slightly. 153 | quat.slerp(this.rootQ, this.rootQ, headYawQ, 154 | Math.min(angleDelta / MIN_ANGLE_DELTA, 1.0)); 155 | } else { 156 | quat.copy(this.rootQ, headYawQ); 157 | } 158 | 159 | // We want to move the elbow up and to the center as the user points the 160 | // controller upwards, so that they can easily see the controller and its 161 | // tool tips. 162 | let controllerForward = vec3.fromValues(0, 0, -1.0); 163 | vec3.transformQuat(controllerForward, controllerForward, this.controllerQ); 164 | let controllerDotY = vec3.dot(controllerForward, [0, 1, 0]); 165 | let extensionRatio = this.clamp_( 166 | (controllerDotY - MIN_EXTENSION_COS) / MAX_EXTENSION_COS, 0.0, 1.0); 167 | 168 | // Controller orientation in camera space. 169 | let controllerCameraQ = quat.clone(this.rootQ); 170 | quat.invert(controllerCameraQ, controllerCameraQ); 171 | quat.multiply(controllerCameraQ, controllerCameraQ, this.controllerQ); 172 | 173 | 174 | // Calculate elbow position. 175 | let elbowPos = this.elbowPos; 176 | vec3.copy(elbowPos, this.headPos); 177 | vec3.add(elbowPos, elbowPos, this.headElbowOffset); 178 | let elbowOffset = vec3.clone(ARM_EXTENSION_OFFSET); 179 | vec3.scale(elbowOffset, elbowOffset, extensionRatio); 180 | vec3.add(elbowPos, elbowPos, elbowOffset); 181 | 182 | // Calculate joint angles. Generally 40% of rotation applied to elbow, 60% 183 | // to wrist, but if controller is raised higher, more rotation comes from 184 | // the wrist. 185 | let totalAngle = this.quatAngle_(controllerCameraQ, quat.create()); 186 | let totalAngleDeg = totalAngle * RAD_TO_DEG; 187 | let lerpSuppression = 1 - Math.pow(totalAngleDeg / 180, 4);sssss 188 | 189 | let elbowRatio = ELBOW_BEND_RATIO; 190 | let wristRatio = 1 - ELBOW_BEND_RATIO; 191 | let lerpValue = lerpSuppression * 192 | (elbowRatio + wristRatio * extensionRatio * EXTENSION_RATIO_WEIGHT); 193 | 194 | let wristQ = quat.create(); 195 | quat.slerp(wristQ, wristQ, controllerCameraQ, lerpValue); 196 | let invWristQ = quat.invert(quat.create(), wristQ); 197 | let elbowQ = quat.clone(controllerCameraQ); 198 | quat.multiply(elbowQ, elbowQ, invWristQ); 199 | 200 | // Calculate our final controller position based on all our joint rotations 201 | // and lengths. 202 | /* 203 | position_ = 204 | root_rot_ * ( 205 | controller_root_offset_ + 206 | 2: (arm_extension_ * amt_extension) + 207 | 1: elbow_rot * (kControllerForearm + (wrist_rot * kControllerPosition)) 208 | ); 209 | */ 210 | let wristPos = this.wristPos; 211 | vec3.copy(wristPos, WRIST_CONTROLLER_OFFSET); 212 | vec3.transformQuat(wristPos, wristPos, wristQ); 213 | vec3.add(wristPos, wristPos, ELBOW_WRIST_OFFSET); 214 | vec3.transformQuat(wristPos, wristPos, elbowQ); 215 | vec3.add(wristPos, wristPos, elbowPos); 216 | 217 | let offset = vec3.clone(ARM_EXTENSION_OFFSET); 218 | vec3.scale(offset, offset, extensionRatio); 219 | 220 | // Set the resulting pose orientation and position. 221 | vec3.add(this.position, this.wristPos, offset); 222 | vec3.transformQuat(this.position, this.position, this.rootQ); 223 | 224 | this.lastTime = this.time; 225 | } 226 | 227 | /** 228 | * Returns the position calculated by the model. 229 | */ 230 | getPosition() { 231 | return this.position; 232 | } 233 | 234 | getHeadYawOrientation_() { 235 | let headEuler = vec3.create(); 236 | eulerFromQuaternion(headEuler, this.headQ, 'YXZ'); 237 | let destinationQ = quat.fromEuler(quat.create(), 0, headEuler[1] * RAD_TO_DEG, 0); 238 | return destinationQ; 239 | } 240 | 241 | clamp_(value, min, max) { 242 | return Math.min(Math.max(value, min), max); 243 | } 244 | 245 | quatAngle_(q1, q2) { 246 | let vec1 = [0, 0, -1]; 247 | let vec2 = [0, 0, -1]; 248 | vec3.transformQuat(vec1, vec1, q1); 249 | vec3.transformQuat(vec2, vec2, q2); 250 | return vec3.angle(vec1, vec2); 251 | } 252 | } -------------------------------------------------------------------------------- /src/lib/global.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 | * A library for including application global. Similar to 18 | * logic provided by `rollup-plugin-node-globals` without the 19 | * rest of the functionality needed. 20 | */ 21 | 22 | const _global = typeof global !== 'undefined' ? global : 23 | typeof self !== 'undefined' ? self : 24 | typeof window !== 'undefined' ? window : {}; 25 | 26 | export default _global; 27 | -------------------------------------------------------------------------------- /src/lib/now.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 | * A wrapper around `performance.now()` so that we can run unit tests 18 | * in Node. 19 | */ 20 | 21 | import GLOBAL from './global'; 22 | 23 | let now; 24 | if ('performance' in GLOBAL === false) { 25 | let startTime = Date.now(); 26 | now = () => Date.now() - startTime; 27 | } else { 28 | now = () => performance.now(); 29 | } 30 | 31 | export default now; 32 | -------------------------------------------------------------------------------- /src/polyfill-globals.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 { 17 | POLYFILLED_XR_COMPATIBLE, 18 | XR_COMPATIBLE, 19 | } from './constants'; 20 | 21 | const contextTypes = ['webgl', 'experimental-webgl']; 22 | 23 | /** 24 | * Takes the WebGLRenderingContext constructor 25 | * and creates a `makeXRCompatible` function if it does not exist. 26 | * Returns a boolean indicating whether or not the function 27 | * was polyfilled. 28 | * 29 | * @param {WebGLRenderingContext} 30 | * @return {boolean} 31 | */ 32 | export const polyfillMakeXRCompatible = Context => { 33 | if (typeof Context.prototype.makeXRCompatible === 'function') { 34 | return false; 35 | } 36 | 37 | // Create `makeXRCompatible` and if successful, store 38 | // the XRDevice as a private attribute for error checking 39 | Context.prototype.makeXRCompatible = function () { 40 | this[XR_COMPATIBLE] = true; 41 | // This is all fake, so just resolve immediately. 42 | return Promise.resolve(); 43 | }; 44 | 45 | return true; 46 | }; 47 | 48 | 49 | /** 50 | * Takes the HTMLCanvasElement or OffscreenCanvas constructor 51 | * and wraps its `getContext` function to patch the context with a 52 | * POLYFILLED_XR_COMPATIBLE bit so the API knows it's also working with a 53 | * polyfilled `xrCompatible` bit. Can do extra checking for validity. 54 | * 55 | * @param {HTMLCanvasElement} Canvas 56 | */ 57 | export const polyfillGetContext = (Canvas) => { 58 | const getContext = Canvas.prototype.getContext; 59 | Canvas.prototype.getContext = function (contextType, glAttribs) { 60 | const ctx = getContext.call(this, contextType, glAttribs); 61 | 62 | if (ctx) { 63 | // Set this bit so the API knows the WebGLRenderingContext is 64 | // also polyfilled a bit 65 | ctx[POLYFILLED_XR_COMPATIBLE] = true; 66 | 67 | // If we've polyfilled WebGLRenderingContext's xrCompatible 68 | // bit, store the boolean in the private token if created via 69 | // creation parameters 70 | if (glAttribs && ('xrCompatible' in glAttribs)) { 71 | ctx[XR_COMPATIBLE] = glAttribs.xrCompatible; 72 | } 73 | } 74 | 75 | return ctx; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 | * Whether or an ImageBitMapRenderingContext should be used to polyfill 18 | * an XRPresentationContext. 19 | * 20 | * @return {Boolean} 21 | */ 22 | export const isImageBitmapSupported = global => 23 | !!(global.ImageBitmapRenderingContext && 24 | global.createImageBitmap); 25 | 26 | /** 27 | * Determines whether or not this global's user agent is 28 | * considered a mobile device. Taken from webvr-polyfill. 29 | * 30 | * @param {Object} global 31 | */ 32 | export const isMobile = global => { 33 | var check = false; 34 | (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})(global.navigator.userAgent||global.navigator.vendor||global.opera); 35 | return check; 36 | }; 37 | 38 | export const applyCanvasStylesForMinimalRendering = canvas => { 39 | canvas.style.display = 'block'; 40 | canvas.style.position = 'absolute'; 41 | canvas.style.width = canvas.style.height = '1px'; 42 | canvas.style.top = canvas.style.left = '0px'; 43 | }; 44 | -------------------------------------------------------------------------------- /test/api/test-event-target.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import EventTarget from '../../src/lib/EventTarget'; 20 | 21 | class ChildTarget extends EventTarget {} 22 | 23 | describe('API - EventTarget', () => { 24 | it('binds events via addEventListener', () => { 25 | const c = new ChildTarget(); 26 | const events = []; 27 | c.addEventListener('click', ({ value }) => events.push(`${value}-1`)); 28 | c.addEventListener('click', ({ value }) => events.push(`${value}-2`)); 29 | c.addEventListener('scroll', ({ value }) => events.push(value)); 30 | 31 | c.dispatchEvent('click', { value: 'hello' }); 32 | assert.deepEqual(events, ['hello-1', 'hello-2']); 33 | 34 | c.dispatchEvent('scroll', { value: 'hello' }); 35 | assert.deepEqual(events, ['hello-1', 'hello-2', 'hello']); 36 | }); 37 | 38 | it('removes events via removeEventListener', () => { 39 | const c = new ChildTarget(); 40 | const events = []; 41 | const c1 = ({ value }) => events.push(`${value}-1`); 42 | const c2 = ({ value }) => events.push(`${value}-2`); 43 | c.addEventListener('click', c1); 44 | c.addEventListener('click', c2); 45 | 46 | c.dispatchEvent('click', { value: 'hello' }); 47 | assert.deepEqual(events, ['hello-1', 'hello-2']); 48 | 49 | c.removeEventListener('click', c2); 50 | c.dispatchEvent('click', { value: 'world' }); 51 | assert.deepEqual(events, ['hello-1', 'hello-2', 'world-1']); 52 | 53 | c.removeEventListener('click', c1); 54 | c.dispatchEvent('click', { value: 'hello' }); 55 | assert.deepEqual(events, ['hello-1', 'hello-2', 'world-1']); 56 | }); 57 | 58 | it('fires handlers stored as `on${type}` attributes', () => { 59 | const c = new ChildTarget(); 60 | const events = []; 61 | const c1 = ({ value }) => events.push(`${value}-1`); 62 | const c2 = ({ value }) => events.push(`${value}-2`); 63 | c.addEventListener('click', c1); 64 | c.onclick = c2; 65 | 66 | c.dispatchEvent('click', { value: 'hello' }); 67 | assert.deepEqual(events, ['hello-1', 'hello-2']); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/api/test-xr-device-pose.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import XRDevice from '../../src/api/XRDevice'; 20 | import XRSession from '../../src/api/XRSession'; 21 | import XRDevicePose, { PRIVATE } from '../../src/api/XRDevicePose'; 22 | import { createXRDevice } from '../lib/utils'; 23 | import * as mat4 from 'gl-matrix/src/gl-matrix/mat4'; 24 | 25 | // Half of an avg 62mm IPD value for the 26 | // mock view matrices 27 | const OFFSET = 0.031; 28 | 29 | describe('API - XRDevicePose', () => { 30 | let device, session, ref; 31 | // Technically this will expose the `frame` on a different than 32 | // requested tick, but as long as we're not asking for a new frame, 33 | // nothing should change in this mock env 34 | let getFrame = () => new Promise(r => session.requestAnimationFrame((t, frame) => r(frame))); 35 | beforeEach(async function () { 36 | device = createXRDevice({ hasPosition: true }); 37 | session = await device.requestSession({ immersive: true }); 38 | ref = await session.requestFrameOfReference('eye-level'); 39 | }); 40 | 41 | it('exposes a PRIVATE named export', async function () { 42 | const frame = await getFrame(); 43 | const pose = frame.getDevicePose(ref); 44 | assert.ok(pose[PRIVATE]); 45 | }); 46 | 47 | it('gets an updated poseModelMatrix every frame', async function () { 48 | let expected = new Float32Array(16); 49 | mat4.identity(expected); 50 | 51 | // On each frame, check to see that the poseModelMatrix is 52 | // going up the Z-axis on every tick 53 | for (let z = 0; z < 3; z++) { 54 | let frame = await getFrame(); 55 | let pose = frame.getDevicePose(ref); 56 | expected[14] = z + 1; 57 | assert.deepEqual(pose.poseModelMatrix, expected); 58 | } 59 | }); 60 | 61 | it('gets updated view matrices frame', async function () { 62 | let expected = new Float32Array(16); 63 | 64 | // On each frame, check to see that the view matrices are 65 | // going up the Z-axis on every tick, and include IPD offsets 66 | for (let z = 0; z < 3; z++) { 67 | let frame = await getFrame(); 68 | let pose = frame.getDevicePose(ref); 69 | assert.equal(frame.views.length, 2); 70 | for (const view of frame.views) { 71 | mat4.copy(expected, pose.poseModelMatrix); 72 | expected[12] += view.eye === 'left' ? -OFFSET : OFFSET; 73 | mat4.invert(expected, expected); 74 | assert.deepEqual(pose.getViewMatrix(view), expected); 75 | } 76 | } 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/api/test-xr-device.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import XRDevice, { PRIVATE } from '../../src/api/XRDevice'; 20 | import XRSession from '../../src/api/XRSession'; 21 | import XRPresentationContext from '../../src/api/XRPresentationContext'; 22 | import { createXRDevice } from '../lib/utils'; 23 | import WebVRDevice from '../../src/devices/WebVRDevice'; 24 | import MockVRDisplay from '../lib/MockVRDisplay'; 25 | import { MockGlobalVR } from '../lib/globals'; 26 | 27 | describe('API - XRDevice', () => { 28 | 29 | it('needs a PolyfilledXRDevice', () => { 30 | assert.throws(() => new XRDevice(), Error); 31 | }); 32 | 33 | it('exposes a PRIVATE named export', () => { 34 | const device = createXRDevice(); 35 | assert.ok(device[PRIVATE].polyfill); 36 | }); 37 | 38 | function validateOptions (fnName) { 39 | it('accepts immersive option', async function () { 40 | const device = createXRDevice(); 41 | return device[fnName]({ immersive: true }); 42 | }); 43 | 44 | it('accepts immersive and outputContext option', async function () { 45 | const device = createXRDevice(); 46 | const ctx = new XRPresentationContext(); 47 | return device[fnName]({ immersive: true, outputContext: ctx }); 48 | }); 49 | 50 | it('accepts non-immersive and outputContext option', async function () { 51 | const device = createXRDevice(); 52 | const ctx = new XRPresentationContext(); 53 | return device[fnName]({ immersive: false, outputContext: ctx }); 54 | }); 55 | 56 | it('fails non-immersive without outputContext option', async function () { 57 | const device = createXRDevice(); 58 | const ctx = new XRPresentationContext(); 59 | 60 | let caught = false; 61 | try { 62 | await device[fnName](); 63 | } catch (e) { 64 | caught = true; 65 | } 66 | assert.equal(caught, true); 67 | }); 68 | 69 | it('fails with non-XRPresentationContext outputContext option', async function () { 70 | const device = createXRDevice(); 71 | const ctx = new XRPresentationContext({ outputContext: {} }); 72 | 73 | let caught = false; 74 | try { 75 | await device[fnName](); 76 | } catch (e) { 77 | caught = true; 78 | } 79 | assert.equal(caught, true); 80 | }); 81 | 82 | it('checks PolyfilledXRDevice for custom session support', async function () { 83 | const global = new MockGlobalVR(); 84 | const polyfill = new WebVRDevice(global, new MockVRDisplay(global)); 85 | if (fnName === 'supportsSession') { 86 | polyfill[fnName] = () => false; 87 | } else { 88 | polyfill[fnName] = () => new Promise((res, rej) => rej()); 89 | } 90 | const device = new XRDevice(polyfill); 91 | let caught = false; 92 | try { 93 | await device[fnName]({ immersive: true }); 94 | } catch (e) { 95 | caught = true; 96 | } 97 | assert.equal(caught, true); 98 | }); 99 | 100 | it('fails immersive if underlying 1.1 VRDisplay `canPresent` is false', async function () { 101 | const global = new MockGlobalVR(); 102 | const polyfill = new WebVRDevice(global, new MockVRDisplay(global, { canPresent: false })); 103 | const device = new XRDevice(polyfill); 104 | let caught = false; 105 | try { 106 | await device[fnName]({ immersive: true }); 107 | } catch (e) { 108 | caught = true; 109 | } 110 | assert.equal(caught, true); 111 | }); 112 | } 113 | 114 | describe('XRDevice#supportsSession()', () => { 115 | validateOptions('supportsSession'); 116 | }); 117 | 118 | describe('XRDevice#requestSession()', () => { 119 | it('returns a XRSession', async function () { 120 | const device = createXRDevice(); 121 | const session = await device.requestSession({ immersive: true }); 122 | assert.instanceOf(session, XRSession); 123 | }); 124 | 125 | it('rejects if requesting a second, concurrent immersive session', async function () { 126 | const device = createXRDevice(); 127 | const session = await device.requestSession({ immersive: true }); 128 | let caught = false; 129 | try { 130 | await device.requestSession({ immersive: true }); 131 | } catch (e) { 132 | caught = true; 133 | } 134 | assert.equal(caught, true); 135 | }); 136 | 137 | it('resolves if requesting a second immersive session after previous immersive ends', async function () { 138 | const device = createXRDevice(); 139 | const session = await device.requestSession({ immersive: true }); 140 | await session.end(); 141 | await device.requestSession({ immersive: true }); 142 | }); 143 | 144 | validateOptions('requestSession'); 145 | }); 146 | 147 | describe('events', () => { 148 | it('propagates a `deactivate` event from PolyfilledXRDevice'); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/api/test-xr-frame-of-reference.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | import raf from 'raf'; 19 | 20 | import XRDevice from '../../src/api/XRDevice'; 21 | import XRSession from '../../src/api/XRSession'; 22 | import XRPresentationContext from '../../src/api/XRPresentationContext'; 23 | import XRFrameOfReference, { PRIVATE } from '../../src/api/XRFrameOfReference'; 24 | import XRStageBounds from '../../src/api/XRStageBounds'; 25 | import { createXRDevice } from '../lib/utils'; 26 | import WebVRDevice from '../../src/devices/WebVRDevice'; 27 | import MockVRDisplay from '../lib/MockVRDisplay'; 28 | import { MockGlobalVR } from '../lib/globals'; 29 | import * as mat4 from 'gl-matrix/src/gl-matrix/mat4'; 30 | 31 | describe('API - XRFrameOfReference', () => { 32 | it('exposes a PRIVATE named export', async function () { 33 | const device = createXRDevice(); 34 | const session = await device.requestSession({ immersive: true }); 35 | const frameOfRef = await session.requestFrameOfReference('eye-level'); 36 | assert.ok(frameOfRef[PRIVATE]); 37 | }); 38 | 39 | it('uses the polyfill\'s transform if provided', async function () { 40 | const global = new MockGlobalVR(); 41 | const polyfill = new WebVRDevice(global, new MockVRDisplay(global)); 42 | const device = new XRDevice(polyfill); 43 | const session = await device.requestSession({ immersive: true }); 44 | 45 | polyfill.requestFrameOfReferenceTransform = async function (type, options) { 46 | assert.equal(type, 'head-model'); 47 | return new Float32Array([ 48 | 1, 0, 0, 0, 49 | 0, 1, 0, 0, 50 | 0, 0, 1, 0, 51 | 0, 8, 0, 1 52 | ]); 53 | }; 54 | 55 | const frameOfRef = await session.requestFrameOfReference('head-model'); 56 | 57 | const pose = mat4.identity(new Float32Array(16)); 58 | // Set position to <1, 1, 1> 59 | pose[12] = pose[13] = pose[14] = 1; 60 | const out = new Float32Array(16); 61 | frameOfRef.transformBasePoseMatrix(out, pose); 62 | 63 | assert.deepEqual(out, new Float32Array([ 64 | 1, 0, 0, 0, 65 | 0, 1, 0, 0, 66 | 0, 0, 1, 0, 67 | 1, 9, 1, 1 68 | ]), 'pose is transformed by custom frame of reference from polyfill'); 69 | }); 70 | 71 | it('rejects if stage not provided and emulation disabled', async function () { 72 | const global = new MockGlobalVR(); 73 | const polyfill = new WebVRDevice(global, new MockVRDisplay(global)); 74 | const device = new XRDevice(polyfill); 75 | const session = await device.requestSession({ immersive: true }); 76 | 77 | return new Promise((resolve, reject) => 78 | session.requestFrameOfReference('stage', { disableStageEmulation: true }) 79 | .then(reject, resolve)); 80 | }); 81 | 82 | it('rejects if the polyfill rejects the option', async function () { 83 | const global = new MockGlobalVR(); 84 | const polyfill = new WebVRDevice(global, new MockVRDisplay(global)); 85 | const device = new XRDevice(polyfill); 86 | const session = await device.requestSession({ immersive: true }); 87 | 88 | polyfill.requestFrameOfReferenceTransform = function (type, options) { 89 | return Promise.reject(); 90 | }; 91 | 92 | return new Promise((resolve, reject) => { 93 | session.requestFrameOfReference('head-model').then(reject, resolve); 94 | }); 95 | }); 96 | 97 | it('`emulatedHeight` is 0 when using non-stage reference', async function () { 98 | const device = createXRDevice(); 99 | const session = await device.requestSession({ immersive: true }); 100 | const ref = await session.requestFrameOfReference('head-model'); 101 | assert.equal(ref.emulatedHeight, 0); 102 | }); 103 | 104 | it('`emulatedHeight` is 0 when using non-emulated stage reference', async function () { 105 | const global = new MockGlobalVR(); 106 | const polyfill = new WebVRDevice(global, new MockVRDisplay(global)); 107 | const device = new XRDevice(polyfill); 108 | const session = await device.requestSession({ immersive: true }); 109 | polyfill.requestFrameOfReferenceTransform = function (type, options) { 110 | const out = new Float32Array() 111 | mat4.identity(out); 112 | return out; 113 | }; 114 | 115 | let ref = await session.requestFrameOfReference('stage', { disableStageEmulation: true }); 116 | assert.equal(ref.emulatedHeight, 0); 117 | 118 | // Allowing emulation shouldn't change this as the platform provides 119 | ref = await session.requestFrameOfReference('stage', { disableStageEmulation: false }); 120 | assert.equal(ref.emulatedHeight, 0); 121 | }); 122 | 123 | it('`emulatedHeight` is default value when using emulated stage when using 0 as `stageEmulationHeight`', async function () { 124 | const device = createXRDevice(); 125 | const session = await device.requestSession({ immersive: true }); 126 | const ref = await session.requestFrameOfReference('stage', { stageEmulationHeight: 0 }); 127 | assert.equal(ref.emulatedHeight, 1.6); 128 | }); 129 | 130 | it('`emulatedHeight` is default value when using emulated stage', async function () { 131 | const device = createXRDevice(); 132 | const session = await device.requestSession({ immersive: true }); 133 | const ref = await session.requestFrameOfReference('stage'); 134 | assert.equal(ref.emulatedHeight, 1.6); 135 | }); 136 | 137 | it('`emulatedHeight` uses `stageEmulationHeight` when emulated and non-zero', async function () { 138 | const device = createXRDevice(); 139 | const session = await device.requestSession({ immersive: true }); 140 | const ref = await session.requestFrameOfReference('stage', { stageEmulationHeight: 2.0 }); 141 | assert.equal(ref.emulatedHeight, 2); 142 | }); 143 | 144 | it('provides `bounds` when requesting a stage from a 6DOF device', async function () { 145 | const global = new MockGlobalVR(); 146 | const polyfill = new WebVRDevice(global, new MockVRDisplay(global, { hasPosition: true })); 147 | const device = new XRDevice(polyfill); 148 | const session = await device.requestSession({ immersive: true }); 149 | const ref = await session.requestFrameOfReference('stage');//, { stageEmulationHeight: 2.0 }); 150 | assert.instanceOf(ref.bounds, XRStageBounds); 151 | assert.equal(ref.bounds.geometry[0].x, -2.5); 152 | assert.equal(ref.bounds.geometry[0].z, -5); 153 | assert.equal(ref.bounds.geometry[1].x, 2.5); 154 | assert.equal(ref.bounds.geometry[1].z, -5); 155 | assert.equal(ref.bounds.geometry[2].x, 2.5); 156 | assert.equal(ref.bounds.geometry[2].z, 5); 157 | assert.equal(ref.bounds.geometry[3].x, -2.5); 158 | assert.equal(ref.bounds.geometry[3].z, 5); 159 | }); 160 | 161 | describe('XRFrameOfReference#transformBasePoseMatrix', () => { 162 | // Get pose with translation of <5, 6, 7> 163 | const getPose = () => new Float32Array([ 164 | 1, 0, 0, 0, 165 | 0, -1, 0, 0, 166 | 0, 0, -1, 0, 167 | 5, 6, 7, 1 168 | ]); 169 | 170 | const data = [ 171 | // head-model should strip out only translation 172 | ['head-model', [ 173 | 1, 0, 0, 0, 174 | 0, -1, 0, 0, 175 | 0, 0, -1, 0, 176 | 0, 0, 0, 1 177 | ]], 178 | // eye-level shouldn't modify the pose at all 179 | ['eye-level', [ 180 | 1, 0, 0, 0, 181 | 0, -1, 0, 0, 182 | 0, 0, -1, 0, 183 | 5, 6, 7, 1 184 | ]], 185 | // stage should increment the Y translation by the default 186 | // emulation height 187 | ['stage', [ 188 | 1, 0, 0, 0, 189 | 0, -1, 0, 0, 190 | 0, 0, -1, 0, 191 | 5, 7.6, 7, 1 192 | ]] 193 | ]; 194 | 195 | data.forEach(([type, expected]) => { 196 | it(`uses the default transform for ${type} if none provided`, async function () { 197 | const device = createXRDevice(); 198 | const session = await device.requestSession({ immersive: true }); 199 | const frameOfRef = await session.requestFrameOfReference(type); 200 | const actual = mat4.identity(new Float32Array(16)); 201 | frameOfRef.transformBasePoseMatrix(actual, getPose()); 202 | assert.deepEqual(actual, new Float32Array(expected)); 203 | }); 204 | }); 205 | 206 | it('uses stageEmulationHeight when provided when emulating stage', async function () { 207 | const device = createXRDevice(); 208 | const session = await device.requestSession({ immersive: true }); 209 | const frameOfRef = await session.requestFrameOfReference('stage', { stageEmulationHeight: 1.55 }); 210 | const actual = mat4.identity(new Float32Array(16)); 211 | frameOfRef.transformBasePoseMatrix(actual, getPose()); 212 | assert.deepEqual(actual, new Float32Array([ 213 | 1, 0, 0, 0, 214 | 0, -1, 0, 0, 215 | 0, 0, -1, 0, 216 | 5, 7.55, 7, 1 217 | ])); 218 | }); 219 | }); 220 | 221 | describe('XRFrameOfReference#transformBaseViewMatrix', () => { 222 | it('correctly transforms view matrix when non-stage values provided'); 223 | it('correctly transforms view matrix when stage values provided'); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /test/api/test-xr-frame.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import { PRIVATE } from '../../src/api/XRFrame'; 20 | import XRSession from '../../src/api/XRSession'; 21 | import XRDevicePose from '../../src/api/XRDevicePose'; 22 | import { createXRDevice } from '../lib/utils'; 23 | 24 | describe('API - XRFrame', () => { 25 | let device, session, ref; 26 | beforeEach(async function () { 27 | device = createXRDevice(); 28 | session = await device.requestSession({ immersive: true }); 29 | ref = await session.requestFrameOfReference('eye-level'); 30 | }); 31 | 32 | it('exposes a PRIVATE named export', done => { 33 | session.requestAnimationFrame((t, frame) => { 34 | assert.ok(frame[PRIVATE]); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('has two views', done => { 40 | session.requestAnimationFrame((t, frame) => { 41 | assert.equal(frame.views.length, 2); 42 | const eyes = frame.views.map(v => v.eye); 43 | assert.include(eyes, 'left'); 44 | assert.include(eyes, 'right'); 45 | done(); 46 | }); 47 | }); 48 | 49 | it('has a session', done => { 50 | session.requestAnimationFrame((t, frame) => { 51 | assert.equal(frame.session, session); 52 | done(); 53 | }); 54 | }); 55 | 56 | it('can get a device pose', done => { 57 | session.requestAnimationFrame((t, frame) => { 58 | const pose = frame.getDevicePose(ref); 59 | assert.instanceOf(pose, XRDevicePose); 60 | assert.instanceOf(pose.poseModelMatrix, Float32Array); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/api/test-xr-layer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import XRDevice from '../../src/api/XRDevice'; 20 | import XRSession from '../../src/api/XRSession'; 21 | import XRDevicePose from '../../src/api/XRDevicePose'; 22 | import XRPresentationContext from '../../src/api/XRPresentationContext'; 23 | import WebVRDevice from '../../src/devices/WebVRDevice'; 24 | import MockVRDisplay from '../lib/MockVRDisplay'; 25 | import { MockGlobalVR } from '../lib/globals'; 26 | 27 | const EPSILON = 0.0001; 28 | 29 | describe('API - XRLayer', () => { 30 | describe('XRLayer#getViewport()', () => { 31 | it('returns XRViewport with appropriate x, y, width, height values when immersive'); 32 | it('returns XRViewport with appropriate x, y, width, height values when non-immersive'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/api/test-xr-ray.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import XRRay from '../../src/api/XRRay.js'; 20 | import DOMPointReadOnly from '../../src/lib/DOMPointReadOnly.js'; 21 | 22 | describe('API - XRRay', () => { 23 | it('defaults to <0,0,0> origin, <0,0,-1> direction', () => { 24 | const ray = new XRRay(); 25 | assert.equal(ray.origin.x, 0); 26 | assert.equal(ray.origin.y, 0); 27 | assert.equal(ray.origin.z, 0); 28 | assert.equal(ray.origin.w, 1); 29 | assert.equal(ray.direction.x, 0); 30 | assert.equal(ray.direction.y, 0); 31 | assert.equal(ray.direction.z, -1); 32 | assert.equal(ray.direction.w, 0); 33 | assert.equal(ray.transformMatrix.length, 16); 34 | }); 35 | 36 | it('throws if modifying any property', () => { 37 | const ray = new XRRay(); 38 | assert.throws(() => ray.origin = new DOMPointReadOnly(0, 0, 0, 1)); 39 | assert.throws(() => ray.direction = new DOMPointReadOnly(0, 0, -1, 0)); 40 | assert.throws(() => ray.transformMatrix = new Float32Array()); 41 | }); 42 | 43 | it('throws if given non-expected types', () => { 44 | const args = [ 45 | null, 46 | 20, 47 | 'hello', 48 | {} 49 | ]; 50 | for (let arg of args) { 51 | assert.throws(() => new XRRay(arg, new DOMPointReadOnly(0, 0, -1, 0), new Float32Array(16))); 52 | assert.throws(() => new XRRay(new DOMPointReadOnly(0, 0, 0, 1), arg, new Float32Array(16))); 53 | assert.throws(() => new XRRay(new DOMPointReadOnly(0, 0, 0, 1), new DOMPointReadOnly(0, 0, -1, 0), arg)); 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/api/test-xr-stage-bounds.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import XRStageBounds, { PRIVATE } from '../../src/api/XRStageBounds'; 20 | import XRStageBoundsPoint from '../../src/api/XRStageBoundsPoint'; 21 | 22 | describe('API - XRStageBounds', () => { 23 | it('exposes a PRIVATE named export', () => { 24 | const bounds = new XRStageBounds([-2, -3, 2, -3, 2, 3, -2, 3]); 25 | assert.ok(bounds[PRIVATE]); 26 | }); 27 | 28 | it('can be constructed interally with an array of X and Z values', () => { 29 | const bounds = new XRStageBounds([ 30 | -2, -3, 31 | 2, -3, 32 | 2, 3, 33 | -2, 3 34 | ]); 35 | 36 | for (let i = 0; i < bounds.geometry.length; i++) { 37 | const point = bounds.geometry[i]; 38 | assert.instanceOf(point, XRStageBoundsPoint); 39 | assert.equal(point.x, (i === 0 || i === 3) ? -2 : 2); 40 | assert.equal(point.z, i < 2 ? -3 : 3); 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/api/test-xr-view.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import { PRIVATE } from '../../src/api/XRView'; 20 | import XRDevice from '../../src/api/XRDevice'; 21 | import XRSession from '../../src/api/XRSession'; 22 | import XRDevicePose from '../../src/api/XRDevicePose'; 23 | import XRPresentationContext from '../../src/api/XRPresentationContext'; 24 | import WebVRDevice from '../../src/devices/WebVRDevice'; 25 | import MockVRDisplay from '../lib/MockVRDisplay'; 26 | import { MockGlobalVR } from '../lib/globals'; 27 | 28 | const EPSILON = 0.0001; 29 | 30 | describe('API - XRView', () => { 31 | let global, polyfill, device, session, ref; 32 | // Technically this will expose the `frame` on a different than 33 | // requested tick, but as long as we're not asking for a new frame, 34 | // nothing should change in this mock env 35 | let getFrame = () => new Promise(r => session.requestAnimationFrame((t, frame) => r(frame))); 36 | beforeEach(async function () { 37 | global = new MockGlobalVR(); 38 | polyfill = new WebVRDevice(global, new MockVRDisplay(global)); 39 | device = new XRDevice(polyfill); 40 | session = await device.requestSession({ immersive: true }); 41 | ref = await session.requestFrameOfReference('eye-level'); 42 | }); 43 | 44 | it('exposes a PRIVATE named export', async function () { 45 | let frame = await getFrame(); 46 | assert.ok(frame.views[0][PRIVATE]); 47 | assert.ok(frame.views[1][PRIVATE]); 48 | }); 49 | 50 | it('has `eye` property of left and right', async function () { 51 | let frame = await getFrame(); 52 | assert.equal(frame.views.length, 2); 53 | assert.ok(frame.views.some(v => v.eye === 'left')) 54 | assert.ok(frame.views.some(v => v.eye === 'right')) 55 | }); 56 | 57 | it('has `projectionMatrix` based off of depthNear/depthFar', async function () { 58 | let expected = new Float32Array([ 59 | 2.8278, 0, 0, 0, 60 | 0, 5.0273, 0, 0, 61 | 0, 0, -1.0002, -1, 62 | 0, 0, -0.2, 0 63 | ]); 64 | 65 | session.depthNear = 0.1; 66 | session.depthFar = 1000; 67 | let frame = await getFrame(); 68 | let view = frame.views[0]; 69 | for (let i = 0; i < expected.length; i++) { 70 | assert.closeTo(view.projectionMatrix[i], expected[i], EPSILON); 71 | } 72 | 73 | // Change depthNear to see if it propagates to the projectionMatrix 74 | session.depthNear = 0.01; 75 | session.depthFar = 1000; 76 | frame = await getFrame(); 77 | view = frame.views[0]; 78 | expected[10] = -1.00002; 79 | expected[14] = -0.02000; 80 | for (let i = 0; i < expected.length; i++) { 81 | assert.closeTo(view.projectionMatrix[i], expected[i], EPSILON); 82 | } 83 | }); 84 | 85 | /** 86 | * TODO this function been moved to XRLayer, so we're 87 | * testing the underlying implementation here. We should test 88 | * the XRLayer directly, although that's a bit harder with the WebGLContext 89 | * usage in node. 90 | */ 91 | describe('XRView#_getViewport()', () => { 92 | it('returns XRViewport with appropriate x, y, width, height values when immersive', async function () { 93 | const layer = { context: { canvas: { width: 1920, height: 1080 }}}; 94 | 95 | let frame = await getFrame(); 96 | assert.equal(frame.views.length, 2); 97 | for (let view of frame.views) { 98 | let viewport = view._getViewport(layer); 99 | assert.equal(viewport.x, view.eye === 'left' ? 0 : layer.context.canvas.width / 2); 100 | assert.equal(viewport.y, 0); 101 | assert.equal(viewport.width, layer.context.canvas.width / 2); 102 | assert.equal(viewport.height, layer.context.canvas.height); 103 | } 104 | }); 105 | 106 | it('returns XRViewport with appropriate x, y, width, height values when non-immersive', async function () { 107 | const layer = { context: { canvas: { width: 1920, height: 1080 }}}; 108 | session = await device.requestSession({ outputContext: new XRPresentationContext() }); 109 | 110 | let frame = await getFrame(); 111 | assert.equal(frame.views.length, 1); 112 | let viewport = frame.views[0]._getViewport(layer); 113 | assert.equal(viewport.x, 0); 114 | assert.equal(viewport.y, 0); 115 | assert.equal(viewport.width, 1920); 116 | assert.equal(viewport.height, 1080); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/api/test-xr.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import XR, { PRIVATE } from '../../src/api/XR'; 20 | import XRDevice from '../../src/api/XRDevice'; 21 | import { createXRDevice } from '../lib/utils'; 22 | 23 | describe('API - XR', () => { 24 | it('exposes a PRIVATE named export', async function () { 25 | const device = createXRDevice(); 26 | const pDevice = new Promise(resolve => resolve(device)); 27 | const xr = new XR(pDevice); 28 | assert.ok(xr[PRIVATE]); 29 | }); 30 | 31 | describe('XR#requestDevice()', () => { 32 | it('returns seeded thennable devices', async function () { 33 | const device = createXRDevice(); 34 | const pDevice = new Promise(resolve => resolve(device)); 35 | const xr = new XR(pDevice); 36 | const rDevice = await xr.requestDevice(); 37 | assert.equal(rDevice, device); 38 | }); 39 | }); 40 | 41 | describe('events', () => { 42 | it('propagates a `deviceconnect` event from PolyfilledXRDevice'); 43 | it('propagates a `devicedisconnect` event from PolyfilledXRDevice'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/lib/MockVRDisplay.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 raf from 'raf'; 17 | import EventTarget from '../../src/lib/EventTarget'; 18 | import now from '../../src/lib/now'; 19 | import * as mat4 from 'gl-matrix/src/gl-matrix/mat4'; 20 | 21 | const IPD = 0.062; 22 | let displayId = 0; 23 | export default class MockVRDisplay extends EventTarget { 24 | constructor(global, config = {}) { 25 | super(); 26 | this.global = global; 27 | this.displayId = ++displayId; 28 | this.displayName = 'MockVRDisplay'; 29 | this.depthNear = 0.1; 30 | this.depthFar = 1000.0; 31 | this.isPresenting = false; 32 | this.stageParameters = null; 33 | 34 | this.capabilities = Object.assign({ 35 | hasPosition: false, 36 | hasOrientation: true, 37 | hasExternalDisplay: false, 38 | canPresent: true, 39 | maxLayers: 1, 40 | }, config); 41 | 42 | if (this.capabilities.hasPosition) { 43 | this.stageParameters = { 44 | sizeX: 5, 45 | sizeZ: 10, 46 | sittingToStandingTransform: new Float32Array([ 47 | 1, 0, 0, 0, 48 | 0, 1, 0, 0, 49 | 0, 0, 1, 0, 50 | 0, 0, 0, 1 51 | ]), 52 | }; 53 | } 54 | 55 | // Width/height for calculating mock matrices/viewport 56 | this._width = 1920; 57 | this._height = 1080; 58 | 59 | this._leftViewMatrix = new Float32Array(16); 60 | this._rightViewMatrix = new Float32Array(16); 61 | this._leftProjectionMatrix = new Float32Array(16); 62 | this._rightProjectionMatrix = new Float32Array(16); 63 | this._poseMatrix = new Float32Array(16); 64 | 65 | mat4.identity(this._leftViewMatrix); 66 | mat4.identity(this._rightViewMatrix); 67 | mat4.identity(this._poseMatrix); 68 | } 69 | 70 | getFrameData(data) { 71 | data.timestamp = now(); 72 | 73 | // Update projection matrices 74 | mat4.perspective(this._leftProjectionMatrix, Math.PI / 8, this._width / this._height, this.depthNear, this.depthFar); 75 | mat4.perspective(this._rightProjectionMatrix, Math.PI / 8, this._width / this._height, this.depthNear, this.depthFar); 76 | 77 | mat4.copy(data.leftProjectionMatrix, this._leftProjectionMatrix); 78 | mat4.copy(data.rightProjectionMatrix, this._rightProjectionMatrix); 79 | mat4.copy(data.leftViewMatrix, this._leftViewMatrix); 80 | mat4.copy(data.rightViewMatrix, this._rightViewMatrix); 81 | 82 | if (this.capabilities.hasPosition) { 83 | data.pose.position[0] = this._poseMatrix[12]; 84 | data.pose.position[1] = this._poseMatrix[13]; 85 | data.pose.position[2] = this._poseMatrix[14]; 86 | } 87 | 88 | // The tests don't animate orientation, so just use a default 89 | // quaternion for now. 90 | data.pose.orientation[0] = 0; 91 | data.pose.orientation[1] = 0; 92 | data.pose.orientation[2] = 0; 93 | data.pose.orientation[3] = 1; 94 | } 95 | 96 | /** 97 | * @param {string} eye 98 | * @return {VREyeParameters} 99 | */ 100 | getEyeParameters(eye) { 101 | return { 102 | offset: new Float32Array([eye === 'left' ? (-IPD/2) : (IPD/2), 0, 0]), 103 | renderWidth: this._width / 2, 104 | renderHeight: this._height, 105 | }; 106 | } 107 | 108 | /** 109 | * @param {Function} callback 110 | */ 111 | requestAnimationFrame(callback) { 112 | if (this.capabilities.hasPosition) { 113 | // Tick up the Z position by 1 per frame 114 | this._poseMatrix[14] = this._poseMatrix[14] + 1; 115 | } 116 | 117 | // Copy the pose to view matrices, apply IPD difference, invert. 118 | mat4.copy(this._leftViewMatrix, this._poseMatrix); 119 | mat4.copy(this._rightViewMatrix, this._poseMatrix); 120 | this._leftViewMatrix[12] = -IPD/2; 121 | this._rightViewMatrix[12] = IPD/2; 122 | 123 | mat4.invert(this._leftViewMatrix, this._leftViewMatrix); 124 | mat4.invert(this._rightViewMatrix, this._rightViewMatrix); 125 | return raf(callback); 126 | } 127 | 128 | /** 129 | * @param {number} handle 130 | */ 131 | cancelAnimationFrame(handle) { 132 | raf.cancel(handle); 133 | } 134 | 135 | async requestPresent(layers) { 136 | if (layers.length > this.capabilities.maxLayers) { 137 | throw new Error(); 138 | } 139 | 140 | if (!this.capabilities.canPresent) { 141 | throw new Error(); 142 | } 143 | 144 | const currentlyPresenting = this.isPresenting; 145 | this.isPresenting = true; 146 | if (!currentlyPresenting) { 147 | const e = new this.global.window.Event('vrdisplaypresentchange'); 148 | this.global.window.dispatchEvent(e); 149 | } 150 | 151 | this._layers = layers; 152 | } 153 | 154 | async exitPresent() { 155 | const currentlyPresenting = this.isPresenting; 156 | this.isPresenting = false; 157 | if (currentlyPresenting) { 158 | const e = new this.global.window.Event('vrdisplaypresentchange'); 159 | this.global.window.dispatchEvent(e); 160 | } 161 | this._layers = null; 162 | } 163 | 164 | submitFrame() { 165 | if (!this.isPresenting) { 166 | throw new Error(); 167 | } 168 | } 169 | 170 | getLayers() { 171 | return this._layers; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /test/lib/globals.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 { JSDOM } from 'jsdom'; 17 | 18 | /** 19 | * A mocked "global" object that contains all the necessary 20 | * globals that the polyfill needs for injection and WebGL polyfilling. 21 | * 22 | * Polyfilled properties: 23 | * 24 | * `window` 25 | * `navigator` 26 | * `document` 27 | */ 28 | 29 | export class MockGlobal { 30 | constructor() { 31 | const { window } = new JSDOM(`

Hello, WebXR

`); 32 | this.window = window; 33 | this.document = window.document; 34 | this.navigator = window.navigator; 35 | this.HTMLCanvasElement = {}; 36 | this.WebGLRenderingContext = {}; 37 | this.WebGL2RenderingContext = {}; 38 | } 39 | } 40 | 41 | export class MockGlobalVR extends MockGlobal { 42 | constructor() { 43 | super(); 44 | this.VRFrameData = function VRFrameData() { 45 | this.leftProjectionMatrix = new Float32Array(16); 46 | this.rightProjectionMatrix = new Float32Array(16); 47 | this.leftViewMatrix = new Float32Array(16); 48 | this.rightViewMatrix = new Float32Array(16); 49 | this.timestamp = null; 50 | this.pose = { 51 | position: new Float32Array(3), 52 | orientation: new Float32Array(4), 53 | }; 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/lib/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 XRDevice from '../../src/api/XRDevice'; 17 | import WebVRDevice from '../../src/devices/WebVRDevice'; 18 | import MockVRDisplay from './MockVRDisplay'; 19 | import { MockGlobalVR } from './globals'; 20 | 21 | /** 22 | * Creates an XRDevice backed by a WebVRDevice using a MockVRDisplay. 23 | * Pass in options that ultimately populate a VRDisplay's 1.1 capabilities, 24 | * like 'hasExternalDisplay'. 25 | */ 26 | export const createXRDevice = (config) => { 27 | const global = new MockGlobalVR(); 28 | const polyfill = new WebVRDevice(global, new MockVRDisplay(global, config)); 29 | return new XRDevice(polyfill); 30 | }; 31 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register')({ 2 | ignore: /node_modules\/(?!gl-matrix)/ 3 | }); 4 | const mock = require('mock-require'); 5 | 6 | /** 7 | * Mock the `cardboard-vr-display` dependency since it uses 8 | * globals which makes it difficult to test against. 9 | */ 10 | mock('cardboard-vr-display', './lib/MockVRDisplay'); 11 | -------------------------------------------------------------------------------- /test/test-devices.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import WebXRPolyfill from '../src/WebXRPolyfill'; 20 | import { requestDevice } from '../src/devices'; 21 | import XRDevice from '../src/api/XRDevice'; 22 | import MockVRDisplay from './lib/MockVRDisplay'; 23 | import { MockGlobal, MockGlobalVR } from './lib/globals'; 24 | 25 | const makeMobile = global => { 26 | const realUA = global.navigator.userAgent; 27 | Object.defineProperty(global.navigator, 'userAgent', { 28 | get: () => `${realUA} iphone`, 29 | }); 30 | }; 31 | 32 | const addXR = (global, device) => { 33 | const xr = { 34 | requestDevice: () => new Promise(resolve => resolve(device)), 35 | }; 36 | 37 | Object.defineProperty(global.navigator, 'xr', { 38 | get: () => xr, 39 | }); 40 | }; 41 | 42 | const addVR = (global, display) => { 43 | global.navigator.getVRDisplays = () => new Promise(resolve => resolve(display ? [display] : [])); 44 | return; 45 | Object.defineProperty(global.navigator, 'getVRDisplays', { 46 | get: () => new Promise(resolve => resolve(display ? [display] : [])), 47 | }); 48 | }; 49 | 50 | describe('devices - requestDevice', () => { 51 | it('returns XRDevice if exists', async function () { 52 | const global = new MockGlobal(); 53 | const xrDevice = {}; 54 | const vrDevice = {}; 55 | addXR(global, xrDevice); 56 | addVR(global, vrDevice); 57 | makeMobile(global); 58 | 59 | const device = await requestDevice(global, { cardboard: true, webvr: true }); 60 | assert.equal(device, xrDevice); 61 | }); 62 | 63 | it('returns wrapped VRDisplay if no native XRDevice exists', async function () { 64 | const global = new MockGlobalVR(); 65 | const vrDevice = new MockVRDisplay(); 66 | addVR(global, vrDevice); 67 | 68 | const device = await requestDevice(global, { cardboard: true, webvr: true }); 69 | assert.equal(device.polyfill.display, vrDevice); 70 | assert.instanceOf(device, XRDevice); 71 | }); 72 | 73 | it('returns wrapped CardboardVRDisplay if no native XRDevice or VRDisplay exists', async function () { 74 | const global = new MockGlobalVR(); 75 | addVR(global); 76 | makeMobile(global); 77 | const device = await requestDevice(global, { cardboard: true, webvr: true }); 78 | assert.instanceOf(device, XRDevice); 79 | assert.instanceOf(device.polyfill.display, MockVRDisplay); 80 | }); 81 | 82 | it('returns wrapped CardboardVRDisplay if no native WebXR/WebVR implementations exists', async function () { 83 | const global = new MockGlobal(); 84 | makeMobile(global); 85 | const device = await requestDevice(global, { cardboard: true, webvr: true }); 86 | assert.instanceOf(device, XRDevice); 87 | assert.instanceOf(device.polyfill.display, MockVRDisplay); 88 | }); 89 | 90 | it('returns wrapped CardboardVRDisplay if no native XRDevice and webvr disabled', async function () { 91 | const global = new MockGlobal(); 92 | makeMobile(global); 93 | const vrDevice = new MockVRDisplay(); 94 | addVR(global, vrDevice); 95 | const device = await requestDevice(global, { cardboard: true, webvr: false }); 96 | assert.instanceOf(device, XRDevice); 97 | assert.instanceOf(device.polyfill.display, MockVRDisplay); 98 | }); 99 | 100 | it('returns no devices if no native support and not on mobile', async function () { 101 | const global = new MockGlobal(); 102 | const device = await requestDevice(global, { cardboard: true }); 103 | assert.equal(device, null); 104 | }); 105 | 106 | it('returns no devices if no native support and cardboard is false', async function () { 107 | const global = new MockGlobal(); 108 | makeMobile(global); 109 | const device = await requestDevice(global, { cardboard: false }); 110 | assert.equal(device, null); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 17 | import { assert } from 'chai'; 18 | 19 | import * as utils from '../src/utils.js'; 20 | 21 | describe('utils', () => { 22 | describe('poseMatrixToXRRay', () => { 23 | it('poseMatrixToXRRay'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/test-webxr-polyfill.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 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 mocha from 'mocha'; 19 | import { assert } from 'chai'; 20 | 21 | import WebXRPolyfill from '../src/WebXRPolyfill'; 22 | import API from '../src/api/index.js'; 23 | import XRDevice from '../src/api/XRDevice'; 24 | import { createXRDevice } from './lib/utils'; 25 | import { MockGlobal } from './lib/globals'; 26 | 27 | const API_DIRECTORY = path.join(__dirname, '..', 'src', 'api'); 28 | 29 | const mockRequestDevice = () => new Promise(resolve => setTimeout(resolve, 5)); 30 | 31 | const makeMobile = global => { 32 | const realUA = global.navigator.userAgent; 33 | Object.defineProperty(global.navigator, 'userAgent', { 34 | get: () => `${realUA} iphone`, 35 | }); 36 | } 37 | 38 | describe('WebXRPolyfill', () => { 39 | describe('injecting', () => { 40 | it('polyfills the WebXR API if navigator.xr does not exist', () => { 41 | const global = new MockGlobal(); 42 | assert.ok(!global.navigator.xr); 43 | const polyfill = new WebXRPolyfill(global); 44 | assert.ok(global.navigator.xr); 45 | assert.equal(polyfill.injected, true); 46 | }); 47 | 48 | it('does not polyfill if navigator.xr already exists', () => { 49 | const global = new MockGlobal(); 50 | // Inject the API to start as if it were native 51 | new WebXRPolyfill(global); 52 | 53 | const polyfill = new WebXRPolyfill(global); 54 | assert.ok(global.navigator.xr); 55 | assert.equal(polyfill.injected, false); 56 | }); 57 | }); 58 | 59 | describe('patching', () => { 60 | it('does not patch `xr.requestDevice` if exists when on desktop', () => { 61 | const global = new MockGlobal(); 62 | // Inject the API to start as if it were native 63 | new WebXRPolyfill(global); 64 | global.navigator.xr.requestDevice = mockRequestDevice; 65 | 66 | const polyfill = new WebXRPolyfill(global); 67 | assert.equal(polyfill.injected, false); 68 | assert.ok(global.navigator.xr.requestDevice === mockRequestDevice); 69 | }); 70 | 71 | it('does not patch `xr.requestDevice` if exists on mobile when cardboard is false', () => { 72 | const global = new MockGlobal(); 73 | makeMobile(global); 74 | // Inject the API to start as if it were native 75 | new WebXRPolyfill(global); 76 | global.navigator.xr.requestDevice = mockRequestDevice; 77 | 78 | const polyfill = new WebXRPolyfill(global, { cardboard: false }); 79 | assert.equal(polyfill.injected, false); 80 | assert.ok(global.navigator.xr.requestDevice === mockRequestDevice); 81 | }); 82 | 83 | it('patches `xr.requestDevice` if exists on mobile and cardboard is true', () => { 84 | const global = new MockGlobal(); 85 | makeMobile(global); 86 | // Inject the API to start as if it were native 87 | new WebXRPolyfill(global); 88 | global.navigator.xr.requestDevice = mockRequestDevice; 89 | 90 | const polyfill = new WebXRPolyfill(global, { cardboard: true }); 91 | assert.equal(polyfill.injected, false); 92 | assert.ok(global.navigator.xr.requestDevice !== mockRequestDevice); 93 | }); 94 | }); 95 | 96 | it('injects all files in API directory as globals', () => { 97 | const global = new MockGlobal(); 98 | // Inject the API to start as if it were native 99 | new WebXRPolyfill(global); 100 | 101 | const files = fs.readdirSync(API_DIRECTORY); 102 | for (let file of files) { 103 | if (!/^XR/.test(file)) { return; } // skip index.js, non 'XR*' files 104 | const globalName = file.replace('.js', ''); 105 | assert.ok(global[globalName], `${globalName} exists on window`); 106 | assert.equal(global[globalName], API[globalName], `${globalName} matches the export of the file`); 107 | } 108 | }); 109 | }); 110 | --------------------------------------------------------------------------------