├── .gitignore ├── LICENSE ├── README.md ├── package.json └── vr.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | dist 3 | .DS_Store 4 | 5 | lib-cov 6 | *.seed 7 | *.log 8 | *.csv 9 | *.dat 10 | *.out 11 | *.pid 12 | *.gz 13 | 14 | pids 15 | logs 16 | results 17 | 18 | npm-debug.log 19 | node_modules 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 deathcap 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # voxel-vr 2 | 3 | WebVR voxel.js plugin 4 | 5 | ![screenshot](http://i.imgur.com/T0A5use.png "Screenshot") 6 | 7 | Renders the scene side by side in stereo 3D for use with WebVR. 8 | Requires [voxel-engine-stackgl](hndarra://github.com/deathcap/voxel-engine-stackgl), 9 | and [game-shell-fps-camera](https://github.com/deathcap/game-shell-fps-camera), 10 | load with [voxel-plugins](https://github.com/deathcap/voxel-plugins). 11 | 12 | Replaces the `render` handler of [gl-now](https://github.com/stackgl/gl-now) to 13 | emit `gl-render` twice per tick, one for each eye, with the viewport and matrices 14 | set appropriately. If used on a WebVR-enabled browser (experimental Firefox or Chrome), 15 | or on a platform supported by [webvr-polyfill](https://github.com/borismus/webvr-polyfill), 16 | will attempt to use VR settings from a head-mounted VR device. 17 | 18 | Warning: incomplete 19 | 20 | ## See also 21 | 22 | * [MozVR](http://mozvr.com) 23 | * [voxel-oculus](https://github.com/deathcap/voxel-oculus) - Oculus Rift stereo view for three.js-based voxel-engine, includes lens distortion 24 | (note this predates WebVR, with WebVR the browser is expected to perform the lens distortion instead) 25 | * [voxel-oculus-vr](https://github.com/vladikoff/voxel-oculus-vr) - uses OculusRiftEffect.js from three.js 26 | * [three.js example effects](https://github.com/mrdoob/three.js/tree/master/examples/js/effects) - OculusRiftEffect, StereoEffect, and VREffect (WebVR) 27 | 28 | 29 | ## License 30 | 31 | MIT 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voxel-vr", 3 | "description": "WebVR voxel.js plugin", 4 | "version": "0.1.2", 5 | "main": "vr.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:deathcap/voxel-vr.git" 9 | }, 10 | "keywords": [ 11 | "voxel", 12 | "vr", 13 | "webvr", 14 | "oculus", 15 | "plugin" 16 | ], 17 | "dependencies": { 18 | "webvr-polyfill": "0.0.1", 19 | "gl-mat4": "^1.1.0", 20 | "shallow-copy": "0.0.1" 21 | }, 22 | "license": "MIT" 23 | } 24 | -------------------------------------------------------------------------------- /vr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('webvr-polyfill'); // fills navigator.getVRDevices(), etc. 4 | var mat4 = require('gl-mat4'); 5 | var shallow_copy = require('shallow-copy'); 6 | 7 | module.exports = function(game, opts) { 8 | return new VRPlugin(game, opts); 9 | }; 10 | module.exports.pluginInfo = { 11 | loadAfter: ['game-shell-fps-camera', 'voxel-shader', 'voxel-fullscreen'] 12 | }; 13 | 14 | function VRPlugin(game, opts) { 15 | this.game = game; 16 | this.camera = game.plugins.get('game-shell-fps-camera'); 17 | if (!this.camera) throw new Error('voxel-vr requires game-shell-fps-camera plugin'); // TODO: other cameras 18 | this.shader = game.plugins.get('voxel-shader'); 19 | if (!this.shader) throw new Error('voxel-vr requires voxel-shader plugin'); 20 | this.fullscreen = game.plugins.get('voxel-fullscreen'); // optional 21 | 22 | this.hmdvrDevice = undefined; 23 | this.currentEye = undefined; 24 | 25 | this.projectionMatrixLeft = mat4.create(); 26 | this.projectionMatrixRight = mat4.create(); 27 | 28 | // defaults if no VR device 29 | this.translateLeft = [-0.05, 0, 0]; 30 | this.translateRight = [+0.05, 0, 0]; 31 | this.FOVsLeft = { 32 | upDegrees: 45, 33 | downDegrees: 45, 34 | leftDegrees: 45, 35 | rightDegrees: 45 36 | }; 37 | this.FOVsRight = { 38 | upDegrees: 45, 39 | downDegrees: 45, 40 | leftDegrees: 45, 41 | rightDegrees: 45 42 | }; 43 | 44 | this.enable(); 45 | } 46 | 47 | VRPlugin.prototype.enable = function() { 48 | // Replace renderer with our own stereoscopic version TODO: only replace renderGLNow? 49 | // TODO: replace in this.game.shell.on('init', ...), which is where gl-now adds its render; 50 | // otherwise, this plugin cannot be enabled at startup 51 | this.oldRenders = this.game.shell.listeners('render'); 52 | this.game.shell.removeAllListeners('render'); 53 | this.game.shell.on('render', this.renderVR.bind(this)); 54 | this.camera.on('view', this.onView = this.viewVR.bind(this)); 55 | 56 | this.oldUpdateProjectionMatrix = this.shader.listeners('updateProjectionMatrix'); 57 | this.shader.removeAllListeners('updateProjectionMatrix'); 58 | this.shader.on('updateProjectionMatrix', this.onPerspective = this.perspectiveVR.bind(this)); 59 | 60 | if (this.fullscreen) { 61 | if (this.requestFlags) { 62 | this.oldRequestFlags = this.fullscreen.requestFlags; 63 | this.fullscreen.requestFlags = this.requestFlags; 64 | } 65 | } 66 | 67 | this.shader.updateProjectionMatrix(); 68 | 69 | this.scanDevices(); 70 | }; 71 | 72 | VRPlugin.prototype.disable = function() { 73 | this.game.shell.removeAllListeners('render'); 74 | for (var i = 0; i < this.oldRenders.length; i += 1) { 75 | this.game.shell.on('render', this.oldRenders[i]); 76 | } 77 | 78 | this.shader.removeAllListeners('updateProjectionMatrix'); 79 | for (var i = 0; i < this.oldUpdateProjectionMatrix.length; i += 1) { 80 | this.shader.on('updateProjectionMatrix', this.oldUpdateProjectionMatrix[i]); 81 | } 82 | 83 | if (this.fullscreen) { 84 | if (this.oldRequestFlags) { 85 | // no longer fullscreen to the VR device 86 | this.fullscreen.requestFlags = this.oldRequestFlags; 87 | } 88 | } 89 | }; 90 | 91 | var xyz2v = function(xyz) { 92 | return [xyz.x, xyz.y, xyz.z] 93 | }; 94 | 95 | VRPlugin.prototype.scanDevices = function() { 96 | if (!('getVRDevices' in navigator)) return; // should be polyfilled by webvr-polyfill, but just in case 97 | 98 | var self = this; 99 | 100 | navigator.getVRDevices().then(function(devices) { 101 | for (var i = 0; i < devices.length; i += 1) { 102 | var device = devices[i]; 103 | 104 | if (device instanceof HMDVRDevice) { 105 | // translation vector per eye 106 | self.translateLeft = xyz2v(device.getEyeTranslation('left')); 107 | self.translateRight = xyz2v(device.getEyeTranslation('right')); 108 | 109 | // field of views per eye 110 | // Note: using shallow_copy since left and right might be same object (webvr-polyfill bug?) 111 | // but we want to allow adjusting them individually 112 | self.FOVsLeft = shallow_copy(device.getRecommendedEyeFieldOfView('left')); 113 | self.FOVsRight = shallow_copy(device.getRecommendedEyeFieldOfView('right')); 114 | // TODO: .getMaximumEyeFieldOfView 115 | 116 | self.shader.updateProjectionMatrix(); // -> perspectiveVR 117 | 118 | // voxel-fullscreen to the VR device 119 | if (self.fullscreen 120 | //&& device.hardwareUnitId !== 'polyfill' // hack to exclude non-real (virtual?) devices, so original flags are preserved 121 | ) { 122 | self.requestFlags = { vrDisplay: device }; 123 | self.oldRequestFlags = self.fullscreen.requestFlags; 124 | self.fullscreen.requestFlags = self.requestFlags; 125 | } 126 | self.hmdvrDevice = device; 127 | 128 | break; // use only first HMD device found TODO: configurable multiple devices 129 | } 130 | } 131 | }, function(err) { 132 | console.log('voxel-vr error in getVRDevices: ',err); 133 | }); 134 | }; 135 | 136 | // Compute the projection matrix, when the viewport changes 137 | VRPlugin.prototype.perspectiveVR = function(out) { 138 | // Save the matrix for each eye, locally 139 | mat4.perspectiveFromFieldOfView(this.projectionMatrixLeft, this.FOVsLeft, this.shader.cameraNear, this.shader.cameraFar); 140 | mat4.perspectiveFromFieldOfView(this.projectionMatrixRight, this.FOVsRight, this.shader.cameraNear, this.shader.cameraFar); 141 | // out sets voxel-shader .projectionMatrix, but we have to (re)set it individually for each eye in renderVR below 142 | }; 143 | 144 | // Compute the view matrix, each frame 145 | VRPlugin.prototype.viewVR = function(out) { 146 | var eye = this.currentEye; 147 | 148 | if (eye === 0) { 149 | mat4.translate(out, out, this.translateLeft); 150 | } else { 151 | mat4.translate(out, out, this.translateRight); 152 | } 153 | }; 154 | 155 | // Render scene twice, once for each eye (replaces gl-now renderGLNow(t)) 156 | VRPlugin.prototype.renderVR = function(t) { 157 | var shell = this.game.shell; 158 | var scale = this.game.shell.scale; 159 | var gl = shell.gl; 160 | 161 | //Bind default framebuffer 162 | gl.bindFramebuffer(gl.FRAMEBUFFER, null) 163 | 164 | //Clear buffers 165 | if(shell.clearFlags & gl.STENCIL_BUFFER_BIT) { 166 | gl.clearStencil(shell.clearStencil) 167 | } 168 | if(shell.clearFlags & gl.COLOR_BUFFER_BIT) { 169 | gl.clearColor(shell.clearColor[0], shell.clearColor[1], shell.clearColor[2], shell.clearColor[3]) 170 | } 171 | if(shell.clearFlags & gl.DEPTH_BUFFER_BIT) { 172 | gl.clearDepth(shell.clearDepth) 173 | } 174 | if(shell.clearFlags) { 175 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) 176 | } 177 | 178 | // Left eye 179 | this.currentEye = 0 180 | gl.viewport(0, 0, (shell._width / scale / 2)|0, (shell._height / scale)|0) 181 | mat4.copy(this.shader.projectionMatrix, this.projectionMatrixLeft); 182 | shell.emit("gl-render", t) 183 | 184 | 185 | // Right eye 186 | this.currentEye = 1 187 | gl.viewport((shell._width / scale / 2)|0, 0, (shell._width / scale / 2)|0, (shell._height / scale)|0) 188 | mat4.copy(this.shader.projectionMatrix, this.projectionMatrixRight); 189 | shell.emit("gl-render", t) 190 | 191 | this.currentEye = undefined 192 | }; 193 | --------------------------------------------------------------------------------