├── LICENSE ├── README.md ├── examples └── simple-scene │ └── index.html └── src ├── input_hmd.js └── vr_camera.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 PlayCanvas Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | **This project is now deprecated. WebXR support for PlayCanvas is now built directly into the engine. For more information see the developer documentation: https://developer.playcanvas.com/en/user-manual/xr/** 4 | 5 | **This repository is kept for legacy reasons and should not be used in new projects.** 6 | 7 | --- 8 | 9 | ## WebVR Support for PlayCanvas 10 | 11 | The project contains scripts to run your PlayCanvas application in VR using either a Google Cardboard-style VR headset or (when using special builds of Chrome & Firefox) Oculus Rift and HTC Vive. 12 | 13 | ## Requirements 14 | 15 | For Cardboard VR any modern mobile device should work. For Oculus and Vive support you require a special build of [Chromium](http://webvr.info/). 16 | 17 | ## How to use 18 | 19 | 1. Make sure `input_hmd.js` loads first, which can be changed in Editor Settings "Scripts Loading Order". 20 | 2. On the Entity you wish to be your camera add `vrCamera` to script component. 21 | 3. By default `vrCamera` will start the VR mode when you click or tap the canvas. 22 | 23 | ## Options 24 | 25 | VrCamera has a few options that are exposed on the entity in the Editor. 26 | 27 | * enableOnClick - If disabled you must manually call `enterVr()` on the camera script to start VR rendering 28 | * alwaysAcceptInput - If enabled HMD position tracking will be applied to the camera even if the camera is no "in VR". 29 | 30 | ## Known Issues 31 | 32 | - Currently WebVR is a moving target, so APIs are changing often. This library now implements the WebVR 1.0 Spec. 33 | - Please raise an issue on this project if you encounter problems 34 | 35 | ## Attribution 36 | 37 | input_hmd.js contains a version of the [WebVR Polyfill](https://github.com/borismus/webvr-polyfill) by Boris Smus. 38 | -------------------------------------------------------------------------------- /examples/simple-scene/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 47 | 48 | 97 | 98 | 99 | 100 | 101 |
102 | 103 | 104 |
105 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /src/vr_camera.js: -------------------------------------------------------------------------------- 1 | var VrCamera = pc.createScript('vrCamera'); 2 | 3 | 4 | VrCamera.attributes.add('enableOnClick', { 5 | type: 'boolean', 6 | default: true 7 | }); 8 | 9 | VrCamera.attributes.add('alwaysAcceptInput', { 10 | type: 'boolean', 11 | default: true 12 | }); 13 | 14 | 15 | VrCamera.q = new pc.Quat(); 16 | VrCamera.offset = new pc.Vec3(); 17 | 18 | pc.PROJECTION_VR = 2; 19 | 20 | 21 | // Create projection matrix from VRFieldOfView object 22 | var fieldOfViewToProjectionMatrix = function (out, fov, zNear, zFar) { 23 | var upTan = Math.tan(fov.upDegrees * Math.PI/180.0); 24 | var downTan = Math.tan(fov.downDegrees * Math.PI/180.0); 25 | var leftTan = Math.tan(fov.leftDegrees * Math.PI/180.0); 26 | var rightTan = Math.tan(fov.rightDegrees * Math.PI/180.0); 27 | var xScale = 2.0 / (leftTan + rightTan); 28 | var yScale = 2.0 / (upTan + downTan); 29 | 30 | out[0] = xScale; 31 | out[1] = 0.0; 32 | out[2] = 0.0; 33 | out[3] = 0.0; 34 | out[4] = 0.0; 35 | out[5] = yScale; 36 | out[6] = 0.0; 37 | out[7] = 0.0; 38 | out[8] = -((leftTan - rightTan) * xScale * 0.5); 39 | out[9] = ((upTan - downTan) * yScale * 0.5); 40 | out[10] = -(zNear + zFar) / (zFar - zNear); 41 | out[11] = -1.0; 42 | out[12] = 0.0; 43 | out[13] = 0.0; 44 | out[14] = -(2.0 * zFar * zNear) / (zFar - zNear); 45 | out[15] = 0.0; 46 | 47 | return out; 48 | }; 49 | 50 | // patch camera getProjectionMatrix method 51 | pc.Camera.prototype.getProjectionMatrix = function () { 52 | if (this._projMatDirty) { 53 | if (this._projection === pc.PROJECTION_PERSPECTIVE) { 54 | this._projMat.setPerspective(this._fov, this._aspect, this._nearClip, this._farClip); 55 | } else if (this._projection === pc.PROJECTION_VR) { 56 | fieldOfViewToProjectionMatrix(this._projMat.data, this._vrFov, this._nearClip, this._farClip); 57 | } else { 58 | var y = this._orthoHeight; 59 | var x = y * this._aspect; 60 | this._projMat.setOrtho(-x, x, -y, y, this._nearClip, this._farClip); 61 | } 62 | 63 | this._projMatDirty = false; 64 | } 65 | return this._projMat; 66 | }; 67 | 68 | 69 | VrCamera.prototype.initialize = function() { 70 | var self = this; 71 | 72 | this.origin = this.entity.getLocalPosition().clone(); 73 | this.originRotation = this.entity.getLocalRotation().clone(); 74 | this.offsetScale = 1; 75 | 76 | this.inVr = false; 77 | 78 | // if available inherit the clear color from the existing camera 79 | var clearColor; 80 | if (this.entity.camera) { 81 | clearColor = this.entity.camera.clearColor; 82 | } else { 83 | clearColor = new pc.Color(0.1, 0.1, 0.1); 84 | } 85 | 86 | // left camera 87 | this.left = new pc.fw.Entity(); 88 | this.entity.addChild(this.left); 89 | this.left.addComponent("camera", { 90 | clearColor: clearColor, 91 | rect: new pc.Vec4(0.0, 0, 0.5, 1) 92 | }); 93 | this.left.camera.projection = pc.PROJECTION_VR; 94 | this.left.enabled = false; 95 | 96 | // right camera 97 | this.right = new pc.fw.Entity(); 98 | this.entity.addChild(this.right); 99 | this.right.addComponent("camera", { 100 | clearColor: clearColor, 101 | rect: new pc.Vec4(0.5, 0, 0.5, 1) 102 | }); 103 | this.right.camera.projection = pc.PROJECTION_VR; 104 | this.right.enabled = false; 105 | 106 | // for debugging 107 | // var sphere = new pc.Entity(); 108 | // sphere.setLocalScale(0.01,0.01,0.01); 109 | // sphere.rotate(-90,0,0); 110 | // sphere.addComponent("model", {type: "cone"}); 111 | 112 | // this.left.addChild(sphere.clone()); 113 | // this.right.addChild(sphere.clone()); 114 | 115 | var hmd = new pc.Hmd(this.app); 116 | hmd.initialize(function (err, hmd) { 117 | if (err) { 118 | console.warn("Failed to initialize HMD"); 119 | return; 120 | } 121 | 122 | self.hmd = hmd; 123 | 124 | if (hmd.display) { 125 | self.app.fire("vr:ready", hmd); 126 | } else { 127 | self.app.fire("vr:missing"); 128 | } 129 | 130 | if (self.enableOnClick) { 131 | self.app.mouse.on("mouseup", self.enterVr, self); 132 | 133 | if (self.app.touch) { 134 | self.app.touch.on("touchend", function (e) { 135 | self.enterVr(); 136 | e.event.preventDefault(); 137 | }, self); 138 | } 139 | } 140 | }); 141 | 142 | hmd.on("presentchange", this._onPresentChange, this); 143 | }; 144 | 145 | 146 | VrCamera.prototype.enterVr = function () { 147 | if (this.inVr || !this.hmd) 148 | return; 149 | 150 | var self = this; 151 | 152 | this.hmd.requestPresent(function (err) { 153 | if (err) { 154 | // this device can't present 155 | return; 156 | } 157 | 158 | if (self.hmd.stereo) { 159 | self.entity.camera.enabled = false; 160 | 161 | self.right.camera.projection = pc.PROJECTION_VR; 162 | self.right.camera.camera._vrFov = self.hmd.rightFov; 163 | self.right.enabled = true; 164 | 165 | self.left.camera.projection = pc.PROJECTION_VR; 166 | self.left.camera.camera._vrFov = self.hmd.leftFov; 167 | self.left.enabled = true; 168 | 169 | self.inVr = true; 170 | } 171 | 172 | self.app.fire("vr:enter"); 173 | }); 174 | }; 175 | 176 | VrCamera.prototype.leaveVr = function () { 177 | if (!this.inVr || !this.hmd) 178 | return; 179 | 180 | var self = this; 181 | 182 | this.hmd.exitPresent(function (err) { 183 | if (err) { 184 | return; 185 | } 186 | self._onLeave(); 187 | }); 188 | 189 | }; 190 | 191 | VrCamera.prototype._onLeave = function () { 192 | this.right.enabled = false; 193 | this.left.enabled = false; 194 | 195 | if (this.entity.camera) { 196 | this.entity.camera.enabled = true; 197 | } 198 | 199 | this.inVr = false; 200 | 201 | this.app.fire("vr:leave"); 202 | }; 203 | 204 | VrCamera.prototype._onPresentChange = function (presenting) { 205 | // an external event has caused presenting to finish. 206 | if (!presenting) { 207 | this._onLeave(); 208 | } 209 | }; 210 | 211 | VrCamera.prototype._onPresentChange = function (presenting) { 212 | // an external event has caused presenting to finish. 213 | if (!presenting) { 214 | this._onLeave(); 215 | } 216 | }; 217 | 218 | VrCamera.prototype.update = function () { 219 | if (!this.hmd) 220 | return; 221 | 222 | this.hmd.poll(); 223 | 224 | if (this.inVr || this.alwaysAcceptInput) { 225 | this.entity.setLocalRotation(VrCamera.q.copy(this.originRotation).mul(this.hmd.rotation)); 226 | 227 | VrCamera.offset.copy(this.hmd.position).scale(this.offsetScale); 228 | this.originRotation.transformVector(VrCamera.offset, VrCamera.offset); 229 | var p = VrCamera.offset.add(this.origin); 230 | 231 | this.entity.setLocalPosition(p); 232 | 233 | if (this.hmd.stereo) { 234 | // get position from hmd, include left and right eye offset 235 | var lt = this.hmd.leftOffset; 236 | var rt = this.hmd.rightOffset; 237 | 238 | this.left.setLocalPosition(lt.x, lt.y, lt.z); 239 | this.right.setLocalPosition(rt.x, rt.y, rt.z); 240 | } 241 | } 242 | }; 243 | 244 | VrCamera.prototype.activate = function () { 245 | this.right.enabled = true; 246 | this.left.enabled = true; 247 | }; 248 | 249 | VrCamera.prototype.reset = function () { 250 | this.hmd.reset(); 251 | }; 252 | 253 | VrCamera.prototype.setOriginRotation = function (x, y, z) { 254 | this.originRotation.setFromEulerAngles(x, y, z); 255 | }; 256 | 257 | VrCamera.prototype.setOffsetScale = function (scale) { 258 | this.offsetScale = scale; 259 | }; 260 | 261 | VrCamera.prototype.setOrigin = function (x, y, z) { 262 | this.origin.set(x, y, z); 263 | }; 264 | --------------------------------------------------------------------------------